Compare commits

..
1257 Commits
Author SHA1 Message Date
Donkie 362d80e55d Merge pull request #1284 from Donkie/fix/1208-remember-page-size
Remember the Library page size
2026-10-18 09:11:31 +02:00
Donkie 5fc8e6cf42 Remember the Library page size 2026-10-18 09:05:23 +02:00
Donkie b7a282d2eb Merge pull request #1231 from dajiaohuang/fix-reject-odd-length-tag-uids
Reject odd-length tag UIDs
2026-10-17 09:44:13 +02:00
Donkie c979e63513 Merge pull request #1214 from dajiaohuang/fix-extra-field-integer-reject-bool
Reject booleans for integer extra fields
2026-10-17 09:40:49 +02:00
Donkie 0e88109cc6 Merge pull request #1216 from dajiaohuang/fix-websocket-idempotent-disconnect
Make websocket subscriber cleanup idempotent
2026-10-17 09:39:41 +02:00
Donkie ed9124cb06 Merge pull request #1279 from Donkie/fix/1278-register-sl-ko
Offer Slovenian and Korean in the language picker
2026-10-14 19:54:30 +02:00
Donkie f399855d3b Offer Slovenian and Korean in the language picker 2026-10-14 19:27:48 +02:00
github-actions[bot] d102f9475b Merge pull request #1273 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-10-12 15:09:37 +00:00
Julia 61e85c29bf Translated using Weblate (Polish)
Currently translated at 100.0% (629 of 629 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-10-11 15:10:02 +02:00
Julia ef3dee4728 Translated using Weblate (Polish)
Currently translated at 100.0% (628 of 628 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-10-11 15:08:09 +02:00
Donkie 8656e04055 fix(deps): patch sponsors image decoder vulnerabilities
Update sharp from 0.35.3 to 0.35.5 and its matching prebuilt binaries, addressing GHSA-wq5f-xc86-pv6w and GHSA-rgj7-g3m4-5g8c (Dependabot alerts #273 and #256).

Verified npm ci reports zero vulnerabilities and sharp renders the existing sponsors SVG to an 800x570 PNG. No application dependencies or workflow configuration changed.
2026-10-11 15:08:01 +02:00
Donkie 744284391d Merge pull request #1252 from Donkie/dependabot/npm_and_yarn/tests_frontend_v2/sharp-0.35.5
Bump sharp from 0.35.4 to 0.35.5 in /tests_frontend_v2
2026-10-11 14:54:49 +02:00
Donkie f8642bedd6 Merge pull request #1270 from Donkie/dependabot/npm_and_yarn/client/i18next-http-backend-4.0.2
build(deps): bump i18next-http-backend from 3.0.6 to 4.0.2 in /client
2026-10-11 14:54:39 +02:00
Donkie 8285a4da45 Merge pull request #1272 from Donkie/deps/drop-influxdb-client
Drop the unused influxdb-client dependency
2026-10-11 14:54:22 +02:00
Donkie f6f28a8df8 Merge pull request #1274 from Donkie/feat/1173-lot-in-grouped-rows
Add a Show lot numbers toggle to the library
2026-10-11 14:52:15 +02:00
Donkie abdfc73b8d Make lot numbers on spool rows a Filter menu toggle
Always showing the lot was too bulky. It now sits behind "Show lot
numbers" in the Filter menu, next to the archived and no-spools
toggles, off by default. When on, every spool row in any grouping ends
its details line with the lot. Like showEmpty it lives in the URL (lots=1)
and is remembered per browser, since it only adds to what's shown.
2026-10-11 14:40:51 +02:00
Donkie 90713bf5e2 Merge pull request #1275 from Donkie/fix/print-filament-picker-pages
Page the filament picker when printing labels
2026-10-11 14:19:29 +02:00
Donkie 636e12e737 Merge pull request #1266 from Donkie/fix/1181-import-vendor-by-name
Reuse a same-name manufacturer when importing from SpoolmanDB
2026-10-11 14:17:02 +02:00
dependabot[bot] 896e473807 Bump sharp from 0.35.4 to 0.35.5 in /tests_frontend_v2
Bumps [sharp](https://github.com/lovell/sharp) from 0.35.4 to 0.35.5.
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.4...v0.35.5)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-11 10:04:47 +00:00
Donkie b01e4e5bbe Give each platform its own uv cache in the image build
The parallel per-platform builds shared one uv cache mount, so they
queued on uv's cache lock. When uv.lock changed, the emulated armv7
sync held it long enough for the others to hit uv's 300 s lock timeout
and fail publish-images.
2026-10-11 12:02:59 +02:00
Donkie c881740e69 Page the filament picker when printing labels
The filament picker loaded up to 1000 filaments once and filtered them
in the browser, so anything past the first 1000 could not be printed.
It now works like the spool picker: 100 per page with the same pager
and sort, search through the backend, and All adding the current page.
2026-10-11 11:45:00 +02:00
Donkie dc18a3017f Show the lot number on spool rows grouped by filament
Spools of one filament looked alike in the grouped list, with only the
usage status to tell them apart. Add the lot number after it when the
spool has one. A long lot is cut by the existing ellipsis, with the full
text on hover. Flat rows and other groupings are unchanged.
2026-10-11 11:43:21 +02:00
Donkie f7c3988fed Merge remote-tracking branch 'origin/master' into fix/1181-import-vendor-by-name
# Conflicts:
#	client_v2/src/lib/api/spoolSource.test.ts
2026-10-11 11:39:55 +02:00
github-actions[bot] 8d363bbec4 Merge pull request #1199 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-10-11 09:39:21 +00:00
github-actions[bot] 57ef303c93 Merge pull request #1200 from Donkie/weblate
Translations update
2026-10-11 09:39:14 +00:00
Donkie bcf340bdc7 Drop the unused influxdb-client dependency
It came in by accident with the sponsors workflow change and nothing
imports it. Removing it also drops urllib3, reactivex, six and
python-dateutil from the image.
2026-10-11 11:38:01 +02:00
dependabot[bot] 69e541ce67 build(deps): bump i18next-http-backend from 3.0.6 to 4.0.2 in /client
Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 3.0.6 to 4.0.2.
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/compare/v3.0.6...v4.0.2)

---
updated-dependencies:
- dependency-name: i18next-http-backend
  dependency-version: 4.0.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-11 09:36:38 +00:00
Micha 6746bc4fce Translated using Weblate (German)
Currently translated at 100.0% (628 of 628 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-10-11 11:33:51 +02:00
baked7015 c492cc05fe Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (613 of 613 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hant/
2026-10-11 11:33:50 +02:00
EtienneB 1b039bca19 Translated using Weblate (Dutch)
Currently translated at 56.1% (344 of 613 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/nl/
2026-10-11 11:33:50 +02:00
EtienneB bd9553342f Translated using Weblate (Dutch)
Currently translated at 46.9% (288 of 613 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/nl/
2026-10-11 11:33:49 +02:00
Julia 6a9904adbb Translated using Weblate (Polish)
Currently translated at 100.0% (613 of 613 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-10-11 11:33:49 +02:00
EtienneB 74f93b4afb Translated using Weblate (Dutch)
Currently translated at 37.0% (227 of 613 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/nl/
2026-10-11 11:33:48 +02:00
baked7015 073bf15108 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 97.9% (287 of 293 strings)

Co-authored-by: baked7015 <bingxyz1024@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client LEGACY
2026-10-11 09:33:44 +00:00
EtienneB 15f0124a4c Translated using Weblate (Dutch)
Currently translated at 100.0% (293 of 293 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: EtienneB <eboeziek@icloud.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client LEGACY
2026-10-11 09:33:43 +00:00
Donkie 1c87bb9c0e Merge pull request #1269 from Donkie/deps/bump-all
Bump dependencies within their declared ranges
2026-10-11 11:33:25 +02:00
Donkie 715cd47b9f Merge pull request #1268 from Donkie/fix/tag-scan-unknown-wording
Mention filaments in the unknown-tag and any-reader texts
2026-10-11 11:32:37 +02:00
Donkie fa92ab465d Merge pull request #1267 from Donkie/fix/location-rename-slash
Allow renaming a location whose name contains a slash
2026-10-11 11:31:52 +02:00
Donkie a9e1802c2a Merge pull request #1265 from Donkie/fix/add-spool-zero-values
Keep a typed 0 for tare, price and temperatures
2026-10-11 11:30:55 +02:00
Donkie 72031f0ea5 Merge pull request #1264 from Donkie/ci/bot-pr-checks
Run CI on bot PRs and merge Weblate PRs weekly
2026-10-11 11:30:16 +02:00
Donkie a3977509eb Bump dependencies within their declared ranges
Backend via uv lock --upgrade, client and client_v2 via npm update.
SQLAlchemy stays on 2.0.x: 2.1 is in range, but sqlalchemy-cockroachdb
2.0.4 requires <2.1 and uv would otherwise downgrade the dialect to
2.0.3, which lacks that cap. Paraglide stays on 2.23.0: 2.27.0 (with
@inlang/sdk 4) hangs compiling the inlang project.
2026-10-11 10:35:46 +02:00
Donkie 3a8ac87b13 Mention filaments in the unknown-tag and any-reader texts
Tags can be linked to filaments since 0.27, but the unknown-tag toast
only said to open the spool, and the any-reader setting said scans open
spools.
2026-10-11 10:33:44 +02:00
Donkie 3e9c3b68bd Allow renaming a location whose name contains a slash
PATCH /location/{location} returned 404 for names like "Shelf 1/2",
even URL-encoded, because the path is decoded before routing and the
default converter stops at a slash. Use the path converter.
2026-10-11 10:33:19 +02:00
Donkie 9708d02498 Reuse a same-name manufacturer when importing from SpoolmanDB
The import only looked for a vendor by external_id, so a manufacturer
the user had created by hand was missed and a duplicate was made. Fall
back to the existing case-insensitive name match before creating one.
Both external_id lookups now send a quoted term, since the filter is a
substring match otherwise and "Prusa" would match "Prusament".
2026-10-11 10:32:30 +02:00
Donkie 58a01e3389 Keep a typed 0 for tare, price and temperatures
The add-spool form and the new-filament flow turned a typed 0 into
"not set", so a spool or filament could not store a tare or price of 0
and fell back to the filament or vendor value instead. Blank still means
not set; 0 is now sent as 0. Comma decimals go through parseDecimal.
2026-10-11 10:31:49 +02:00
Donkie bb0bcc1b5f Run CI on bot PRs and merge Weblate PRs weekly
Dependabot and Weblate branches are excluded from push runs, and the
root jobs skipped same-repo pull_request runs, so their PRs got no
checks and stayed blocked. Let those PRs run the pull_request suite.

Weblate PRs are now merged by a Monday schedule instead of as soon as
each sync goes green, to cut down the merge commits on master.
2026-10-11 10:24:34 +02:00
Donkie 6b6c13e4f1 Merge pull request #1259 from Donkie/fix/silent-list-caps
Page through filament spools and dashboard groups
2026-10-11 10:11:51 +02:00
Donkie b0faab5cfd Merge pull request #1195 from Donkie/worktree-fix-1165-inverted-qr
Decode inverted QR codes in client_v2 scanner on Firefox
2026-10-11 10:11:45 +02:00
Donkie 62963935eb Merge branch 'master' into fix/silent-list-caps 2026-10-11 09:19:00 +02:00
Donkie 3f8cb8f1d1 Merge pull request #1263 from Donkie/fix/screenshots-wait-for-data
Wait for page data before taking Argos screenshots
2026-10-11 09:11:10 +02:00
Donkie 89fba3f4a8 Wait for page data before taking Argos screenshots
The screenshot spec shot each page as soon as the logo was visible, which
paints before any API data arrives. Argos only waits for aria-busy, which
client_v2 never sets, so the first shot of a page often caught it mid-load
(an empty Library list, half-loaded dashboard cards) and the Registered
blanking ran before the inspector had rendered. Every PR got the same
handful of noise diffs, master's baseline included.

Wait until no API request has been in flight for 300 ms before shooting.
2026-10-11 09:05:23 +02:00
Donkie e72b2d1821 Merge pull request #1262 from Donkie/fix/empty-locations-in-picker
Offer empty dashboard locations in the location pickers
2026-10-10 18:00:25 +02:00
Donkie 4658f42798 Merge pull request #1260 from Donkie/fix/1242-dashboard-card-height
Let dashboard cards use the screen height
2026-10-10 17:59:17 +02:00
Donkie abe5bd909f Merge pull request #1258 from Donkie/fix/low-stock-zero
Keep a stored threshold of 0 across reloads
2026-10-10 17:54:05 +02:00
Donkie 645ef72937 Merge pull request #1257 from Donkie/fix/1243-deep-link-unloaded
Open deep-linked items the list hasn't loaded
2026-10-10 17:53:01 +02:00
Donkie 12199575de Merge pull request #1256 from Donkie/fix/1246-backup-lock
Fix nightly backups failing on network shares, and the backup hang
2026-10-10 17:51:04 +02:00
Donkie 975e0cc702 Merge pull request #1255 from Donkie/fix/write-check-tempfile
Probe data dir writability with a uniquely named temp file
2026-10-10 17:49:37 +02:00
Donkie 15381fcf01 Merge pull request #1254 from Donkie/fix/metrics-stale-spools
Stop exporting archived and deleted spools in /metrics
2026-10-10 17:48:54 +02:00
Donkie ca007aa561 Merge pull request #1253 from Donkie/fix/measure-zero-tare
Keep an explicit 0 tare when measuring a spool
2026-10-10 17:47:03 +02:00
Donkie 43101b2a2d Offer empty dashboard locations in the location pickers
The spool inspector and the add spools dialog listed only what GET
/location returns, which is the locations that hold a spool right now.
An empty location kept as a dashboard card, like an idle toolhead,
could not be picked without typing it or parking another spool there
first. The old client merged its saved `locations` setting into these
dropdowns; this does the same with the dashboard's saved location
cards (which fall back to that old setting). The library's location
filter still lists only locations in use.

Fixes #1261
2026-10-10 15:03:43 +02:00
Donkie a31beee46b Let dashboard cards use the screen height
Card bodies were capped at min(52vh, 420px), so a group of more than
about eight spools scrolled inside its card while most of a desktop
screen stayed empty below it.

The cap now reaches down to the footer for a single row of cards, but
stops at 1000px. A card only fetches its next page when its list is
scrolled, so a body taller than one page of chips (about 1500px) shows
no scrollbar and never loads the rest; the cap proposed in the issue,
calc(100dvh - 220px), did exactly that on a 2160px tall screen. Narrow
touch layouts keep the old cap so a card can't swallow the page's
scroll. A Playwright spec pins the paging on a tall screen.
2026-10-10 14:59:07 +02:00
Donkie 6447198fe3 Page through filament spools and dashboard groups
The filament inspector asked for its first 100 spools and the dashboard
for its first 1000 groups, and both showed the result as if it were
everything. A filament with more spools got a wrong count and a wrong
total remaining, and groups past the first 1000 never got a card.

Both now page through the whole list with a small allPages helper. The
inspector's sort gets an id tie-break so offset paging is stable, and
duplicates from a spool moving up mid-load are dropped.
2026-10-10 14:50:42 +02:00
Donkie c35983ee52 Keep a stored threshold of 0 across reloads
The low-stock threshold was read back with `Number(stored) || default`,
so a threshold of 0 g came back as 150 on every reload. The search
box's colour-similarity slider had the same bug the other way round
(`n > 0`), turning a stored 0 (exact colour only) back into 20.

Read both with parseDecimal, which tells "nothing stored" apart from a
real 0, and still rejects NaN, negatives and other garbage.
2026-10-10 14:44:10 +02:00
Donkie 1e800ec5f8 Open deep-linked items the list hasn't loaded
A QR code or bookmark pointing at a spool that isn't on the first page of
the list left the inspector on "Loading..." for good. Arriving on the
Library rewrites the URL to restore the remembered view, which re-parses
the same selection into a new object. The inspector's fetch effect
followed that object, so it re-ran, its cleanup aborted the request, and
the re-run skipped the id it had already "attempted". Same for filament
and manufacturer links, and for ids that 404.

Follow the selection by value instead, and forget an attempt whose
request was cancelled before it answered.
2026-10-10 14:40:50 +02:00
Donkie 016ca6791e Back up to the target without taking file locks on it
Backup folders on network shares often refuse SQLite's file locks (CIFS
without nobrl, NFS without lockd). SQLite reports a refused lock as
SQLITE_BUSY, so every backup to such a folder retried until the 60 second
give-up and blamed a locked database, while the live database was not
locked at all (#1246).

The target is a scratch .pending file that nothing else opens, so open it
with nolock=1. The give-up message now names the live database, since that
is the only file a backup can still wait on.
2026-10-10 14:36:01 +02:00
Donkie e5944c94e0 Probe data dir writability with a uniquely named temp file
can_write_to_data_dir() touched and then deleted data/test.txt, which
silently wiped any file of the user's that happened to have that name.
It now uses tempfile.NamedTemporaryFile in the data dir, which picks an
unused name and removes only the file it created.
2026-10-10 14:33:28 +02:00
Donkie a35331e50a Stop exporting archived and deleted spools in /metrics
The metrics refresh only ever set label sets, never removed them, so a
spool that was archived or deleted (or a deleted filament, or a renamed
filament's old info labels) kept being exported with its last values
until the server restarted.

Each refresh now drops the label sets the database no longer has. The
current values are set before the stale ones are removed rather than
clearing the gauges first, because /metrics is served from a worker
thread and a scrape landing mid-refresh would otherwise see live series
briefly disappear.
2026-10-10 14:32:47 +02:00
Donkie bcc1ba1ace Stop a locked database from hanging the server during backup
sqlite3's Connection.backup() retries forever while the source is busy,
calling progress with 0 of 0 pages each time. The backup ran synchronously
on the event loop, so a lock that did not clear froze every request and
spammed "Copied 0 of 0 pages." into the log (#1191).

Run backups in a worker thread, give up after the source has been locked
for 60 seconds, and serialize backups now that they can overlap. Per-page
progress drops to debug.
2026-10-10 14:32:21 +02:00
Donkie 629b8228d8 chore: update sponsors image 2026-10-10 09:15:57 +00:00
Donkie 7fbe604cc2 Keep an explicit 0 tare when measuring a spool
measure() treated a spool_weight of 0 the same as a missing one and fell
back to the filament's tare, and since #1164 the vendor's. A refill wound
on no spool has a real tare of 0, so its scale reading was short by the
filament's spool weight. Only a missing tare now falls back.
2026-10-10 11:07:37 +02:00
Donkie bff2a485e6 Merge pull request #1248 from Donkie/feat/filament-manufacturer-list
Filament and manufacturer lists in the Library
2026-10-10 10:17:09 +02:00
Donkie 7e83451226 Catalog lists: Escape closes toolbar menus, tidier footer, list by id
Escape now closes the Filter, Group and Sort menus when they were opened
with a click, which left focus on their button where the menu never saw
the key.

The pager's page numbers and page-size picker wrap as one, so the
catalog's longer summary line no longer strands the picker on a line of
its own on phones.

"Show as a list" in the manufacturer inspector filters by the
manufacturer's id rather than its name, so a namesake stays out.
2026-10-10 10:02:26 +02:00
Donkie 30928407a7 Catalog lists: every sort and filter, plain #ids
The filament and manufacturer lists now sort by every field of the
filament or manufacturer, their custom fields included, plus spool and
filament counts, and offer the same filters as the spool view. A filter
on the spools themselves leaves out the filaments with none, as it does
there.

/spool/group takes the matching sort keys: filament.<column>,
filament.vendor.<column>, filament.extra.<key>,
filament.vendor.extra.<key> and group.filament_count. Keys it can't
resolve are still ignored.

Rows show the id as #id on the left like spool rows, and the Library
tab menu loses its descriptive sublines.
2026-10-10 10:02:26 +02:00
Donkie 02f5c07c13 Add filament and manufacturer lists to the Library
The Library gets two power-user sub-views, /?view=filaments and
/?view=manufacturers, reached from a menu on the Library tab, the search
panel and the manufacturer inspector. Rows are lists rather than tables so
translated labels and long names never widen them. The spool view is
unchanged.

The group endpoint gains what the lists need, all additive: a
group.vendor_name sort, include_empty for group_by=vendor, filament_count
on vendor groups, an opt-in preview of a group's first spools or
filaments, and X-Total-Spools / X-Total-Remaining-Weight headers.
2026-10-10 10:02:26 +02:00
Donkie 022285d64d Add Argos visual snapshots of client_v2 pages to CI
A screenshots Playwright project seeds a fixed dataset on the fresh CI
database and captures the main pages in light/dark at desktop and phone
widths. The Argos reporter uploads them from CI and posts the diff against
master on the PR. Fork PRs upload tokenless.

Also scope the transparency spec's swatch locator to the inspector header,
it picked the first list row once other spools sorted ahead of its own.
2026-10-10 09:35:51 +02:00
Donkie 6b1e01ecd8 chore: update sponsors image 2026-10-04 09:11:19 +00:00
dajiaohuang 594e14b465 Check UID length before byte parity 2026-10-02 23:12:06 +08:00
dajiaohuang bce0d8069a Reject odd-length tag UIDs 2026-10-02 23:06:38 +08:00
dajiaohuang cc34b8ea24 Make websocket subscriber cleanup idempotent 2026-10-02 16:21:16 +08:00
dajiaohuang 82137bf35b Reject booleans for integer extra fields 2026-10-02 16:00:57 +08:00
Donkie 7c4dcc7da7 chore: update sponsors image 2026-10-01 09:38:56 +00:00
Donkie dcb4618399 chore: update sponsors image 2026-09-30 09:11:16 +00:00
Donkie 7a1114f8fc chore: update sponsors image 2026-09-29 09:20:58 +00:00
Donkie 423cf234be Decode inverted QR codes in client_v2 scanner on Firefox
Fixes #1165
2026-09-27 08:58:56 +02:00
Donkie 2c15af00b8 Merge pull request #1187 from Donkie/worktree-phone-nfc-auto-navigate
Auto-navigate on tags read by the phone's own NFC
2026-09-27 08:52:59 +02:00
Donkie ac9a32fa23 Merge pull request #1184 from IdefixRC/feat/scanner-camera-switch
Let the QR scanner pick between cameras
2026-09-27 08:48:16 +02:00
Donkie 787d912dec Merge pull request #1194 from Donkie/worktree-qr-custom-template-fields
Resolve label fields in custom QR URL templates
2026-09-27 08:46:52 +02:00
Donkie 8badd51707 Merge remote-tracking branch 'origin/master' into worktree-phone-nfc-auto-navigate 2026-09-27 08:44:40 +02:00
Donkie 53f09eb2bc Use node:crypto for test tag UIDs 2026-09-27 08:44:40 +02:00
Donkie b3d35d751d Merge pull request #1193 from Donkie/worktree-update-indicator-1192
Add update available indicator
2026-09-27 08:43:27 +02:00
Donkie 9f29826588 Resolve label fields in custom QR URL templates
A spool label's custom QR target can now use fields like {filament.id},
so it can open the filament page while still printing spool fields such
as the lot number. Closes #1152.
2026-09-27 08:41:26 +02:00
Donkie f7c53187c1 Test phone NFC reads with a stand-in NDEFReader
Headless Chromium has no Web NFC, so install a small fake of the surface
client_v2 uses (NDEFReader, scan, reading event, nfc permission) and cover
auto-navigate from a phone read, the unknown-tag toast, starting once
permission is granted, and the link dialog's own read.
2026-09-27 08:38:53 +02:00
Donkie 1e9d74cd6e Merge branch 'master' into worktree-update-indicator-1192 2026-09-27 08:38:16 +02:00
Donkie 1a04139c1d Add update check against GitHub releases (#1192)
The server checks the latest GitHub release in the background at startup
and every 12h, logs a warning when a newer version exists, and exposes
latest_version/update_available on /info. The client_v2 footer shows a
link to the new release. Disable with SPOOLMAN_UPDATE_CHECK=FALSE.
2026-09-27 08:32:57 +02:00
Donkie aae2b4df56 ci: skip the duplicate pull_request run for same-repo branches
Now that every branch builds on push, opening a PR from one ran the whole
workflow twice for the same commit.

Concurrency cannot dedupe these: push and pull_request carry different refs, and
forcing them into one group only races them, killing whichever run started
first. That is the wrong outcome roughly half the time, since publish-images
runs on push and not on pull_request, so the branch image would vanish.

Guard the three jobs that have no `needs` instead. A skipped dependency skips its
dependents, so this cascades to all twelve. Forks are unaffected: a fork's push
fires no event in this repo, so its pull_request run is the only one there is.
2026-09-27 08:32:44 +02:00
Donkie e1ad9feb30 ci: build every branch and publish it to GHCR under the branch name
Branch pushes matched no trigger, so an unmerged fix could not be handed to a
bug reporter as an image. Push now covers all branches except weblate (constant
locale-only commits) and dependabot (arrives as PRs anyway).

publish-images was already gated only on "not a pull request", so it picks
branch pushes up for free; it just had no tag to publish them under. Adding
type=ref,event=branch to the GHCR metadata gives one, skipping master since that
is already :edge. Docker Hub deliberately keeps only edge and tagged releases,
so users browsing it do not see half-finished branches.

Concurrency now cancels superseded runs on any ref except master and tags, which
publish images and must not be interrupted mid-manifest.

Note: once a branch has an open PR, push and pull_request both fire and it runs
twice.
2026-09-27 08:32:44 +02:00
github-actions[bot] 5b93b3b37e Merge pull request #1190 from Donkie/weblate
Translations update
2026-09-26 14:04:04 +00:00
github-actions[bot] bd4b9351aa Merge pull request #1189 from Donkie/weblate
Translations update
2026-09-26 13:57:54 +00:00
jernejp21 958d5bd9ee Translated using Weblate (Slovenian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: jernejp21 <jernejp21@tuta.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/
Translation: Spoolman/Web Client LEGACY
2026-09-26 13:57:53 +00:00
github-actions[bot] c9b1215cb6 Merge pull request #1188 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-26 13:57:39 +00:00
jernejp21 586299aaa6 Translated using Weblate (Slovenian)
Currently translated at 22.3% (136 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sl/
2026-09-26 13:51:41 +00:00
jernejp21 7e98987b87 Translated using Weblate (Slovenian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: jernejp21 <jernejp21@tuta.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/
Translation: Spoolman/Web Client LEGACY
2026-09-26 13:51:35 +00:00
RRH74 ba62681856 Translated using Weblate (Spanish)
Currently translated at 22.6% (138 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/es/
2026-09-26 13:51:32 +00:00
IdefixRCandClaude Opus 5.5 05e4da7a10 Pick the scanner camera from a list instead of cycling
Review feedback on #1184. A native select replaces the cycle button, still
shown only when more than one camera is listed. Picking directly suits
desktops, where webcams have real names, and on Android the select opens the
system's full-screen picker. It also removes a trap in the cycle: qr-scanner
opens a default camera when the requested one fails, and cycling on from that
default could never reach the cameras after the failing one. Every camera is
now one choice away, and the select shows the camera actually streaming.

Cameras with no label are listed as "Camera 1", "Camera 2" and so on. The
list now comes from enumerateDevices() directly, because qr-scanner's
listCameras() fills gaps with an untranslated "Default Camera" / "Camera N"
that can't be told apart from a real device name.

A failed switch now shows the same classifyStartFailure() message as a failed
start, instead of leaving the stage black with only a console error. The
picker stays up so another camera can be tried. After a failed open the
scanner is stopped, and setCamera() only restarts a running scanner (or does
nothing for the same id), so a retry starts it explicitly.

The caption and its n/N position are gone, since the select names the
current camera itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:48:46 +08:00
Donkie 0543b9145b Auto-navigate on tags read by the phone's own NFC
Auto-navigate only listened to the scan relay, so a tag tapped against an
Android phone running the client did nothing outside the link-tag dialog.
While auto-navigate is on, also run a background Web NFC reader and feed
its reads through the same handler, looking the holder up by UID.
2026-09-25 20:29:26 +02:00
Donkie 25d25e3701 Merge pull request #1186 from Donkie/worktree-armv7-build-speed
Speed up armv7 image builds when the layer cache misses
2026-09-25 16:11:38 +02:00
Donkie a353983f85 Speed up armv7 image builds on layer-cache misses
Persist uv's cache of source-built wheels (asyncpg, greenlet, psycopg2,
cffi, ...) across CI runs via actions/cache + buildkit-cache-dance, so a
lockfile or base-image change no longer recompiles everything under QEMU
(~11 min -> ~3 min). Move the greenlet patchelf step into the cached
dependency layer so source-only builds skip an apt-get under QEMU, and
drop python3-dev, which only pulled in Debian's unrelated python3.11.
2026-09-25 16:04:21 +02:00
Donkie bed3f14ee7 Bump version to 0.27.0 2026-09-25 13:35:18 +00:00
Donkie 27ea256e2d Merge pull request #1078 from romasku/fix/sqlite-unicode-search
fix: make SQLite searches Unicode case-insensitive
2026-09-25 15:33:02 +02:00
Donkie 7b82f2afb2 Merge pull request #1164 from Niko11111/fix-1117-vendor-tare-on-measure
Resolve the empty spool weight up to the vendor when measuring
2026-09-25 15:24:21 +02:00
Donkie 45cc8246d3 Merge pull request #1185 from Donkie/fix-1169-location-comma
fix(api): keep quoted string filter terms whole when they contain commas
2026-09-25 15:23:57 +02:00
Donkie 0c4caa527e fix(api): keep quoted string filter terms whole when they contain commas
String filters split their value on every comma before checking for quotes, so an exact
match like location="Top shelf, Rack 3" became two fuzzy terms that matched nothing.
The dashboard and library filters always send quoted values, so a location with a comma
showed as empty. Split with a quote-aware helper shared by the built-in string filters
and extra-field filters. Unquoted values split exactly as before.

Fixes #1169
2026-09-25 15:14:26 +02:00
Donkie 2ab68cdff9 Merge pull request #1168 from Donkie/dependabot/uv/anyio-4.14.2
build(deps): bump anyio from 4.14.1 to 4.14.2
2026-09-25 15:10:29 +02:00
Donkie fc6aa55d27 fix(labels): let the print dialog pick sheet orientation, clamp migrated margins
Two more problems from #1149.

Sheet mode set @page size to the paper's exact dimensions. A 62x34 page makes
the browser send it as landscape, and Chromium hides the orientation picker
when the page CSS sets a size. Label printer drivers disagree on which way
round a roll is: a Brother QL needed landscape, a Niimbot B1 needed portrait,
so any fixed choice prints rotated for someone. Sheet mode now uses size: auto
like v1 did, and the page is still laid out in mm. Label mode keeps its exact
size.

The v1 migration derived the label size from the raw margins. v1 users paired
-20mm margins with a 1.5mm printer margin to pull an oversized cell back onto a
62x34 page, which migrated as a 102x74 label. Margins are now clamped at
migration; the safe-zone alone gives the same content inset (max(safe, margin)
in both versions), and the label comes out at the paper size.
2026-09-25 15:08:36 +02:00
Donkie 92e093b16d fix(labels): keep sheet-printed labels at the right size and shape
Two bugs in the sheet-mode safe-zone inset, both reported in #1149.

The label raster has a fixed aspect ratio, but labelImg sized its width and
height independently, so a safe-zone squashed the label (1.5mm top/bottom on a
34mm label is a 9% vertical squeeze). v1 padded a box of reflowing HTML, where
that was harmless; a raster has to scale uniformly, so the inset box now gets
object-fit: contain.

Negative page margins made it much worse. v1 accepted them and used them to size
the grid cells; in v2 the label size comes from the design and the margin only
positions an absolutely placed grid inside an overflow:hidden page, so a negative
margin can only clip. It also inflated overSafe (safe - margin), which turned a
1.5mm safe-zone into a 21.5mm inset: at margin -20 the label rendered 22x12mm in
the corner of a 62x34mm page, and sheetGrid invented a second row off the paper.

Margins are now clamped to zero on read, so existing presets carrying v1's
negative values are fixed without the user editing them, and the margin inputs
refuse new negative values.
2026-09-25 15:08:36 +02:00
Donkie 55d11d0dc4 Merge pull request #1166 from Donkie/dependabot/npm_and_yarn/client_v2/devalue-5.9.2
build(deps): bump devalue from 5.9.0 to 5.9.2 in /client_v2
2026-09-25 15:08:27 +02:00
IdefixRCandClaude Opus 5.5 6c5d03d4ec Let the QR scanner switch between cameras
The scanner asks for the 'environment' camera and takes whichever rear camera
the browser hands it. On a phone with several lenses that can be the
telephoto: an Oppo X9 Ultra lists its rear cameras as camera 5, 4, 3, 2, 0 and
Chrome opens the zoom lens, which only fits a label in frame from across the
room. On a desktop the same thing happens with a virtual webcam, and once the
permission is saved the browser never offers the choice again (#693).

When more than one camera is listed, a switch button over the video steps to
the next one and wraps around. Each switch briefly shows the camera's name and
position, since phone lenses are only told apart by numbers. The camera the
user settles on is remembered per browser and opened directly next time.

A remembered camera that is no longer listed falls back to 'environment'.
That is checked before starting, because qr-scanner retries an unavailable
deviceId with no constraint at all, which can land on the front camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:46:02 +08:00
github-actions[bot] 058490a3c6 Merge pull request #1183 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-24 10:57:40 +00:00
Julia 9131454aa2 Translated using Weblate (Polish)
Currently translated at 100.0% (609 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-09-24 10:51:34 +00:00
github-actions[bot] c73bce4301 Merge pull request #1182 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-22 23:56:58 +00:00
Юрій «Zloj Kashtan» Скоріянтов 1c0f668c16 Translated using Weblate (Ukrainian)
Currently translated at 22.4% (137 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/uk/
2026-09-23 01:51:31 +02:00
github-actions[bot] de81990a29 Merge pull request #1177 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-21 18:04:23 +00:00
klim e4a78bb352 Translated using Weblate (French)
Currently translated at 100.0% (609 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/fr/
2026-09-21 17:57:26 +00:00
Steam Flow 196ebd66d0 Translated using Weblate (Czech)
Currently translated at 75.8% (462 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-09-21 17:57:25 +00:00
github-actions[bot] d1f3401b37 Merge pull request #1176 from Donkie/weblate
Translations update
2026-09-21 17:57:12 +00:00
klim d848ddef97 Translated using Weblate (French)
Currently translated at 99.3% (291 of 293 strings)

Co-authored-by: klim <klim@free.fr>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client LEGACY
2026-09-21 17:51:33 +00:00
github-actions[bot] 5c65cb5495 Merge pull request #1175 from Donkie/weblate
Translations update
2026-09-20 13:57:15 +00:00
github-actions[bot] 72d2cbe986 Merge pull request #1174 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-20 13:57:12 +00:00
Steam Flow 3ecdf5dfc6 Translated using Weblate (Czech)
Currently translated at 99.6% (292 of 293 strings)

Co-authored-by: Steam Flow <black23@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client LEGACY
2026-09-20 15:51:31 +02:00
Steam Flow d5a49e3340 Translated using Weblate (Czech)
Currently translated at 74.2% (452 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-09-20 13:51:30 +00:00
github-actions[bot] 83897b9744 Merge pull request #1172 from Donkie/weblate
Translations update
2026-09-19 06:45:17 +00:00
github-actions[bot] c386ac34e2 Merge pull request #1170 from Donkie/weblate
Translations update
2026-09-19 06:40:03 +00:00
Dr_Perry_Coke 205d8fb813 Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-09-19 06:39:59 +00:00
dimquea c74f77b478 Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: dimquea <dimquea@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-09-19 06:39:59 +00:00
github-actions[bot] a40b20df8c Merge pull request #1171 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-19 06:39:45 +00:00
Dr_Perry_Coke dc56305a97 Translated using Weblate (Russian)
Currently translated at 100.0% (609 of 609 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/
2026-09-19 08:34:33 +02:00
Dr_Perry_Coke ae2319a215 Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-09-19 08:34:23 +02:00
dimquea 9f90c2238d Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: dimquea <dimquea@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-09-19 08:34:23 +02:00
dependabot[bot] c157946eca build(deps): bump anyio from 4.14.1 to 4.14.2
Bumps [anyio](https://github.com/agronholm/anyio) from 4.14.1 to 4.14.2.
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Commits](https://github.com/agronholm/anyio/compare/4.14.1...4.14.2)

---
updated-dependencies:
- dependency-name: anyio
  dependency-version: 4.14.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 18:07:53 +00:00
dependabot[bot] 0966ecada9 build(deps): bump devalue from 5.9.0 to 5.9.2 in /client_v2
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.9.0 to 5.9.2.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.9.0...v5.9.2)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.9.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 05:22:31 +00:00
Niko11111 9ae9827f4c Resolve the empty spool weight up to the vendor when measuring
measure() walked the tare from the spool to its filament and then fell back to
zero, so a spool whose tare only exists on its vendor was recorded as if it had
no core at all. A filament copies its vendor's empty spool weight in create(),
which makes that inheritance a snapshot: a tare given to a vendor later never
reaches the filaments that already exist, and the value is then known one level
up while measure() treats it as absent.

With a vendor tare of 250 g, a 1000 g filament and a 704 g reading, the spool
came back with 704 g remaining and 296 g used instead of 454 g and 546 g.

The vendor is now selected alongside the filament in the query that already
runs, and its tare is the level below the filament's, in the same order
create() resolves the value in. The join is an outer join on purpose:
filament.vendor_id is nullable, and a filament without a vendor still has to
return its row, or measuring it fails with "Filament not found for spool".

Fixes #1117
2026-09-16 01:54:14 +02:00
Donkie e9e76a088f Merge pull request #1163 from Donkie/fix-1155-label-spool-pages
fix(labels): page the print tab's spool list
2026-09-14 19:29:21 +02:00
Donkie a4c749ad14 fix(labels): page the print tab's spool list
The print tab only listed the newest 50 spools, so printing labels for a whole collection meant searching for them one by one. The list is now paged with the same pager as the library, 100 spools per page by default.

All and Recent add to the selection instead of replacing it, so a collection can be selected page by page. Recent has its own newest-first loader, so it still covers every spool added in the last 24 hours on any page and in either sort order.

Fixes #1155
2026-09-14 19:23:55 +02:00
Donkie 994e5d422b Merge pull request #1162 from Donkie/dependabot/npm_and_yarn/client/react-router-7.18.3
build(deps): bump react-router from 7.18.1 to 7.18.3 in /client
2026-09-13 22:13:25 +02:00
dependabot[bot] 1db2ed3851 build(deps): bump react-router from 7.18.1 to 7.18.3 in /client
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.18.1 to 7.18.3.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router@7.18.3/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.18.3/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.18.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-13 10:59:46 +00:00
Donkie b9b5784108 Merge pull request #1153 from Donkie/dependabot/npm_and_yarn/client_v2/multi-d0c2d048a7
build(deps): bump @vitest/mocker and vitest in /client_v2
2026-09-13 12:58:55 +02:00
Donkie c01342d182 Merge pull request #1159 from Donkie/dependabot/npm_and_yarn/client/js-yaml-3.15.2
build(deps-dev): bump js-yaml from 3.15.1 to 3.15.2 in /client
2026-09-13 12:58:38 +02:00
Donkie 0388fb60bf Merge pull request #1161 from Donkie/feat-1136-paginated-external-search
Page through SpoolmanDB search results in the filament pickers
2026-09-13 12:57:12 +02:00
Donkie feb67ab500 feat(external): page through SpoolmanDB search results
The filament pickers only ever showed the first 8 SpoolmanDB matches, so entries further down a broad search could not be reached. They now load 20 at a time and fetch the next page when the result list is scrolled to the bottom.

GET /external/filament/search takes an optional offset and returns the total match count in x-total-count, like the other list endpoints. Requests without offset behave as before. The lowercased search text is built once per catalog load instead of on every search, since paging means one scan per page.

Fixes #1136
2026-09-13 12:19:50 +02:00
Donkie ed946223d9 Merge pull request #1160 from Donkie/fix-1124-weight-wrap
Keep the spool weight unit on one line in the library list
2026-09-13 12:12:09 +02:00
Donkie c405739b1d fix(library): keep the spool weight unit on one line
The remaining weight cell was a fixed 44px, so values like 987.5 g or 10.2 kg wrapped the unit onto a second line. Make the width a minimum and stop the text from wrapping.

Fixes #1124
2026-09-13 11:52:12 +02:00
Donkie e744049343 Show filament and manufacturer IDs in their inspectors
The spool inspector shows its #id next to the title, but filaments and
manufacturers had no visible ID anywhere, which makes them hard to reference
from integrations or the API. Both headers now carry the same muted #id badge.
2026-09-13 10:36:09 +02:00
Donkie 179fc0a117 Show a toast when a tag scan navigates
With auto-navigate on, a tapped tag moves the browser to its spool or filament.
The reader is often in another room from the screen, and a page that changes by
itself with no word why reads as a glitch, so the navigation now says what it
opened: "Tag scanned: opened spool #12, Prusament Galaxy Black."

It uses the info toast, which stays up long enough to be read after walking
back from the reader, and only fires when the scan actually navigates. Scans
that are ignored (settings pages, open dialogs, a focused input) stay silent,
and unknown tags keep their existing message.

filamentLabel now accepts an absent vendor, so a filament with no manufacturer
is named by itself rather than every caller guarding for it.

The Playwright tag specs assert the toast for both a spool and a filament.
2026-09-13 10:19:59 +02:00
Donkie 6520ce6d1b Let NFC tags identify filaments as well as spools
A tag can now be linked to a filament type, not just to one spool of it. The
tag table already carried filament_id and target_type for exactly this, so
there is no migration.

API, all additive:

  POST   /filament/{id}/tag        link; 409 if anything else holds the UID
  DELETE /filament/{id}/tag/{uid}  unlink
  GET    /filament?tag=<uid>       exact lookup on the normalized UID
  Filament.tags                    also inside every spool's embedded filament
  TagScan.matched_filament_id      always present, like matched_spool_id
  TagConflictMessage.filament_id   names a filament holder on a 409

One UID still identifies one thing across both kinds: a spool's tag cannot be
linked to a filament or the reverse, and neither ?tag= lookup answers with the
other kind. The scan debounce key now includes the kind, since spool 7 and
filament 7 are different answers.

SpoolTag and SpoolTagParameters are renamed Tag and TagLinkParameters while
they are unreleased, so the schema does not describe filament tags as spool
tags forever.

client_v2: the filament inspector gets the Tags section, the link dialog names
and moves a tag held by either kind, and scanning a filament's tag opens that
filament when auto-navigate is on.

Tag having two delete-orphan parents was checked on SQLAlchemy 2.0.51: new tags
flush, unlinking deletes the row, and deleting either parent cascades.

Proven on sqlite, postgres, mariadb and cockroachdb: 558 integration tests
each. Unit tests, svelte-check and the Playwright tag specs pass.
2026-09-13 10:10:13 +02:00
dependabot[bot] 83b0238bb4 build(deps-dev): bump js-yaml from 3.15.1 to 3.15.2 in /client
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.1 to 3.15.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.2/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.15.1...3.15.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-13 05:13:28 +00:00
Donkie f55188e346 chore: update sponsors image 2026-09-10 07:42:55 +00:00
dependabot[bot] 4fee8db323 build(deps): bump @vitest/mocker and vitest in /client_v2
Bumps [@vitest/mocker](https://github.com/vitest-dev/vitest/tree/HEAD/packages/mocker) to 4.1.11 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `@vitest/mocker` from 4.1.10 to 4.1.11
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/mocker)

Updates `vitest` from 4.1.10 to 4.1.11
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/mocker"
  dependency-version: 4.1.11
  dependency-type: indirect
- dependency-name: vitest
  dependency-version: 4.1.11
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-10 06:35:14 +00:00
github-actions[bot] 81636f253e Merge pull request #1143 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-04 20:56:09 +00:00
DrLous 0f705e80c0 Translated using Weblate (French)
Currently translated at 29.0% (174 of 600 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/fr/
2026-09-04 22:51:26 +02:00
github-actions[bot] 3c7f68e2bd Merge pull request #1140 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-03 14:58:52 +00:00
Micha 64b29a561b Translated using Weblate (German)
Currently translated at 100.0% (600 of 600 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-09-03 16:52:28 +02:00
Donkie 8f9c214dac Merge pull request #1135 from Donkie/dependabot/npm_and_yarn/client/fast-uri-3.1.7
build(deps): bump fast-uri from 3.1.5 to 3.1.7 in /client
2026-09-02 18:58:43 +02:00
Donkie 1305857efb Merge pull request #1134 from Donkie/dependabot/npm_and_yarn/client/sanitize-html-2.17.7
build(deps-dev): bump sanitize-html from 2.17.5 to 2.17.7 in /client
2026-09-02 18:27:23 +02:00
Donkie dd5a0a89dc Merge pull request #1133 from Donkie/dependabot/npm_and_yarn/client/browserslist-4.28.8
build(deps): bump browserslist from 4.28.4 to 4.28.8 in /client
2026-09-02 18:14:15 +02:00
dependabot[bot] 1eeb2948b2 build(deps): bump fast-uri from 3.1.5 to 3.1.7 in /client
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 16:00:29 +00:00
Donkie 348499f469 Tag issues opened by sponsors
The Supporter tier promises that issues a sponsor opens get an exclusive tag
so they surface first.

The sponsorship lookup is viewer-relative, so it runs on the PAT rather than
GITHUB_TOKEN, whose viewer is the Actions bot that nobody sponsors. Reading
privacyLevel needs that token's read:user scope.

Private sponsors are skipped. The label is public and applying it would
disclose a sponsorship they chose to keep hidden.
2026-09-02 17:59:16 +02:00
Donkie d34fb88b31 chore: update sponsors image 2026-09-02 15:48:39 +00:00
dependabot[bot] a63af285f5 build(deps-dev): bump sanitize-html from 2.17.5 to 2.17.7 in /client
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.5 to 2.17.7.
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/HEAD/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.17.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 03:33:01 +00:00
Donkie 798d8b1ded chore: update sponsors image 2026-09-01 20:32:50 +00:00
Donkie f8ffaa0837 Push the sponsors image with the PAT 2026-09-01 22:32:29 +02:00
dependabot[bot] 09fea9f668 build(deps): bump browserslist from 4.28.4 to 4.28.8 in /client
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.4 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.4...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-01 19:23:02 +00:00
Donkie 789b7fd88f Render a sponsors board in the README
A nightly workflow runs sponsorkit against the GitHub Sponsors API and commits
.github/media/sponsors.svg when it changes, so the board stays current without
any manual work.

Ko-fi has no API to query, so tips that arrive there are kept in a small
hand-maintained list in the config and fed in through a custom provider. A tip
counts toward a tier for 30 days and then drops to Past Sponsors on its own,
which is how sponsorkit treats real Ko-fi tips and how a one-time GitHub
sponsorship ages out. marko-p's $10 tip is the first entry.
2026-09-01 21:21:20 +02:00
github-actions[bot] 5424a638b1 Merge pull request #1132 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-09-01 16:07:57 +00:00
Samuel Wang c281114c6f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (600 of 600 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hans/
2026-09-01 18:03:07 +02:00
Julia 9566f7848d Translated using Weblate (Polish)
Currently translated at 100.0% (600 of 600 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-09-01 18:03:05 +02:00
Donkie 4f9cc1b75d Merge pull request #1120 from Niko11111/feat/last-uid-per-reader
Offer the reader's last UID in the Add tag dialog
2026-08-29 18:34:56 +02:00
github-actions[bot] 42721c7ebc Merge pull request #1125 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-27 05:51:16 +00:00
github-actions[bot] d1dc40b3cc Merge pull request #1126 from Donkie/weblate
Translations update
2026-08-27 05:51:07 +00:00
Samuel Wang 92be2975b8 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client LEGACY
2026-08-27 07:46:37 +02:00
darkk-k1 a62e4ce6c1 Translated using Weblate (Polish)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: darkk-k1 <softdev@darkabo.eu>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client LEGACY
2026-08-27 07:46:37 +02:00
Julia 0a3404cefe Translated using Weblate (Polish)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Julia <juliazero2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client LEGACY
2026-08-27 07:46:37 +02:00
Samuel Wang c2002e43f7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (598 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hans/
2026-08-27 07:46:19 +02:00
github-actions[bot] 85e3ac7163 Merge pull request #1123 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-26 16:16:46 +00:00
Samuel Wang 33aa3de515 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 28.4% (170 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hans/
2026-08-26 17:51:02 +02:00
Julia e238592e4d Translated using Weblate (Polish)
Currently translated at 100.0% (598 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-08-26 17:51:00 +02:00
Micha 107a134da8 Translated using Weblate (German)
Currently translated at 100.0% (598 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-26 17:50:58 +02:00
github-actions[bot] 3cf7a9fa61 Merge pull request #1121 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-26 09:38:35 +00:00
Zack zhou (WeAPoN.Z) 1e5b28ab7d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 26.0% (156 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hans/
2026-08-26 11:33:46 +02:00
Niko11111 035922b49f style(tags): give the last-seen offer the weight of an action
It sat in the same quiet link style as the phone reader, which is an
alternative route rather than the thing to press. Same 600 as a UID gets
elsewhere in the tag UI, and no wrapping between verb and object.
2026-08-26 08:40:39 +02:00
Niko11111 eced7f22db feat(tags): offer what the reader already has in the Add tag dialog
The dialog waits for a fresh tap, which is right for a sticker in your hand and
awkward for a reader that already holds the tag. On a scale the spool is on the
pad to be weighed, so filling in the field means lifting it off and putting it
back; from the user's side that looks like the dialog not working.

With the last UID now in the registry, the dialog can simply say what the reader
it listens to last read, and offer it. Paired to one reader, that reader answers
or nothing does. Listening to all of them, the most recently seen one wins,
which is the one the person most likely just used. The offer disappears once the
field already holds that UID, so it never sits there as a no-op.

Nothing changes for the tap: it still fills the field the moment it arrives.
2026-08-26 07:34:17 +02:00
Niko11111 4c2febbba6 feat(tags): remember the last UID each reader reported
The Add tag dialog waits for a fresh tap. That is fine when the tag is a
sticker in your hand, and awkward when the reader is a scale: the spool is
sitting on the pad to be weighed, and filling in the field means lifting it
and putting it back. A reader also cannot help here, because it has no reason
to report a tag that has not moved.

The registry already tracks each reader by id, name and last_seen, all in
memory. Keeping the last UID beside them is enough for a client to offer what
is on the reader right now, with no back channel to the device, no protocol
change, and nothing to update on the agent side - every existing ESPHome
reader gets this for free.

One UID, not a history: this answers "what is on the reader", not "what has
been". It lives and dies with the registry entry, so it is gone on restart
like the rest of it, and no scan is written anywhere.
2026-08-26 07:34:17 +02:00
github-actions[bot] e37045dcd4 Merge pull request #1119 from Donkie/weblate
Translations update
2026-08-25 21:07:10 +00:00
Micha f83b9d9ea4 Translated using Weblate (German)
Currently translated at 97.9% (287 of 293 strings)

Co-authored-by: Micha <hoagman@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client LEGACY
2026-08-25 23:00:17 +02:00
github-actions[bot] 0b3d7ba140 Merge pull request #1118 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-25 21:00:03 +00:00
Micha b6217c4b30 Translated using Weblate (German)
Currently translated at 93.9% (562 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-25 22:55:33 +02:00
Donkie 09953643dc Merge pull request #1116 from Donkie/fix-1115-scan-debounce-match
Key the tag scan debounce on the resolved match
2026-08-25 19:53:03 +02:00
Donkie a6512da598 Key the tag scan debounce on the resolved match
The relay debounced on (uid, reader_id) and time alone, so a scan whose match
had changed looked like a duplicate of one that resolved to nothing. An agent
that taps an unlinked tag, links it in its own UI, then re-reports the scan had
its correction swallowed for the whole 3 s window, leaving every subscriber that
trusts the broadcast still showing an unknown tag.

The key now includes matched_spool_id, so a repeat is only a repeat while the
scan still resolves to the same thing. Repeats of a still-unlinked tag keep
collapsing on (uid, reader, None), which is the reason the debounce exists.
should_broadcast takes the match as a required argument rather than an optional
one so a future call site cannot quietly drop it again.

Fixes #1115
2026-08-25 17:23:50 +02:00
Donkie 6da0902c8e Merge pull request #1113 from itCarl/fix/dashboard-spool-initial-weight
fix: dashboard total weight displayes filament weight instead of actual spool weight
2026-08-25 16:33:29 +02:00
github-actions[bot] 6bcda3d307 Merge pull request #1114 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-24 18:11:23 +00:00
Micha 9dd0f73be1 Translated using Weblate (German)
Currently translated at 83.7% (501 of 598 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-24 20:06:20 +02:00
Maximilian Mewes 5171330c44 fix: dashboard total weight displayes filament weight instead of spool weight 2026-08-24 09:56:10 +02:00
Donkie 009f9e3bae Merge pull request #1111 from Donkie/github-sponsors-button
Replace Ko-fi buttons with GitHub Sponsors
2026-08-23 18:09:36 +02:00
Donkie a039095360 Replace Ko-fi buttons with GitHub Sponsors
Both frontends and the README now point at github.com/sponsors/Donkie.

The GitHub-suggested iframe embed is not usable here: Spoolman is often
run on a LAN with no internet access, so it would render as an empty box,
and it cannot be translated or themed. The existing buttons keep their
shell and get an inlined Octicon heart plus a translated label instead.

Only the en locale source is touched; the new "sponsor" key goes out to
the other locales via Weblate.
2026-08-23 17:38:31 +02:00
Donkie 205de5f0ce Update FUNDING.yml 2026-08-23 17:19:51 +02:00
Donkie 05a1b12e33 Merge pull request #1110 from Donkie/dependabot/npm_and_yarn/client/multi-e855e31deb
build(deps): bump brace-expansion in /client
2026-08-23 17:19:02 +02:00
dependabot[bot] f458995104 build(deps): bump brace-expansion in /client
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 2.1.1 to 2.1.4
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.1.1...v2.1.4)

Updates `brace-expansion` from 1.1.15 to 1.1.18
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.1.1...v2.1.4)

Updates `brace-expansion` from 5.0.7 to 5.0.9
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.1.1...v2.1.4)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.1.4
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-23 10:17:19 +00:00
Donkie 9f048a34c8 Merge pull request #1096 from Donkie/worktree-nfc-design-docs
Identify spools by NFC/RFID tags
2026-08-23 12:14:03 +02:00
Donkie ba891d5830 client_v2: let a scanned tag keep the view you were in
Auto-navigate rewrote the URL to a bare `/?sel=spool:N`, which since the filter
menu landed means a tap throws away the grouping, sort and filter chips you had
set up -- and lands on the shipped default view rather than the remembered one.

openSearchResult is the navigation a picked search result already gets, and a
tag scan asks the same question: on the Library it merges the selection into
the current view, elsewhere it opens the Library on just that spool. The
inspector resolves a selection by id on its own, so the spool still opens when
the active filters exclude it from the list behind it.
2026-08-23 10:07:02 +02:00
Donkie f841da6561 Point SpoolTag.from_db at the class the table actually has
d43552b renamed the ORM class from SpoolTag to Tag and missed this annotation.
It is evaluated when the class body runs, so importing spoolman.api.v1.models
raised AttributeError -- the server could not start and every test in the tree
failed to collect.
2026-08-23 10:07:02 +02:00
Donkie 931e3c9b3b Merge remote-tracking branch 'origin/master' into worktree-nfc-design-docs
Master split filter application in two: the filters a filament can answer on
its own (_apply_filament_filters) now serve both the spool query and the new
include_empty query, which selects FROM the filaments so that a filament with
no spools still produces a group.

The tag filter stays where it was, in _apply_spool_filters, and the comment
there now says why it cannot move: a tag is linked to a spool, so a filament
on its own can no more answer "which tag is on you" than it can answer "which
shelf are you on". Nothing routes a tag filter into the filament-driven query
-- it never calls this builder, and the group endpoint takes no `tag` -- so
there is no silently-dropped filter to guard against.
2026-08-23 10:06:30 +02:00
Donkie 9775c2c42e Merge pull request #1097 from Donkie/client-v2-issue-1080-rest
client_v2: sort direction, out-of-stock filaments and truncation tooltips
2026-08-23 09:04:47 +02:00
github-actions[bot] a9e50559bb Merge pull request #1109 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-22 17:08:31 +00:00
Micha 91d5dc8f42 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s 8528847957 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s 5799fac1a6 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s ce1c781ae7 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s dc72f3d5eb Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s aa5821f005 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:36 +02:00
cedrik-s 13f7667bf4 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:35 +02:00
cedrik-s ed732170d1 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:35 +02:00
Micha 8465481ab3 Translated using Weblate (German)
Currently translated at 70.9% (384 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-22 19:04:35 +02:00
github-actions[bot] 9935d74886 Merge pull request #1107 from Donkie/weblate
Translations update
2026-08-21 19:59:59 +00:00
rupefelo 35d10f899d Translated using Weblate (Portuguese)
Currently translated at 99.6% (292 of 293 strings)

Co-authored-by: rupefelo <rupefelo@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client LEGACY
2026-08-21 19:56:01 +00:00
github-actions[bot] 785984e685 Merge pull request #1106 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-21 19:55:49 +00:00
cedrik-s 8b71fb9345 Translated using Weblate (German)
Currently translated at 57.8% (313 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-21 21:51:23 +02:00
Micha 9bb8973af2 Translated using Weblate (German)
Currently translated at 57.8% (313 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-21 21:51:22 +02:00
Donkie 332b3e0fe4 Revert "Give phase 2 somewhere to record a tag's instance identity"
This reverts 9aea271.

The column bought one thing, that phase 2 would need no migration, and
cost something worse: it commits the schema to a shape nobody has run a
decoder against. Width, and above all the choice not to make it unique,
were read off a design document rather than off working code, and
goeland86's codec in #880 computes an effective_instance_uuid that may
not mean the same thing. Getting that wrong leaves us altering a
populated column on four databases, which is the exact operation the
"settle the shape before release" rule exists to avoid.

That rule earns its keep for target_type, target_value and filament_id,
where retrofitting a discriminator or a nullability change is painful.
Appending one nullable column later is not painful, so phase 2 can add
it when there is something to write into it.
2026-08-20 19:15:35 +02:00
Donkie 8d9eb7395d Merge pull request #1105 from jpapiez/upstream-chore-prettier-eol
chore(client): let prettier respect the working tree's line endings
2026-08-20 19:12:15 +02:00
Donkie 413254b79a Merge pull request #1104 from jpapiez/upstream-fix-backup-connections
fix(database): close the sqlite connections used to write a backup
2026-08-20 19:11:56 +02:00
Jeff PapiezandCopilot App 21c7b98844 Close the sqlite connections used to write a backup
A sqlite3.Connection used as a context manager is a transaction manager: it
commits or rolls back on exit, it does not close the connection. Both handles
stayed open past the `with` block in Database.backup().

backup_and_rotate() then unlinks and moves the .pending file it just wrote.
POSIX allows unlinking a file with open handles, so this was invisible on
Linux, but on Windows it raises WinError 32 and breaks both the scheduled
midnight backup and the manual backup endpoint. It also leaked a pair of
connections per backup on every platform.

Wrap both connections in contextlib.closing instead. Connection.backup()
commits on the destination internally, so nothing is lost by dropping the
transaction-managing context manager.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-19 21:50:54 -07:00
Jeff PapiezandCopilot App 97758f2c4c Let prettier respect the working tree's line endings
The root .gitattributes sets `* text=auto`, so a Windows checkout gets CRLF in
the working tree. Prettier defaults to `endOfLine: "lf"` and therefore reports
every text file as mis-formatted -- 163 files in client_v2 alone -- which makes
`npm run lint` and `lefthook run ci` unusable locally. The pre-commit hook is
worse: it runs `prettier --write .` with stage_fixed, so any commit made from
Windows rewrites and stages the whole client tree.

Set `endOfLine: "auto"` in both client configs. Git still normalizes to LF in
the index and in the repo thanks to `text=auto`, and CI runs on ubuntu-latest,
so LF in version control is unaffected; prettier just stops fighting the
checkout. This is Prettier's documented fix for this scenario.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-19 21:50:44 -07:00
Donkie 9aea271ee5 Give phase 2 somewhere to record a tag's instance identity
OpenPrintTag derives an instance_uuid from the brand uuid plus the tag's
hardware UID, and phase 2 needs it stored so that rescanning a tag updates
the spool it already created instead of making a second one.

Added now, into the migration that has shipped to nobody, because the
table is the expensive half to change once it is populated on four
databases. Nothing writes the column yet, which is the same position
filament_id is already in.

Indexed for that lookup and deliberately not unique. uid already enforces
that one physical tag identifies one thing, and copying a tag's payload
onto a second sticker can legitimately put one instance identity on two
rows pointing at the same spool, which a unique constraint would reject.
2026-08-19 22:47:37 +02:00
Donkie 6ab045cde8 Merge remote-tracking branch 'origin/master' into client-v2-issue-1080-rest
# Conflicts:
#	client_v2/src/lib/components/library/ListToolbar.svelte
2026-08-19 20:38:38 +02:00
Donkie 90492ff75d client_v2: make the no-spools filaments an opt-in view
Listing every filament you own no spools of turned out to be the wrong default.
Finding a spool to print with is the daily job and those rows cannot serve it,
and how many of them there are scales with the size of a user's catalogue -- a
no-op for one library and a tripling of another. Off by default, one click to
turn on, remembered afterwards: a missed feature beats a primary view that is
permanently worse until you find the switch.

The toggle sits beside Show Archived and shows a dismissible chip when on, and
is offered only while grouping by filament, since there is nowhere else to put
the empty groups.

Remembered through viewPrefs, which until now deliberately stored nothing that
hides rows -- a hidden filter restored days later reads as missing data. This
one only ever adds rows, so it cannot cause that confusion; the comment there
now says which side of the line it falls on and why. Entries written before the
flag existed keep working: a missing field means off rather than a corrupt entry
that would throw away the grouping the user picked.

The badge now reads "No spools" rather than "Out of stock". Spoolman knows what
you have recorded, not what is on your shelf.
2026-08-19 17:20:26 +02:00
github-actions[bot] 1f2670cf81 Merge pull request #1102 from Donkie/weblate
Translations update
2026-08-19 09:44:00 +00:00
github-actions[bot] 565abec742 Merge pull request #1101 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-19 09:38:59 +00:00
Julia afb3094c1b Translated using Weblate (Polish)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Julia <juliazero2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client LEGACY
2026-08-19 11:35:03 +02:00
Julia 96065fbd9f Translated using Weblate (Polish)
Currently translated at 100.0% (541 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-08-19 11:34:54 +02:00
github-actions[bot] 8d65e53e7c Merge pull request #1100 from Donkie/weblate
Translations update
2026-08-18 19:56:58 +00:00
Icezaza Ch 48221c270a Translated using Weblate (Thai)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Icezaza Ch <icezaza.jar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/
Translation: Spoolman/Web Client LEGACY
2026-08-18 19:51:27 +00:00
github-actions[bot] f9c2c3838a Merge pull request #1098 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-18 17:56:20 +00:00
Icezaza Ch bd877c1ec7 Translated using Weblate (Thai)
Currently translated at 100.0% (541 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/th/
2026-08-18 19:51:40 +02:00
Liudas Ališauskas 328c508c39 Translated using Weblate (Lithuanian)
Currently translated at 34.7% (188 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/lt/
2026-08-18 19:51:30 +02:00
Micha 052d3e9eb9 Translated using Weblate (German)
Currently translated at 51.0% (276 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-18 19:51:25 +02:00
cedrik-s 9a98e8ba5d Translated using Weblate (German)
Currently translated at 51.0% (276 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-18 19:51:24 +02:00
Donkie 6dce8717a0 Fold the empty-group query back into find_groups
The first cut of include_empty grew a second 177-line function next to
find_groups that repeated its filters, ordering, pagination and hydration, so
every future change to grouping had to be made twice or silently diverge.

Only the FROM ever differed. Writing the aggregates so they hold for a group
with no spools in it -- COUNT over a column instead of COUNT(*), one explicit
zero for the one SUM -- removes the need for a second set, and with it the
COALESCE that CockroachDB rejected and the CAST that worked around it. What is
left is one query with a branch over how its rows are selected, and one shared
tail.

Spool-level filters are now refused alongside include_empty rather than quietly
producing a contradiction (every filament as empty, or no empty groups at all).
That also leaves the archived rule as the only spool-level condition the
filament-driven query has to carry, in its JOIN.

Group ordering now breaks ties on the grouped column. Every ordering can tie,
and empty groups tie on all four aggregates, so paging could otherwise repeat or
drop a group between pages.
2026-08-18 18:04:34 +02:00
Donkie 5b480f7db7 tests_frontend_v2: catch the tag spec up with the renamed strings
38b77a9 renamed the auto-navigate toggle to "Auto-navigate" and reworded
the pairing prompt to "Tap any tag...", but the spec still selected on the
old wording, so three tests could not find the control at all.

Checked every string the spec selects on against en/common.json rather
than the two CI happened to reach first, since fixing one stale selector
would have moved the failure to the next one.
2026-08-18 17:53:55 +02:00
Donkie 1b4beb9593 Merge pull request #1094 from Donkie/client-v2-filter-menu
client_v2: filter menu colours, materials, checkmarks and multi-select
2026-08-18 17:49:33 +02:00
Donkie 33ad8d7173 Show filaments you have no spools of
Grouping by filament aggregates over spools, so a filament you own no spools of
could never produce a group and simply vanished from the library. A filtered
view then looked fully stocked when it was the opposite, and the thing you
needed to re-order was the one thing not on screen (#1092).

GET /api/v1/spool/group takes include_empty, which inverts the query: spools are
aggregated per filament in a subquery that the filament list is LEFT JOINed
against, so filaments with no matching spools survive with their aggregates
folded to zero. Which side of that join a filter goes is the whole subtlety, and
the docstring spells it out - spool-scoped conditions have to stay in the
subquery or they delete the very rows the flag exists to keep.

The client asks for it while grouping by filament and every active filter is one
a filament can answer on its own. "Which of these are on Shelf A" has no answer
for a filament that is nowhere, and including them there would bury the shelf
under the whole catalogue. Such a group renders as an out-of-stock row: no
disclosure, no fetch, and a marker in place of the zeroes it would otherwise
report.

The group header also picks up the truncation tooltip added in the previous
commit. Its title sits under a link stretched across the whole row, so the
pointer never lands on the text itself and the tooltip has to hang off the row.
2026-08-18 17:33:29 +02:00
Donkie 713cb51f09 client_v2: reveal truncated text on hover
Text cut off with an ellipsis gave no way to read the value behind it, and that
value — a filament name, a location, a manufacturer — is usually the whole
reason the row is on screen (#1093).

A shared action measures the element and gives it a title only while the text
really is cut off, so rows that fit stay quiet instead of popping a redundant
tooltip under every pass of the pointer. It re-measures on resize and on text
changes alike: the list pane is resizable, and a live update can lengthen a name
without the column moving at all.

Applied to the spool rows, the unused-spool rows, the search results and the
dashboard chips.
2026-08-18 17:32:01 +02:00
Donkie fa39c6d08e client_v2: flip the sort direction from the chip's own arrow
The sort chip showed the direction but opened the menu wherever you clicked it,
so reversing an order meant reopening the dropdown and finding the field that
was already selected (#1091).

Split it into two controls sharing one frame: the field label still opens the
menu, the arrow only flips the direction. A divider and a hover of its own mark
the arrow as separate, and both halves stretch to fill the chip so the 44px
mobile tap target survives the split.
2026-08-18 17:31:38 +02:00
Donkie 61e2c0c27c client_v2: keep the frontend suite in step with the filter menu
Turning filter values into checkboxes that leave the menu open broke the
`addFilter` helper in two ways, and with it the grouping test that uses it
to pin the list down to its own spools.

An explicit `role="menuitemcheckbox"` replaces a button's implicit role, so
the helper's `getByRole("button")` no longer matched a value row at all;
and having picked one, the helper handed back a toolbar with the menu still
covering it, which the next call could not open through. It now asks for
the role the row actually has, and closes the menu itself, restoring the
postcondition every caller was already relying on.

The row's accessible name needed a real space rather than the margin that
had been standing in for one: without it a filament read as one
run-together word, "Galaxy BlackPLA".
2026-08-18 17:29:07 +02:00
Donkie 83ea5001fd Keep the NFC design notes out of the source tree
They are working notes and a wiki draft, not something the repo needs to
carry. The user-facing half is now published at
https://github.com/Donkie/Spoolman/wiki/Tag-scanners, with the diagram
source alongside it in the wiki repository.
2026-08-17 21:55:06 +02:00
github-actions[bot] 6fd64f9ed4 Merge pull request #1095 from Donkie/weblate
Translations update
2026-08-17 19:44:29 +00:00
Icezaza Ch 2edd96dbb0 Translated using Weblate (Thai)
Currently translated at 96.5% (283 of 293 strings)

Co-authored-by: Icezaza Ch <icezaza.jar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/
Translation: Spoolman/Web Client LEGACY
2026-08-17 21:40:22 +02:00
Liudas Ališauskas 61e1b62a38 Translated using Weblate (Lithuanian)
Currently translated at 99.3% (291 of 293 strings)

Co-authored-by: Liudas Ališauskas <liu2kas3@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lt/
Translation: Spoolman/Web Client LEGACY
2026-08-17 21:40:22 +02:00
Donkie 7e2e98b30a design: correct two claims the shipped relay made false
The phase 1 doc still said 'not yet implemented', and pointed at a
TagsSection path the frontend did not use.

More worth fixing: both documents offered an unguessable reader_id as a
weak secret for people who care. The reader registry that shipped
alongside undoes that entirely, since GET /api/v1/tag/reader hands every
recently-seen reader id to any caller by design, so the picker can list
them. Offering obscurity that the same API gives away is worse than
offering none.
2026-08-17 21:36:48 +02:00
Donkie 6d97cd34b3 design: rename the diagram boxes to what they are
Tag Reader, Spoolman Server and Web Browser, rather than Reader,
Spoolman and Your browser. The websocket edge says what it does.
2026-08-17 21:36:40 +02:00
Donkie 5624a0a605 client_v2: pick several filter values without reopening the menu
Narrowing a library to three filaments meant opening the filter menu,
stepping into Filament, picking one, and doing all of it again twice
more. The menu closed on every pick, and nothing in it said which values
were already on — the chips that say so sit in the toolbar, behind the
menu that covers them.

A value row now carries a checkbox and its state, the same way the "Show
archived" row already does, and picking one leaves the menu open. The two
go together: once the values are marked, the list reads as a multi-select,
and closing after a single pick is the surprising half. Escape and a click
outside still close it, and picking a filter property or a date range still
does too, since neither is a set.

Nothing has to be preserved by hand for this to work. Each toggle is a
query-only navigation that keeps the component mounted, and it already
asked to keep focus and not to scroll, so the search box stays focused and
the list stays where it was.

The Ctrl-modifier the issue suggested is deliberately not implemented: a
checkbox list that stays open needs no hint text to explain itself.

Closes #1090
Closes #1089
2026-08-17 21:00:17 +02:00
Donkie 6e289d76ca client_v2: show colors and material in the filament filter
The filament filter listed nothing but a label, which is the manufacturer
and the name. That is not enough to pick from: the colour is the thing
you actually recognise a filament by, and two filaments may legitimately
carry the same name in different materials, leaving the menu offering two
entries that read identically.

Each option now carries whatever it has beyond its label, so the menu
draws a swatch and the material where they exist and stays a plain list
of names where they don't (locations, lot numbers). The material sits
with the label rather than off at the row's right edge, since it is there
to qualify the name it follows. None of it costs a request: the option
list is built from whole filaments already fetched.

The search box narrows on the material too. It is on screen, so a query
that visibly matches a row has to keep it — and since terms match in any
order, "prusament petg" now finds one filament out of two same-named
ones.

Closes #1087
Closes #1088
2026-08-17 20:59:29 +02:00
Donkie 5b39b2848f design: give the diagram boxes a title and a subtitle
Each box is now a container whose label is the title, pulled inside with
label.near, over a borderless child holding the subtitle.

The obvious route, an |md| label, is a trap and the header comment says
so at length. d2 clips md text to a computed box in both dimensions and
no width or height will grow the clip region, so subtitles lost their
tails silently: "which spool has this tag UID?" rendered as "which spool
has this". It hides well, too, because the clipping is done by the
browser against a <foreignObject> and the full string stays in the SVG,
so grepping the file finds text that nobody can read.

The tag stays a plain node. It has nothing to put under its title, and
container padding is not adjustable, so opening one would have cost a
third of the height for no content.
2026-08-17 20:02:19 +02:00
github-actions[bot] 99932ceda8 Merge pull request #1084 from Donkie/weblate
Translations update
2026-08-16 16:57:36 +00:00
github-actions[bot] fbaa83eead Merge pull request #1083 from Donkie/weblate
Translations update
2026-08-16 16:53:33 +00:00
Dr_Perry_Coke d2db504cc5 Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-08-16 18:53:29 +02:00
github-actions[bot] 74a86683b3 Merge pull request #1082 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-16 16:53:14 +00:00
Dr_Perry_Coke 2434b424db Translated using Weblate (Russian)
Currently translated at 100.0% (541 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/
2026-08-16 18:48:22 +02:00
Dr_Perry_Coke 678213f67e Translated using Weblate (Russian)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-08-16 18:48:06 +02:00
Donkie 21e0b71efe design: draw the tag scan flow instead of spelling it in ASCII
The picture now also shows the reply to the POST, which the ASCII block
had no room for and which is the half that drives an LED.

Kept theme-independent on purpose. d2's --dark-theme bakes a
prefers-color-scheme query into the SVG, but it only survives if whatever
embeds the file leaves the stylesheet intact, and a stripped one would
leave dark arrows on a dark page. Every colour is pinned instead, over a
white canvas, so it renders the same wherever it lands.
2026-08-15 11:36:03 +02:00
Donkie 6a786bac88 design: draft the Tag scanners wiki page
The client already links Settings to the wiki page at
github.com/Donkie/Spoolman/wiki/Tag-scanners, which does not exist yet.
This is its source, kept in the tree so it can be reviewed and corrected
alongside the code it documents.

Everything in it was checked against a running instance rather than read
off the source: the scan/link/unlink/lookup responses and status codes,
the two websocket pools and their isolation from the entity streams, the
debounce, and the derived reader id. Both ESPHome examples compile for
ESP32 on ESPHome 2026.7.4.
2026-08-15 11:10:37 +02:00
Donkie 38b77a9b14 Updated some strings 2026-08-15 10:55:36 +02:00
Donkie fb1567a9b0 client_v2: say what the scanner settings actually do
Nothing in this section sets a scanner up -- that is hardware, and the link
above it goes to the wiki for it. Every control here answers one question:
when a tag is scanned, does this screen jump to its spool, and which reader is
it listening to. "Tag scanner" as a heading promised the former and delivered
the latter.

So the section is "Scanned tags", the reader row talks about which scans open
spools here rather than about listening in the abstract, and "Listen to all"
becomes "Use any reader" -- the same thing said in terms of the outcome rather
than of the plumbing. The auto-navigate description drops its first sentence,
which only repeated the label it sits under, and spends the room on the limits
instead.

The e2e assertions that named this copy now match on the clause that
distinguishes the two states rather than on whole sentences, since prose gets
reworded and the assertion is about which state is showing.
2026-08-15 10:25:40 +02:00
Donkie 01c7ff65be client_v2: stop keeping a second copy of a reader's name
Pairing by tapping a tag showed "desk" where pairing with the same reader from
the recently-seen list showed "Desk reader". Not a display bug: the client was
persisting its own copy of the reader's name into localStorage at pairing time,
so there were two sources of truth for something the server owns and that
changes without us.

They drift by design. The server's registry keeps the last name each reader
gave (scanrelay.py:98, deliberately, so a trimmed-down agent doesn't blank a
label). A scan event carries only the name *that* scan sent. Pair from the list
and you read the registry's remembered name; pair by tapping a scan that
carried none and you stored null and fell back to the id. Same reader, two
answers.

Only the reader id is persisted now -- that is the durable half, and the only
thing pooling actually depends on. Names are read: merged from the registry and
from scans as they arrive, held in memory because they mirror state the server
also holds in memory, and falling back to the id when nobody knows a name,
which is the honest answer after either end restarts.

Verified against the reported scenario -- a reader that sent a name once and
then stopped -- pairing both ways and comparing the label.
2026-08-15 09:50:31 +02:00
Donkie d43552bd26 Generalise the tag table beyond spools before it ships
A tag maps a UID to "the thing to bring up", and only some of those things are
spools -- or rows at all. Locations are the case that settles the shape: a
location in Spoolman is a String(64) on Spool, not a table, so a tag meaning
"show me Shelf A" can never be a foreign key.

Getting this wrong is asymmetrically expensive. Renaming the table, dropping
spool_id's NOT NULL and adding a discriminator all mean altering a populated
table on four databases; adding an endpoint or a target kind later is additive
and free. The migration has shipped to nobody -- it is not on master and
v0.26.1 predates it -- so the schema is settled now and the unshipped revision
is rewritten in place rather than chased with a second one.

spool_tag becomes tag:

  uid           unchanged, still globally unique -- one physical tag, one thing
  target_type   which kind of target is in force, rather than leaving readers
                to infer it from whichever column is non-null
  spool_id      nullable now; targets that ARE rows keep real foreign keys, so
  filament_id   the database still cascades deletes and refuses orphans
  target_value  targets that are not rows; a new kind of this sort costs no
                migration at all, only a new target_type string

Exactly-one-target is enforced in database/tag.py rather than by a CHECK: no
migration in this tree uses one, and MySQL below 8.0.16 parses and silently
ignores them, so a constraint here would be real on three databases out of
four. Single write path beats a constraint that is only sometimes a constraint
-- the same argument, in the same module, as normalize_uid.

filament_id gets a column and a foreign key but no ORM relationship: nothing
writes filament tags, and a selectin collection on Filament would add a query
to every filament listing for rows that cannot exist. ON DELETE CASCADE holds
integrity until the feature that needs the relationship arrives.

No API surface changes. POST /spool/{id}/tag, ?tag=, the spool response's tags
and matched_spool_id are all exactly as they were; the reads are just explicit
now about wanting a spool rather than assuming the tag they found is one.
TagConflictError's spool_id becomes optional for the same reason, which the
client already handled -- it only offers "move it here" when there is an id.

Proven on sqlite, postgres, mariadb and cockroachdb: 526 integration tests each.
2026-08-15 09:35:13 +02:00
Donkie ff2e3ec31a client_v2: keep scans off the pages you are configuring
Pairing a reader is done by tapping a tag on it, and that tap is delivered to
the auto-navigate subscription as well as to the settings page waiting for it.
So setting a scanner up navigated you away from the screen you were setting it
up on, the instant it worked -- and the same would happen to anyone mid-edit
when someone taps a tag in the next room.

Scans now only act on the Library and Dashboard: the pages where being moved to
a spool is what you wanted. Settings and the label designer are work in
progress, and neither navigates nor toasts. Matched on route id rather than
pathname, since an instance under SPOOLMAN_BASE_PATH serves the library from
somewhere that is not "/".

Also: the paired reader is now named in a chip beside the buttons instead of
inline in a sentence. Ids are routinely a bare "desk" or an "ip-192-168-1-50"
derived from the address, and "Listening to desk only." reads as a typo rather
than as the name of a device. The raw id stays on hover for when a friendly
name hides it.

The scanner section's explanatory paragraph is replaced by a link to the wiki.
How a reader reaches Spoolman needs wiring and firmware, which is an article,
not a caption.
2026-08-15 09:16:34 +02:00
Donkie 5144b64a8a scripts: add a fake tag reader
The reader-to-Spoolman contract is one ordinary HTTP POST, which means a
complete conforming reader fits in a hundred lines of stdlib and needs no NFC
hardware at all. That is worth having as a tool: pairing, auto-navigate and
filling the link dialog are otherwise only reachable by someone who owns an
ESP32 and has already flashed it.

Interactive by default, because the thing you want while clicking around the
UI is to tap the same tag again, or a new one, without retyping a command.
Blank input re-taps the last UID -- which is also how to watch the server's
de-duplication behave, since it answers every repeat while broadcasting only
the first, exactly as it does for a real reader re-reading a tag that is
sitting on it.

Writes nothing and deletes nothing, unlike its neighbours in this directory:
a scan is an event, not a record.
2026-08-15 09:05:17 +02:00
Donkie 9d77b08114 design: record what the NFC frontend decided, and two traps it hit
The handoff's four open questions are answered by the code now, so it says
which way each went and why rather than leaving the next reader to infer it
from the diff.

Also two corrections worth not rediscovering: Playwright for client_v2 lives
in tests_frontend_v2/, not the client_v2/e2e/ the design doc names (that
directory is only the a11y audit); and incrementing a $state counter from
inside an $effect makes the effect depend on its own write, which is how the
tag dialog's suppression hold first came out -- a reactive loop that presented
as a flood of "WebSocket ... Insufficient resources".
2026-08-15 08:39:18 +02:00
Donkie b4292398f5 tests_frontend_v2: cover tags, and stop assuming a filament is on page 1
Six specs for the tag feature. They are a reader: the contract a scanner
implements is one ordinary POST, so linking, tap-to-fill, moving a tag off
another spool, auto-navigate, the unknown-tag report and pairing are all
drivable without NFC hardware. Web NFC is the one path with no automated
cover -- there is no way to present a physical tag to headless Chromium --
so what is asserted instead is that its control is absent where it cannot
work, which is the failure mode that would otherwise reach a desktop user.

Two things the specs have to be careful about, both documented in place: the
server broadcasts an identical (uid, reader_id) only once every few seconds,
so polling invents a new reader id per attempt rather than having its retries
swallowed; and tags never expire, so UIDs are minted per run rather than
hard-coded, since the specs turn on whether a UID is free or taken.

The translucency spec found its filament by looking for its group header in
the library, which only works while the instance holds fewer than the 20
groups a page shows. Every spec adds a filament, so that assumption was going
to expire on its own -- these six merely get it there sooner. It now goes
through search, like the specs that already had to.
2026-08-15 08:39:09 +02:00
Donkie 9ec5181d4c client_v2: link NFC tags to spools, and act on a reader's scans
The client half of design/nfc-phase-1-tag-identity.md, against the API that
landed in de533bc.

A spool's inspector grows a Tags section listing what is linked to it, with
adding as a quiet section action rather than another button in a header that
already carries the spool's primary actions. The dialog behind it takes a UID
from whichever source is to hand -- a reader tapping the tag, this phone over
Web NFC, or typing -- and resolves it as it settles, so "already on spool #12"
arrives before you commit rather than as a rejected submit. That case is
offered as one button that moves the tag, because the alternative is sending
the user to find the other spool and unlink it by hand.

The relay client is deliberately not part of live.ts. That module is one socket
per entity resource, each feeding the inventory cache, and a scan is not an
entity: it has no id, nothing to cache it under, and its URL carries a reader
id where live.ts's carries a resource name. The connection behaviour is worth
sharing, so the backoff, the wake-on-online/visible listeners, the keepalive
and the 401 probe are copied rather than generalized over both.

Which reader a browser listens to is settled by walking over and tapping a tag
on it: subscribe to every reader, take the id off the first scan, narrow to
that one. No codes, no server state, and the binding is self-evidently right
because the user made it with their hands.

Auto-navigate is off by default and subscribes only while it is on, so a
browser not using NFC holds no relay socket at all. Exactly one subscriber, in
the root layout, since two components reacting to one tap is how you get a
double navigation. It stands down while a dialog is open, while the tag dialog
holds it, and while a field is being typed in -- a tag tapped in the next room
must not take a half-typed value with it.

Web NFC is progressive enhancement: absent, not disabled, on desktop, iOS,
Firefox and every plain-HTTP install. A tag's serial is readable without
decoding its contents, so phase 1 needs no NDEF parsing.

Verified with six Playwright specs driving the relay over plain POSTs, which is
what the reader contract being ordinary HTTP buys -- the whole feature is
exercised without NFC hardware. Web NFC itself has no automated cover and
cannot; what is asserted is that its control is absent where it cannot work.
2026-08-15 08:38:34 +02:00
Donkie 91dec46936 client_v2: give toasts a neutral kind
Every message was either a green tick or a red warning, which leaves nowhere
to put the ones that are neither. A tag scanned that no spool claims is the
ordinary first step of enrolling one: reporting it as an error puts a warning
on a working flow, and as a success ticks a check at nothing having happened.

The neutral kind needs no styling of its own -- the base toast is already
neutral -- so it is an icon and a longer timer. The timer now follows from
whether a message is a confirmation rather than from whether it is an error:
a confirmation is read at a glance, an instruction has to be read properly.
2026-08-15 08:37:30 +02:00
Donkie d67a2afbe4 Merge remote-tracking branch 'origin/master' into worktree-nfc-design-docs 2026-08-14 23:14:49 +02:00
Donkie 64e70076f1 Make BackupResponse.path required, as it already always is
The field is typed str with default=None, which is a default the type does not
allow. Nothing relied on it: the backup endpoint returns 500 when there is no
path and only constructs the model with one. Dropping the default makes the
documented response schema match what is actually sent.
2026-08-14 23:09:45 +02:00
Donkie ac7865ecbc Trim the tag endpoint's docstring and parameter descriptions
These descriptions ship in the public OpenAPI docs, where an integrator wants
the rule rather than the reasoning. Says the UID is case insensitive and that
separators are optional, and shows both spellings as examples, instead of
explaining why normalization exists.
2026-08-14 23:09:45 +02:00
Donkie 419a4763d5 design: revise NFC phase 1 with the scanner contract and build plan
Replaces the thin "client work" section with what a scanner actually is: the
full device contract, what the hardware must do and what to buy, complete
ESPHome and nfcpy reference firmware, and a table of the projects that already
POST somewhere on a tag read and would get shorter by switching to this.

Adds an implementation architecture section covering where each piece of code
goes and why there, on both sides.

Softens the host-USB-reader stance from "explicitly rejected, permanently" to
"not in phase 1, and nothing here forecloses it". The scan endpoint is public
HTTP rather than an internal hook, so a first-party host reader can arrive
later as an optional extra, a sidecar, or a standalone agent without any
architectural change.

Drops #884 and the QR-carries-the-tag-id cheap win, leaving #783.
2026-08-14 23:09:36 +02:00
Donkie 3f1d3b4c7b Fix mixed naive/aware clocks in the scan relay, and hand off the frontend
register() kept datetime.utcnow() while should_broadcast() and readers() moved
to datetime.now(tz=utc), so the naive last_seen it stored was compared against
an aware cutoff during pruning: GET /tag/reader raised "can't compare
offset-naive and offset-aware datetimes" as soon as any reader had scanned.

The existing unit tests could not catch it because they inject `now` into every
call. Added one that exercises each method's own clock instead.

Also adds design/nfc-phase-1-frontend-handoff.md: the API shapes the client
will consume, the behaviours it has to get right, two stale paths in the phase
1 doc's frontend plan, and the open UI decisions.
2026-08-14 23:03:05 +02:00
Roman Skurikhin 1507f9b3b8 fix: make SQLite searches Unicode case-insensitive 2026-08-14 20:48:28 +03:00
github-actions[bot] 7f42bd6f02 Merge pull request #1077 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-14 06:50:21 +00:00
KnightArch fd9b06d33c Translated using Weblate (Portuguese (Brazil))
Currently translated at 31.2% (169 of 541 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pt_BR/
2026-08-14 08:46:28 +02:00
Donkie de533bc74d NFC phase 1 backend: tag identity, lookup, and the scan relay
Implements the backend half of design/nfc-phase-1-tag-identity.md. Format
agnostic: it keys only on a tag's hardware UID, so it works with blank NTAG
stickers, Prusa NFC-V tags, and the Bambu/Creality/Qidi MIFARE tags whose
contents Spoolman can never decrypt.

Answers the question the whole external ecosystem has been working around:
which spool is this physical tag? Every project so far invented its own way to
smuggle a Spoolman id into the tag contents, which fails the moment the tag is
a vendor tag you cannot write to.

Data model: new spool_tag table, additive only, with a unique index on uid.
Many tags per spool, since copying a vendor tag onto a blank sticker leaves one
spool with two UIDs. UIDs are normalized to uppercase hex with separators
stripped, in the database layer rather than a Pydantic validator, so the unique
constraint holds for callers that never arrive over HTTP.

API, all additive:
  GET    /spool?tag=<uid>          exact match on the normalized UID
  POST   /spool/{id}/tag           201, 409 with the conflicting spool id
  DELETE /spool/{id}/tag/{uid}     204
  POST   /tag/scan                 broadcast and return the match
  GET    /tag/reader               readers seen recently
  WS     /tag/scan[/{reader_id}]   the relay

Link and unlink emit the existing spool event, so clients need no new wiring.
Spool responses gain a tags array.

The relay decouples where the reader is from where the user is looking: an
agent POSTs a scan, a paired browser reacts. Scans are ephemeral, never
persisted, and go through a SECOND WebsocketManager instance rather than the
shared one. The shared tree propagates to subscribers at every level, and the
root /api/v1/ pool means "any change", so putting scans there would inject a
novel resource into every existing consumer's stream. An integration test pins
that they never reach the entity websockets.

Repeated identical (uid, reader_id) scans are debounced before the broadcast
only, since readers re-read a tag that is sitting still. The response always
carries the match, so a de-duplicated scan never looks to a device like a
failed lookup.

Migration creates the table and its indexes in one revision, following the
extra-fields tables rather than the doc's suggested split. The DDL/DML split
cockroachdb needs does not apply when there is no data migration, and the 4-DB
suite confirms it.

Verified: 506 integration tests on sqlite, postgres, mariadb and cockroachdb;
263 unit tests; ruff clean.

Frontend, docs, and the #783 action links are not included.
2026-08-13 18:56:35 +02:00
github-actions[bot] 8979f80e74 Merge pull request #1074 from Donkie/weblate
Translations update
2026-08-13 16:56:18 +00:00
Donkie f261dee61b Fix one dead websocket subscriber starving a whole pool
SubscriptionTree.send let a per-socket failure escape the broadcast loop, so
every subscriber after the failing one silently received nothing. A socket can
start closing between the CONNECTED state check and the send, and starlette
then raises "connection is closing"; on the shared tree that means one browser
tab closing could stop Moonraker hearing about a spool.

Each subscriber is now isolated: a write failure drops that socket and the
broadcast continues. Iterate a snapshot too, since dropping mutates the set.

Dropping also had to stop going through remove(path, ...). By the time send()
is walking a node, path is the part of the route still to be traversed, not the
pool the socket subscribed with, so remove() descended into a child and looked
in the wrong set: the dead socket stayed put to fail again on the next event.

Observed in the integration suite as a subscriber missing an event it was
subscribed to, alongside "Failed to send websocket message" on an unrelated
filament broadcast.
2026-08-13 18:56:16 +02:00
Alessandro “Alessandrol” L 422657d18f Translated using Weblate (French)
Currently translated at 99.3% (291 of 293 strings)

Co-authored-by: Alessandro “Alessandrol” L <alessandrolond@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client LEGACY
2026-08-13 18:51:25 +02:00
github-actions[bot] 01be8ddd5b Merge pull request #1073 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-13 16:27:07 +00:00
Alessandro “Alessandrol” L bf265c12c3 Translated using Weblate (French)
Currently translated at 25.4% (137 of 539 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/fr/
2026-08-13 18:22:34 +02:00
Donkie c90cc3acab Merge pull request #1072 from Donkie/client-v2-new-filament-on-change
client_v2: create a filament while changing what a spool holds
2026-08-13 18:22:18 +02:00
Donkie 9423acbc9e client_v2: create a filament while changing what a spool holds
The change-filament dialog could only pick a filament that already existed,
in the catalog or in SpoolmanDB. But the only way to create one anywhere in
client_v2 is the add-spools flow, which always mints at least one spool — so
refilling a shelf slot with a roll nobody had catalogued meant adding a
throwaway spool and deleting it again, handing out exactly the new spool id
the feature exists to avoid (issue #1010).

The dialog now offers the same third path the add-spools flow does. Picking
it swaps the search for the new-filament form and applies as one action:
the filament (and its manufacturer) are created, then the spool is repointed
at it. The spool keeps its number, location, dates and usage history.

The form itself moves to NewFilamentCards, shared with AddSpoolModal so both
dialogs keep the per-record card layout from #1038, with its rules and its
mapping onto the API in lib/filament/draft.ts. The one difference is the
weight/spool weight/price trio: the add-spools flow writes those to the spool
it creates as well, and so keeps showing them in its spool block, while here
they belong to the filament alone.
2026-08-13 18:11:56 +02:00
Donkie 05628eb593 Merge pull request #1071 from Donkie/client-v2-form-required-fields
client_v2: point at missing required fields instead of disabling the add button
2026-08-12 22:27:21 +02:00
Donkie aac4a26cde Merge master into client-v2-form-required-fields
Master reorganized the spool half of the add form into headline / fill /
where / money groups and added the roll-size presets. Kept that layout and
re-applied the required-field wiring on top of it: the data-field hooks, the
per-field touch handlers and the err() calls that decide when an error shows.

FIELD_ORDER follows the new reading order too, so Fill level now comes before
Spool Weight when the form picks which bad field to jump to.
2026-08-12 22:20:39 +02:00
Donkie 2d08061c45 client_v2: require the material when creating a filament
Leaving Material blank sent you to Density instead. Density is only prefilled
for a material Spoolman recognises, so a blank material surfaced as "Density
is required" — an error on the field you didn't fill in because of the field
you did miss, and inside the collapsed Advanced block at that.

Material is now required on this form, so the error lands on the field that
actually needs attention and Advanced stays shut. Typing a material we don't
have specs for still asks for a density afterwards, which is the right order
to be asked in.

Client-side only: the API still accepts a filament with no material, so
existing ones are unaffected.
2026-08-12 20:25:00 +02:00
Donkie aec9b276b2 client_v2: point at the first bad field instead of listing them all
The summary panel above the add button was more machinery than the problem
needed, and it spent five translation strings restating what the inline
field errors already say. Drop it.

Pressing Add still reveals every outstanding error and still opens the
collapsed Advanced block when the problem is hiding in there; it just takes
you to the first bad field and lets the error under it do the talking. The
issue count on the Advanced toggle and the "* required" legend go too.

The change now adds no new translation strings at all.
2026-08-12 20:12:36 +02:00
Donkie 0931b03a80 client_v2: say what is missing instead of disabling the add button
An incomplete add-spool form was answered with a dead Add button, leaving
"which field haven't I filled in?" to be found by eye. Worst case the answer
was invisible: density is required but only prefilled for known materials,
and it lives inside the collapsed Advanced block, so the form could look
complete and still refuse.

Add stays clickable. Pressing it on an incomplete form reveals every
outstanding error at once, lists them above the button as rows that jump to
their field, opens the section hiding one, and puts the caret in the first.
The list stays as a checklist and shrinks as fields are fixed. Per-field
errors now appear once you leave a field rather than immediately, so a
freshly opened form is quiet.

Make the asterisk mean one thing: every required field carries it and
nothing else does. Count was missing one. A "* required" legend says what it
means, and the controls carry aria-required/aria-invalid so it is not
conveyed by a glyph alone.

The extra-fields editor in settings follows the same convention: asterisks
on key, name and choices, and a save error now outlines and focuses the
field it came from.
2026-08-12 19:53:11 +02:00
Donkie 740b1ad808 Merge pull request #1070 from Donkie/client-v2-vendor-extra-fields
client_v2: edit the manufacturer's extra fields when the add flow creates one
2026-08-12 19:51:38 +02:00
Donkie 379354159a Merge pull request #1068 from Donkie/client-v2-searchable-menus
client_v2: make the long toolbar menus searchable
2026-08-11 22:48:02 +02:00
Donkie e9b06d91c7 Merge pull request #1067 from Donkie/worktree-issue-1059-transparency
client_v2: bring back translucent filament colors
2026-08-11 22:43:53 +02:00
Donkie fb723f0af8 client_v2: stop the alpha slider tiling colour under its border
A `background` shorthand resets background-origin to the padding box, so
the slider's gradient was sized to the padding box but painted out under
its 1px border — and backgrounds repeat, so the strip under the border
showed the far edge of the next tile: a hairline of full-opacity colour
down the transparent end. It surfaced wherever the border doesn't cover
that strip exactly, i.e. Firefox at fractional display scaling.

Anchor every layer to the border box, and keep the colour gradient from
repeating at all.
2026-08-11 22:35:15 +02:00
github-actions[bot] 93d5e234b6 Merge pull request #1066 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-11 20:33:27 +00:00
Micha 7a31afaa39 Translated using Weblate (German)
Currently translated at 50.8% (273 of 537 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-11 20:28:29 +00:00
Donkie b2c46e22c7 Merge pull request #1065 from Donkie/client-v2-weight-presets
client_v2: quick-pick roll sizes when adding spools
2026-08-11 22:28:08 +02:00
Donkie 84caec0f81 client_v2: drop the More details disclosure from the add-spool form
Lot number, usage dates and comment are back in the open. The lot number
moves next to Location, since it identifies the spool rather than pricing it.
2026-08-11 22:23:27 +02:00
Donkie 250aa7c308 client_v2: reorganize the add-spool form around what you know at a glance
The spool fields were one grid of interchangeable boxes, so the roll-size
shortcuts had nowhere to go but a second row under the weight field. Order
them by how easily they are answered instead: count and weight share the top
line, with the sizes beside the weight rather than under it; fill level and
location follow; spool weight and price drop to their own row as the numbers
that come off a product page; and lot number, usage dates and comment fold
into a 'More details' disclosure like the filament half above already does.
Net result is a shorter form than before the shortcuts existed.
2026-08-11 22:13:13 +02:00
github-actions[bot] 34a990efcf Merge pull request #1063 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-11 03:14:38 +00:00
Taylan Tatlı 7eaf37fa35 Translated using Weblate (Turkish)
Currently translated at 29.2% (157 of 537 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/tr/
2026-08-11 03:01:01 +00:00
KnightArch f462245622 Translated using Weblate (Portuguese (Brazil))
Currently translated at 26.8% (144 of 537 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pt_BR/
2026-08-11 03:00:56 +00:00
Micha a46caaabf5 Translated using Weblate (German)
Currently translated at 50.6% (272 of 537 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-11 03:00:52 +00:00
Donkie 5deb6f8ca6 client_v2: quick-pick buttons for common roll sizes
The weight field in the add-spool modal now offers 250 g / 500 g / 750 g /
1 kg / 2 kg / 3 kg as one-click shortcuts, with the one matching the current
value marked. Closes #1051.
2026-08-10 20:44:39 +02:00
Donkie 948c95866d client_v2: bring back translucent filament colors
The legacy client could set a filament's alpha channel, and the API and
database have stored an 8-digit #RRGGBBAA code ever since the color_hex
alpha migration, but client_v2 had no way to edit it (#1059).

Each row of the colour editor gets an opacity slider and a percentage
field, both backed by the hex code's alpha byte, so a single- or
multi-colour filament can be marked translucent. Full opacity drops the
alpha channel again, keeping plain colours 6-digit for the API and for
ecosystem consumers, and the native RGB picker no longer wipes an alpha
that is already set.

Swatches layer a translucent colour over a checkerboard so it reads as
transparent rather than as a different, lighter colour.
2026-08-10 19:38:16 +02:00
Donkie 0cc77dccb3 client_v2: make the long toolbar menus searchable
The Library's sort and filter menus grow with the install. Every custom
extra field adds a sort field and a filter property, and a filter's
values are the whole library: every filament, every location, every lot
number. Past a point the only way through them was to scroll.

Each list now carries a search box once it is long enough to earn one,
so the group menu's five fixed entries stay as they were. Matching is a
plain substring test — these lists are on screen while you type, so a
fuzzy match that surfaced "Location" for "lot" would read as a bug —
widened only by folding diacritics and by letting whitespace-separated
terms match in any order, since a filament reads "<maker> <name>".

Escape clears the query, then closes the menu; Enter takes the first
entry still listed. The box takes focus as the menu opens, but only
where the pointer is fine: on a touch device that same focus would
raise the keyboard over the list it filters.

Closes #1045
2026-08-10 19:26:07 +02:00
Donkie 16feb404ba client_v2: edit the manufacturer's extra fields when the add flow creates one
The add-spool form can create a manufacturer, but offered no way to fill its
custom fields, unlike the filament and spool it creates alongside. Show them
in the manufacturer card, and send them with the POST that creates it.

The inputs only appear while the typed name is a new one: matching an
existing manufacturer links that record, and must never rewrite its values.

Fixes #1055
2026-08-10 19:14:34 +02:00
github-actions[bot] 3843785690 Merge pull request #1061 from Donkie/weblate
Translations update
2026-08-10 05:55:58 +00:00
KnightArch a17e351e25 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (292 of 293 strings)

Co-authored-by: KnightArch <knightarch@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client LEGACY
2026-08-10 05:51:26 +00:00
dimquea 9bcaece766 Translated using Weblate (Russian)
Currently translated at 99.3% (291 of 293 strings)

Co-authored-by: dimquea <dimquea@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client LEGACY
2026-08-10 05:51:24 +00:00
Donkie f02ebbc93e design: NFC/RFID tag support, phase 1 and phase 2
Phase 1: format-agnostic tag identity (spool_tag table, indexed UID
lookup, link/unlink API) plus the scan relay and scanner-to-browser
pairing. Phase 2: OpenPrintTag codec, import and export.

Based on an investigation of 9 open issues, 3 discussions, PR #880 and
the surrounding tag-standard ecosystem.
2026-08-09 22:39:22 +02:00
github-actions[bot] 292f8ee305 Merge pull request #1058 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-09 20:01:26 +00:00
Micha 423c06abc8 Translated using Weblate (German)
Currently translated at 35.1% (182 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-09 21:56:39 +02:00
Donkie 7dc17f12da Merge pull request #1054 from Donkie/client-v2-date-filters
client_v2: filter the library by date
2026-08-09 21:56:27 +02:00
Donkie bfabb54c3c Merge pull request #1056 from Donkie/client-v2-add-flow-sections
client_v2: separate manufacturer, filament and spool in the add flow
2026-08-09 21:53:59 +02:00
Donkie dd16672a4a Parse the datetime range grammar in one place
The previous commit claimed the built-in timestamps and the datetime extra
fields share a grammar, then wrote that grammar out twice: once in
add_where_clause_datetime_opt for the typed columns, once inline in
add_where_clause_extra_field for the JSON-stored ones. Two copies of a
grammar drift, which would quietly falsify the thing the change was for.

The `<start>|<end>` split and its "at least one end" rule now live in
split_datetime_range_filter, which both call.

Only the parsing is shared, because only the parsing can be: an extra
field is JSON text in a joined key/value table, reached through a subquery,
while a built-in is a typed column on the row. The comparison that follows
the split therefore stays separate, and the two are not equivalent. The
built-in path parses the bound and compares real datetimes; the extra-field
path compares decoded ISO strings, so an offset or a missing '.000' changes
the answer there and not here. That asymmetry is now written down at both
sites, and tracked in #1057.
2026-08-09 21:52:10 +02:00
Donkie 587521eff6 client_v2: reword the manufacturer section note 2026-08-09 21:45:52 +02:00
Donkie 2c84acfd24 Filter dates with the grammar the extra fields already use
The two commits before this one invented `<field>_after`, `<field>_before`
and `<field>_unset` for the built-in timestamps. That was a third way to
ask a question this API already answers: extra-field filters have taken
`<start>|<end>` for datetimes and `<min>:<max>` for numbers since v0.26.0,
with an empty value meaning "unset", the same as it means for `location`
or `lot_nr`.

Two grammars for one question is bad on its own, but the split was worse
than cosmetic. Someone who learned `extra.opened_on=2025-01-01T00:00:00Z|`
and tried `last_used=2025-01-01T00:00:00Z|` got every spool back, because
an unknown query param is silently ignored.

So the eight parameters become three, named after their columns and
taking the shipped grammar:

    /spool?last_used=2026-08-01T00:00:00Z|      since
    /spool?last_used=|2026-01-01T00:00:00Z      until
    /spool?last_used=<start>|<end>              between
    /spool?last_used=                           never used

Commas OR several of those, and quotes are accepted and ignored, so every
filter on the endpoint is spelled the same way. `_unset` disappears: the
empty value already means unset everywhere else here. No new convention
enters the API, and the next filterable column costs one parameter rather
than three.

Two consequences worth naming. Malformed input is now a 400 with a message
instead of FastAPI's automatic 422, matching the other unparseable filters
on this endpoint. And "has a timestamp" (the old `_unset=false`) has no
spelling any more, because the shipped grammar has none; nothing offered
it in the UI.

The client is unchanged apart from how it builds the request. The chip
value in the app URL keeps its own grammar, since it also carries relative
offsets and "never", neither of which the API takes.
2026-08-09 15:18:46 +02:00
Donkie 43bf140675 client_v2: separate manufacturer, filament and spool in the add flow
Step 2 of the add-spool modal creates up to three records at once, but laid
them out as one flat run of fields, so there was no way to tell which record
a field would land on. The vendor combobox in particular sat among the
filament fields while quietly creating a second entity.

Lay the step out as one labelled block per entity: the manufacturer and the
new filament each get their own card, and the spool fields get a heading of
their own instead of just following on. Weight, spool weight and price are
written to both the new filament and the spool, so say so rather than let
the headings imply otherwise.

Closes #1038
2026-08-09 11:19:40 +02:00
Donkie b6d220a3ef Filter the library by spools that have never been used
A date range cannot express "no date at all": a NULL fails every
comparison, so no bound, however far back it reaches, selects the spools
that have never been used. That state needed a filter of its own.

Backend: /spool and /spool/group take first_used_unset / last_used_unset.
True matches only the spools missing that timestamp, false only those
that have one. Registered gets no such param — the server sets it at
creation and a spool never lacks one.

Client: the chip value `never` sits alongside the range grammar, and
Last Used / First Used offer it under their presets. It is labelled
"Never", deliberately not "unused": the list already calls a spool unused
when no weight has come off it, which is a different claim — a spool can
be unused and still carry a last_used date, or have used weight and no
date at all.
2026-08-09 11:02:26 +02:00
github-actions[bot] 421ad925a7 Merge pull request #1053 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-09 08:56:32 +00:00
dimquea 01f13357b4 Translated using Weblate (Russian)
Currently translated at 75.8% (393 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/
2026-08-09 08:51:30 +00:00
변성운 e5daed84e6 Translated using Weblate (Korean)
Currently translated at 100.0% (518 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-09 08:51:27 +00:00
Sungjun Park bd244e8387 Translated using Weblate (Korean)
Currently translated at 100.0% (518 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-09 08:51:26 +00:00
변성운 d81e43aa56 Translated using Weblate (Korean)
Currently translated at 100.0% (518 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-09 08:51:25 +00:00
변성운 18e31bf4b1 Translated using Weblate (Korean)
Currently translated at 100.0% (518 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-09 08:51:24 +00:00
변성운 dd8e7868be Translated using Weblate (Korean)
Currently translated at 100.0% (518 of 518 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-09 08:51:23 +00:00
Donkie 179ea3b4d0 Filter the library by date range
The library could filter by every discrete property a spool has but by
none of its timestamps, so "what have I used this week" and "what has
been sitting untouched for a year" were questions the UI could not ask.

Backend: /spool and /spool/group take first_used / last_used /
registered `_after` and `_before` bounds, each optional, inclusive, and
resolved against the UTC-naive form the columns store (an offset-aware
value is converted; a naive one is read as UTC). A spool with no
timestamp matches neither end.

Client: the Filter menu gains the three timestamps, each opening presets
(last 24 hours ... older than 1 year) plus a custom range whose empty
end means open. A range lives in the URL as a chip value, `<from>..<to>`,
where an end is a calendar day or an offset back from now. Relative ends
stay relative all the way into the URL and are resolved at request time,
so a bookmarked "last 24 hours" keeps meaning the last 24 hours instead
of drifting into "since the day I made it".
2026-08-09 09:53:42 +02:00
Donkie 64d41c78b3 Merge pull request #1049 from Donkie/dependabot/npm_and_yarn/client/js-yaml-3.15.1
Bump js-yaml from 3.15.0 to 3.15.1 in /client
2026-08-09 08:22:14 +02:00
dependabot[bot] 352d36cb86 Bump js-yaml from 3.15.0 to 3.15.1 in /client
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.0 to 3.15.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.15.0...3.15.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-08 20:50:40 +00:00
Donkie 56c181f634 Merge pull request #1048 from Donkie/deps/batch-1018-1022-1030
build(deps): bump cryptography, fast-uri and postcss
2026-08-08 22:33:18 +02:00
github-actions[bot] 8b00fc0d43 Merge pull request #1047 from Donkie/weblate
Translations update
2026-08-08 18:55:50 +00:00
변성운 2225db0b5f Translated using Weblate (Korean)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: 변성운 <kamyu1537@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/
Translation: Spoolman/Web Client LEGACY
2026-08-08 20:51:26 +02:00
Miloslav Kos 30fa35d38b Translated using Weblate (Czech)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client LEGACY
2026-08-08 20:51:24 +02:00
Donkie 8680805416 build(deps): bump fast-uri to 3.1.5 and postcss to 8.5.26 in /client 2026-08-08 20:23:51 +02:00
Donkie aa83c0c1e9 build(deps): bump cryptography from 49.0.0 to 50.0.0 2026-08-08 20:23:14 +02:00
Donkie 7f19ff0952 Merge pull request #1046 from Donkie/client-v2-numeric-inputs
client_v2: accept decimal commas in every numeric field
2026-08-08 20:10:03 +02:00
Donkie e09d63b443 client_v2: accept decimal commas in every numeric field
Most of the world writes 1,75 rather than 1.75, and `<input type="number">`
only accepts the separator the browser locale dictates: everything else it
reports as an empty value. A user typing a comma therefore watched a weight
or a diameter silently come out blank or, once the separator was dropped,
a hundred times too large. The same input type also accepts "e", "+" and "-"
anywhere in the text, since they are legal exponent syntax, and then reports
the whole value as empty too, so "1e" read back as nothing at all.

Numeric fields are now text inputs with `inputmode="decimal"`, guarded by a
shared `numericInput` action that filters at the keystroke: both separators
go in, letters cannot, and a pasted "12,5 g" is cleaned rather than refused.
Reading is centralised in `parseDecimal`, which takes either separator and
returns null instead of NaN for text that is not a number, so an unset field
stays distinguishable from a real 0. What the parent, and the API, receive is
always canonical.

The typed separator stays on screen: each field keeps a draft of the text and
gives it up only when the value moves somewhere that text no longer means, so
storing 1.5 does not rewrite a typed "1,5" between keystrokes. Arrow-key
stepping, which was native to the old input type, is now the component's own.

Covers the spool/filament/vendor inspectors, the add-spool modal (whose price
box was a bare text input accepting anything at all), extra fields of every
numeric type, the extra-field editor's order box and the low-stock threshold.
2026-08-08 19:42:39 +02:00
github-actions[bot] 41e6c65199 Merge pull request #1043 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-08 08:17:22 +00:00
변성운 d7c2e52a67 Translated using Weblate (Korean)
Currently translated at 69.0% (355 of 514 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-08 08:12:32 +00:00
Sungjun Park 4ad904d270 Translated using Weblate (Korean)
Currently translated at 69.0% (355 of 514 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-08 08:12:32 +00:00
변성운 9b652b5d77 Translated using Weblate (Korean)
Currently translated at 69.0% (355 of 514 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-08 08:12:31 +00:00
Donkie dd13f6ea34 Merge pull request #1042 from Donkie/client-v2-label-export-formats
client_v2: pluggable label export formats, add AML
2026-08-08 10:12:11 +02:00
Donkie 4e3e0e849d tests_frontend_v2: follow the label export rename, cover AML
The export mode button is "Files" now, not "Image files", and the action is
"Save as PNG" / "Save as AML" rather than "Save as Image", so the label spec
timed out clicking a button that no longer exists.

While updating it, split the export step in two and assert both formats:
the PNG carries the layout's resolution in its pHYs chunk (300 dpi = 11811
px/m), and the AML is an LPAPI document embedding the same raster. That makes
the .aml path covered end to end against a real deployment, which is as close
as CI can get without a label printer.
2026-08-08 10:07:47 +02:00
Donkie c321346571 Merge pull request #1041 from Donkie/client-v2-remember-view
client_v2: remember the library's grouping and sorting
2026-08-08 10:07:10 +02:00
Donkie 93f49449a0 client_v2: restore the remembered view by navigating, not by parsing
Folding the remembered grouping into parseLibraryState left the address
bar saying nothing while the list was grouped, and that gap broke the
group control: picking the default grouping serialises back to the very
same bare URL, and an unchanged URL means SvelteKit reuses its cached
load, so the choice never took effect. The load was also no longer a
pure function of the URL, which is what that per-URL cache assumes -
preloading the nav tab cached a redirect built from the old preference
and replayed it.

The page now navigates on arrival to the URL that spells the remembered
view out, replacing the history entry. The load goes back to reading the
URL alone, the view stays linkable, and every distinct view has a
distinct URL again. Only on arrival: a goto is a choice the user just
made and a popstate is Back, and overruling either would be wrong.
2026-08-08 10:01:10 +02:00
Donkie 538c78e7b3 client_v2: remember the library's grouping and sorting
Group and sort live in the URL, so they survive a reload of a library
link but not a trip to Settings and back: every other route links to a
bare `/`, which handed back the shipped view and made a deliberately
chosen grouping something the user re-picked several times a day.

A URL that mentions neither grouping nor sorting now means "however I
had it last", restored from localStorage; one that mentions either still
wins outright, so a shared link shows its recipient what its sender saw.
Only the layout is remembered - filters, search and archived-visibility
hide spools, and one silently restored days later reads as missing data.

Fixes #1036
2026-08-08 10:00:33 +02:00
Donkie bef99d5891 client_v2: stamp exported PNGs with their real resolution
Label designs are authored in millimeters and rasterized at the layout's
DPI, but canvas.toDataURL() writes no pHYs chunk, so every reader falls back
to 96 dpi. A 50x25mm label exported at 300 dpi opened as a ~156x78mm image,
and printing it "at 100%" produced a label three times too big.

Stamp pHYs with the layout's density on the way out, replacing any existing
chunk and inserting after IHDR so it precedes IDAT as the spec requires.
Anything that doesn't parse cleanly as a chunk stream is passed through
untouched — a wrong DPI beats a corrupted download.

Verified independently with Pillow: the stamped files are valid PNGs and
read back as exactly 300 and 203 dpi.

AML is unaffected; it carries the label size in mm in its own header.
2026-08-08 09:36:28 +02:00
Donkie 2f1a2a2b2d client_v2: make label export pluggable and add .aml
The label designer's "image" mode could only ever write PNG. Move the
encoding step behind a small format registry ($lib/labels/export): the
renderer produces one PNG raster per label, and the selected format turns
that into file bytes. Adding a format is now one module plus a registry
entry, with no changes to the render, download or zip path.

Add AML as the second format. It is the label-document format the companion
apps of most cheap thermal label printers read (Phomemo/Aimo, Niimbot and
friends) — XML carrying the physical label size in mm, wrapping our raster
as a single full-bleed base64 image. That size is the part a bare PNG cannot
express, so the app scales to the loaded media instead of guessing at pixels.

AML has no published spec, so the output is modelled field-for-field on real
.aml files written by those apps; aml.test.ts pins the document, page and
image field sets against a Phomemo PM-241-BT template.

The chosen format is saved per design (PrintLayout.exportFormat) and resolved
leniently, so a design naming a format this build lacks falls back to PNG.
2026-08-08 09:23:30 +02:00
github-actions[bot] 633e1fa2b0 Merge pull request #1040 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-08 06:47:39 +00:00
Miloslav Kos 4c1c905a83 Translated using Weblate (Czech)
Currently translated at 82.8% (425 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-08-08 06:43:28 +00:00
Donkie 71c607122f Merge pull request #1039 from Donkie/client-v2-resizable-list
client_v2: make the library list column resizable
2026-08-08 08:43:14 +02:00
Donkie 16e34da538 client_v2: make the library list column resizable
The list was a fixed 470px, which truncated longer filament names with no
way to see the rest. The divider between the list and the inspector is now
a draggable, focusable separator, and the width is remembered per browser.

Fixes #1034
2026-08-08 08:33:43 +02:00
github-actions[bot] 3e0a143966 Merge pull request #1032 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-07 08:09:13 +00:00
github-actions[bot] b315b63ddc Merge pull request #1031 from Donkie/weblate
Translations update
2026-08-07 08:08:13 +00:00
변성운 590b510f20 Translated using Weblate (Korean)
Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: 변성운 <kamyu1537@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/
Translation: Spoolman/Web Client LEGACY
2026-08-07 09:58:03 +02:00
Miloslav Kos 216eba5d26 Translated using Weblate (Czech)
Currently translated at 76.2% (391 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-08-07 07:57:27 +00:00
Donkie f38720314b Bump version to 0.26.1 2026-08-06 18:44:00 +00:00
github-actions[bot] 910f0b079b Merge pull request #1029 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-06 07:32:00 +00:00
변성운 29113812f3 Translated using Weblate (Korean)
Currently translated at 29.8% (153 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-06 09:27:17 +02:00
Miloslav Kos a03229be6f Translated using Weblate (Czech)
Currently translated at 75.8% (389 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-08-06 09:27:16 +02:00
github-actions[bot] 007a0e0ff5 Merge pull request #1028 from Donkie/weblate
Translations update
2026-08-06 06:31:12 +00:00
Miloslav Kos 2d6945f5f6 Translated using Weblate (Czech)
Currently translated at 99.3% (291 of 293 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client LEGACY
2026-08-06 06:25:40 +00:00
github-actions[bot] e3800433e7 Merge pull request #1027 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-06 06:25:17 +00:00
Sungjun Park 3583c5d011 Translated using Weblate (Korean)
Currently translated at 29.8% (153 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-06 08:20:30 +02:00
Sungjun Park e4813ce86c Translated using Weblate (Korean)
Currently translated at 28.8% (148 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-06 08:15:47 +02:00
Daniel Hultgren c1a0882ee9 Translated using Weblate (Swedish)
Currently translated at 56.7% (291 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sv/
2026-08-06 08:13:40 +02:00
Sungjun Park 52deaf7c8c Translated using Weblate (Korean)
Currently translated at 28.2% (145 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-06 08:13:38 +02:00
Miloslav Kos b0c9f572f7 Translated using Weblate (Czech)
Currently translated at 70.7% (363 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-08-06 08:13:37 +02:00
Donkie 5cfdde4cc6 Merge pull request #1025 from Donkie/worktree-issue-1019-unassigned-extra-group
Show every unassigned spool when grouping by an extra field
2026-08-05 19:49:29 +02:00
Donkie dd39a11c7e Show every unassigned spool when grouping by an extra field
Fixes #1019. A dashboard card grouped by a custom spool field counted more
spools than it listed: the group aggregate summed them, the card's own query
returned a subset.

Grouping and filtering disagreed about what "no value" means. group_by keys on
the decoded value, so a spool holding the JSON empty string a cleared text box
writes became its own group, separate from the null one; the client renders
both as "unassigned" and adds the counts. The card then fetches its spools with
`extra.<key>=`, whose empty branch matched only NULL and JSON null - never the
empty string. Built-in string columns have matched both NULL and '' since
forever (add_where_clause_str_opt), which is why grouping by location was fine.

- The empty extra-field filter matches a stored empty string too.
- Value-keyed group axes (extra fields, location, material) fold a blank into
  NULL, so "unassigned" is one group rather than two the client can only draw
  as one.
- Emptying a text extra field clears it instead of storing "", so no new spool
  lands in the state above.
- The dashboard clears an extra field with a real JSON null. It sent the string
  'null', which the API rejects as not-a-string, so dragging a spool onto the
  unassigned card 400'd on every extra-field board.
2026-08-05 19:42:59 +02:00
github-actions[bot] 3e1c4ffcd7 Merge pull request #1024 from Donkie/weblatev2
Translations update from Hosted Weblate
2026-08-05 17:03:58 +00:00
Anonymous 5208b47007 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hans/
2026-08-05 18:59:23 +02:00
Anonymous 0d73e0d34e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/zh_Hant/
2026-08-05 18:59:17 +02:00
Anonymous 5e2c4b0c0c Translated using Weblate (Ukrainian)
Currently translated at 25.3% (130 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/uk/
2026-08-05 18:59:12 +02:00
Anonymous 63eee038d7 Translated using Weblate (Turkish)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/tr/
2026-08-05 18:59:07 +02:00
Anonymous f38743b179 Translated using Weblate (Thai)
Currently translated at 26.3% (135 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/th/
2026-08-05 18:59:02 +02:00
Anonymous 9bf4022776 Translated using Weblate (Tamil)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ta/
2026-08-05 18:58:56 +02:00
Anonymous de8c6de460 Translated using Weblate (Swedish)
Currently translated at 32.1% (165 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sv/
2026-08-05 18:58:50 +02:00
Anonymous 0d75364b07 Translated using Weblate (Slovenian)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sl/
2026-08-05 18:58:45 +02:00
Anonymous 7d07905091 Translated using Weblate (Slovak)
Currently translated at 1.7% (9 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sk/
2026-08-05 18:58:39 +02:00
Anonymous 08bbd6480b Translated using Weblate (Russian)
Currently translated at 68.6% (352 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/
2026-08-05 18:58:33 +02:00
Anonymous 86204ffddb Translated using Weblate (Romanian)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ro/
2026-08-05 18:58:28 +02:00
Anonymous 4be91b365a Translated using Weblate (Portuguese)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pt/
2026-08-05 18:58:23 +02:00
Anonymous 5af5ca17d1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pt_BR/
2026-08-05 18:58:18 +02:00
Anonymous 8013dd1ce2 Translated using Weblate (Polish)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/pl/
2026-08-05 18:58:13 +02:00
Anonymous ee9cf0d830 Translated using Weblate (Dutch)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/nl/
2026-08-05 18:58:08 +02:00
Anonymous 3e8eaf6aaf Translated using Weblate (Norwegian Bokmål)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/nb_NO/
2026-08-05 18:58:03 +02:00
Anonymous 2af3b95d4f Translated using Weblate (Latvian)
Currently translated at 0.9% (5 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/lv/
2026-08-05 18:57:58 +02:00
Anonymous 7707dad1a4 Translated using Weblate (Lithuanian)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/lt/
2026-08-05 18:57:53 +02:00
Anonymous 8382a9037d Translated using Weblate (Korean)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ko/
2026-08-05 18:57:48 +02:00
Anonymous fc4cf78451 Translated using Weblate (Japanese)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ja/
2026-08-05 18:57:43 +02:00
Anonymous 13b1ab7f94 Translated using Weblate (Italian)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/it/
2026-08-05 18:57:38 +02:00
Anonymous 7262563e4d Translated using Weblate (Hungarian)
Currently translated at 25.3% (130 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/hu/
2026-08-05 18:57:29 +02:00
Anonymous 7cf8f6f0eb Translated using Weblate (Hindi (Latin script))
Currently translated at 0.3% (2 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/hi_Latn/
2026-08-05 18:57:24 +02:00
Anonymous 01462d3df3 Translated using Weblate (French)
Currently translated at 26.3% (135 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/fr/
2026-08-05 18:57:19 +02:00
Anonymous faa4659b6d Translated using Weblate (Persian)
Currently translated at 26.3% (135 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/fa/
2026-08-05 18:57:14 +02:00
Anonymous aaa8714f66 Translated using Weblate (Estonian)
Currently translated at 3.1% (16 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/et/
2026-08-05 18:57:09 +02:00
Anonymous da5eeef1a0 Translated using Weblate (Spanish)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/es/
2026-08-05 18:57:02 +02:00
Anonymous e73f603f39 Translated using Weblate (Greek)
Currently translated at 22.4% (115 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/el/
2026-08-05 18:56:56 +02:00
Anonymous a7be90ef4a Translated using Weblate (German)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/de/
2026-08-05 18:56:49 +02:00
Anonymous 5b889c2e7a Translated using Weblate (Danish)
Currently translated at 26.7% (137 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/da/
2026-08-05 18:56:44 +02:00
Anonymous 796ef5941e Translated using Weblate (Czech)
Currently translated at 42.8% (220 of 513 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/
2026-08-05 18:56:37 +02:00
github-actions[bot] 58f4b13cd2 Merge pull request #1023 from Donkie/weblate
Translations update
2026-08-05 16:45:44 +00:00
Anonymous e50c2ec72f Translated using Weblate (Latvian)
Currently translated at 4.5% (13 of 288 strings)

Translated using Weblate (Slovak)

Currently translated at 10.4% (30 of 288 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lv/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sk/
Translation: Spoolman/Web Client LEGACY
2026-08-05 18:41:21 +02:00
Donkie fcf0e8b145 Merge pull request #1021 from Donkie/worktree-weblate-ru-conflict
ci: merge Weblate PRs instead of squashing them
2026-08-05 18:38:51 +02:00
Donkie a4c113ba8e ci: merge Weblate PRs instead of squashing them
Squash merging replaces Weblate's commits with a new one whose hash, and
for multi-commit PRs whose patch-id, matches nothing Weblate knows. Its
clone then rebases those commits onto a master that already contains the
same strings and hits a content conflict, after which Weblate stops
syncing until it is reset by hand. PR #1004 squashed two Russian commits
and broke ru/common.json exactly this way.

A merge commit keeps each Weblate commit reachable at its original hash,
so the rebase drops them as already-upstream. Nothing required squash
here: allow_merge_commit is on, the master ruleset has no linear-history
rule, and human PRs already land as merge commits.
2026-08-05 18:36:16 +02:00
Donkie b12e129e5b Merge pull request #1020 from Donkie/worktree-v2-change-vendor
client_v2: let a filament's manufacturer be changed
2026-08-05 18:23:50 +02:00
Donkie b2f5088081 client_v2: let a filament's manufacturer be changed
The manufacturer was decided once, in the add-spool modal, and there was no
way back: a filament created under the wrong brand — or under none — stayed
there, and since the library groups by manufacturer that mis-files every
spool of it. The companion to the spool's change-filament (#1010), one level
up the chain.

Adds a "Change" action to the MANUFACTURER section of the filament
inspector, opening a dialog with the three destinations the API allows: one
of your manufacturers, a new one named on the spot (as the add-spool form
already allows), or none at all. There is no SpoolmanDB side to this one —
the external catalog carries filaments and materials, not manufacturers — so
the picker fetches the vendor list whole and filters it in the browser.

The dialog spells out the one consequence that isn't obvious. A filament
with no empty spool weight of its own takes its manufacturer's, and that is
the figure `PUT /spool/{id}/measure` subtracts from a reading off the scale,
so the note says what it becomes — or that it is about to have none. A
filament that has its own keeps it and gets no note: unlike the spool's full
weight, that field is editable right there in the panel.

Clearing the manufacturer travels as an explicit `vendor_id: null`, which is
what the API reads as "no manufacturer"; dropping the key instead would have
made that choice a silent no-op.
2026-08-05 18:14:28 +02:00
Donkie 7aa864e457 Merge pull request #1016 from Donkie/worktree-fix-1012-unused-grouping
client_v2: file unused spools under their own filament
2026-08-03 20:35:11 +02:00
Donkie 032b3f05c2 client_v2: file unused spools under their own filament
A collapsed "×N unused" row speaks for its whole pile with one filament's
name, swatch, weight and location, all read off the first spool. That only
holds within a filament group. Under Manufacturer (or Material/Location) a
group spans many filaments, so the whole unused pile was filed under
whichever filament happened to sort first: five different SUNLU woods
listed as "Wood Cherry ×5 unused" (#1012).

Bucket the pile by filament before collapsing it, keeping the backend's
order via first appearance. Buckets of one render inline as before.

Fixes #1012
2026-08-03 20:27:33 +02:00
Donkie 7db40b4036 Merge pull request #1015 from Donkie/worktree-spool-field-overrides
client_v2: per-spool weight and spool weight overrides
2026-08-03 20:17:42 +02:00
Donkie 4ed1fbfc67 client_v2: let a label print the spool's own tare weight
The v1 client offered the spool's empty-spool weight and its filament's as two
separate label tokens. v2 kept only the filament's, so `{spool_weight}` in a
migrated v1 design had no equivalent to map to and resolved to "?" — and a
design rebuilt in v2 could only print the filament's figure, which is not
necessarily the one that spool is measured against.

`spool.spoolWeight` falls back to the filament the way `spool.price` already
does, so a `spool.*` token prints what actually applies to that spool.
2026-08-03 20:09:42 +02:00
Donkie 155e8f3737 client_v2: make the override marks footnotes rather than badges
Accent-coloured pills next to four fields read as warnings about something
being wrong, when all they report is that a spool holds a value of its own.
Keep the accent colour, which is what makes them findable, and drop the border,
weight and uppercasing that gave them the urgency.

Both ends now render identically. The winning and shadowed levels were
distinguished by colour, but the wording already says which is which
("Overrides filament" vs "Overridden by this spool"), so the variant carried
nothing the label didn't.
2026-08-03 19:59:45 +02:00
Donkie b885108a3d client_v2: let a spool override the values it inherits from its filament
A spool can hold its own full weight, empty-spool (tare) weight and price,
falling back to its filament's where it has none. The API has had all three
since #339, and the v1 client exposed them, but client_v2 never mapped
spool_weight at all and had no field for it or for the full weight.

The result was silent wrong arithmetic (#1013): the API gives every spool it
creates a copy of the filament's tare weight, so correcting the *filament*
later looked like it had worked — the spool panel showed the new figure — while
"adjust by measured weight" went on subtracting the old one. A reporter's empty
261 g spool came back with 129 g still on it, exactly the difference.

Both ends of a disagreement are now marked. The spool's field says what it
shadows, the filament's row says what applies instead and is struck through,
and the same treatment covers the manufacturer's empty-spool weight, which
seeds a filament and is then left behind. Only where the values actually
differ: merely having one of its own is already visible in the field, and
marking every spool would teach everyone to stop reading the marks.

Also fixes a 500 from PUT /spool/{id}/measure when nothing in the chain has a
tare weight — reachable in one click now that the field can be cleared.
2026-08-03 18:06:05 +02:00
Donkie 1d5b8c1d3d Merge pull request #1014 from Donkie/worktree-issue-1010-change-filament
client_v2: let a spool's filament be changed
2026-08-03 17:24:30 +02:00
Donkie f2b060f6ca client_v2: let a spool's filament be changed
The v1 client had a filament dropdown on the spool edit form; client_v2
dropped it, leaving no way to re-point a spool at a different filament
(#1010). People who organise their shelves by spool number need slot #37
to stay slot #37 when the roll in it is replaced, and deleting and
re-adding the spool hands them a new id.

Adds a "Change" action to the FILAMENT section of the spool inspector,
opening a dialog that searches the local catalog and SpoolmanDB (an
external pick is imported on apply, as in the add-spools flow). The spool
keeps its id, location, dates, lot, comment and used weight.

The full weight needs a decision the dialog makes explicit: a spool
created from a filament records that filament's weight as its own, so it
would otherwise keep the old figure forever and this client has no other
field for it. When the new filament's weight differs, the dialog offers
to take it (checked by default) and spells out the resulting remaining
weight. A spool with no weight of its own follows its filament anyway —
that case just says so.
2026-08-02 22:51:16 +02:00
Donkie 9d53a3f292 Merge pull request #1008 from Donkie/worktree-client-v2-401-reload
Reload on 401 so a forward-auth session can recover
2026-08-02 15:06:57 +02:00
Donkie 708af05ab2 ci: run the client_v2 unit tests
The ci hook lints, formats and type checks client_v2 but never ran its unit
tests, so everything under src/**/*.test.ts was local only and nothing would
have caught a broken one. The backend group already runs pytest here, so this
is just the missing counterpart. The browser suite in tests_frontend_v2/ is
unaffected, it covers different ground and runs in its own job.

Adds about 4s to the style job, which already installs client_v2 deps. Left
out of pre-commit so committing stays fast.

Also points the type check comment at the pre-commit copy it meant, rather
than at itself.
2026-08-02 14:59:38 +02:00
Donkie ae12072cdd Merge remote-tracking branch 'origin/master' into worktree-client-v2-401-reload
# Conflicts:
#	client_v2/src/lib/api/search.ts
2026-08-02 14:13:41 +02:00
Donkie 2a03f9f64c client_v2: reload on 401 so a forward-auth session can recover
Behind a forward-auth proxy (Authelia, Authentik, oauth2-proxy) an idle
tab starts getting 401s once the session expires. The proxy can only
redirect a top-level navigation, so a page that is already loaded never
gets offered the login portal and simply fails every request until the
user works out that a manual reload fixes it.

Do that reload for them. It counts as a navigation, so it earns the
redirect and brings the user back to the page they were on. Rate limited
to once per 30s so a 401 that a reload cannot fix does not put the tab in
a loop, and skipped for writes so a half filled form is not lost to a
login redirect.

Both transports are covered. REST goes through http.ts. Websockets get an
HTTP probe instead, since a handshake refused by a proxy exposes no status
to the browser and is otherwise indistinguishable from a backend that is
down. search.ts moves onto the shared fetch helper so it is covered too.

Dormant without a proxy in front, since Spoolman never answers 401 itself.

Refs #923
2026-08-02 12:47:13 +02:00
Donkie cefd2caa42 Merge pull request #987 from Sanjays2402/fix/color-hex-strip-hash
fix(api): strip leading # from filament color codes
2026-08-02 11:23:16 +02:00
Donkie 3beb2b4483 Merge pull request #1006 from Donkie/worktree-search-spool-pills
Offer a filament's spools in the search results
2026-08-02 10:32:08 +02:00
Donkie 0c3ce54da9 Merge pull request #1007 from Donkie/worktree-issue-994-reorder-choices
Allow reordering of extra field choices
2026-08-02 10:31:55 +02:00
Donkie ac3ec7d1a2 Merge pull request #1005 from Donkie/search-material-tag
Distinguish the search result material tag from the match tag
2026-08-02 10:26:44 +02:00
Sanjay Santhanam 7e4d2ee419 fix(api): repair filament rows already stored with a leading #
The validator fix only guards the write path. Instances that already hit #780
keep 500ing on GET /filament because the stored value exceeds the response
model's max_length, and the UI can't repair what it can't list.

Filament.from_db now strips the # and returns None when the stored value still
isn't a valid 6/8 character code, so a single bad row no longer takes down the
whole list. A migration chained from 415a8f855e14 rewrites existing rows one by
one through op.get_bind() rather than with SQL string functions, so it behaves
the same on sqlite, postgres, mysql and cockroachdb. downgrade() is a no-op.
2026-08-02 00:22:27 -07:00
Donkie 46beda2264 Offer a filament's spools in the search results
Searching a filament name only ever returned the filament: a spool matches
on its own text (comment, location, lot number, extra fields) or its exact
id, so a name that lives on the filament could not reach any of its spools.
The hit named the filament you own and then dead-ended.

Each filament result now carries its first few spools as pills, with an
overflow link to the filament for the rest. GET /search gains an opt-in
spools_per_filament parameter; at 0, the default, the response is byte for
byte what it was, so nothing else that reads the endpoint changes.

The spools are fetched in one query over the matched filaments and grouped
in Python rather than with a per-group window function, which keeps the
behaviour identical on all four supported databases, and yields the exact
total count for free. They obey allow_archived like the rest of the
response, and are ordered by id so "first" means the same thing everywhere.

Fixes #993
2026-08-02 08:40:41 +02:00
Donkie 18f114419f Let extra field choices be reordered by dragging
Choice values could only be appended, so the only way to get a value into
the right place was to remove everything after it and type it all back in.
Both clients now render each choice with a grip: drag it, or focus it and
press the left/right arrow keys, to move it. The backend already accepts a
reordered list — it only refuses removals.

Fixes #994
2026-08-02 08:35:18 +02:00
Donkie 3c1d8bfa6d client_v2: separate the search result's material tag from the match tag
The material chip and the "matched: <field>" chip sat side by side at the
trailing edge with identical styling, so two unrelated facts read as one kind
of thing. Move material next to the filament name (mirroring the group header)
and give it an outline variant, leaving the filled chip to mean "why this
result matched".
2026-08-02 08:23:10 +02:00
Weblate (bot)andDr_Perry_Coke 20462e86e2 Translations update from Hosted Weblate (#1004)
* Translated using Weblate (Russian)

Currently translated at 74.4% (344 of 462 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/

* Translated using Weblate (Russian)

Currently translated at 76.0% (353 of 464 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/ru/

---------

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
2026-08-02 06:21:00 +00:00
Donkie a2cfe33cd3 Merge pull request #1003 from tomaioo/fix/security/unsafe-json-parsing-of-application-setti
Security: Unsafe JSON parsing of application settings
2026-08-02 08:16:32 +02:00
Donkie f8db2a0f09 Merge pull request #1001 from Donkie/fix/library-header-affordances
fix(library): make the group header's destinations findable
2026-08-02 08:15:07 +02:00
tomaioo 95230eaa73 fix(security): unsafe json parsing of application settings
The `useCurrency` and `useCurrencyFormatter` functions parse currency settings using `JSON.parse` directly on values fetched from the backend. If the backend returns malformed JSON or unexpected data types, this will throw an unhandled exception and crash the UI component relying on these hooks.

Signed-off-by: tomaioo <203048277+tomaioo@users.noreply.github.com>
2026-08-01 23:12:38 -07:00
2cdfcc1bae Translations update from Hosted Weblate (#1002)
* Translated using Weblate (Czech)

Currently translated at 47.6% (220 of 462 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/cs/

* Translated using Weblate (Swedish)

Currently translated at 35.7% (165 of 462 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sv/

---------

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
2026-08-02 05:17:00 +00:00
Donkie e175708b3e fix(ci): diff PRs from the merge base in guard-translations
The guard compared the base branch tip against the PR head directly, so
every commit that landed on master after a PR forked counted as one of
that PR every time it re-ran. Merging a Weblate sync therefore failed
unrelated PRs with client_v2/locales/sv/common.json in the offending
list, and the only way out was rebasing a PR that had never touched a
translation file.

Diffing BASE...HEAD resolves the merge base first, which is what "the
changes this PR makes" means, and it stays right however far master
moves on. Verified against PR 1001: the old form flags sv, the new form
passes, and a real hand-edit of sv is still caught.
2026-08-01 22:11:42 +02:00
Donkie a5e4bc0bf3 fix(library): drop the group header arrow
The hover underline and colour already say the title is a link, and the
arrow was one more glyph on a row that has a chevron doing something
else. Touch users lose the at-rest cue, but the manufacturer link below
is visible without hover either way.
2026-08-01 21:55:42 +02:00
Weblate (bot)andDaniel Hultgren cbc6458c5d Translated using Weblate (Swedish) (#1000)
Currently translated at 36.6% (161 of 439 strings)

Translation: Spoolman/Spoolman Web UI v2
Translate-URL: https://hosted.weblate.org/projects/spoolman/spoolman-web-ui-v2/sv/

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
2026-08-01 15:18:43 +00:00
Donkie 13b7a9b62f Merge pull request #999 from Donkie/worktree-issue-992-detail-fields
Show all filament and manufacturer fields in the library detail views
2026-08-01 17:18:29 +02:00
Donkie 7c29d5f54a Merge pull request #998 from Donkie/worktree-client-v2-delete-991
client_v2: add delete for filaments and manufacturers
2026-08-01 11:14:23 +02:00
Donkie 6663a42ab5 client_v2: add delete for spools
The old client could delete spools (canDelete on the resource, rendered
by Refine's Show header buttons); client_v2 shipped with archive only.
That left the filament delete a dead end: archiving does not release the
foreign key, and there is no way to reassign a spool either, so a
filament that had ever held a spool could never be removed.

The confirm warns when filament is left on the spool, since archiving is
probably what was meant in that case.

Also corrects the blocked-filament hint, which suggested moving spools
to another filament - something this UI cannot do.
2026-08-01 11:03:24 +02:00
Donkie b34b872140 client_v2: make duplicate and delete quiet icon actions
Both are occasional next to "Add spools of this", so they no longer
compete with it: a hairline separator, then two icon-only ghost buttons
that stay muted until hovered. The accent button gets the end of the row
back.

Adds an ariaLabel prop to Button, since an icon has no text to name it.
2026-08-01 10:56:32 +02:00
Donkie dadc2ba760 Fold mirrored extra fields into the grid above them
In the spool view the filament's custom fields sat under their own "Extra
fields" heading, styled exactly like the FILAMENT and MANUFACTURER headings
beside it, so it read as a third peer section rather than as more of the
filament. Emit those rows straight into the preceding field grid instead.

Only the read-only mirrors change; an entity's own extra-fields section
keeps its heading, which is also where the "Manage fields" link lives.
2026-08-01 10:54:25 +02:00
Donkie 35cc1c7d43 Show all filament and manufacturer fields in the detail views
The spool view's filament section only listed the specs, and neither the
spool nor the filament view showed anything of the manufacturer beyond its
name. Fill both in, and render URLs in text fields as links.

- Spool view's filament section gains price, article number, external id,
  registered and comment, and always shows spool weight.
- New read-only VendorSection, shown in both the spool and filament views,
  with the manufacturer's fields and extra fields plus a link to it.
- Any URL in a comment or text extra field is now a link opening in a new
  tab with rel="nofollow noopener": inline in read-only values, and as an
  icon beside the input where the value stays editable.

Fixes #992
2026-08-01 09:52:52 +02:00
Donkie e08c930ef8 client_v2: add delete for filaments and manufacturers
Closes #991.

Both inspectors get a Delete button backed by a new ConfirmDialog that
states the consequence instead of asking a bare "are you sure?".

A filament that still has spools cannot be deleted (the spool FK is not
nullable), so the dialog says how many hold it down rather than letting
the click come back as a generic error toast. The count comes from the
server with allow_archived=true: the panel hides archived spools, but
the foreign key does not care.

A manufacturer with filaments can be deleted, and silently orphans them.
The dialog says so before the fact, and the cache clears the vendor off
those filaments to match what the server did.

Pending inline edits are dropped before the delete, so the unmount flush
does not PATCH a row on its way out.

Adds integration tests for both API behaviours, which were untested.
2026-08-01 09:52:49 +02:00
Donkie 8318a5f970 fix(library): make the group header's two destinations findable
Clicking a filament group header opens the filament, but nothing said so:
its only hover signal was the same background tint a spool row gets, and
the one glyph on the row is the collapse chevron, which says "fold", not
"open". Reaching a manufacturer was worse off -- the breadcrumb, the
search box and the group menu were the only routes, and each one asks you
to already know it is there (#989).

The title now takes a link's underline and colour on hover, beside an
arrow that stays faintly visible at rest: hover is the one affordance a
touch user never sees, and they are a good share of who could not find
this. The manufacturer already printed on every filament group is now a
link to its inspector, so the shortest route to a vendor is one click
from the main list. Inspector breadcrumbs underline on hover too, for the
same reason.

Anchors cannot nest, so the entity link became an overlay covering the
whole row, with the manufacturer link and the disclosure stacked above
it. The row keeps its full click target, and the overlay is a real
element rather than a stretched pseudo-element so tap-target tooling
measures what people actually press. The manufacturer takes its colour
from --accent-soft: --accent-link is tuned for the lighter surfaces links
normally sit on and manages only 4.2:1 against the sunken header in the
light theme, where --accent-soft clears AA in both (4.9 / 7.3).

Row height is unchanged at 52px and the mobile a11y audit reports the
same zero axe violations it did before the change. The manufacturer link
is a 28px tap target rather than the audit's preferred 44px: at 44 it
would span most of the row and start swallowing clicks meant for the
filament link underneath it.

Fixes #989
2026-08-01 09:51:19 +02:00
Donkie c7b59fe322 Merge pull request #997 from Donkie/fix/988-filament-extra-fields
fix(add): let filament extra fields be filled in while creating a filament
2026-08-01 09:32:37 +02:00
Donkie c9c59b3d9e fix(add): let filament extra fields be filled in while creating a filament
The add-spool modal is the only filament create form client_v2 has, and its
new-filament section had no extra fields at all: a value could only be set
afterwards from the inspector, so anyone with custom filament fields lost the
thing the legacy client's create form gave them (#988). Duplicating a filament
carried the original's values along invisibly, with no way to adjust them.

The section renders itself away when no filament fields are defined, so it
costs nothing for the majority who have none. It sits outside "Advanced specs"
— a custom field only exists because someone defined it, which makes it the
opposite of an advanced detail to them.

Defaults are also applied more reliably than before: field definitions are
fetched when the modal opens, so opening straight into a preset filament or a
duplicate used to run the seeding before the definitions had arrived and left
the defaults off. They are now topped up when the definitions land, which fixes
the same latent miss on the spool section.

Covered end to end in tests_frontend_v2: define a filament field, fill it in
while creating the filament, and find the value back on the filament itself.

Claude-Session: https://claude.ai/code/session_01EaLK21WpQYDE1DkKwZHK3P
2026-07-31 20:56:10 +02:00
Donkie 09c29703f0 Merge pull request #990 from Donkie/fix/last-used-null-sort
fix: last-used sort order and the phantom "in use" label
2026-07-31 16:46:59 +02:00
Donkie a8da5e312b fix(library): stop unopened spools reporting themselves as "in use"
A spool still in its vacuum pack, with no first- or last-used date, showed
"in use" in the library and its inspector (#986). Two separate causes.

Registering a spool by its measured weight computes `net + spool - measured`
in floating point, and a full 1 kg spool on a 128.11 g core lands on 2.3e-13
rather than 0 -- so `used_weight === 0` was false and the spool counted as
used from the moment it was added. It also rendered "1000.0 g" where its
identical neighbour showed "1 kg". The subtraction is now rounded at the
source, `unused` allows a hundredth of a gram (under any scale's resolution,
and under the display's, so nothing real is rounded away), and weightAuto
rounds before choosing its unit. Existing spools already carrying dust read
correctly again without touching the database.

The label itself was also wrong. "in use" was the fallback for "used, but no
date on record", and it claims something Spoolman cannot know: there is no
live link to a printer and no notion of a spool being mounted. It now reads
a plain "used", via one shared usageLabel() so the row, the spool inspector
and the filament inspector can no longer drift apart.

Claude-Session: https://claude.ai/code/session_01EaLK21WpQYDE1DkKwZHK3P
2026-07-31 14:39:03 +02:00
Donkie c8be881e03 fix(sort): keep never-used spools and filaments at the bottom
Sorting the library by "last used" led with everything that had never been
used, on PostgreSQL and CockroachDB. Those two default to NULLS FIRST on
DESC, while SQLite and MySQL treat NULL as the lowest value and put it last;
`last_used` is NULL until a spool is first used, so the default library
ordering came out inverted for half the supported databases (#984, #985).
Grouped views showed it worst: a group's date is the max over its spools, so
a filament holding nothing but unused spools aggregates to NULL and floated
to the top, above filaments used minutes ago.

order_by_clauses() now orders on an explicit "is it null" flag ahead of the
field itself, so an absent value sorts last in both directions everywhere.
It is written as `expr IS NULL` rather than SQLAlchemy's nullslast(), which
renders a literal NULLS LAST that MySQL and MariaDB reject. Applied to every
sort site -- spools, groups, filaments, vendors, extra fields -- so ordering
no longer depends on which database is underneath.

Verified against all four: 438 integration tests pass on SQLite, PostgreSQL,
MariaDB and CockroachDB, and both new regression tests fail on PostgreSQL
without the fix.

Claude-Session: https://claude.ai/code/session_01EaLK21WpQYDE1DkKwZHK3P
2026-07-31 14:38:50 +02:00
Sanjay Santhanam 57f2b30a4c fix(api): strip leading # from filament color codes
The color_hex and multi_color_hexes validators normalized a local copy of
the value (uppercasing it and removing a leading "#") in order to validate
it, but then returned the caller's original string. A "#"-prefixed 8-digit
color such as "#FF000000" was therefore stored with 9 characters, which no
longer fits the String(8) color_hex column, so the write failed and every
subsequent read of that filament returned a 500.

Both validators now return the value with the "#" removed, matching the
un-prefixed form the rest of the codebase and the frontend already assume.

Adds tests/test_color_hex_validation.py covering both validators for the
prefixed and un-prefixed, 6- and 8-digit forms.
2026-07-31 04:37:45 -07:00
Donkie 7405ce77fd Merge pull request #983 from Donkie/worktree-weblate-client-v2
Point the translation workflows at client_v2, close the legacy client to translation
2026-07-31 10:53:59 +02:00
DonkieandClaude Opus 4.8 673ca1d2fb Drop the README wording change
Leave the feature-list line as it was; this PR is about the workflows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:52:54 +02:00
DonkieandClaude Opus 4.8 4e1e3c50c9 Point the translation workflows at client_v2, close the legacy client
Weblate translates client_v2 from now on. The two translation workflows
still only knew about client/public/locales/, which meant a Weblate sync
for the new client would be rejected by the auto-merge scope check and
hand-edits to client_v2 translations would sail through unguarded.

- weblate-automerge: accept locale JSON under either client_v2/locales/
  or client/public/locales/. The legacy tree stays on the allowlist only
  so a sync opened before its component was locked isn't stranded; the
  Weblate-side lock, not this allowlist, is what ends legacy translation.

- guard-translations: guard both trees, with different rules. For
  client_v2, modifying or deleting an existing non-English file is
  blocked while adding a brand-new language stays allowed (bootstrapping
  one also needs entries in project.inlang/settings.json and
  src/lib/i18n/languages.ts, which only a human PR can add — the failure
  message now says so). For the legacy client every non-English change
  is blocked, adds included, and the message redirects translators to
  the client users actually get.

- README: 18 -> 26 languages, and say outright that translations come
  through Weblate rather than pull requests.

Verified: the guard script passes 9 synthetic PR cases (en edits and new
v2 languages allowed; v2 edits/deletes, legacy edits and legacy new
languages blocked; both sections render when a PR trips each rule), and
the auto-merge regex accepts both locale trees while still rejecting
code, workflows, non-JSON and path-prefix smuggling.

Also confirmed the format risk is a non-issue: if Weblate's i18next v4
writer emits nested JSON instead of the repo's flat dotted keys,
paraglide still compiles it — nested German round-tripped intact, and a
nested _one/_other pair compiled to a correct CLDR plural with other
locales still falling back to English.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:35:01 +02:00
Donkie f369253c67 Bump version to 0.26.0 2026-07-31 08:19:20 +00:00
Donkie dd6b334a79 Test config.js rendering without importing main
The unit tests are supposed to run with no database, no server and no
built client, but tests/test_configjs.py imported spoolman.main, which
builds the SPA handler at module level and so needs client_v2/build to
exist. That passed locally, where the bundle is lying around, and failed
CI's style job, which never builds the client.

Moves the rendering into client.render_config_js, which the endpoint now
calls, so the test exercises the real code path without the app.
2026-07-31 10:00:47 +02:00
Donkie a8b3641a90 Document the two origin/host settings in .env.example
The CORS example was schemeless (source1.domain.com:p1), which is not an
origin and so never matches anything -- anyone following it got silent
non-functionality. Adds SPOOLMAN_ALLOWED_HOSTS alongside it and points at
the new wiki Security page.
2026-07-31 09:50:55 +02:00
Donkie 72daa29cd2 security: finish the audit's low-severity items
Task 10: config.js JSON-encodes the base path, so a backslash cannot
escape the string literal the way the old " check allowed; search.py
escapes LIKE wildcards at both its call sites, using a helper moved into
database/utils.py so it and extra_field_query share one implementation.

Task 9 was a pair of decisions rather than fixes. /info keeps its paths:
both clients render them and removing response fields breaks API v1, while
the disclosure buys nothing against an API that is unauthenticated anyway.
/metrics keeps its shape but now says what it publishes when switched on.

Also pins the unit tests to a throwaway data directory -- importing
spoolman.main creates one as a side effect, and it was the real one.
2026-07-31 09:42:32 +02:00
Donkie 7e9533965a security: make the host guard opt-in
Defaulting it on was the wrong trade. The only hostname it can refuse is a
registrable public domain, so it broke exactly the reverse-proxy-on-a-real-
domain deployments that configure nothing today -- a 400 on every request
until they found a new variable -- while the deployments rebinding can
actually reach are addressed by IPs, single-label names and .local, which
it never refuses anyway. The group that breaks and the group that gains
barely overlap.

SPOOLMAN_ALLOWED_HOSTS is now the on switch as well as the allowlist, so
there is no state where an operator has to think about this without having
asked for it. SPOOLMAN_CORS_ORIGIN deliberately does not enable it, or
setting CORS for a Fluidd instance would start refusing your own hostname.
2026-07-31 09:36:41 +02:00
Donkie 97524b1107 security: refuse requests addressed to a host we do not answer to
Closes the DNS rebinding hole, which the origin guard structurally cannot:
under rebinding the attacker's page keeps its own name, so Origin and Host
agree with each other and the origin guard trusts them both, while the
address behind that name is the victim's Spoolman instance.

The default allows everything a stranger cannot point at someone else's
machine -- any IP literal, any single-label name, and the non-registrable
suffixes -- because what the attack needs is a registrable public domain.
That leaves a reverse proxy on a real domain, which cannot be told apart
from the attack and so must declare itself via SPOOLMAN_ALLOWED_HOSTS.
2026-07-31 09:29:47 +02:00
Donkie e3dbd3e609 Move the security notes out of the README and into the Wiki's remit
The README carries no config reference at all -- env vars and deployment
guidance live in the Wiki -- so documenting SPOOLMAN_CORS_ORIGIN and the
reverse-proxy host requirement here gave them two homes to drift apart in.
2026-07-31 09:19:42 +02:00
Donkie 388e87ec96 Merge branch 'master' into security/backend-audit-todo
# Conflicts:
#	pyproject.toml
2026-07-31 09:15:22 +02:00
Donkie ff7170f0f7 Build on Node 24, which is not end-of-life
Every workflow pinned node-version '20'. Node 20 reached end-of-life on
2026-04-30, so for three months CI has been building the artifacts that
go into released images on a runtime that no longer gets security fixes.
Vite 8 also floors at ^20.19, which setup-node's '20' happens to satisfy
today only because it resolves to the latest 20.x -- a floor we were
clearing by accident is not one worth keeping.

24 rather than 22: it is Active LTS until 2026-10-20 and supported
through 2028-04-30, so this pin outlives the 22 line by a year. Node 26
does not ship until October.

The legacy client declared engines "20.x", which would have made every
npm ci in that package print EBADENGINE against the new runner. It is
widened to the floor of the Vite 7 it already runs on, so it now
describes what the package actually supports instead of naming a single
line.

Verified locally on Node 24.13.0: lefthook ci green, both clients
install clean and build, client_v2's unit tests and the
tests_frontend_v2 Playwright suite pass. tests_frontend needs the built
image so it is left to CI; its only dependency declares node >=18.

The setup-node pin itself is untouched and still on v4.0.3, whose action
runtime is the other deprecated Node 20 in the logs -- GitHub already
force-runs it on 24. That is an action upgrade, and a separate change.
2026-07-31 09:00:16 +02:00
Donkie 3c414503d6 Move client_v2 to Vite 8 and refresh the rest of its dependencies
Dependabot #982 opened the door: an esbuild advisory that can only be
closed by going forward, since the fix is upstream of us in
vite-plugin-svelte and vitest. It could not merge on its own, though --
Vitest 4 no longer augments Vite's defineConfig, so the `test` block in
vite.config.ts became a type error and the style job went red on a file
Dependabot is not allowed to touch. The config now takes defineConfig
from vitest/config, which is where that overload lives.

With the chain forced open anyway, everything else that had drifted goes
with it: SvelteKit, Svelte, paraglide, Playwright, prettier-plugin-svelte
4, globals 17 and ESLint 10. TypeScript stays on 5.9 -- 7.0.2 is out, but
typescript-eslint pins <6.1.0 and svelte-check ^5 || ^6, so it is
peer-blocked on both sides rather than merely untested.

Vite 8 replaces Rollup and esbuild with Rolldown, so the toolchain is now
one Rust binary plus lightningcss. Measured against a clean checkout of
the old tree: the production build drops from 7.7s to 4.6s, shipped
JavaScript from 383 KB gzipped across 42 chunks to 360 KB across 36. Dev
cold start is unchanged at ~2s, as expected -- dev serves unbundled ESM
either way. The armv7 image is not exposed to Rolldown's native binaries:
build-client runs on amd64 and the arch jobs only download its artifact.

ESLint 10 turns on no-useless-assignment, which caught three initialisers
that every branch overwrites; they become bare declarations. The
allowScripts block pinning esbuild@0.21.5 goes too, as esbuild is no
longer in the tree at all and nothing in the repo reads that field.

The lockfile is regenerated rather than patched: the old one pinned
vite-plugin-svelte 4 firmly enough that npm could not resolve a path to
7 without it.

Verified on Node 24: lefthook ci green, 50 unit tests, a11y audit, and
the full tests_frontend_v2 Playwright suite (21/21) against a real
backend serving the new build.
2026-07-31 08:59:46 +02:00
Donkie b00c1b875d Add dashboard and label-designer screenshots to the README
The library screenshot shows one of the client's four pages. The two
that are hardest to guess at from a feature list -- spools as draggable
cards grouped by location, and labels built from arbitrary spool fields
-- now sit under it in a two-column table, so they cost one screen's
height between them rather than one each.

They are captured at a narrower viewport than the library shot rather
than scaled down from it: at half width a 1440px page reduces to
unreadable, whereas 900px reflows the dashboard to two columns and the
designer to a tighter grid, so the text survives the downscale. Both are
cropped to a 3:2 aspect and have the nav bar removed -- it is already in
the shot above -- so the pair sits level.

Fixed along the way, since it would otherwise have been immortalised in
the dashboard screenshot: the card separators lost a space where they
met an {#if} boundary, which Svelte trims, rendering "Bambu Lab -Bambu
Green" and "1 kg- Last used 2 days ago". The optional parts are now
built as one expression, which has no block edge to trim.

demo_seed gains a note on why label designs aren't seeded: they're a
blob of a client-side schema, so writing one from Python would duplicate
it. The designer's own "Create your first label" is the honest route.
2026-07-31 08:37:47 +02:00
Donkie 0c03f8b404 Stop promoting a spool's comment to its library row title
Grouped by filament, a row had nothing of its own to put in the bold
title slot -- #id, fill, weight and location all have columns -- so it
borrowed the comment when one was set, and dropped the usage status to a
quiet second line behind it.

That assumed comments are short labels. The field is a free-form
textarea: people keep order URLs, multi-line notes and pasted receipts
there. Whatever is in it got the widest, loudest slot in the row, pushed
out "used N ago" -- the one thing every row can say -- and, since .name
is nowrap, ran any newlines together into one line before ellipsing
mid-word.

Rows now always show the usage status. The comment is unabridged in the
inspector, which is the only place it was ever readable in full.

The screenshot is regenerated against the new behaviour, and demo_seed's
comments go back to natural length -- they were written short only to
survive the row.
2026-07-30 22:09:06 +02:00
Donkie 67022e7c6d Regenerate the README screenshot from catalog-imported filaments
The old one was seeded with stress_seed.py, which builds a filament name
from one random pool and its color hex from another, so the picture
contradicted itself: "PETG Copper Bronze" had a blue swatch and "PVA
Sparkle Silver" a tan one. That is the first thing a visitor sees.

scripts/demo_seed.py imports every filament from SpoolmanDB instead,
issuing the same request the client's Add-spools flow does, so names,
colors, densities and temperatures come from one source and agree with
each other. Twenty filaments across eight real vendors are pinned by
external id, chosen for material spread and a varied swatch column; if
one leaves the catalog the script fails with the list rather than
quietly drifting.

The spools on top of them are still made up, because that half really is
per-user data -- partial weights, locations, lot numbers, prices,
first/last-used dates, a few archived and a few still sealed -- plus the
handful of extra fields a real user defines, so the inspector shows
content where it used to show its empty state.

Two details are shaped by how the list renders rather than by realism:
spools consume a whole number of grams, since a decimal wraps onto a
second line in the narrow weight column, and comments stay under ~20
characters, since the list shows a comment in place of "used N ago" and
truncated the longer ones mid-word.
2026-07-30 22:03:19 +02:00
Donkie 4ab99768ae Abort last workflow run for PRs when new push 2026-07-30 21:48:55 +02:00
Donkie c1d5c6b46e Merge pull request #978 from Donkie/feature/v2-client
New frontend
2026-07-30 21:48:03 +02:00
Donkie aad337234e Compile paraglide before type-checking client_v2
The type check assumed $lib/paraglide existed, but it's gitignored
output of the paraglide vite plugin, so it only appears once something
runs vite. svelte-check doesn't, which left the job resolving ~40
imports of $lib/paraglide/{messages,runtime} against nothing on any
fresh checkout -- CI every time, and a new clone locally. It went
unnoticed because the job had been dying at exit 127 before it could
get far enough to report, and because a working tree that has ever run
dev or build has the directory sitting there.

The compiler ships a CLI, so this costs a compile rather than a full
vite build. Its flags live in a package.json script that `check` and
the hook both call, leaving one copy to keep in sync with the plugin
options in vite.config.ts.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:34:26 +02:00
Donkie 9bdf4d1a5e Install client_v2 deps in the style job
lefthook's ci hook grew a client_v2 group, but the style job only ran
npm ci in client/, so all three of its jobs died at exit 127 on missing
./node_modules/.bin binaries -- eslint, prettier and svelte-kit. The
hook is invoked from the repo root and the paths are hardcoded (for
Windows' sake), so nothing resolved them from client/'s tree. That has
been failing every run since the group was added, on a branch where
client_v2 is the client that ships, so its lint, format and type checks
were the ones not running.

client_v2/docs is gitignored scratch space that prettier still walked,
which fails the hook locally on files CI never sees; the ignore files
now agree.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:29:07 +02:00
Donkie 24e6f7fd34 Escape backslashes in the a11y report's table escaper
CodeQL flagged esc() in the Playwright global teardown: it replaced the
markdown cell separator with an escaped one but left backslashes alone,
so an input ending in a backslash produced a doubled backslash followed
by a bare pipe -- the backslash escaped itself and the pipe went on to
split the row. axe help strings and CSS selectors are arbitrary page
content, so that is reachable, if only ever in a local audit report.

Escaping both metacharacters in a single pass fixes it: nothing the
replacer emits is re-examined, so an escape can't be consumed by the
character before it. Newlines get flattened at the same time, since a
newline ends a table row just as effectively as a stray pipe did.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:28:43 +02:00
Donkie f4eba480d6 Catch client_v2 up to 0.25.0
The merge brought master's version bump, which ran before bump.py knew
about client_v2 -- so client/package.json says 0.25.0 and client_v2 was
left one release behind. Set it to what the bump would have written.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:02:39 +02:00
Donkie 59672ecb30 Merge master into feature/v2-client
Brings the branch up to 0.25.0 and picks up cosign image signing, the
armv7/arm64 smoke tests, the armv7 greenlet/libstdc++ fix, the
PUID/PGID and Kubernetes SPOOLMAN_PORT entrypoint guards, and the
externaldb unwritable-path fix.

The only conflict was publish-images' needs list, where both sides had
added a job: test-frontend-v2 here, test-multiarch-smoke on master.
Resolved as the union, so nothing publishes until every test job has
passed. The rest of the graph is unchanged, and the new smoke tests
inherit client_v2 coverage for free -- build-arm64 and build-armv7
already download the client_v2 artifact, so booting those images to
/health now also proves the default client is present in them.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:02:14 +02:00
Donkie c781869d09 Refresh the README screenshot and note the legacy-client rollback
The screenshot was the 2023 React client -- a different application from
the one a new visitor now installs. The replacement is client_v2's
library with a spool's details open beside it, which is where anyone
using Spoolman spends their time, and it lives in the repo rather than
on user-content URLs so it can be regenerated with the client.

The feature list gains search/group/filter, which client_v2 makes a
first-class part of the library rather than per-column table filters.

Installation now mentions SPOOLMAN_LEGACY_CLIENT. Someone whose setup
the new client misbehaves on will look here before .env.example, and
the useful thing to know is that both clients ship in every release and
share the API, so falling back costs nothing.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:00:36 +02:00
Donkie a76c8ed3d9 tests_frontend_v2: cover labels, settings, search and installability
Browser coverage was smoke plus CRUD, mirroring the old React suite --
which left the subsystems with the most new code in them completely
untested. Everything added here needs a real deployment to mean
anything, which is why it lives in this suite and not in vitest.

search.spec.ts drives the top-bar search (a filament, a manufacturer
and a location each land in their own section; spools match on their
own text, not their filament's, so a location is what finds one) and
the toolbar's group / filter / sort. Group, sort and filters live in
the URL, so each is checked to survive a reload. Sorting a grouped list
by something the backend cannot order groups by flattens the view, and
that is asserted rather than left as a surprise.

labels.spec.ts round-trips a design through the label_designs server
setting and exports a label for a real spool through the "Image files"
mode, asserting a non-trivial PNG comes back -- the only way to prove
the QR encoding, template substitution, logo and mm-to-pixel maths
actually ran. The print modes are left alone on purpose: they open the
browser's print dialog, which would stall the session.

settings.spec.ts separates the two persistence models the page mixes
with no visual cue -- currency, rounding and base URL on the server,
theme in the browser -- and takes an extra field from definition to the
library's filter menu and back out again. pwa.spec.ts checks the
deployed manifest rather than the source file: served at the deploy
root, with a type browsers accept, all icons resolving, all URLs
relative.

The add-spool flow moves out of crud.spec.ts into a helper so each spec
creates its own data instead of leaning on what an earlier one left
behind, and filters pin assertions to that data -- the library is
paginated, so an unfiltered assertion about a specific row is a coin
toss on an instance with real data in it. Anything with instance-wide
reach is put back afterwards. Verified against both an empty database
and one holding ~50 seeded spools.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:00:24 +02:00
Donkie 7e353595c0 bump.py: keep client_v2's version in sync with the release
The bump script has only ever touched client/package.json, so
client_v2 has sat at 0.0.1 since it was created. Nothing reads it --
the footer takes the version from GET /info -- but both clients ship in
every release, and a package that claims 0.0.1 while the release says
0.25.0 is confusing to anyone who looks.

The indent has to be per-client: each tree is formatted by its own
prettier config and client_v2 uses tabs, so writing 2 spaces back would
make `npm run lint` fail on the release commit itself.

Also brings client_v2 up to the current version so the first bump after
this is a normal increment rather than a jump from 0.0.1.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:59:53 +02:00
Donkie 6ca753bca0 Move the stress-seed script into scripts/
It has been sitting untracked in client_v2/ -- the wrong place twice
over: it is a backend tool that talks to the API over HTTP and knows
nothing about the client, and client_v2 is excluded from ruff, so
nothing was checking it.

scripts/ already holds the repo's operational scripts, and a poe task
puts it next to the other things you run against a dev instance. The
docstring gains the warning the flags deserve: --purge deletes every
vendor, filament and spool on the target, not only the generated ones.

Ruff now sees it, so scripts/ gets per-file ignores for the rules a
generator legitimately breaks -- stdout is its user interface, its data
is deliberately random and full of literal weights and diameters, and
the generators are long by nature. Type annotations and everything else
stay enforced.

Also fix --skip-fields: it skipped creating the extra-field definitions
but still generated values for them, so every POST failed with "Unknown
extra field" and the run produced nothing.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:59:41 +02:00
Donkie c9ae367dc0 Say what to do when the client bundle is missing
The clients are build artifacts and are not committed, so a source
checkout has none until `npm run build` has been run in the client
directory -- and a `git pull` that brings new client sources does not
bring a bundle. Spoolman then refuses to start with StaticFiles' own
message, "Directory 'client_v2/build' does not exist", which says
nothing about what is wrong or how to fix it.

This has always been true of client/dist, but it bites harder now: an
existing source install that upgrades into client_v2 has never built
this directory even once, so it hits the wall on the upgrade rather than
on a fresh install. Docker images and spoolman.zip both ship the bundles
pre-built, so neither is affected.

Check before handing the directory to StaticFiles and name the command
to run, derived from the client directory so it is right for whichever
client is mounted.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:59:30 +02:00
Donkie b38468ffe2 client_v2: ship a web app manifest and PWA icons
The legacy client got its manifest and icons from vite-plugin-pwa.
client_v2 has no such plugin, and nothing replaced them: static/ held
only favicon.svg, spoolman.svg and the Ko-fi logo. So the upgrade
silently un-installs the app for everyone who had added Spoolman to a
home screen -- no manifest means no standalone display mode, no
home-screen icon, and no install prompt ever offered again.

The manifest and icons are plain files in static/ rather than plugin
output. The icons are the v1 set, unchanged, so an already-installed
app keeps the icon it had; name, colors and display mode are v1's too,
and start_url resolves to the same URL at a root deploy, which keeps the
app identity stable across the upgrade instead of orphaning the old
install.

Every URL inside the manifest is relative to the manifest itself, so the
one file is correct under any SPOOLMAN_BASE_PATH with no rebuild. Only
the document's link to it needs help: the prerendered pages emit
"./manifest.webmanifest" and resolve fine, but the SPA fallback (200.html,
which serves deep routes like the /spool/show/<id> a printed QR points at)
carries absolute paths, so the existing base-path fixup gains two
patterns for the manifest and the touch icon.

Also register the .webmanifest media type. StaticFiles takes Content-Type
from the stdlib mimetypes registry, whose built-in table only learned
this extension in recent Pythons and otherwise falls back to the host's
/etc/mime.types -- absent in slim images. We support 3.10+, so leaving it
to the environment means some installs serve the manifest as text/plain.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:58:49 +02:00
Donkie d2cabd5d29 client_v2: unregister the legacy client's service worker on upgrade
The old React client injected a vite-plugin-pwa registration into its
index.html, so every visitor who ever opened Spoolman has an active
worker at the deploy root -- not just people who installed it to a home
screen. Its precache holds a complete copy of the old app shell, and
`createHandlerBoundToURL("index.html")` serves that shell for every
navigation.

Upgrading does not touch any of that. client_v2/build ships no sw.js and
SinglePageApplication 404s missing non-.html files, so the browser's
update check fails -- and per the service worker spec a failed update
leaves the registration intact. The result is the old React UI, fully
working against the new backend, with nothing to indicate the upgrade
happened. Chrome has historically unregistered on a 404, but that is
browser-dependent and not something to rest a rewrite launch on.

So answer the update check instead of 404ing it. static/sw.js takes over
(skipWaiting, so it does not queue behind the outgoing worker in
"waiting" -- a state that only clears when every tab is closed, which is
exactly the user who is stuck), drops every cache, unregisters itself,
and reloads the windows it controls, which unregister alone leaves
sitting on whatever the old worker handed them. static/ lands in the
build root, so it answers at `sw.js` relative to the deploy base path --
the exact URL the old registration polls, base path or not.

The test stands in a worker with the two behaviours that do the trapping
(a precache and a navigate handler answering from it), because only one
client is mounted at a time and the deployment under test already serves
the real sw.js at the legacy registration's URL. It proves the stale
worker does keep serving the old shell, then swaps the real one back in
and asserts the new UI returns with no reload from the test. Removing
build/sw.js fails it on the user-visible symptom: still on the old shell.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 19:52:17 +02:00
Donkie 87a07a5560 tests_frontend_v2: follow the locations page to the dashboard
The suite has been red since 2165121 turned the locations page into the
dashboard: two of its three tests click a top-bar tab named "Locations",
which no longer exists, and spent 30s timing out on it.

The dashboard groups by location out of the box, so the CRUD test's last
step still works as written once it goes to the right tab -- the new
location is a card of its own.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 18:11:25 +02:00
Donkie ae3208a9c3 Let an extra field's value be cleared again
Picking the empty option of a single-choice extra field did nothing --
in either client, on spools, filaments and vendors alike. Emptying an
integer, range, datetime or multi-choice field was just as stuck. There
was no way to get a value back to unset once it had one.

Nothing in the API could express it. A key left out of the map means
"leave it alone", and every value that could stand in for empty is
rejected by the field's type check: "" is not one of a choice field's
choices. So `extra` values now accept null, meaning the entity has no
value for that field -- no row is stored, which clears whatever was
there. Null was a 422 before it reached any logic, so nothing that used
to work behaves differently.

A filament's and a vendor's extra were also REPLACED wholesale by a
patch rather than merged per key, unlike a spool's since 28f6f8e
(v0.22.0). Anyone patching one field of a filament -- as client_v2 did
-- silently destroyed the rest of its custom fields; anyone relying on
the replacement to delete a key can now say so with null. All three
entities merge per key now, and the field descriptions say so instead
of saying nothing.

client_v2's saver was where the frontend half died: an emptied input
produced no patch at all. It sends the cleared key as null now, and
keeps sending only the keys that changed, which is what stops two people
editing different fields of one entity from overwriting each other. It
also captures the entity id when the edit is made -- the inspectors are
reused across selections rather than remounted, so a debounced patch
could otherwise land on whatever you clicked next.

The legacy client cleared a filament's or vendor's field by omitting it,
which the merge would now ignore, so its form sends null for a field
left empty. That also buys it clearing on spools, which merged (and so
ignored the omission) all along.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 18:11:02 +02:00
Donkie 04d6ee72bd client_v2: make the group-by hint go where it points
With no custom spool fields defined, the group-by menu has a single
entry and a note explaining that adding a text or choice field to spools
in Settings gets you more views. It told you where to go and then left
you to find it: three pages away, in a section that has to be scrolled
to and whose entity tab has to be switched.

The note is the link now, straight to the spool tab of the extra-fields
manager -- the same /settings?fields=spool#extra-fields deep link the
inspectors use, so it selects the tab and scrolls to the section.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 18:10:18 +02:00
Donkie 680f2a063b client_v2: let the dashboard header wrap instead of squeezing
The header packed the title, the group-by button, the drag hint and
"New group" into one non-wrapping row. On a phone there is nowhere near
enough width for that, so the hint -- the longest and least urgent thing
in the row -- was squeezed into a column one word wide, six lines tall,
with the group-by button stranded beside it (baseline alignment lines it
up with the first line of a paragraph that is now a column).

The row wraps now, and below 640px the hint takes the whole of the
second line while the three controls share the first. Alignment moves
from baseline to centre, which is what looks right once the items are
different heights rather than all being text. On a desktop the row is
unchanged: everything still fits on one line.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 18:09:47 +02:00
Donkie bf15e64fe5 client_v2: scale the label preview down to the screen it's on
The designer's canvas zoom was solved against a hard-coded 500x340 px
viewport, so on a phone a perfectly ordinary 50x25 mm label rendered 500
px wide inside a ~300 px stage and lost both its edges. Nothing recovers
from that: there is no zoom control, and the stage's scrollbar only
reaches what you already can't see.

The stage now measures itself and the fit uses the width it really has.
The measuring box is a full-width child whose own width comes from the
grid track, never from the canvas inside it, so feeding it back into
pxPerMm can't oscillate. The 2 px/mm floor stays for an absurdly large
label, and .stage-area keeps scrolling for that case.

Two things had to give way for the label to actually get the width. The
"Preview with" row was pushing the stage wider than the column (its
select had a fixed max-width and no room to shrink), which scrolled the
label out of view sideways; it now shrinks with the stage. And below
560px the stage drops most of its padding, which is worth ~28 px of
label on a 393 px screen.

The print tab's preview had the same fixed 260 px assumption and gets
the same treatment -- it fit a 393 px phone by 12 px, and didn't fit a
320 px one at all.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 18:08:22 +02:00
Donkie 814ddfcd92 client_v2: point people at extra fields instead of hiding them
The extra-fields section only rendered once you had defined a field, so
the feature was invisible to anyone who hadn't -- and nothing in an
inspector said where the fields you DO see come from.

Spool, filament and vendor details now always show the section, with a
"Manage fields" link beside the header and a line saying there are none
yet. Forms and read-only mirrors keep the old hide-when-empty behaviour:
the add-spool modal and the filament block inside the spool inspector
would only be advertising an empty section someone else owns. The
section waits for the definitions to actually load, so "none yet" is an
answer rather than a flash.

The link has to land somewhere specific, so the manager's entity tab
moves into the URL:

    /settings?fields=filament#extra-fields

which selects the tab and scrolls the page's own scroller to the
section, on a cold load as well as a client-side one. The tabs write
that URL as you switch, so the address bar always names the tab you are
looking at and it can be shared or bookmarked.

Switching entity now also closes an open field editor. It edits one
entity's field, and leaving it open across a tab change meant a save
could land on the wrong entity.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 14:27:19 +02:00
Donkie aa47cd015a client_v2: fold a group's spools away behind its header
A filament you own thirty spools of pushed the rest of the library off
the screen with no way to shut it, and nothing said the rows under a
header were ALL of that group's spools rather than the first few.

The group header answers both. Its right-hand meta gains a second line
under the weight -- mirroring the title/subtitle opposite it -- and a
disclosure sits at the trailing edge:

    [swatch]  PLA universal filament  PLA        1.7 kg   [v]
              Clas Ohlson . 1.75 mm            13 spools

Putting it in the header rather than in a strip of its own is what makes
it work: the header is sticky, so the fold stays reachable while you are
scrolled deep inside a long group -- which is exactly when you want it.
The row is two sibling controls, not one nested in the other: the body
still opens the filament/vendor (inert for material/location groupings,
as before) and the disclosure is a real button, so both are keyboard
reachable. The glyph rotates, not the button, which would swing its box
past the edge of the row.

A collapsed group doesn't fetch at all. A page of groups costs one
request per visible group, so this is most of what collapsing buys on a
big library; collapsing mid-flight aborts through the existing effect
cleanup, and expanding keeps what was already loaded so the rows paint
at once and refresh behind it.

Collapsing also has to leave you where you were. Folding a long group
shrinks the scroller and the browser clamps it to the shorter content,
which used to leave you somewhere else in the library entirely, so the
group is pulled back into view afterwards (`nearest`, so it is a no-op
when it never moved). Measured on a 160-spool instance: scrollTop
1200 -> 1197 while the content shrank 346px.

The collapsed set is remembered per browser in localStorage, keyed
`field:key` so it works for vendor/material/location groupings too, not
just filament. Only collapsed groups are stored, so expanded stays the
default: a library that grows a new filament shows it, and a key that no
longer matches anything is simply never asked about again.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 14:26:43 +02:00
Donkie 372554adbd client_v2: stop the add-spool form assuming you know what it means
Spool Weight asked beginners for a number they have no way of knowing, so
its help now names the usual ranges and offers two one-click starting
values. Location read as abstract metadata rather than the shelf the
spool goes on, so it gets an always-visible hint (not another popup —
testers didn't open those) and examples that look like places.
2026-07-30 11:22:43 +02:00
Donkie c4be425dcc client_v2: give a new label design something worth printing
The default design was a QR and one text block holding name, material and
spool id. It printed, but it looked like a debug dump: no manufacturer
anywhere, no color anywhere, and no visual hierarchy, because a text
element carries one size, weight and color for its whole template.

Lay out the right-hand column as four elements instead of one:

    Clas Ohlson          <- manufacturer, 2.5mm grey
    PLA universal        <- name, 3.2mm bold, the only wrapping block
    filament
    PLA . #2             <- material . id, 2.5mm
    [============]       <- color swatch, bottom-aligned with the QR

Splitting them is what buys the hierarchy; it also lets the manufacturer
sit in a conditional block, so a filament with no vendor prints nothing
there rather than the bare "?" a plain placeholder resolves to. The
material does the same inside the id line, taking its " . " separator
with it.

DEFAULT_SPOOL_TEXT / DEFAULT_FILAMENT_TEXT now hold just that id line,
which is the only part that differs between the two label kinds. That
would have quietly broken the spool/filament toggle for designs already
saved: setDesignKind retargets a text block only when it still matches
the *other* kind's default, and every existing design carries the old
three-line template. So the comparison runs over a list of known default
pairs, newest first, with the pre-redesign pair kept as the second entry.

Also explain "Skip Items", which reads like a mystery until you know
sheets are reusable. The hint goes full-width under the row rather than
in the field's own column, where at three columns it stacked seven lines
deep, and opens with the field name like helpMargin and
helpPrinterMargin already do.

Verified in the browser against a real spool, and the kind-switch
retargeting -- current default, pre-redesign default, and a user-edited
template that must stay untouched -- is covered by unit tests.
2026-07-30 09:47:40 +02:00
Donkie aa447dd05b Let any spool field value be renamed everywhere at once
Renaming a dashboard card only worked for Location, which has a
server-side rename that moves every spool in one statement. A custom
field had no equivalent, so the card name was read-only unless the group
was empty -- renaming it from the client would have meant patching each
spool one at a time, and only the ones the board had actually paged in.

Generalise the location rename to the spool's other string fields:

    PATCH /spool/field/{field}  {"value": "Shelf A", "new_value": "Shelf B"}
      -> {"spools_updated": 6}

where field is `location` or `extra.<key>`. Archived spools are included
-- the value is the value, and skipping them would leave half the spools
behind. Renaming onto a value already in use merges the two, though the
dashboard still refuses that so it can't happen by accident.

The old value travels in the body rather than the path, unlike
/location/{name}. Field keys are ^[a-z0-9_]+$ and so are path-safe, but
values are not: a location like "Rack A/B" needs an encoded slash, which
proxies routinely mangle. No reason to carry that flaw into the general
form.

Only fields the spool owns can be renamed. material/vendor/filament are
refused because they belong to another entity, and rewriting them "for
these spools" would change filaments that other spools share -- the same
rule that decides what the dashboard can group by, so grouping and
renaming now share one validator.

Like rename_location, no spool event is broadcast per row: the change is
one statement over what may be hundreds of spools, and that fan-out is
what the websocket layer avoids elsewhere.

Verified on all four databases, and in the browser on a 40-spool group
of which the board had loaded 30 -- all 40 moved, and the card renamed in
place without losing its position.
2026-07-30 09:08:58 +02:00
Donkie 21651216af Turn the locations page into a group-by-anything dashboard
The locations board could only ever answer one question: where is this
spool. The same card-and-drag interaction works for any field a spool
carries, so make the field a view mode and keep the rest.

Location is still the default view and behaves exactly as before. The
other views come from the user's own spool fields: any single-value text
or choice field now appears in a Group-by menu. A choice field renders a
card per choice, including the empty ones, which turns a "Status" field
into a kanban board.

Which field a card belongs to is decided by one small interface
(src/lib/dashboard/fields.ts), so the page never special-cases a field.
A field only qualifies if the SPOOL OWNS IT, because a drag moves one
spool and must mean exactly one change. That rules out material and
vendor, where writing one spool's card would silently rewrite every other
spool of the same filament, and lot number, which is stamped on by the
manufacturer and would be recording something untrue. Those stay library
filters. It also rules out numbers, dates, booleans and multi-choice,
whose values don't name a card you can drag into.

Backing changes:

- /spool/group grows group_by=extra.<key> for a spool's custom fields.
  The field table is aliased so it stays a distinct FROM element -- the
  extra-field filters reference the un-aliased table inside correlated
  subqueries, and without the alias those would correlate to this join
  instead of standing alone. Grouping is rejected for field types whose
  value isn't a single repeatable string. group_by is no longer a
  Literal, so an unsupported value is a 400 rather than a 422.

- The saved layout is per field: dashboard_groups and
  dashboard_spoolorders replace the location-only pair. The old
  `locations` / `locations_spoolorders` are read once as a seed for the
  location view and never written back, so an upgrade keeps the shelves
  it had and the old client keeps working.

- The view mode lives in the URL (?by=) so a board can be bookmarked and
  shared, and is mirrored to localStorage so a bare /dashboard resumes
  the last one looked at. The URL wins when it says something; a stale
  ?by= naming a deleted field falls back to location.

Verified against SQLite, PostgreSQL, MariaDB and CockroachDB -- Postgres
caught a real difference here, it sorts NULLs last where SQLite sorts
them first, so the new group assertions are keyed rather than ordered.
2026-07-30 08:41:36 +02:00
Donkie bf3480cfba client_v2: let you duplicate a filament into another color
Buying the same filament in several colors meant re-entering every spec
through the add-spool modal for each one. Add a duplicate flow reachable
from three places, all sharing one startDuplicate() path:

- a Duplicate button in the filament inspector header
- a "duplicate" link on the chosen filament in add-spool step 2, for when
  you found the right product but the wrong color
- "Add & new" after creating a filament, which now hands back the same
  form pre-copied instead of an empty search

The copy carries manufacturer (reusing the existing vendor), material,
specs, weights, price, comment and custom fields. Color is cleared and
the article number dropped since it is a per-color SKU; external_id never
copies. The name is kept as a starting point with the caret at its end,
plus a nudge that stays until it is changed.

Also fixes two things this surfaced in the add-spool modal: the advanced
block had its Extruder Temp and Bed Temp labels swapped (stored data was
always correct, only the labels lied), and the name field showed a red
"Required" before the user had typed anything, which left no room for the
new naming hint. The error now waits for first input or blur; validation
and canSubmit are unchanged.
2026-07-29 21:21:51 +02:00
Donkie 39f6ededfd client_v2: show spool weight in the add-spool search results
Vendors list the same filament in several sizes, so "Polymaker · PLA"
rows for the 1 kg and 250 g variants were indistinguishable in both the
catalog and SpoolmanDB result lists. Show the net weight on each row,
and on the chosen-filament card so the pick can be confirmed on step 2.

The row weight sits outside .res-name rather than being appended to the
vendor/material line: that element truncates with an ellipsis, which
would drop the one field that tells two matching results apart. The
chosen card's .cs line wraps instead, so a plain append is safe there.

A local filament's weight maps to 0 when unrecorded, so both row sites
guard on it rather than rendering "0 g".
2026-07-29 20:57:23 +02:00
+2 4219230aa1 Translations update (#977)
* Translated using Weblate (Estonian)

Currently translated at 17.3% (50 of 288 strings)

Translated using Weblate (Persian)

Currently translated at 96.5% (278 of 288 strings)

Translated using Weblate (French)

Currently translated at 98.9% (285 of 288 strings)

Translated using Weblate (Greek)

Currently translated at 72.9% (210 of 288 strings)

Translated using Weblate (Dutch)

Currently translated at 99.3% (286 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 72.2% (208 of 288 strings)

Translated using Weblate (Danish)

Currently translated at 85.4% (246 of 288 strings)

Translated using Weblate (Thai)

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (286 of 288 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ja/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/
Translation: Spoolman/Web Client

* Translated using Weblate (Ukrainian)

Currently translated at 93.0% (268 of 288 strings)

Translated using Weblate (Greek)

Currently translated at 72.9% (210 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 72.2% (208 of 288 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.3% (286 of 288 strings)

Translated using Weblate (Hindi (Latin script))

Currently translated at 3.8% (11 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 94.7% (273 of 288 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.0% (268 of 288 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hi_Latn/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client

* Translated using Weblate (Danish)

Currently translated at 87.1% (251 of 288 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client

* Translated using Weblate (Czech)

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client

* Translated using Weblate (Korean)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (225 of 288 strings)

Added translation using Weblate (Korean)

Co-authored-by: 김태남 <imedia0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/
Translation: Spoolman/Web Client

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Kayz C <kayzed.x@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client

* Translated using Weblate (Slovenian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Slovenian)

Currently translated at 38.1% (110 of 288 strings)

Translated using Weblate (Slovenian)

Currently translated at 14.9% (43 of 288 strings)

Added translation using Weblate (Slovenian)

Co-authored-by: Jernej Pangerc <jernejp21@tuta.com>
Co-authored-by: jernejp21 <jernejp21@tuta.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/
Translation: Spoolman/Web Client

* Translated using Weblate (Slovak)

Currently translated at 10.4% (30 of 288 strings)

Translated using Weblate (Slovak)

Currently translated at 9.7% (28 of 288 strings)

Added translation using Weblate (Slovak)

Co-authored-by: Pavol Vrba <pvrba2000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sk/
Translation: Spoolman/Web Client

* Translated using Weblate (Hungarian)

Currently translated at 94.0% (271 of 288 strings)

Co-authored-by: Tamas Veres <tamas.veres@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translation: Spoolman/Web Client

* Translated using Weblate (Danish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Danish)

Currently translated at 92.0% (265 of 288 strings)

Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client

* Translated using Weblate (Latvian)

Currently translated at 4.5% (13 of 288 strings)

Added translation using Weblate (Latvian)

Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lv/
Translation: Spoolman/Web Client

* Translated using Weblate (Russian)

Currently translated at 100.0% (288 of 288 strings)

Translation: Spoolman/Web Client
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/

---------

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Co-authored-by: srbjessen <srbjessen@gmail.com>
Co-authored-by: Miloslav Kos <kos.m@post.cz>
Co-authored-by: 김태남 <imedia0@gmail.com>
Co-authored-by: Kayz C <kayzed.x@gmail.com>
Co-authored-by: jernejp21 <jernejp21@tuta.com>
Co-authored-by: Pavol Vrba <pvrba2000@gmail.com>
Co-authored-by: Tamas Veres <tamas.veres@gmail.com>
Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com>
Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com>
Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
2026-07-29 18:06:17 +00:00
Donkie 17f8b9fee1 client_v2: add Playwright frontend integration tests
Mirrors tests_frontend, but drives the Svelte client instead of the legacy
React one: same production image, same PostgreSQL-backed compose stack, same
"only the first navigation uses a URL" discipline.

- smoke.spec.ts clicks through every top-bar tab, asserting each route's title
  and the app shell, with no browser console errors; a second test proves the
  library reached the API rather than falling into its offline state.
- crud.spec.ts walks the first-use happy path. client_v2 folds the legacy
  client's three create forms into one "Add spools" modal, so a manufacturer,
  a filament and a spool are created by a single submit, then verified back in
  the library list and on the locations page.

Two client_v2-specific helpers earn their keep: the top bar renders its nav
twice (desktop + mobile, one hidden by CSS) so nav locators need narrowing to
the visible one, and NumberInput's accessible name picks up its stepper and
help buttons, so numeric fields are found via their form item instead of
getByLabel.

The suite publishes on host port 8001 so it can run alongside the legacy one.
CI gains a test-frontend-v2 job cloned from test-frontend, gating
publish-images alongside it.

Also fixes tests_frontend/run.py: it only built client/dist, but the Dockerfile
copies client_v2/build unconditionally, so the image could not be built from a
clean checkout.
2026-07-29 19:08:12 +02:00
Donkie 6af053b682 client_v2: split the accent fill from --accent so white text clears AA
White on the dark theme's --accent (#be682f) is 4.03:1, so every solid accent
button failed WCAG AA -- the a11y audit flagged .btn.primary and .seg-btn.active
as serious. No foreground fixes it: on #be682f the best any color reaches is
5.21:1 (pure black), and every readable one scores worse than white.

So the fill becomes its own token rather than the accent moving. --accent-fill is
#b2612c, the minimal same-hue, same-saturation darkening that clears 4.5:1 (4.53)
-- 3.3 L* below --accent and imperceptible beside it. --accent is untouched and
still drives links, borders, washes and focus rings, where it sits on a dark
surface and passes comfortably.

Worth being precise about what is and isn't the brand color here: Spoolman orange
is #dc7734 (static/spoolman.svg, and the v1 client's colorPrimary). It appears
nowhere in client_v2/src -- both themes already ship a darkened adaptation of it,
7.5 L* down in dark and 12 L* in light, the latter labelled as such in app.css.
White on the brand orange itself would be 3.12:1. So this is a third step along an
adaptation already two steps in, not a new brand color, and the logo is untouched.

The fill DARKENS on hover (--accent-fill-hover #9b5526, 5.64:1) where --accent
lightens; lightening would drop straight back below AA. The light theme already
darkened on hover, so the two themes now agree. Light's own fill tokens just
mirror its existing accent pair (4.72:1 / 5.88:1) -- they exist so both themes
resolve the same variables, not because light needed changing.

Applied to every white-on-accent surface, not just the two axe caught: primary
buttons, the extra-fields manager's primary button, the mobile add button, the
spool inspector's active mode button, the settings segmented tab, and the date
picker's selected day and Done button. Toggle's track and the locations loading
bar keep plain --accent -- no text sits on them.

Audit is now clean on all six surfaces in both themes: zero axe violations.
2026-07-29 11:08:03 +02:00
Donkie 2befcfc389 client_v2: give inspector form controls accessible names
The a11y audit reports 14 `label` and 2 `select-name` violations on the inspector
drawer, both critical, in both themes. Every editable field in the filament, spool
and vendor inspectors is affected: a screen reader announces the whole panel as a
column of unnamed edit boxes, so it is unusable there.

The cause is structural. AddSpoolModal wraps each control in a <label>, which names
it implicitly -- that surface has no violations. The inspectors instead use
FieldGrid, a two-column grid where <Field> emits the label as one cell and the
control as its sibling. Nothing connects them.

<Field> now publishes the id of its label text through context, and the generic
controls that can sit in a value cell -- EditableField, NumberInput, Combobox --
point aria-labelledby at it. Outside a <Field> the context is undefined and the
attribute is omitted, so the same components keep their implicit labels in
AddSpoolModal. aria-labelledby rather than <label for>: a value cell can hold two
controls (a range's min/max) or no labelable one at all (ColorEditor), where a
`for` would have to pick exactly one and would dangle when there isn't one. The id
goes on an inner span around just the label text, because the ⓘ help toggle is a
sibling and would otherwise fold "Help" into every name.

ExtraFieldInput names its inputs from field.name directly, since it already knows
it; range inputs get "<field> min"/"<field> max" so the pair is distinguishable.
This also fixes its boolean Toggle, which announced the type ("Boolean") rather
than the field, so every boolean extra field had the same name.

Audit is clean on all six surfaces in both themes. Verified the names resolve to
the visible labels rather than merely existing -- Name, Material, Diameter,
Density, ..., Integer range min, Integer range max, Single choice.
2026-07-29 10:55:02 +02:00
Donkie 8f7794e487 client_v2: make the FilamentList live refetch leading-edge
The list coalesces websocket events because one revision bump refetches the group
page and every visible GroupRow, so a burst -- a bulk import, a printer streaming
use updates, the initial sync -- would otherwise fire that whole fan-out per event.

It did so with a trailing debounce, which charges the 300 ms to every event rather
than to bursts. The overwhelmingly common case is a single change: one edit in
another tab, one printer reporting one spool. Each of those waited out the full
window before anything moved, which is exactly the latency a live view should not
have, and the burst it was paying for never arrived.

Same window, leading edge instead: the first event refetches immediately and opens
a cooldown, and anything arriving during it is folded into one catch-up refetch when
the cooldown ends. An isolated change is now instant; a burst still costs one
fan-out per window rather than one per event. The magic number is named
COALESCE_MS and the comment says what the window is trading off.
2026-07-29 10:37:12 +02:00
Donkie 7ab45ca1ab client_v2: don't quote "0 g each" for unused spools of unknown weight
UnusedRow's sameWeight check only asks whether the collapsed spools agree, so a
group where they all agree on nothing takes the "Y g each" branch. mapSpool falls
back to 0 when a spool has no initial_weight and its filament has no weight either,
which makes that branch print "0 g each" and the divider "N unused - 0 g each" for
spools that are in fact full.

Treat a weight of 0 as unknown and fall through to the mixed variants, which state
the count and location and simply omit the quantity -- the same thing already done
when the spools genuinely differ.
2026-07-29 10:36:48 +02:00
Donkie e8b363c10b client_v2: restore the muted/dim/faint contrast ramp
18214ee fixed a real WCAG AA failure by brightening --text-muted/--text-dim/
--text-faint (dark) and darkening them (light), but it moved all three to nearly
the same place: 0xa0/0x97/0x90 in dark and 0x5f/0x66/0x6d in light, about 2.6 L*
apart. Three tokens exist to express three levels of emphasis; at that spacing they
render as one gray and the hierarchy they encode is gone.

The squeeze came from anchoring the whole ramp above the AA floor rather than on it.
#959595 is the darkest gray that clears 4.5:1 on the lightest dark surface
(--surface-raised #2e2e2e), so faint now sits exactly there and muted/dim step up
from it in even ~7 L* increments -- the same perceptual spacing the pre-AA ramp had.
Light mirrors it from the #676767 ceiling over --surface-sunken #e8e6e2, with
tighter ~4.5 L* steps because --text-2 #444444 caps the range.

Contrast against the worst-case surface in each theme is 7.0/5.6/4.5 (dark) and
6.3/5.4/4.5 (light). Every one of the six values moved further from its background
than 18214ee left it, so this cannot regress the audit it was fixing. The comments
now record where the floor comes from, so the next person retuning these knows
which end is fixed and why.
2026-07-29 10:36:27 +02:00
Donkie 64e94bd97f client_v2: re-enable no-navigation-without-resolve, scoped per file
5eb660f turned svelte/no-navigation-without-resolve off globally because every hit
at the time was a false positive: the href had already been resolved by a params.ts
helper or handed in as a pre-resolved prop. Switching the rule off repo-wide also
removed the only automated guard against the real bug it exists to catch, and that
bug is one nobody developing at the root path can see -- a missing resolve() works
perfectly until an operator deploys under SPOOLMAN_BASE_PATH.

The rule is on again. The eleven known-good sites are silenced where they live:

- Ten .svelte files carry a file-level disable inside their <script>, with the
  reason attached via the `--` description syntax. Per-line disables don't work for
  most of them -- the rule anchors on the href attribute, which prettier puts on its
  own line inside a multi-line element, where an HTML comment can't go.
- params.ts's two goto() calls take eslint-disable-next-line each. Both targets are
  base-path-independent already: a bare `?query` resolves against the current URL,
  and the other is built from resolve('/').

A new file, or a new component that builds an href itself, is caught again. Verified
by adding a raw <a href="/spool/show/1"> and watching the rule fire.
2026-07-29 10:36:03 +02:00
Donkie 89ffd3e4b1 Key the external filament catalog cache on size as well as mtime
_load_filaments memoizes the parsed catalog on the cache file's st_mtime alone.
Two writes landing in the same mtime tick therefore look identical to the cache and
the second one is never picked up -- the process keeps serving the stale parse until
some later sync happens to change the stamp.

The window is small on Linux, where st_mtime is nanosecond-resolution, but it is not
theoretical: Spoolman's data directory is routinely a bind mount, and filesystems
with 1-second mtime granularity are common there. A catalog refresh triggered close
after another write would be silently dropped.

Including st_size makes the key sensitive to any rewrite that changes length, which
is the realistic shape of a catalog update.
2026-07-29 10:35:50 +02:00
Donkie 2e396d6cda Fail loudly when the SPA fallback base-path rewrite matches nothing
load_and_rewrite_fallback_base_path does blind string replacement on 200.html to
insert the operator's base path into SvelteKit's absolute asset references and its
hardcoded `base: ""`. str.replace on a pattern that isn't there is a silent no-op,
so a SvelteKit upgrade that changes any of those three emission shapes would ship a
fallback document whose assets 404 and whose client-side router builds URLs without
the base path.

That failure is invisible in development and in CI, both of which run at the root
path where this code doesn't execute at all. It would surface only on an operator's
reverse-proxied instance, as a blank page on exactly the routes the rewrite exists
to serve -- direct loads of /spool/show/<id>, the target of printed QR labels.

Check each pattern is present before rewriting and raise naming the ones that are
not. A build/server mismatch is a packaging error, so refusing to start with an
actionable message beats serving a broken page.
2026-07-29 10:35:41 +02:00
Donkie f08e27c6fd Record why remaining_length does not need the dropped fan-out
Comments only. 2924850 removed the filament->spool fan-out, but neither comment
named remaining_length, which is the field 6ade405 added the fan-out for. That
omission is how someone re-adds it.

Spell out that remaining_length is derived from density and diameter and is not
cached client-side -- SpoolInspector computes it from the $derived filament, so
the filament event alone refreshes it -- while the stored initial/remaining
weights are the part that genuinely needs reweighSpoolsOfFilament.
2026-07-28 21:16:24 +02:00
Donkie 733dcf9209 client_v2: add vitest and cover migrateV1 and format
client_v2 had no unit tests and no test script; the only spec was the Playwright
a11y audit, which is a separate run.

Adds vitest (`npm test`) configured for src/**/*.test.ts, and starts with the two
files the review called out.

labels/migrateV1.ts is the priority: it rewrites label templates and print presets
users authored in the v1 client, runs unattended at first load, and its output is
what they see afterwards -- a silent mistranslation quietly damages work they
can't recover. Covers the whole path-rewriting table, the bare/wrapped/extra-field
token forms, that a path with no v2 equivalent is left alone rather than mangled,
and the guards that keep a hand-edited preset from yielding a zero-size canvas.

utils/format.ts is second: it formats every weight, length and percentage on
screen. Pins the deliberate rounding rules -- notably that weightAuto floors kg so
a displayed "1.1 kg" never overstates what is on the spool -- and checks
lengthMeters against the known ~335 m for 1 kg of 1.75 mm PLA.

40 tests.
2026-07-28 21:03:48 +02:00
Donkie 1262eb664a Document SPOOLMAN_LEGACY_CLIENT in .env.example
The var is the operator's rollback path if the new client misbehaves, but it was
only discoverable by reading env.py or the Dockerfile. Nothing else documents
Spoolman's env vars, so .env.example is the place.
2026-07-28 20:56:17 +02:00
Donkie 9a93892a91 Add allow_archived to /search and stop the candidate cap hiding new rows
Two gaps in the new /search endpoint, neither yet released:

Archived spools were always included, unlike GET /spool. /search now takes
allow_archived (default false) and applies the same false-or-null clause
spool.find uses, so the two endpoints agree on what "archived" hides. The
exact-id shortcut honours it too -- otherwise searching an archived spool's id
surfaced it regardless.

Candidate selection ordered by id ASC before LIMIT 200, so on a database with
more than 200 matches for a term only the 200 lowest ids were ever ranked and a
newly added spool could not be found at all. Ordering descending makes the newest
rows the ones that get ranked, which is the useful bias for an inventory that
only grows. Ranking still re-sorts the survivors by match quality, so this
changes which rows are considered, not how results are ordered.

Integration tests cover both defaults and both opt-ins.
2026-07-28 20:56:10 +02:00
Donkie 2924850317 Drop the filament->spool websocket fan-out
filament.update re-broadcast every spool of the filament (archived included)
after commit, so a filament shared by hundreds of spools turned a single PATCH
into hundreds of ORM loads and websocket frames inside one request. This is the
unbounded fan-out flagged in review; the same reasoning is why no equivalent was
ever added for vendor.update.

Subscribers now treat a filament event as invalidating the spools that reference
it. In client_v2 the cache is already normalized -- spools hold a filamentId and
resolve their filament through filamentById -- so nearly everything a spool
displays was live regardless. The exception is a spool with no initial_weight of
its own, whose initial/remaining weights the server derived from filament.weight.
The cache now recomputes exactly those, mirroring Spool.from_db's formula
including its clamp at 0, which needs used_weight and the initial_weight override
carried on the cached spool.

Reverts the behavior added in 6ade405 (unreleased). Its integration test is
repurposed to pin the decision from both sides: no spool event reaches a /spool
subscriber, and the filament event subscribers rely on is still emitted.
2026-07-28 20:51:36 +02:00
Donkie b369cf049e Clamp grouped remaining weight at 0 for over-used spools
Spool.from_db reports remaining_weight as max(initial - used, 0), but
find_groups' remaining_expr omitted the clamp. A spool used past empty
therefore contributed a negative number to total_remaining_weight, so a
group's total no longer matched the sum of the spools it contains — two
500 g spools, one with 200 g used and one with 800 g, reported 0 g instead
of 300 g.

Mirror the clamp with CASE rather than func.greatest, which is not portable
across all four supported databases. Covered by a new integration test.
2026-07-28 20:27:59 +02:00
Donkie d1ab338b91 client_v2: stop dropdown Escape from closing the add-spool modal
AddSpoolModal closes on any Escape reaching <svelte:window>, but Combobox and
DateTimeField let their own Escape keep propagating. Dismissing a location,
vendor or material dropdown — or the date popover — inside the modal therefore
discarded the whole form. Regression from bf3896b, which moved the modal's
keyboard close onto the window.

Both now stopPropagation() after closing their own popup. DateTimeField's
listener is on document, which is still ahead of window in the bubble path.
2026-07-28 20:27:37 +02:00
Donkie a2e9e66153 client_v2: fix WS reconnect race clobbering the live socket
reconnectAllNow() skips only OPEN/CONNECTING sockets, so a wake that fires
while a socket is still CLOSING opens a replacement. The old socket's onclose
then unconditionally cleared sock.pingTimer and nulled sock.ws, killing the
replacement's keepalive and leaving a live socket the code believes is down.

Guard the handlers with an identity check: onopen/onclose only touch the shared
ResourceSocket state while they are still the current socket, and each socket
now clears its own keepalive interval so a superseded one can't leak it.
2026-07-28 20:27:15 +02:00
Donkie 5d812e269e fix: return 400 instead of 500 for malformed sort parameters
Each list endpoint parsed `sort` inline with a bare split(":"), so `?sort=name`
raised "not enough values to unpack" and `?sort=name:sideways` a KeyError from
SortOrder[...]. Both surfaced as a 500 (todo.md task 8).

parse_nested_field also gated on hasattr, so any attribute that merely existed
passed through to order_by() -- `?sort=metadata:asc` got as far as calling
.asc() on the MetaData object. It now checks the SQLAlchemy mapper's columns,
so only real columns are accepted; relationship traversal through filament. and
vendor. still works.

Sort parsing moves into a shared parse_sort() used by all four call sites, which
tolerates surrounding whitespace and rejects a missing colon, an empty field
name or an unknown direction with a message saying what was expected.

database/vendor.py turned out not to use parse_nested_field at all -- it did a
bare getattr(models.Vendor, fieldstr) -- so /vendor kept returning 500 even
after the helper was fixed. It now uses the shared helper like filament.py.

All three endpoints verified end-to-end: 400 for each malformed form, 200 for
valid single and multi-field sorts, and no unhandled exceptions in the log.
2026-07-27 20:03:14 +02:00
Donkie 7d683eb246 security: escape CSV formulas and bound extra-field sizes
Two lower-severity findings from the audit (todo.md tasks 6 and 7).

A spreadsheet executes a cell that begins with =, +, -, @, tab or CR, so a
vendor named `=cmd|' /C calc'!A0` runs when someone opens the CSV export
(CWE-1236). Such cells are now prefixed with a single quote, which spreadsheets
read as "this is literal text" and do not display. Only strings are escaped --
numbers reach the writer as numeric types, so a negative number cannot be
mistaken for a formula and must not grow a stray quote. Header names need no
escaping: they are fixed attribute names or extra-field keys, which are already
constrained to ^[a-z0-9_]+$.

Exports also gained Content-Disposition, so they download under a sensible name
rather than rendering in the browser.

Extra-field values had no size limit and the columns are Text(), so a single
request could store megabytes: unbounded database growth, and the resulting
websocket event overflowed the 1 MiB default frame limit, which killed live
updates for every connected client. Values are now capped at the same 65535
characters as settings. The number of extra fields per entity is capped too --
the per-entity key count was already bounded, since unknown keys are rejected,
but the registry itself could be grown without limit, and it is cached in memory
and embedded in every entity response.

Finally, a setting larger than the column limit returned 500; the ValueError is
now caught and returned as a 400.

Verified end-to-end: a 2 MB extra value is refused on both create and update, an
oversized setting returns 400 and leaves the setting unset, normal values still
work, and a vendor named `=cmd|' /C calc'!A0` exports as `'=cmd|' /C calc'!A0`
while the JSON export keeps the raw name.
2026-07-27 19:58:52 +02:00
Donkie 1b1dbbb78f security: drop CORS credentials under a wildcard origin, document the model
allow_origins=["*"] with allow_credentials=True makes Starlette echo the
caller's origin back and set Access-Control-Allow-Credentials, which tells the
browser it may send cookies to whichever site asked. Spoolman has no cookies of
its own, so this looked harmless, but plenty of instances sit behind a reverse
proxy that does the authentication with one -- and there it let any website the
user visited make authenticated requests as them. Now credentials are refused
whenever the resolved origin list contains "*" (todo.md task 5).

Adds a README Security section. The README has no configuration reference at
all -- that lives in the Wiki -- so rather than starting one, this documents the
things a user has to know before deciding where to run Spoolman: that there is
no authentication by design, that it belongs behind an authenticating reverse
proxy if it is reachable beyond the LAN, what SPOOLMAN_CORS_ORIGIN is for, why
"*" is a bad idea on a shared network, and that a reverse proxy needs to forward
the original Host.

Verified with SPOOLMAN_CORS_ORIGIN=* against a real server: the response now
carries `access-control-allow-origin: *` and no credentials header, where the
audit saw the caller's origin echoed with credentials allowed.
2026-07-27 19:36:47 +02:00
Donkie 404b03173b security: stop backup rotation from churning away the restore history
Rotating deletes the oldest restore point and shifts the rest down, so enough
calls in quick succession leave nothing but snapshots of whatever state the
database is in now. POST /backup takes no parameters, so a bodyless cross-origin
form post reached it; the audit destroyed a full six-deep history that way
(todo.md task 2).

The origin guard from the previous commit already refuses that request, but the
rotation itself should not be this easy to abuse, so two further changes:

Rotation is skipped when the database has not actually changed. Not by comparing
the live database file, which the audit suggested: under WAL the live file does
not yet contain recent commits, so comparing it would skip backups that were
genuinely needed. Instead the snapshot is written to a pending file first and
compared against the newest backup -- sqlite's backup API reads through the
connection, so it sees WAL content. Identical means the pending file is
discarded and the history is left alone. This is fail-safe: if the comparison
ever came out unequal for an unchanged database, the result is the old behaviour
of rotating, never a missed backup.

Rotation is also rate-limited to one per five minutes, returning the newest
existing backup instead. Never applied when there is no backup to fall back on.

BackupResponse gained a `created` field so a client can tell the difference.
Additive, so v1-compatible, but load-bearing: telling a user they have a fresh
restore point when they do not is exactly the wrong failure mode here.

Verified against a real server -- six rapid calls now leave exactly one backup
and never rotate.
2026-07-27 19:33:07 +02:00
Donkie c33f65243c security: refuse cross-origin writes and websockets, and non-JSON setting bodies
Closes the two confirmed CSRF findings and the cross-site websocket hijack
from the backend security audit (todo.md tasks 1 and 3).

POST /setting/{key} took a bare `str` body, and FastAPI only JSON-parses
application/*json -- anything else arrived as raw bytes that lax-mode Pydantic
coerced. A text/plain body was therefore accepted, which is exactly what
<form enctype="text/plain"> sends, so any website the user visited could
rewrite base_url, label_designs, locations or extra_fields_* through their
browser. Now requires a JSON content type.

The audit suggested Body(media_type="application/json") for this, but that only
annotates the OpenAPI schema and is not enforced at runtime -- a text/plain post
to a pinned endpoint still returns 200. Hence an explicit dependency. Both
clients already send application/json, and a charset parameter is accepted, so
this is wire-compatible.

TrustedOriginMiddleware refuses state-changing HTTP methods and every websocket
handshake from an untrusted origin, using the helper added in the previous
commit. A middleware rather than per-endpoint dependencies so that none of the
nine websocket endpoints can be missed, and one added later cannot forget to
opt in. Reads are left alone -- the same-origin policy already covers them --
but every websocket is guarded, since websockets are exempt from CORS and
otherwise leak the whole inventory to any origin that opens one.

It is ordered inside the CORS middleware so a legitimate cross-origin client
can read the 403 body instead of an opaque CORS error.

Writing a malformed extra_fields_* array through the settings endpoint bypassed
validate_extra_field and left GET /field/{entity} permanently returning 500;
that array is now validated, and the registry cache is invalidated on write so
the /setting and /field paths no longer disagree until restart.

Verified against a real uvicorn server by replaying the audit's exploit: the
text/plain write is refused, a bodyless form post that would have reset a
setting is refused, a websocket from a foreign origin gets HTTP 403 on the
handshake, and clients that send no Origin at all -- Moonraker, OctoPrint,
Home Assistant, curl -- are unaffected on both HTTP and websockets.
2026-07-27 19:24:59 +02:00
Donkie 7d4728ae9b security: add shared origin-trust helper
Tasks 1-3 of the backend security audit all need to answer "is this browser
origin someone we trust". Build it once, driven entirely by the existing
SPOOLMAN_CORS_ORIGIN rather than a new env var.

- Unset means same-origin only, which is right for nearly every deployment.
  Non-browser clients (Moonraker, OctoPrint) send no Origin and are unaffected.
- A list means those origins plus our own.
- "*" and debug mode mean "I do not want origin checks" and are honoured as
  such, with a loud startup warning. An operator who writes "*" is opting out;
  reinterpreting it as something narrower would leave them no way to say so.

Also accept X-Forwarded-Host, so a reverse proxy that rewrites Host (nginx
without proxy_set_header Host $host, Apache without ProxyPreserveHost On) does
not see the guard reject its own web UI. A malicious page cannot set that
header on any of the vectors the guard exists to stop: forms cannot set request
headers, nor can the browser WebSocket API, and a fetch that adds one needs a
CORS preflight it will not get.

get_cors_origin() now normalizes its entries -- it was a bare split(","), so
"https://a, https://b" yielded " https://b", which no Origin header can match.
CORS itself benefits from the same fix. The raw value is kept in the startup
log so a typo stays visible.

Adds tests/ for backend unit tests, runnable with `poe test` and wired into the
lefthook ci group. Nothing enforces this helper yet; that is tasks 1-3.
2026-07-27 08:51:11 +02:00
Donkie 06ae678586 client_v2: stop idle-tab data/CPU drain (WS backoff + coalesced refetch)
A tab left open would rack up gigabytes of transfer and heavy lag over
time. Two causes, both fixed:

- live.ts retried a dropped backend every fixed 2s forever (x3 sockets,
  no backoff). Each failed connect is a console error the browser can't
  GC, so an overnight tab bloated memory and janked. Now backs off
  1s->30s with jitter, resets on connect, and reconnects immediately on
  'online' / tab-visible (laptop sleep/wake). Measured churn ~15x lower.

- Every live event refetched the group list AND every visible GroupRow
  (~21-31 uncached requests) once per event. A burst (bulk import,
  printer streaming use-updates, initial sync) detonated that fan-out
  per event. A 300ms debounce collapses each burst into one refetch
  cycle (~10x fewer requests on a 20-event burst).
2026-07-26 20:37:27 +02:00
Donkie bf3896b1f2 client_v2: fix nested-interactive a11y on add-spool modal
The overlay was role=button wrapping the whole dialog, so screen readers
announced every control as nested inside one button. Make the click-outside
catcher a sibling <button> behind the modal instead of a parent, and move
Escape-to-close to a window keydown listener.
2026-07-26 16:30:55 +02:00
Donkie 1d5164b629 client_v2: label form controls for a11y (select-name/label)
Add aria-labels to the Settings language select, currency/base-URL/
low-stock inputs, and the Labels toolbar + preview-bar selects, which
axe-core flagged as unnamed (WCAG select-name/label, critical) under the
mobile a11y audit. Adds a labels.selectDesign message key.
2026-07-26 16:22:46 +02:00
Donkie cd5ef6f8d6 client_v2: close Combobox dropdown on blur (tab-away)
The suggestion list only closed on Escape, outside pointerdown, or
selection, so tabbing focus out of the input left the portaled dropdown
open. Add an onblur handler that closes the list, guarding against blurs
that hand focus into the list itself.
2026-07-26 10:34:28 +02:00
Donkie f6827c0a69 client_v2: improve mobile tap targets and inspector actions
Address the pervasive sub-44px tap-target audit finding and related
mobile UX issues:

- Add a centralized 44px touch-target layer in app.css (gated on the
  existing <=860px mobile breakpoint) raising the shared Button/tab/chip/
  select/etc. primitives; grow TopBar scan/add to 44px and the number
  steppers so they clear the WCAG 24px floor. Audit: 148 -> 46 sub-44px.
- Collapse the mobile search into an icon in the top row that expands to a
  full-width overlay on tap, dropping the top bar from three rows to two.
  Give the search toggle the same outlined look as the scan button.
- Fix breadcrumb links not vertically centering inside their 44px tap area.
- Stop hiding the spool/filament inspector action rows on narrow panels;
  reflow them onto their own row, and drop to icon-only (labels kept in the
  a11y tree) below 560px so three actions stay on a single row.
2026-07-25 21:22:47 +02:00
Donkie 18214ee0f8 client_v2: fix low-contrast text/accent tokens for WCAG AA
The mobile a11y audit reported color-contrast (serious) on ~11-17 nodes
per page, the nav tabs being the recurring offender. Every flagged node
routed through a design token, so retune the tokens rather than patch
components: --text-muted/--text-dim/--text-faint and light --accent-soft
now clear 4.5:1 on the surfaces they sit on in both themes (down to the
lightest dark surface and darkest light surface), keeping the
muted->dim->faint ramp monotonic. --text-2 already passed and is
unchanged.
2026-07-25 20:30:59 +02:00
Donkie 386ec36d1f client_v2: set <html lang>/<dir> to the resolved locale at runtime
app.html shipped `%paraglide.lang%`/`%paraglide.dir%` placeholders, but SSR
is off and there is no handle hook, so they were never substituted — axe
reported html-lang-valid (serious) on every page from the literal value.

Ship a valid `lang="en" dir="ltr"` static default and apply the real locale
(getLocale/getTextDirection) to <html> in +layout.svelte at mount, alongside
where the theme is applied. Verified in-browser: en→ltr, de→ltr, fa→rtl.

Also remove the unused Locale class in paraglide.svelte.ts (dead code, never
imported).
2026-07-25 20:16:20 +02:00
Donkie 8a5cf7ec73 client_v2: add mobile accessibility audit (Playwright + axe-core)
Adds an npm run audit:a11y target that runs axe-core (WCAG 2.0/2.1 A+AA
plus the 2.2 target-size rule) and a custom tap-target measurement across
the main surfaces under Pixel 5 emulation, reusing the dev server on 5174.
Results are aggregated to a11y-report.md via globalTeardown so they survive
Playwright worker restarts.
2026-07-25 19:07:24 +02:00
Donkie b7772ca743 client_v2: fix base path for direct loads of non-prerendered routes
SvelteKit's adapter-static emits the SPA fallback (200.html) with absolute
asset paths (/_app/...) and a hardcoded base: "", since it can't know the
deploy base path at build time. Prerendered per-route documents use relative
paths and a runtime-computed base, so they work under any base path, but the
fallback is used for non-prerendered dynamic routes (/spool/show/<id>,
/filament/show/<id>) -- exactly the URLs printed into QR labels. Under a base
path those direct loads 404'd every asset and rendered a blank page.

Rewrite the fallback server-side using the base path the backend already
knows, mirroring the legacy-client asset rewrite: prefix /_app/ and /favicon
references and set the runtime base. Prerendered docs are still served
verbatim, and the legacy client is unaffected (its tweaked_html is always
produced when rewrite_asset_paths is on, so the file_response guard change is
equivalent there).
2026-07-25 17:49:11 +02:00
Donkie 2f9259227d client_v2: drop "g each" from unused summary when spool weights differ 2026-07-24 10:49:51 +02:00
Donkie e6e3be2773 client_v2: add filament total-remaining summary; show large weights in kg
Add an aggregate remaining-weight line to the FilamentInspector spools
section (respects the showArchived toggle), answering the recurring
total-available ask (#572/#347/#265).

Switch inventory weight displays (group totals, library list, both
inspectors) to weightAuto so amounts >=1000 g render in kg. Floor the
kg value to one decimal so the figure never overstates what's on the
spool. Remove the now-unused kg() helper.
2026-07-24 10:41:24 +02:00
Donkie eced411a51 ci: bump issue-manager to 0.8.1 and fix waiting delay
Bump tiangolo/issue-manager 0.5.1 -> 0.8.1. Correct the waiting label
delay from 60400s (~16.8h) to 604800s (7 days) to match its message.
2026-07-24 10:22:08 +02:00
Donkie 7e309e36b9 client_v2: add material suggestions to filament inspector
Extract the AddSpoolModal material-list building into a shared, session-
cached loadMaterials() helper (mirrors getExternalMaterials' cache), and
use it to give the filament inspector's material field autocomplete via
Combobox. The inspector uses only the name suggestions and never prefills
density/diameter/temps, unlike the add-spool form.
2026-07-24 09:56:29 +02:00
DonkieandClaude Opus 4.8 054c59948e client_v2: portal inspector popovers to fix mobile misplacement
The Combobox location dropdown and DateTimeField calendar are position:fixed
and anchored to their trigger's viewport rect. On mobile the inspector renders
inside DetailPane's bottom sheet (transform) and the .insp panel
(container-type), both of which become the containing block for fixed
descendants, re-basing the popover's coordinates and dropping it far below its
field.

Add a portal action that reparents the popover to <body> so position:fixed
anchors to the viewport regardless of offsetting ancestors. DateTimeField's
outside-click handler now also checks the portaled popover so picking a date
doesn't close it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:40:30 +02:00
DonkieandClaude Opus 4.8 5222df98a3 client_v2: hide color swatch on non-filament group headers
Group headers rendered a Swatch unconditionally, but only filament
groups carry colors — vendor/material/location groups pass an empty
list, which swatchStyle() paints as a meaningless gray (#555) chip.
Only render the swatch when the group actually has colors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:30:29 +02:00
DonkieandClaude Opus 4.8 5eb660f420 client_v2: add ESLint + wire lint/format/typecheck into lefthook
client_v2 has matured, so enforce its checks like the old client:

- Add ESLint (typescript-eslint + eslint-plugin-svelte) with a flat
  config; `lint` now runs prettier --check and eslint.
- Add a "Client v2" group to lefthook pre-commit and ci: eslint,
  prettier, and svelte-check type checking (svelte-kit sync first).
- Exclude generated paraglide output from prettier.

Fix the linting warnings this surfaced: drop dead vars and an unused
svelte-ignore, mark intentional reactive reads with `void`, remove a
useless children snippet, and clean up useless string-literal mustaches.

Disable svelte/no-navigation-without-resolve (base-path resolution is
centralized in params.ts helpers, so the rule only false-positives and
wrapping call sites would double-apply the base path). Keep
svelte/prefer-svelte-reactivity on with per-line disables at the
transient-local Set/Map sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:23:17 +02:00
Donkie 52b0580df9 removed unused locales 2026-07-23 09:04:49 +02:00
Donkie 01be7b7572 minor package lock json update 2026-07-23 09:01:40 +02:00
DonkieandClaude Opus 4.8 f69d098473 client_v2: approve esbuild install script for npm 11 allowScripts
npm 11 holds back dependency install scripts unless allow-listed, printing
a notice for esbuild's postinstall. Approve it explicitly so `npm install`
runs cleanly; older npm versions ignore the unknown field harmlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:00:16 +02:00
Donkie df2cd09bcc client_v2: use real <a href> links for navigation
Navigation across the app was done with <button onclick> calling the
params.* router helpers, so the browser never treated these as links: no
middle-click/ctrl-click open-in-new-tab, no copy-link-address, no hover
URL preview. Add href-builder twins of the URL mutators in
library/params.ts (selectHref, selectHrefFromState, pageHrefFromState,
libraryHref, searchResultHref) and point real anchors at them; SvelteKit
intercepts the clicks for SPA nav while data-sveltekit-keepfocus/-noscroll
preserve the old goto() semantics.

Converted: spool/filament rows in the library list and inspectors,
breadcrumbs, filament/vendor group headers (material/location headers,
which navigated nowhere, are now inert divs), the spool inspector's
open-filament link and print-label button (Button gains an optional
href), search results, location chips (still drag items), and pagination.
Genuinely non-navigational actions (modals, sort/filter/group toggles,
expand/collapse, label-canvas selection) stay buttons.
2026-07-23 08:56:39 +02:00
Donkie 6e1065009c Bump version to 0.25.0 2026-07-22 14:10:59 +00:00
Aleksei Sviridkin c2c6a8dd8f fix(entrypoint): reject an unusable PUID or PGID (#962)
A non-numeric PUID or PGID makes the [ -ne ] comparison error out, so usermod
is skipped and the app silently runs as the default UID 1000 instead of the
requested one. PUID 0 is worse: the container starts as root and drops to the
"app" user via gosu, so a PUID of 0 leaves that user at uid 0, the "am I still
root?" check never turns false, and the entrypoint re-execs itself forever.

Reject both up front with a clear message.

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-22 16:09:22 +02:00
dependabot[bot] 0076058280 Bump fast-uri from 3.1.3 to 3.1.4 in /client (#972)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 16:08:58 +02:00
dependabot[bot] 54b67f53ef Bump setuptools from 78.1.1 to 83.0.0 (#970)
Bumps [setuptools](https://github.com/pypa/setuptools) from 78.1.1 to 83.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v78.1.1...v83.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 83.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 15:56:09 +02:00
Aleksei Sviridkin 129650237a fix(docker): link libstdc++ into the armv7 greenlet extension (#971)
greenlet ships no 32-bit ARM wheel, so on armv7 it is compiled from source.
setuptools links the C++ extension with gcc, which leaves libstdc++ out of the
.so's NEEDED list. The extension then fails to import with an undefined
libstdc++ typeinfo symbol (_ZTVN10__cxxabiv120__si_class_type_infoE), which
takes the whole app down during the startup DB migration -- the container
crashloops on any armv7 host such as a 32-bit Raspberry Pi.

Add libstdc++ to the extension's NEEDED list with patchelf so the dynamic
loader pulls it in. amd64 and arm64 use a correctly linked prebuilt wheel and
are untouched.

Verified by patching the shipped 0.24.0 armv7 image's greenlet .so the same
way: `alembic upgrade head` then runs to completion instead of crashing.

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-22 15:55:33 +02:00
Aleksei Sviridkin 5c5e8b44e1 fix(entrypoint): ignore Kubernetes service-link value in SPOOLMAN_PORT (#961)
Kubernetes sets a Docker-style service-link variable for every Service that
exists in the namespace when a pod starts, so a Service named "spoolman" makes
SPOOLMAN_PORT=tcp://<clusterIP>:8000 inside the container. That is the Service
address, not a port, and uvicorn refuses to start on it, so the pod crashloops
- seemingly at random, since it only happens when the Service predates the pod.

Ignore the service-link form and keep the default port.

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 22:30:21 +02:00
Donkie a0987e063e Add smoke tests in CI for armv7/arm64 targets 2026-07-21 22:16:00 +02:00
Aleksei Sviridkin fc606ec8fc ci: sign the released images with cosign (#963)
Published images carry no signature, so there is no way for a user to verify
that the image they pull was built by this workflow from this repository.

Sign them keyless, using the Github OIDC token, so verification needs no key
material on either side. The manifest is signed by digest rather than by tag:
every tag produced for a release points at the same digest, and the same
digest is valid in both registries the build pushes to, so signing each tag
would only repeat the same signature.

Verify with:

    cosign verify ghcr.io/donkie/spoolman:<version> \
      --certificate-identity-regexp '^https://github\.com/Donkie/Spoolman/\.github/workflows/ci\.yml@refs/' \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 22:07:57 +02:00
Aleksei Sviridkin f7da164599 fix(externaldb): report the unwritable path instead of raising NameError (#964)
The fallback to an in-memory cache never worked when the data directory
itself is not writable. Evaluating cache_path calls get_cache_dir(), which
creates the data directory, so a PermissionError raised there propagates out
before cache_path is ever bound. The handler then dereferences it and dies
with "NameError: name 'cache_path' is not defined", which both hides the real
cause and takes the application down at import time, before the startup code
that would have reported the permission problem in plain language runs.

Take the path from the exception instead. It is set for both cases: the data
directory that could not be created, and the cache directory underneath it.

Verified against the published image by running it as a UID that owns neither
directory, which is what a restricted Kubernetes SecurityContext does:

  docker run --rm --user 1000670000:0 ghcr.io/donkie/spoolman:0.24.0

Before, that failed with the NameError above. Now it logs "Ensure the path
/.local is writable" and carries on to the regular startup check.

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 21:50:06 +02:00
Donkie 3d6888ec2c Added filament labels 2026-07-21 19:29:50 +02:00
Donkie ca8dba9e54 client_v2: redirect legacy /spool/show/<id> QR URLs to the library
Spoolman v1 QR labels (and the label designer's `url` encoding) point at
/spool/show/<id>, a route that no longer exists in client_v2. Add a
non-prerendered route whose load 307-redirects to /?sel=spool:<id>, so the
SPA fallback boots the app and the client-side redirect opens the spool.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 17:52:49 +02:00
Donkie 3a2fc68e1e client_v2: configurable custom QR URL target with {id} template
The QR "URL" encoding was hardcoded to <base_url>/spool/show/{id}. Add a
per-QR-element "Custom URL" encoding with a urlTemplate field so a printed
QR can point at a third-party scanner/app or a different host. The {id}
placeholder reuses the existing qrContent substitution.

The inspector seeds the field from the default URL form and warns that a
custom target won't scan back into Spoolman's own scanner. The no-spool
editor-canvas preview now renders the actual per-encoding template so the
QR module density matches what will print.

Requested in #68 and #741.
2026-07-21 17:45:53 +02:00
Donkie acb367262b client_v2: don't collapse a lone unused spool under a header
A group with a single unused spool now renders it inline as a normal
SpoolRow instead of behind an 'X unused' collapsing header, which added
a click and a row for no vertical-space benefit. The header only appears
when 2+ spools are unused.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 12:32:04 +02:00
Donkie 9b79135c1c client_v2: show an add-spools CTA when the library is empty
When the list has zero results and no filters are active the library is
genuinely empty (a fresh install), so nudge the user toward adding their
first spool instead of the generic "nothing matches the filters" line.
The CTA button opens the same add-spool modal as the top-right button.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 12:27:12 +02:00
Donkie ecd43bd51a backend: make color similarity search scale with matches, not catalog size
find_by_color loaded every filament as a full ORM object (with its vendor
join) into memory and computed delta-E in Python, then the endpoint re-queried
the matched ids anyway. The full-catalog ORM load dominated the cost: a color
search ran at a flat ~100ms regardless of how few results it returned, ~5-20x
slower than an equivalent regular filtered search, and each concurrent request
cloned the whole catalog into memory.

Scan only id/color_hex/multi_color_hexes and return the matched ids; callers
load full rows for just the matches. The matching algorithm is unchanged.
Color search now scales with the match count like any other filter: ~5-16x
faster here and the concurrency memory spike (+21MB at 8 concurrent) is gone.
Drops the "Slow operation!" note from the color_hex query param since it no
longer applies.

Verified across SQLite/Postgres/MariaDB/CockroachDB (372 integration tests
pass on each).

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 11:53:08 +02:00
Donkie 64d5f84bc3 Fixed swatch being messed up in addspoolmodal 2026-07-21 11:51:59 +02:00
Donkie 6ade40517f backend: notify spools when their filament changes
Spool responses carry fields derived from the filament (remaining_length,
plus the initial_weight/price fall-backs). Editing a filament emitted only a
filament WS event, so subscribers to that filament's spools kept stale derived
values until each spool was next touched — visible in client_v2 as a length
inconsistent with the displayed density, but affecting every consumer.

filament.update now re-broadcasts the filament's spools (archived included)
as spool "updated" events after commit. Adds a WS integration test asserting
a density patch reaches /api/v1/spool with the new density and changed length.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 10:58:09 +02:00
Donkie 990f16a8e9 client_v2: fetch deep-linked entities missing from the cache
The library inspector read purely from the reactive cache, which is only
filled by the list page, search and live events. A selection reached by
bookmark/QR/shared link (?sel=spool:4093) for an entity not on the loaded
page rendered the empty "select…" state.

Add fetchSpool/fetchFilament/fetchVendor single-entity fetches to
spoolSource (upserting the entity plus embedded relations; a 404 resolves
to undefined via swallow404). Inspector.svelte runs a fetch-on-select
effect that resolves a missing selection by id, showing a loading state
while in flight and a distinct not-found state for a deleted entity.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-21 10:45:39 +02:00
Donkie 9582962e37 client_v2: keep grouped-view sorting coherent
The Library's group list can only be ordered three ways by the backend
(group.title, group.total_remaining, group.last_used), yet the sort menu
offers ~20 fields. Picking any other sort while grouped silently fell back
to alphabetical group order and only reordered spools within each collapsed
group — an invisible, confusing no-op.

Enforce the invariant that a grouped view is only ever ordered by a
group-orderable sort (the axis's own name → group.title, remaining weight,
or last used). Picking a per-spool ranking now switches to the flat list
where the ranking is actually visible; regrouping while holding an
incompatible sort resets to the default group ordering. Axis-name sorts map
to group.title with a working direction toggle, and name/text sorts default
to ascending.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 23:17:28 +02:00
Donkie a34d52a597 client_v2: fall back empty locale values to base language
Weblate writes an empty string for every untranslated key. Paraglide
compiles a present-but-empty value into a message function that returns
"", so the site rendered a blank instead of falling back to English. A
key that is absent from a locale, by contrast, is re-exported from the
base locale and falls back correctly.

Add scripts/strip-empty-locales.mjs to delete empty/whitespace-only
leaves from the non-base locales/*/common.json before paraglide compiles
them, turning "present-but-empty" into "absent". Idempotent, base en
untouched, key order and formatting preserved. Wired as predev/prebuild
so builds stay correct if empties reappear from a later Weblate sync;
deliberately not a lint/CI gate, since empties are a benign expected
input to be neutralised, not forbidden.

Cleans 125 existing empties across 10 locales.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 16:32:04 +02:00
Donkie 5b5e07e2c1 client_v2: flatten color swatch and vary multi-color gradient
Replace the glossy 3D swatch (diagonal sheen + directional inset shade)
with a flat chip that keeps only two non-directional hairline rings for
edge definition on light and dark backgrounds.

Multi-color swatches now split into per-colour bands with softly feathered
boundaries instead of a full-slot blur, and the gradient angle follows the
filament's layout: coaxial splits side-by-side, longitudinal along the
strand, unknown stays diagonal. Direction is threaded through every Swatch
callsite, including the filament-grouped library header (already present in
the /spool/group response, just not mapped through before).

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 11:34:53 +02:00
Donkie 38397424ee client_v2: page grouped "show more" incrementally
The Library's grouped view fetched every "show more" click as a fresh window
from offset 0, so the request grew with each click (8 → 28 → 48 …), wasting
bandwidth and backend work. buildScopedSpoolQuery now takes an explicit offset,
and GroupRow's "show more" fetches only the next slice (offset = loaded count,
limit = 20) and appends it — a constant-size request per click.

The base-load effect still refetches from offset 0, but for as many rows as are
already shown (spools.length read via untrack, so appending doesn't re-trigger
it), so a live-event refresh keeps the expanded window instead of collapsing to
the first page. "Show more" is disabled during a base load so its append can't
race the replace, and its request aborts on unmount like the base one.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 11:17:03 +02:00
Donkie 128a0a4a34 client_v2: localize datetime formatting for extra fields and labels
Datetime extra fields rendered through their own path (native datetime-local
input when editable, new Date(v).toLocaleString() when readonly) while builtin
datetimes like Last Used render via DateTimeField with a localized label.
Extract that label logic into formatDateTime/dateLocale (utils/datetime.ts);
DateTimeField reuses it, and ExtraFieldInput now uses DateTimeField for editing
and formatDateTime for readonly display so both format identically.

Also localize two datetime helpers in map.ts: shortDate (registered/first-used
labels) used the browser locale instead of the app locale, and relTime
(last-used labels) hard-coded English unit letters. Both move to datetime.ts as
formatShortDate/formatDurationShort, using the app locale and Intl.NumberFormat
unit style. Formatters are memoized per locale since they run over every spool.

Also correct two mislabeled fields in the spool inspector's filament section
(weight and spool weight i18n keys).

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 11:05:10 +02:00
Donkie 4118ca5f2c client_v2: stop locations grid flickering as cards load
The locations page reflowed the whole grid every time a location's spool
query landed, on both axes:

- Vertical: each card started empty (~110px) and jumped to full height when
  its first page arrived, shoving cards below it down (bottom cards moved
  ~1162px on the 4158-spool stress DB). Reserve the body's final height up
  front from the location's spool count (known from the aggregate before any
  spool row is fetched), clamped to the same cap as max-height.

- Horizontal: a .shelf is a flex item, so its default min-width:auto grew as
  chips loaded, dropping the grid from N to N-1 columns and re-wrapping (cards
  jumped up to ~1575px sideways). Pin min-width:0 so column count depends only
  on flex-basis and available width, not content.

Measured with a per-animation-frame position sampler: horizontal travel now
0px and vertical ~4px (the loading bar) across 1440/1600/1920/2560 widths.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 09:06:25 +02:00
Donkie 2f25e76f5e client_v2: lighten print tab spool load, keep Recent complete
The print tab loaded up to 1000 spools on open — an extremely heavy query.
Preload only the latest 50 instead and move the search box to the backend
(/search for spool-native fields plus a single filament.id expansion so a
filament/vendor-name query still turns up spools), resolving results through
the reactive inventory cache so live edits/deletes reflect.

To keep the "Recent" quick-select accurate past the first 50, loadInitial now
pages newest-first while the batch is still inside the 24h window (batches of
100, capped at 1000, id-deduped), so every spool added today is loaded.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 08:28:50 +02:00
Donkie a733366be9 client_v2: abort in-flight list queries on navigation
Spamming between the Library and Locations pages queued up a heap of
spool queries that kept running after their view was gone: the results
were discarded, but the requests still cost bandwidth and backend work,
and piled up behind the browser's per-host connection limit.

Reads now take an optional AbortSignal (getList/getJson, SpoolQuery and
GroupQuery gain a `signal`), and every list view cancels what it no
longer needs on effect-cleanup / unmount: FilamentList, GroupRow,
PrintLayoutPanel and the locations page — whose MAX_INFLIGHT queue also
drops turns still waiting when the page went away. Writes deliberately
keep no signal, since a request that has left the browser has already
changed the server. isAbortError() takes the signal as the reliable
cancellation test, because an aborted fetch does not always reject with
a DOMException named AbortError.

Measured on a ~4900-spool backend, leaving the Library mid-load went
from 17 row queries still completing after navigation to 0 (all
cancelled), with no console noise and no stuck cards on return.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 08:01:40 +02:00
Donkie 505faf3b35 Load entity extra-fields with selectin instead of joined
The spool listing already joins spool -> filament -> vendor, so joined
eager loading of all three `extra` collections multiplied their rows
together. With ~12 spool, ~9 filament and ~9 vendor extra fields, a
single GET /spool?limit=30 made the database materialize ~35k wide rows
(one per field combination) and then spent most of its time in
SQLAlchemy de-duplicating them back into 30 objects.

Switch the three `extra` relationships to lazy="selectin" so each
collection is fetched in its own WHERE id IN (...) query and nothing
multiplies. Measured on a 4911-spool SQLite database, GET /spool?limit=30
drops from 431 ms to 12 ms and limit=100 from 1358 ms to 23 ms, with
byte-identical responses across the spool/filament/vendor/group/search
endpoints and all 371 integration tests green on SQLite, PostgreSQL,
MariaDB and CockroachDB.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-20 08:01:22 +02:00
Donkie e2a08c0cfd client_v2: load the locations page lazily instead of all at once
The page fetched every spool up front (limit 1000, sorted by location) and
re-ran that query on every live spool event. On a 4158-spool collection that
was 14.2 s and 1.82 MB, and it silently truncated: 76% of the spools never
appeared, so the per-card counts were wrong too.

One aggregate query (/spool/group?group_by=location, 10 ms / 2.1 KB) now
supplies every location and its exact count, and each card fetches its own
spools 30 at a time — starting when it scrolls into view, continuing as its
list is scrolled, capped at three requests in flight so a screenful of cards
doesn't queue behind the browser's per-host connection limit. Card bodies get
a max height and scroll internally. Live spool events are applied to the
loaded cards directly, with a single debounced aggregate refresh to
re-authorize the counts, rather than reloading everything.

Because a card now holds a page rather than the whole location, its saved
custom spool order can only speak for what is loaded; ids from the previous
order that aren't loaded are carried over behind the loaded ones instead of
being dropped.

Also fixes shelf reordering, which never worked under Svelte 5. The grid's
drag zone was armed by setting dragDisabled from the grip's own mousedown
handler, but svelte-dnd-action only attaches its mousedown/touchstart
listeners while dragDisabled is already false, and Svelte 5 flushes state
changes asynchronously — so the action updated after the press had passed and
the drag was never seen. Arm it from pointerdown (which precedes both
mousedown and touchstart) via flushSync so the listener is in place in time.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 14:39:42 +02:00
Donkie d580924d13 client_v2: add field help toggles to the inspector details
Adds an optional `help` prop to <Field> that renders the same ⓘ toggle and
popup the add-spool form uses, and wires up the existing fieldsHelp strings
for location, lot nr, price, article number, weight and spool weight in the
spool/filament/vendor inspectors.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:26:31 +02:00
Donkie 4e4313204e client_v2: add weight and spool weight to the filament inspector
Both fields were missing from the filament inspector's specs grid, and
filamentPatchToApi dropped them, so they couldn't be edited at all.

Spool weight is optional: blank sends an explicit null to clear the tare
weight, while net weight falls back to its current value when emptied.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:20:43 +02:00
Donkie c90cf9eb43 client_v2: fix crash inspecting a spool of a colorless filament
A filament with neither color_hex nor multi_color_hexes maps to an empty
colors array, and the spool inspector's color-hex row indexed [0]
unconditionally, throwing on render. Show the usual "—" placeholder
instead.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:18:35 +02:00
Donkie 5bd4fa594d client_v2: don't link a missing manufacturer in the breadcrumbs
A filament with no vendor resolved through inventory.vendorOf(), whose
placeholder shows as "?" — rendered as a clickable crumb leading to a
broken vendor page. The inspectors now resolve the vendor directly and
fall back to a muted, non-navigable "No manufacturer" crumb, and drop
the "?" from the filament subtitle and spool title as well.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:18:26 +02:00
Donkie 06eff55ccb client_v2: finish spool archiving in the library
The archive button wrote the flag but nothing reflected it afterwards:
the spool inspector showed no archived state and offered no way back,
and a filament's spool list silently dropped its archived spools.

- Spool inspector: ARCHIVED badge next to the id, and the button toggles
  between archive/unarchive. The optimistic cache patch is now rolled
  back when the PATCH fails instead of leaving a wrong local state.
- Filament inspector: "Show Archived" switch feeding allow_archived into
  the scoped spool query, off by default as in the library list.
  Archived rows are dimmed and labelled.
- archiveSpool(id) -> setSpoolArchived(id, archived).

Reuses existing locale keys, so no new strings for Weblate.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:12:59 +02:00
Donkie 43034dce50 client_v2: show help text on an empty locations page
When there are no saved locations and no spools, the page rendered as
just a header and an empty grid. Show locations.noLocationsHelp centered
instead, and hide the drag hint since there is nothing to drag.

Emptiness is only decided once both the spool list and the locations
setting have loaded, so the text doesn't flash on the way in.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:03:59 +02:00
Donkie 5bfaf9c5b2 client_v2: reuse v1 QR link strings in the label inspector
The QR element inspector had its own labels.enc*/labels.hintEnc* strings
duplicating printing.qrcode.useHTTPUrl.*, which already ships translated.
Switch to the existing keys and drop the five orphans from the en locale.

Adds the encoding preview the old print dialog had (useHTTPUrl.preview),
via a new qrTemplate() that returns the encoded string with a literal
{id}; qrContent() is now a substitution on top of it.

Reworded useHTTPUrl.tooltip in en only - same meaning, parallel sentences
per dropdown option. Existing translations of it still read the old
phrasing; v1's nested copy is untouched.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 10:01:36 +02:00
Donkie c72036f650 client_v2: save labels as images, with a configurable DPI
Adds a third mode to the label print panel, alongside Sheet and Label
printer: Image files. It saves one PNG per selected spool at the design's
label size, using the same Konva rasterizer that printing uses. Sheet
geometry and the copy count are ignored there — an exported image is meant
for a label printer or an image editor, where a tiled A4 page would just
have to be cut apart again, and copies would be byte-identical files.

Up to ZIP_THRESHOLD (5) labels download as individual PNGs, spaced apart
because browsers throttle bursts of programmatic downloads; past that they
arrive as a single zip built with fflate, dynamically imported so it stays
out of the main bundle and stored rather than deflated since PNG is already
compressed.

Also adds a per-design DPI setting that applies to printing as well as
export. Printing rasterizes through the same path, so a 300 dpi raster sent
to a 203 dpi thermal printer gets resampled by the driver either way — that
resolution mismatch, not the file format, is what issue #730 was actually
describing. Designs saved before this pick up the default via the
DEFAULT_LAYOUT spread in api/labelDesigns.ts; the v1 migration sets it
explicitly.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 09:53:35 +02:00
Donkie 593b653841 Match every whitespace-separated term in search
The search treated the whole query as one substring match, so "bambu
petg-cf" had to appear verbatim in a single field and returned nothing,
even though "bambu" and "petg-cf" each matched on their own. Vendor-name
matching also ran as a separate query, so a term could never be satisfied
by the vendor while another was satisfied by the filament.

Split the query on whitespace and require every term to match some
searchable field, letting different terms match different fields:

- Filaments search vendor.name in the same pass as their own fields via an
  outerjoin, replacing the separate vendor query. This is what makes
  "bambu petg-cf" work.
- Extra-field matching moves from a follow-up query to a correlated EXISTS
  inside each term's OR-clause, so extras participate in the AND too.
- Ranking generalizes to a per-field-kind base rank (native 0, vendor 2,
  extra 4) with prefix hits beating substrings. A row ranks by its worst
  term, so rows matching everything strongly sort first; the reported
  match_field is the single best hit.

Single-term queries behave as before. Standard SQL only (ilike, correlated
EXISTS), so this holds on all four supported databases.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 09:31:02 +02:00
Donkie 3f3f872310 client_v2: make the mobile inspector drawer draggable
The bottom sheet could only be dismissed by tapping, which felt static
next to a native mobile drawer. The grab handle now responds to drags:
the sheet tracks the pointer 1:1, snaps back below ~30% travel, and
dismisses past it or on a downward flick. Upward pulls are resisted
since there is nothing above to reveal, and the scrim fades in step with
the drag.

Pointer tracking lives on window rather than the sheet element — an
upward drag moves the pointer onto the scrim, and element-bound handlers
would stop firing mid-gesture.

Dragging from inside the scrollable body is only picked up when the body
is already at the top and the pull is downward, so ordinary scrolling is
untouched. On touch Chrome that gesture usually goes to the scroller
instead (see the note in the source); the handle, which is
touch-action:none, is the affordance that always works.

Claude-Session: https://claude.ai/code/session_014Cwk5Yyze9yPvkYkUtMAP4
2026-07-19 09:21:31 +02:00
Donkie 0e5573bc1f Added save/error toasts 2026-07-19 09:06:04 +02:00
Donkie d194cf3483 Reused some old strings 2026-07-19 08:55:47 +02:00
Donkie 57869fab88 Fix 500 on /info from unresolved get_external_db_name
The info endpoint called externaldb.get_external_db_name(), but the
api-layer externaldb module never re-exported that name from
spoolman.externaldb, so every /info request raised AttributeError ->
HTTP 500. Import the function directly from the core module and call it.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 23:55:11 +02:00
Donkie 29f0f362d4 client_v2: add multi-color filament support
Add a shared ColorEditor component (single/multi toggle, per-color
pickers + hex fields, add/remove colors, illustrated coaxial vs
longitudinal cards that preview the actual colors) and wire it into
both the add-spool create form and the library filament inspector,
which also gains its first color picker.

Colors and multi_color_direction round-trip through the API via a new
colorFieldsToApi helper that keys single- vs multi-color by count and
nulls out the other fields, so switching between them stays consistent.

Make color type filterable in the library toolbar (Single / Coextruded
/ Longitudinal) backed by a new additive filament.multi_color_direction
query param on the spool and spool/group endpoints; the Single option
sends an empty value to match single-color (NULL) filaments.

Also tidy a few inspector field labels to reuse the canonical
filament.fields.* / vendor.fields.* message keys.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 23:42:53 +02:00
Donkie 638466bc70 Bake client_v2 into the image and serve it by default
Build the new Svelte client (client_v2) in CI, bake it into the Docker
image alongside the legacy React client, and serve it by default. Set
SPOOLMAN_LEGACY_CLIENT=TRUE to fall back to the old client.

SinglePageApplication is generalized to serve either client: the React
client keeps its index.html + base-path rewriting, while the Svelte
client uses a 200.html SPA fallback, serves prerendered per-route pages,
and serves relative-path assets verbatim. The tweaked-HTML store is moved
off self.html, which StaticFiles uses as its HTML-mode flag.

The Playwright frontend suite targets the React DOM, so it's pinned to
the legacy client via SPOOLMAN_LEGACY_CLIENT for now.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 23:05:00 +02:00
Donkie 913e9116fa Make external filament library display name configurable
The external filament catalog defaults to SpoolmanDB but operators can
point EXTERNAL_DB_URL at their own catalog. The UI hardcoded "SpoolmanDB"
in a few places, which is misleading in that case.

Add an EXTERNAL_DB_NAME env var (default "SpoolmanDB"), expose it via
GET /info, and have client_v2 read it into a serverInfo store. Badges and
the results header now show the configured name, and the localized
strings that reference the library take it as a {name} parameter.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 19:59:32 +02:00
Donkie 6d2ba20023 search: match filaments by their vendor's name
Extend the cross-entity /search filament results to also include filaments
whose vendor name matches the query, so searching a manufacturer surfaces its
filaments (not just the vendor itself). Vendor-name matches rank below any
native-field match and report match_field "vendor.name". The extra-field
annotation block is factored into a helper to keep _search_filaments within the
complexity budget. The client search bar labels the new match_field as
"Manufacturer".

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 19:38:20 +02:00
Donkie 5811353670 client_v2: reuse cross-entity /search for the add-spool filament picker
The AddSpoolModal local catalog search only matched the filament name
(GET /filament?name=). Route non-empty queries through the same /search backend
the library search bar uses, so the picker also matches material, article
number, comment, extra fields and colour. Empty queries still browse the
catalog (sorted by name) so the picker has content on open.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 19:30:08 +02:00
Donkie ed758a1171 client_v2: search external filament catalog server-side
The external SpoolmanDB catalog (filaments.json, thousands of entries) was
served whole via GET /external/filament and filtered in the browser, so every
client downloaded the entire catalog just to search it.

Add a backend GET /external/filament/search?query=&limit= endpoint that filters
the catalog server-side (same word-AND, case-insensitive semantics as the old
client filter), memoizing the parsed catalog by file mtime. client_v2 now calls
it instead of downloading the whole file. The existing /external/filament
endpoint is unchanged, so external API consumers are unaffected. The small
materials.json is still fetched whole for new-filament presets.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 19:27:07 +02:00
Donkie f7103ec9e2 client_v2: live drag-drop for location cards and spools
Rework the Locations page onto svelte-dnd-action so that container
reordering, reordering spools within a location, and moving spools
between locations all update live while dragging, matching the old
client. Native HTML5 drag couldn't do live cross-card moves because the
dragged node is destroyed when it changes each-block.

Per-location spool order is persisted in the `locations_spoolorders`
setting (keyed by location name, "" for no location), shared with the
old client. Shelf dragging is gated to the grip handle via the library's
dragDisabled pattern so it doesn't hijack the nested spool zones.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 19:02:16 +02:00
Donkie 0c673f0403 client_v2: allow reordering location containers via drag-drop
The grip handle on each location card is now a drag source. Dragging a
card over another live-reorders them, and the order persists to the
server-side `locations` setting (an ordered JSON array) — the same store
the old client uses, so ordering survives reloads and is shared across
clients. The "No location" bucket participates like any other card, with
its slot tracked via an empty-string marker in that array.

Added locations now persist to the setting too (not just locally), so an
empty location gets a delete button to remove it again, and renames keep
a card's saved position.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 18:27:59 +02:00
Donkie 8ae45c03c5 client_v2: make label designer and print panels mobile responsive
Both the design and print tabs used fixed multi-column grids that never
collapsed, cramping the columns on narrow viewports. Add 860px/560px
breakpoints so the inspector/preview drop below and everything stacks on
phones.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 18:00:47 +02:00
Donkie 18b0a818bf client_v2: keep Library group control visible on mobile
The 'Group by' control was styled as a .link-btn, which the mobile media
query hid outright, removing all grouping control on small screens. Keep
both Group/Sort controls visible on mobile in a compact form (drop the
'Group by:'/'Sort by:' prefixes) and anchor the group menu to the right
edge so it stays aligned as the buttons shrink.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 17:57:33 +02:00
Donkie 28d840fc87 client_v2: move mobile Add Spools button into header
Replaces the floating FAB, which overlapped content in the mobile
library view, with a compact accent icon button in the header row
next to the scan button.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 17:51:37 +02:00
Donkie 9b16121ed2 Minor search backend issue fix 2026-07-18 15:55:06 +02:00
Donkie d7486d5d51 Improved filtering/sorting. Allows filtering/sorting by extra fields. 2026-07-18 15:54:58 +02:00
Donkie 9f893fb289 client_v2: auto-focus search box when Add Spools modal opens
Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 13:47:38 +02:00
Donkie 7c188c113a client_v2: quick-select recently added spools when printing labels
The label print picker now defaults to newest-first with a Newest/# sort
toggle, shows each spool's registration date, highlights spools added in
the last 24h, and adds a one-click "Recent (N)" quick-select so printing
labels for a freshly added batch is a single action.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 13:44:20 +02:00
Donkie f23f5611b2 client_v2: replace emoji/glyph icons with @lucide/svelte
Swap all emoji and text-glyph icons (✕ + ☑ ⚖ 🏭 🗓 ⌄ ↑↓ ‹› ▾▸ ◱ T ▧ ▭ ⠿ →)
and the custom scan SVG for Lucide icons across the toolbar, library,
add-spool, labels, locations, settings, datetime and QR scanner UIs.
Adds the missing Archive-button icon and strips the arrow from the
"Open filament" string in favour of an ArrowRight icon. Global .lucide
alignment rule in app.css. Purpose-built NumberInput stepper carets kept.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 13:05:58 +02:00
Donkie c0142c9bcf client_v2: fix Print N Labels count to include copies
The button showed only the number of selected spools, but printLabels
emits one label per copy per spool. Multiply by layout.copies so the
count matches the actual output.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 11:32:58 +02:00
Donkie 2b592abd5f client_v2: make DateTimeField week-start and clock locale-aware
The custom date/time picker used Intl only for month/weekday names but
hardcoded a Monday-first week and a 24-hour clock. Drive both from the
active locale via Intl instead:

- Week start comes from Intl.Locale weekInfo.firstDay (Sunday-first for
  zh-TW/pt-BR/ja-JP/pt-PT/ta-IN/th-TH, Saturday for fa-IR, Monday
  otherwise), reordering the weekday header and calendar grid. Falls
  back to Monday on browsers without week info.
- Locales that prefer a 12-hour clock get a 12/1..11 hour dropdown plus
  a localized AM/PM meridiem selector; the emitted value stays a 0-23
  hour so nothing downstream changes.
- Trigger label formats its time part through Intl too.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 11:31:33 +02:00
Donkie 8ab2060b96 client_v2: rework sheet print layout, import v1 presets, widen label field coverage
Sheet printing: the label size is now the fixed source of truth (set in the
designer), and the sheet lays those fixed-size labels on a user-chosen number of
columns anchored at the top-left margin, warning when the grid is wider than the
page instead of silently overflowing. Adds v1-style safe-zone margins that inset
only the outer labels' content, and a "fit width to columns" button.

Migration: first-time transfer of the v1 client's print_presets into v2 label
designs (layout ~1-1, QR+text elements, template field-paths rewritten), gated on
the label_designs setting never having been written.

Fields: carry vendor comment/externalId/registered and filament
comment/articleNumber/registered through the domain model, API mapping, patch
writers, label resolvers + designer palette, the filament/vendor inspectors, and
the add-spool new-filament form, so every printable field is also viewable and
editable in the app.
2026-07-18 09:53:55 +02:00
Donkie 351a09ca63 client_v2: replace deprecated $app/paths base with resolve()/asset()
SvelteKit deprecated the base/assets/resolveRoute exports from $app/paths
in favour of resolve() and asset() (since 2.26). Migrate all usages:
navigation targets use resolve(), static assets use asset(), and the few
spots that need the bare base string derive it from resolve('/').

Behaviour is unchanged under our paths.relative runtime base-path setup:
resolve()/asset() read the same runtime base, and with no configured
assets path asset() falls back to base, matching the old base + path.
2026-07-18 08:05:21 +02:00
Donkie 871fbdb444 Move locales out of static dir 2026-07-18 08:04:44 +02:00
Donkie ab38eb73ad client_v2: add unit-box numeric input and apply it across forms & inspectors
Give NumberInput a `unit` prop that renders inside the input (left of the
steppers), plus `dense` sizing, a `disabled` state, a numeric/commit
`onchange` mode, and an `onclear` hook for nullable fields. Fix a
min-content overflow so the flex control shrinks inside grid/flex cells.

Apply the unit input to:
- add-spool form (density, diameter, temps, weights)
- label designer, element inspector, print layout panel (mm)
- library adjust-weight field, and the filament/vendor/spool inspector
  detail grids (diameter, density, temps, price, empty-spool-weight)

Move the units out of the English label strings into the inputs; locale
keys are kept so Weblate translations aren't orphaned.
2026-07-17 17:48:47 +02:00
Donkie 287d3010ac client_v2: add light theme with system/light/dark preference
Adds a color-theme preference (system/light/dark) in Settings, persisted
to localStorage and defaulting to the OS preference. An inline script in
app.html resolves the saved theme before first paint to avoid a flash of
the wrong theme; the theme store keeps <html data-theme> and the
theme-color meta in sync reactively (including live OS changes).

The light palette is applied purely by overriding the design tokens under
:root[data-theme='light']. Hardcoded neutral/accent colors scattered
across components were converted to tokens (adding --surface-sunken,
--accent-muted[-2], --track, --swatch-border[-hover]) so both themes stay
in sync.
2026-07-17 16:17:27 +02:00
Donkie a1685f7428 client_v2: render color swatches as glossy chips (flat on labels)
Give the shared on-screen Swatch a physical color-chip look (diagonal
gloss sheen + hairline definition ring) instead of a flat rounded rect.
The printed/canvas label swatch stays flat but gains the same definition
ring so light colors read on white label stock.
2026-07-17 16:03:56 +02:00
Donkie bb2a21cb99 client_v2: store print layout per-design so margin edits are saveable
The Print-tab layout (margins, spacing, paper, copies) lived in a single
global localStorage key, separate from the design. Editing a field never
dirtied the design, so the Save button stayed disabled, and every design
shared one layout.

Move PrintLayout onto LabelDesign so each design carries its own layout:
editing any Print field now dirties the design and enables Save, and the
layout is persisted/recalled with the design. Old designs backfill from
DEFAULT_LAYOUT on load.
2026-07-17 15:31:51 +02:00
Donkie c30bbfea9d client_v2: render unused-spool summary as a sub-section divider under filament group
In the Group: Filament view, used SpoolRows shed their filament identity
(the sticky header already carries it), leaving a bare column grid. The
collapsed UnusedRow, still shaped like a filament-led row, clashed with
them. Under the filament group it now renders as a quiet collapsible
divider ("N unused · Xg each" + hairline rule) that introduces the unused
pile; expanding lists the spools as ordinary rows aligned with the used
siblings. Other group axes keep the filament-name-led summary, where the
name distinguishes different filaments' unused piles.
2026-07-17 14:09:28 +02:00
Donkie ebadc045aa client_v2: add archived-spool visibility to the library
Archived spools were excluded by the backend by default and the client
never opted in, so they were invisible with no way to view them.

Treat "show archived" as a filter: a checkbox at the bottom of the
toolbar's Filter menu, surfaced as a dismissible "Archived" chip among
the active-filter chips when on. It lives in the URL state (arch=1)
alongside group/sort/filters and drives allow_archived=true on the
/spool and /spool/group queries, so both flat and grouped views (and
group aggregates) include archived spools. Archived rows are dimmed and
carry an "Archived" tag. Default stays off, preserving prior behaviour.

Also split the toolbar into a wrapping filters region and a pinned
Group/Sort control cluster, so accumulating filter chips wrap onto new
rows instead of pushing Group/Sort down.
2026-07-17 13:51:30 +02:00
Donkie a9d93520eb Add custom Combobox for location/vendor/material inputs
Replaces native <datalist> inputs, whose dropdown wouldn't open on focus
and couldn't be styled. The new Combobox opens its list immediately on
focus/click, filters as you type, supports keyboard nav, commits on Enter,
and still allows entering brand-new free-text values. Its dropdown is
fixed-positioned so it isn't clipped by scrollable modal bodies.

Used for the SpoolInspector location field (the original request) and the
vendor, material, and location inputs in AddSpoolModal.
2026-07-17 13:28:23 +02:00
Donkie 63c2ad5d7e Use custom DateTimeField in add spool form
Replace native datetime-local inputs for first/last used with the
custom DateTimeField picker already used in the library inspector,
giving consistent, fully-clickable date+time selection across browsers.
2026-07-17 13:14:00 +02:00
Donkie 35f35b8bd3 Align COMMON_MATERIALS densities with SpoolmanDB
Corrected offline fallback densities in AddSpoolModal to match
SpoolmanDB's materials.json: ASA 1.07->1.05, Nylon 1.14->1.52,
PC 1.2->1.3, HIPS 1.04->1.03. These short keys never get overridden
by SpoolmanDB's longer names at runtime, so the values must be
correct on their own.
2026-07-17 13:09:20 +02:00
Donkie 7642eaae27 Fixed inspector not updating when adding new spools 2026-07-17 13:07:37 +02:00
Donkie cae436ee87 make label templates match old client better 2026-07-17 13:02:53 +02:00
Donkie c9cf7a6a12 Fixed some typescript errors 2026-07-17 12:57:35 +02:00
Donkie 532ffc8ccd Added some measurement validation to new spool form 2026-07-17 12:52:33 +02:00
Donkie d2435871de Improved layout of extra fields in inspector 2026-07-17 12:52:19 +02:00
Donkie d9c0a2a049 Empty box for no location in library 2026-07-17 12:36:25 +02:00
Donkie 48650cff96 i18n updates 2026-07-17 12:21:59 +02:00
Donkie bcdcf6f476 i18n updates 2026-07-17 11:18:13 +02:00
Donkie 30e2daf784 i18n wip updates 2026-07-16 22:58:11 +02:00
Donkie d65b50d5a2 Use paraglide for i18n in client_v2. Update base path handling 2026-07-16 18:41:01 +02:00
Donkie 9dcd6d56db Add rich text support for svelte i18n, parsing <p> tags etc. Also fixed some tags. 2026-07-13 23:26:55 +02:00
Donkie 7ea89dc347 Add cross-entity search to v2 client
Implements the search feature for client_v2: a new backend search endpoint
plus a results-panel UI, replacing the old query box that only filtered the
library list by filament name.

Backend (additive, API v1 backward compatibility preserved):
- New GET /api/v1/search returning categorized spool/filament/vendor results,
  each annotated with the field that matched, plus an is_color_query flag.
- Case-insensitive text search over native text fields and text/choice extra
  fields; exact spool-id match for numeric queries; color-similarity search
  (CSS color name or hex) via find_by_color, ordered closest first.
- spoolman/colors.py resolves the CSS4 color names / hex codes to hex.
- Integration tests (run across SQLite/PostgreSQL/MySQL/CockroachDB in CI).

Frontend (client_v2):
- SearchBox overlay with Spools/Filaments/Manufacturers sections, per-result
  match badges, keyboard navigation, a color-similarity threshold slider
  (persisted to localStorage), and an English-only color-name hint.
- Clicking a result opens the matching inspector on the Library page.
- Removes the old q -> filament.name inline list filtering.

Claude-Session: https://claude.ai/code/session_01Ne7RsfHejKZX97uAq3aVMn
2026-07-13 19:33:52 +02:00
Donkie 4bdebbdded Add localization to v2 client (svelte-i18n)
Introduce full i18n for the Svelte client_v2 using svelte-i18n (ICU/formatjs),
mirroring the old client's language support.

- i18n setup in src/lib/i18n/ (lazy per-language loaders, init + waitLocale boot
  guard, setLocale persisted to localStorage, Intl locale helper); 26-language
  registry ported from the old client.
- Language picker on the Settings page.
- scripts/convert-locales.mjs seeds static/locales/<lang>/common.json from the
  old client's Weblate files: {{var}} -> {var}, escapes literal braces, merges
  i18next plural suffixes into ICU plurals. Existing translations are reused via
  the same keys; new v2-only strings are added to en/common.json only and fall
  back to English until Weblate fills them.
- Extracted all hardcoded UI strings across routes and components to $_ lookups;
  DateTimeField derives month/weekday names from Intl in the active locale.
- static/locales is prettier-ignored (mirrors the old client's public/locales).

Follow-up (maintainer): add a Weblate component pointing at
client_v2/static/locales (nested JSON, ICU MessageFormat, English source).

Claude-Session: https://claude.ai/code/session_01Ne7RsfHejKZX97uAq3aVMn
2026-07-13 14:45:37 +02:00
Donkie cf82d5b68f Add camera QR scanner to v2 client
Restores the old client's built-in scanner: a TopBar button opens a
camera modal (qr-scanner) that reads Spoolman label codes and opens the
matching spool in the Library. Parses both the WEB+SPOOLMAN:S-<id> URI
and the /spool/show/<id> URL forms.

Camera-start errors are reported specifically: an insecure (non-HTTPS)
context is detected up front via isSecureContext, and other failures are
re-probed via getUserMedia to recover the real error (permission
blocked, no camera, camera in use) since qr-scanner collapses them all
into a generic string.
2026-07-13 11:04:20 +02:00
Donkie 0f7cdc6faf Add location rename and richer spool info on Locations page
Location names can now be edited inline (renaming moves all spools in
that location to the new name via a new spoolSource.renameLocation).
Spool entries on the Locations page now show vendor, material, full
weight, and last-used time, matching the old client's layout.
2026-07-13 10:49:15 +02:00
Donkie 99928b5469 Fix QR logo backing corners and preserve tab state in label designer
Square off the QR logo's white backing (was rounded, letting black
modules peek through at the corners) and keep both Design/Print tabs
mounted once visited so switching tabs doesn't reset local state.
2026-07-13 10:32:08 +02:00
Donkie 46b69808a6 WIP: add label designer and misc v2 client improvements
Adds a label design/print subsystem (canvas editor, QR/logo/paper
helpers, print layout panel) backed by a new label_designs setting,
plus a footer, datetime field component, and assorted fixes across
the library and settings views.
2026-07-13 10:10:40 +02:00
Donkie c82e3911e1 WIP: add v2 client (Svelte) with spool grouping backend
Introduces client_v2, a new SvelteKit-based client, alongside the
backend it depends on: a /spool/group endpoint that returns
per-group aggregates (count, in-use count, remaining weight,
last-used) grouped by filament, vendor, material or location, so the
client can paginate whole groups instead of fetching every spool.

Not yet complete/wired up for release.
2026-07-12 19:21:34 +02:00
470d8f3bcc Add filtering and sorting for custom fields (#773)
* Add filtering and sorting for custom fields

* Fix tests and make code postgres compatible

* Fix bugs and expand test coverage for custom field filter/sort

* trigger CI

* fix: address extra field filter review feedback

* fix: clean up custom field table state handling

* docs: update extra field table view description

* fix: narrow boolean custom field filter syntax

* Expand test coverage for all custom field types/entities and fix range filter/sort

- Add comprehensive tests covering all 9 field types (text, integer, float,
  boolean, single-choice, multi-choice, datetime, integer_range, float_range)
  for filter and sort on spool, filament, and vendor entities
- Add invalid-filter 400 tests for float, integer_range, and float_range
- Fix integer_range/float_range filter to use LIKE pattern matching against
  Python's deterministic json.dumps output instead of fragile .contains()
- Add integer_range/float_range sort support via a @compiles helper
  (_JsonArrayFirstElement) that emits CAST(col AS JSON)->>0 on PostgreSQL
  and JSON_EXTRACT(col, '$[0]') on SQLite/MariaDB
- Add logger and __all__ to extra_fields.py (aligns with PR #893)
- All tests verified passing on postgres, sqlite, and mariadb

* Add filter UI for all custom field types and fix range filters

- Add text input filter dropdown for text fields (substring search)
- Add range input filter dropdowns (min/max) for integer, float, integer_range, and float_range fields
- Add datetime picker filter dropdown for datetime fields
- Fix boolean "No" filter to use empty value so it correctly matches unset/false fields
- Fix integer_range/float_range filter to use numeric comparisons instead of LIKE-based exact matching (stored_min >= filter_min, stored_max <= filter_max)
- Add range filter support for integer/float fields (min:max format)
- Add _JsonArraySecondElement cross-database helper for extracting second JSON array element

* Range filter for integer/float fields and fix range_field filter semantics

Backend:
- Add _JsonArraySecondElement cross-database helper (mirrors _JsonArrayFirstElement)
- integer_range/float_range filter now uses numeric comparisons:
  stored_min >= filter_min and stored_max <= filter_max (was LIKE exact match)
- integer/float filter now supports min:max range format in addition to exact match:
  stored_value >= min and/or stored_value <= max

Frontend:
- integer and float fields now use a range filter dropdown (min/max inputs)
  instead of a single exact-value input

Tests:
- Fix integer_range/float_range spool and vendor tests broken by new >= semantics
  (filter values updated to discriminate between test entries)
- Add range filter tests (min only, max only, both) for integer and float fields

* Replace 12 copy-pasted numeric field tests with 4 parametrized tests

Introduce a _create_entity helper and @pytest.mark.parametrize("entity_type",
["spool", "filament", "vendor"]) to run each numeric field type test against
all three entity types from a single test function.

- Removes 12 individual tests (integer/float/integer_range/float_range × 3 entities)
- Adds 4 parametrized tests covering the same ground plus the previously missing
  min-only / max-only range filter cases for filament and vendor
- Uses try/finally for cleanup so entities are always deleted even on assertion failure

* Add datetime range filter and restore integer/float range filter support

- Backend: add datetime range filter using '|' separator (ISO dates contain ':')
- Backend: restore integer/float range filter support (lost during attribution rewrite)
- Frontend: replace single DateTimePicker with From/To range pickers for datetime fields

* Use local timezone in filter picker; add range filter tests

The DateTimeRangeFilterDropdown was initializing pickers with dayjs.utc(),
causing them to display and accept UTC times directly. The entry form shows
local time and converts to UTC, so the filter was inconsistent: entering
the same displayed time would produce a filter value 2h offset from what
was stored (in UTC+2), making exact boundaries fail unexpectedly.

Fix: initialize picker values with dayjs() (local mode) so the filter picker
behaves the same as the entry form — shows local times and converts to UTC.

Tests: replace 3 separate entity-specific datetime tests with one parametrized
test covering spool/filament/vendor, and add range filter cases (start|, |end,
start|end).

* Replace all entity-specific tests with parametrized tests

Replace 16 separate spool/filament/vendor tests for text, boolean,
single-choice, multi-choice, and empty-filter with 5 parametrized tests
that run against all three entity types. Each test is now defined once
and executed 3 times, eliminating copy-paste and ensuring consistent
coverage across all entity types.

Parametrize invalid-filter-value 400 tests across all entity types

The non-numeric-value error uses "Invalid integer/float range filter value"
while the missing-colon error uses "Invalid range filter value". Both contain
"range filter value", so use that as the assertion substring.

* Align custom filter dropdowns with Ant Design choice filter style

All three custom filter dropdowns (text, number range, datetime range) now
match the built-in choice filter footer: a border-top separator, Reset as a
link button on the left (disabled when no filter is active), and OK as a
primary button on the right.

* fix(fields): correct custom-field filter edge cases across databases

- Float exact-match filters now compare numerically (cast to Float) instead of
  by JSON string, so float fields stored as integer JSON (e.g. "2") or
  non-canonical decimals (e.g. "2.50") match an equivalent filter.
- integer_range/float_range filter and sort no longer return HTTP 500 on
  CockroachDB: the PostgreSQL '->>' JSON extraction is now also registered for
  the cockroachdb dialect, which previously fell back to the unsupported
  JSON_EXTRACT function.
- Text exact-match and single-choice equality use ensure_ascii=False so
  non-ASCII values match how the frontend's JSON.stringify stores them.

Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

* test(fields): expand custom-field filter/sort coverage

Add tests covering filter composition (multiple extra fields, extra + built-in,
filter one field / sort another), pagination total-count, unknown-field
handling, text case-insensitivity/substring/non-ASCII, numeric multi-value OR,
boolean empty semantics and token rejection, multi-choice substring collisions,
empty filters on numeric fields, null-bounded ranges, and additional
invalid-value 400 paths.

Verified against sqlite, postgres, mariadb and cockroachdb.

Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

* style(client): format column.tsx with prettier

The custom-field filter dropdowns' Reset buttons were written as one-line
onClick handlers that prettier wraps across multiple lines. Apply prettier
so the CI style check passes.

Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

* fix(fields): decode JSON scalars in-DB for text/choice/datetime custom-field filters

Match text, single-choice and datetime custom-field filters against the
database-decoded JSON scalar (new _JsonScalarText cross-dialect helper:
#>> '{}' on postgres/cockroachdb, JSON_UNQUOTE(JSON_EXTRACT) on mysql,
json_extract on sqlite) instead of reconstructing the exact JSON
serialization the client wrote. This decouples matching from
json.dumps/JSON.stringify encoding quirks (non-ASCII escaping, surrounding
quotes). Also escape LIKE wildcards so '%' and '_' in text and multi-choice
queries match literally rather than acting as wildcards.

Verified against sqlite, postgres, mariadb and cockroachdb.

Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

* test(fields): cover LIKE-wildcard escaping in custom-field filters

Add parametrized tests (spool/filament/vendor) asserting that '%' and '_'
are matched literally in text and multi-choice filters, that a literal '/'
(the internal LIKE escape char) matches literally, and that single-choice
equality handles a '%' value exactly.

Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

---------

Co-authored-by: Donkie <daniel.cf.hultgren@gmail.com>
Co-authored-by: akira69 <akira69@gmail.com>
2026-07-08 10:14:27 +02:00
Donkie 103e029434 Bump version to 0.24.0 2026-07-07 09:48:12 +00:00
Donkie 92da7fd110 feat(i18n): add Lithuanian and Turkish locales
Rework check-i18n to measure real translation coverage against en
(non-empty and differing from the reference) instead of file size,
gating inclusion at 50%. Add lt and tr, both ~96% translated.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 11:40:48 +02:00
+1 991feb36bb Translations update (#825)
* Translated using Weblate (Estonian)

Currently translated at 17.3% (50 of 288 strings)

Translated using Weblate (Persian)

Currently translated at 96.5% (278 of 288 strings)

Translated using Weblate (French)

Currently translated at 98.9% (285 of 288 strings)

Translated using Weblate (Greek)

Currently translated at 72.9% (210 of 288 strings)

Translated using Weblate (Dutch)

Currently translated at 99.3% (286 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 72.2% (208 of 288 strings)

Translated using Weblate (Danish)

Currently translated at 85.4% (246 of 288 strings)

Translated using Weblate (Thai)

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (286 of 288 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ja/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/
Translation: Spoolman/Web Client

* Translated using Weblate (Ukrainian)

Currently translated at 93.0% (268 of 288 strings)

Translated using Weblate (Greek)

Currently translated at 72.9% (210 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 72.2% (208 of 288 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.3% (286 of 288 strings)

Translated using Weblate (Hindi (Latin script))

Currently translated at 3.8% (11 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 94.7% (273 of 288 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.0% (268 of 288 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hi_Latn/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client

* Translated using Weblate (Danish)

Currently translated at 87.1% (251 of 288 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client

* Translated using Weblate (Czech)

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client

* Translated using Weblate (Korean)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (225 of 288 strings)

Added translation using Weblate (Korean)

Co-authored-by: 김태남 <imedia0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/
Translation: Spoolman/Web Client

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Kayz C <kayzed.x@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client

* Translated using Weblate (Slovenian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Slovenian)

Currently translated at 38.1% (110 of 288 strings)

Translated using Weblate (Slovenian)

Currently translated at 14.9% (43 of 288 strings)

Added translation using Weblate (Slovenian)

Co-authored-by: Jernej Pangerc <jernejp21@tuta.com>
Co-authored-by: jernejp21 <jernejp21@tuta.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/
Translation: Spoolman/Web Client

* Translated using Weblate (Slovak)

Currently translated at 10.4% (30 of 288 strings)

Translated using Weblate (Slovak)

Currently translated at 9.7% (28 of 288 strings)

Added translation using Weblate (Slovak)

Co-authored-by: Pavol Vrba <pvrba2000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sk/
Translation: Spoolman/Web Client

* Translated using Weblate (Hungarian)

Currently translated at 94.0% (271 of 288 strings)

Co-authored-by: Tamas Veres <tamas.veres@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translation: Spoolman/Web Client

* Translated using Weblate (Danish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Danish)

Currently translated at 92.0% (265 of 288 strings)

Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client

* Translated using Weblate (Latvian)

Currently translated at 4.5% (13 of 288 strings)

Added translation using Weblate (Latvian)

Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lv/
Translation: Spoolman/Web Client

---------

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Co-authored-by: srbjessen <srbjessen@gmail.com>
Co-authored-by: Miloslav Kos <kos.m@post.cz>
Co-authored-by: 김태남 <imedia0@gmail.com>
Co-authored-by: Kayz C <kayzed.x@gmail.com>
Co-authored-by: jernejp21 <jernejp21@tuta.com>
Co-authored-by: Pavol Vrba <pvrba2000@gmail.com>
Co-authored-by: Tamas Veres <tamas.veres@gmail.com>
Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com>
Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com>
2026-07-07 11:34:48 +02:00
Donkie e2ab72b5d0 feat(theme): add 'System' option that follows OS/browser theme (#947)
Replace the light/dark switch with a 3-way System/Light/Dark control. The new
'System' preference follows prefers-color-scheme live via a matchMedia listener.
New installs default to System; existing users keep their stored light/dark
choice (legacy values remain valid preferences).

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 11:28:51 +02:00
Donkie 8c262a640d feat(scanner): accept Data Matrix and other 2D codes (#887)
Widen the code scanner's accepted formats beyond qr_code to the common 2D
matrix codes (micro/rm QR, Data Matrix, Aztec, PDF417) so manually-generated
labels using those symbologies can be scanned. Payloads that don't match the
spoolman spool format are ignored, so this is a safe superset.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 11:28:51 +02:00
Marco Thiel 4854e322dc docs: add MCP server to integrations list (#890)
Add spoolman-mcp to the list of integrations. It's a Model Context
Protocol server that lets you manage your filament inventory through
AI assistants like Claude.
2026-07-07 11:05:19 +02:00
dependabot[bot] ad79373dd0 Bump uuid from 13.0.2 to 14.0.0 in /client (#927)
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.2 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.2...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 10:52:29 +02:00
Donkie 5c8364fcea Remove hashes from requirements.txt 2026-07-07 10:12:35 +02:00
Donkie 2c638da22a Auto-merge Weblate translation PRs on green CI
Weblate opens a PR for every translation sync. These are machine-generated,
always scoped to client/public/locales/, and gated by the full CI suite, so
hand-merging each one is pure friction. This enables GitHub auto-merge on
Weblate-authored PRs so they merge themselves once required checks pass.

A scope-guard step reads the PR's file list from the API and refuses to enable
auto-merge unless every changed file is a .json under client/public/locales/,
so the trust is in the diff, not just the author: a compromised Weblate token
can open a PR but can only be auto-merged if it is confined to translations.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 10:11:28 +02:00
chruoss 295265f60d Update sync interval for external database synchronization (#943)
The external database sync scheduler was ignoring the configured
EXTERNAL_DB_SYNC_INTERVAL environment variable and always using the DEFAULT_SYNC_INTERVAL constant (3600 seconds).
2026-07-07 10:07:21 +02:00
Donkie 4f6763716c Merge pull request #919 from velzi/export
Allow archived spool export
2026-07-07 09:49:36 +02:00
Donkie 48ad01b09b Add CI guard blocking manual edits to non-English translations
Non-English locales are managed exclusively through Weblate; any manual
edit to an existing translation file gets silently overwritten on the
next Weblate sync. This workflow fails any PR (except Weblate's own,
detected via PR author login) that modifies or deletes an existing
non-English file under client/public/locales/. Adding a brand-new
language file is still allowed so contributors can bootstrap a new
language alongside the client/src/i18n.ts entry.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 09:35:12 +02:00
Donkie d9fd44eab9 Add Playwright frontend integration tests; fix broken SPA static serving
Add a browser-driven integration suite (tests_frontend/) that runs against
the real production image + PostgreSQL and drives the UI with Playwright:
a smoke test that navigates every page via the sidebar with no console
errors, and a CRUD test that creates a vendor -> filament -> spool entirely
through the UI. Wired up as `poe itest-frontend` and a new `test-frontend`
CI job (reusing the image built for the backend tests, gating releases).

Navigation goes through real UI buttons (language-independent link targets)
and the app language is forced to English so label matchers are stable
regardless of the runner's browser locale.

This immediately caught a real regression: Starlette 1.3.1 (pulled by a
recent dep bump) removed the `method` kwarg from FileResponse, so
SinglePageApplication 500'd on every static asset and the whole UI was dead.
Drop the removed kwarg — Starlette now derives HEAD handling from the scope.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 09:12:42 +02:00
Donkie da54ead06f Bump deps to clear Dependabot alerts (path-to-regexp, pytest)
- path-to-regexp: force >=8.4.0 (8.4.2) under @ant-design/pro-layout via a
  scoped npm override, fixing the ReDoS advisories (GHSA sequential optional
  groups + multiple wildcards). Express keeps its own 0.1.x.
- pytest 8.3 -> 9.1.1 and pytest-asyncio 0.23/0.25 -> 1.4.0 in
  tests_integration/requirements.txt and the dev group; regenerated uv.lock.

Verified: client build + typecheck pass, npm audit clean, and the full
integration suite (223 tests) passes on postgres, sqlite, mariadb, and
cockroachdb under podman.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-06 20:39:46 +02:00
Donkie 9b6af119c6 Fix commit-before-notify races in write paths (flaky postgres deletes)
Several DB write functions mutated and then sent their websocket notification (or
just returned) without committing, relying on the request session's teardown
commit. On async postgres that commit races the HTTP response, so a fast
follow-up read could still see the pre-write state — e.g. `DELETE /vendor/{id}`
returns 200 but an immediate `GET` still returns 200. Latent since 2023 (a6d527a);
the async-timing dependency bump widened the window enough to intermittently fail
`test (postgres)` in CI.

Commit inline before notifying, matching the functions that already did
(filament.delete, vendor/spool create/update) and the commit-first invariant from
the earlier websocket-ordering fix:
- vendor.delete, spool.delete
- setting.update, setting.delete
- vendor/filament/spool.clear_extra_field
- spool.rename_location

(use/measure already commit via their wrappers; reads are unaffected.)

Verified via `SPOOLMAN_CONTAINER_ENGINE=podman poe itest`: full 4-DB suite green
plus 7/7 repeat postgres runs with zero flakes (was ~66% failing before).
2026-07-06 15:53:40 +02:00
Donkie 3a3273d9a9 Merge PR #798 (current head): tidy unprivileged Docker support
Brings in the latest #798 commit (388082f) that our earlier merge missed: it
drops --chown from the builder COPYs (matching the Podman build fix already on
master), removes the redundant `usermod -G users app`, and simplifies the
entrypoint to re-exec itself via gosu when root instead of duplicating the
uvicorn invocation. Dockerfile comment conflict resolved in favour of the
Podman-explaining comment.
2026-07-06 15:22:03 +02:00
Donkie c81920d23c Merge PR #798: conditional permission drop for unprivileged Docker
Brings in jpiccari's unprivileged-Docker support (rootless entrypoint that only
uses gosu when running as root and only remaps app UID/GID when they differ, plus
dropping the redundant `groupmod -g 1000 users` in the runner stage).

Combines cleanly with the Podman build/test enablement already on master
(builder-stage --chown removal, postgres healthcheck, run.py engine selection),
giving full rootless build + run + test support.
2026-07-06 15:14:49 +02:00
Donkie e3c5f3fc1d Make the integration test suite runnable under Podman
Running tests_integration against rootless Podman surfaced three issues; none
affect the Docker/CI path:

- run.py: add a SPOOLMAN_CONTAINER_ENGINE env var (defaults to "docker") so the
  build/compose commands can be driven by podman instead.
- Dockerfile: drop `--chown=app:app` from the builder-stage COPYs. The "app"
  user only exists in the runner stage; Docker/BuildKit tolerated the dangling
  reference but Podman rejects it. Final ownership is unchanged — it is set when
  the files are copied into the runner stage.
- docker-compose-postgres.yml: add a pg_isready healthcheck and gate spoolman on
  `condition: service_healthy`, matching the mariadb/cockroachdb compose files.
  Without it spoolman raced postgres' initdb and died on connection-refused
  before running migrations.

Verified: full 4-DB suite (sqlite/postgres/mariadb/cockroachdb) green via
`SPOOLMAN_CONTAINER_ENGINE=podman uv run poe itest` — 223 tests each.
2026-07-06 12:16:17 +02:00
Donkie 4e3c228b28 Update client dependencies (npm update)
Refresh transitive client dependencies to the latest versions within the
existing package.json semver ranges (package.json unchanged).

Verified: npm ci, tsc --noEmit, eslint, prettier --check, and npm run build all
pass. (3 high-severity advisories remain that require major/breaking bumps via
`npm audit fix --force`; left for a separate, deliberate change.)
2026-07-06 12:05:25 +02:00
Donkie 059ff94f4b Update Python dependencies (uv lock --upgrade)
Refresh all backend dependencies to the latest versions allowed by the
pyproject constraints and regenerate requirements.txt to match. Notable bumps:
starlette 0.50 -> 1.3, fastapi 0.128 -> 0.139, cryptography 46 -> 49,
uvicorn 0.40 -> 0.50, pydantic 2.12 -> 2.13, sqlalchemy 2.0.45 -> 2.0.51,
alembic 1.17 -> 1.18.

Locally verified: uv sync, ruff check, uv lock --check, app import, and a live
startup smoke (SQLite) — migrations run, /health + /info respond, vendor CRUD
works, and a websocket update event is delivered correctly on starlette 1.x.
The Docker-based 4-DB integration suite still needs to run (CI).
2026-07-06 12:05:25 +02:00
Donkie 9f30c16e4b Document that extra field values are JSON-encoded strings (#849)
The `extra` map on vendors, filaments and spools is intentionally typed as
dict[str, str] where every value is a JSON-encoded string (the client round-
trips them through JSON.parse/stringify). This surprised API consumers, who
saw numeric fields come back as strings.

Spell out the encoding on all three response models via a shared helper so the
generated OpenAPI docs make clear that consumers must JSON-decode each value.
No behaviour change.
2026-07-06 11:50:31 +02:00
Donkie 14a30bc3c5 Commit generated requirements.txt for bare-metal/Moonraker installs (#830)
requirements.txt was moved to .gitignore when the project switched to uv, so
bare-metal installs lost it. Moonraker's update_manager runs
`pip install -r requirements.txt` against the checkout, so upgrading to 0.23
broke with "Invalid path for option `requirements`".

Track requirements.txt again and regenerate it from uv on every release
(spoolman/bump.py exports it alongside the uv.lock bump), keeping uv as the
single source of truth while staying backwards compatible with installs that
expect a requirements.txt.
2026-07-06 11:50:22 +02:00
Donkie 025f18f914 Fix spool price disappearing from the list on live updates (#814)
The REST endpoints serialize with response_model_exclude_none=True (unset fields
are omitted), but websocket events were serialized with a plain .json() that
includes unset fields as explicit null. The spool list fills a missing price
from the filament price only when it is `undefined`, so a live update carrying
`price: null` (e.g. after a quick weight adjust) blanked the price column until
a full page reload.

Fixed on both sides so the two serialization paths can no longer drift:
- Server: websocket payloads now use exclude_none=True (spoolman/ws.py), giving
  live updates and REST responses an identical shape. Note this is a small
  websocket wire-format change external consumers will see: unset fields are now
  omitted instead of sent as null, matching REST.
- Client: collapseSpool and the price column now fall back for both `null` and
  `undefined`, keeping the list resilient regardless of serialization.
2026-07-06 11:50:07 +02:00
Joshua Piccari 388082f27a Tidy up docker file and entrypoint 2026-04-26 14:29:48 -07:00
Veli-Matti Leppänen 7fae319b82 Allow archived spool export 2026-04-21 09:49:30 +03:00
Joshua Piccari 137558639a Add conditional logic to drop permissions for unprivileged Docker
This change enables the container to run in unprivileged mode (without
--privileged or additional capabilities) by making permission drops
conditional:

- Changed users group GID from 1000 to 1001 to avoid conflicts with
  the app user's primary group (UID 1000)
- Only modify user/group IDs when PUID/PGID environment variables differ
  from the default value of 1000, avoiding unnecessary privilege
  requirements
- Skip su-exec entirely when running as the default user, since changing
  user context requires additional capabilities that unprivileged
  containers don't have

This allows the container to work in restricted environments like
Kubernetes with security contexts or rootless Docker while maintaining
backward compatibility for users who customize PUID/PGID.

Fixes Donkie#791
2026-02-28 10:43:50 -08:00
Donkie eafbc64937 Bump version to 0.23.1 2026-02-03 16:27:38 +00:00
Donkie 1be0884729 Merge pull request #822 from Donkie/dependabot/npm_and_yarn/client/lodash-es-4.17.23
Bump lodash-es from 4.17.22 to 4.17.23 in /client
2026-01-27 21:52:28 +01:00
Donkie e77bea1bc2 Merge pull request #823 from Donkie/dependabot/npm_and_yarn/client/lodash-4.17.23
Bump lodash from 4.17.21 to 4.17.23 in /client
2026-01-27 21:52:11 +01:00
Donkie 5dc26946cb Merge pull request #831 from nyanotech/asdf
explicitly disable pagination for useSelect
2026-01-27 21:51:13 +01:00
nyanotech 34c09d3f55 explicitly disable pagination for useSelect 2026-01-27 03:39:48 -08:00
Donkie 7f14cbdaa9 Added rsa variant of aiomysql to add support for better mysql password methods. 2026-01-24 15:59:53 +01:00
Donkie 115e29bdaf Bump version to 0.23.0 2026-01-23 20:21:50 +00:00
Donkie 6d817d8c2b Fix package.json not being written with newline at end of file 2026-01-23 21:21:00 +01:00
Donkie c033a3ffe6 Fix trigger release ci 2026-01-23 21:09:39 +01:00
Donkie 1974d4b8d2 Version bump updates 2026-01-23 21:07:45 +01:00
dependabot[bot] c3ef318cb0 Bump lodash from 4.17.21 to 4.17.23 in /client
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-22 09:57:45 +00:00
dependabot[bot] bc442f5063 Bump lodash-es from 4.17.22 to 4.17.23 in /client
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.22 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits/4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-22 02:00:21 +00:00
Donkie dd0476ced3 Don't use uv specific image in dockerfile, it doesn't support armv7 2026-01-20 22:11:46 +01:00
Donkie 6f9dfd1be7 uvicorn logging fixes 2026-01-19 20:47:06 +01:00
Donkie 29aa9f8d54 Fix fastapi deprecation notices 2026-01-19 20:40:38 +01:00
Donkie 251f7e2ede Removed max height from locations page
Fixes #816
2026-01-19 20:36:11 +01:00
Donkie f137535c1e Merge pull request #811 from kaufi95/master
Bugfix for issue #803 and #805
2026-01-18 23:02:37 +01:00
Donkie fb83220739 Merge pull request #777 from beikeland/AplhanumericQR
All caps short QR codes for alphanumerical encoding
2026-01-18 22:54:10 +01:00
Donkie fdf317fc13 Merge pull request #817 from Donkie/dependabot/uv/setuptools-78.1.1
Bump setuptools from 76.1.0 to 78.1.1
2026-01-18 22:53:36 +01:00
Donkie 205a5c52fa Merge pull request #786 from mnebelung/master
Add support for Fedora to install.sh
2026-01-18 22:45:49 +01:00
Donkie f585c51b18 Merge branch 'master' into master 2026-01-18 22:45:29 +01:00
dependabot[bot] b393f822e8 Bump setuptools from 76.1.0 to 78.1.1
Bumps [setuptools](https://github.com/pypa/setuptools) from 76.1.0 to 78.1.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v76.1.0...v78.1.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 21:30:53 +00:00
Donkie 7cc3bef46f Merge pull request #809 from Donkie/uv
Multiple maintenance updates
2026-01-18 22:29:54 +01:00
Donkie 8a29ce063a Update ci.yml 2026-01-18 17:30:55 +01:00
Donkie fa6343c368 Fix artifact job 2026-01-18 17:11:54 +01:00
Donkie 1cd8c7a962 Added ci job to produce artifact package for all builds 2026-01-18 17:05:00 +01:00
Donkie b361aee956 CI fixes 2026-01-18 16:55:31 +01:00
kaufi95 c179a38f03 fix for issue #803 2026-01-11 00:01:23 +01:00
kaufi95 6e9f6e5af4 fix for issue #805 2026-01-11 00:00:11 +01:00
Donkie 0f5775c2d4 Enforced eslint and prettier, added lefthook config 2026-01-07 23:02:32 +01:00
Donkie b942d1f8a1 Replaced pdm with uv, updated backend deps, python 3.10 minimum, simplified install script 2026-01-07 22:01:02 +01:00
Donkie cda756557d Updated react qr scanner 2026-01-06 13:05:54 +01:00
Donkie b627e151a8 Updated client packages 2026-01-06 01:54:09 +01:00
Donkie 848f5dcc94 Cleaned up favicons, PWA icon fixes 2026-01-06 00:28:31 +01:00
Donkie 6adb6bedc2 Merge pull request #762 from sixiaolong1117/master
Supports PWA.
2026-01-06 00:19:14 +01:00
Donkie edda1e965a some fixes 2026-01-06 00:18:20 +01:00
SI Xiaolong 7bb0c6b1ec Supports PWA.
PWA is supported using vite-plugin-pwa.
2026-01-05 23:58:58 +01:00
Donkie 9124ce0b86 Merge pull request #747 from Shadowsith/745-docker-size-optimization
Reduce Docker container size
2026-01-05 23:54:42 +01:00
Donkie 753140ecc3 Replaced su-exec with gosu, fixed pdm using wrong python version 2026-01-05 23:53:41 +01:00
Donkie 9baf91380a Merge pull request #717 from ajford/spool_edit_weight_iss477
Fix Spool Edit page Empty Weight and Initial Weight bug
2026-01-05 22:58:42 +01:00
Donkie e1349cc760 Merge pull request #715 from JakobGue/spool_count
Add Spoolcount in Locations Page
2026-01-04 20:05:27 +01:00
JakobGue 185c8b07e0 remove show spool count option from settings 2026-01-04 00:54:19 +01:00
JakobGue 572a5eea27 Merge branch 'master' into spool_count 2026-01-02 16:02:00 +01:00
Donkie da7c274d21 Merge pull request #784 from Donkie/dependabot/npm_and_yarn/client/js-yaml-3.14.2
Bump js-yaml from 3.14.1 to 3.14.2 in /client
2026-01-02 14:59:47 +01:00
Donkie 15ca571c1d Merge pull request #770 from Donkie/dependabot/npm_and_yarn/client/vite-6.4.1
Bump vite from 6.3.5 to 6.4.1 in /client
2026-01-02 14:59:31 +01:00
Donkie 7b9e9c00b2 Merge pull request #765 from Donkie/dependabot/npm_and_yarn/client/axios-1.12.2
Bump axios from 1.9.0 to 1.12.2 in /client
2026-01-02 14:59:06 +01:00
Donkie eb2df7cd6a Merge pull request #714 from Donkie/weblate
Translations update
2026-01-02 00:22:24 +01:00
Donkie c84de2b940 Merge pull request #801 from Donkie/dependabot/npm_and_yarn/client/multi-127d05f333
Bump tmp and inquirer in /client
2026-01-02 00:22:03 +01:00
dependabot[bot] 5fd999151f Bump tmp and inquirer in /client
Removes [tmp](https://github.com/raszi/node-tmp). It's no longer used after updating ancestor dependency [inquirer](https://github.com/SBoudrias/Inquirer.js). These dependencies need to be updated together.


Removes `tmp`

Updates `inquirer` from 8.2.6 to 8.2.7
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@8.2.6...inquirer@8.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 
  dependency-type: indirect
- dependency-name: inquirer
  dependency-version: 8.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 22:01:39 +00:00
Vegard Fladby 6f05ce81c7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 80.2% (231 of 288 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 78.1% (225 of 288 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 75.3% (217 of 288 strings)

Co-authored-by: Vegard Fladby <vegard@fladby.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nb_NO/
Translation: Spoolman/Web Client
2026-01-01 23:00:59 +01:00
vjurka a533f2556f Translated using Weblate (Lithuanian)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: vjurka <v@jurka.lt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lt/
Translation: Spoolman/Web Client
2026-01-01 23:00:58 +01:00
Bazzofski c88d443782 Translated using Weblate (French)
Currently translated at 98.9% (285 of 288 strings)

Co-authored-by: Bazzofski <bastien.cocheril@laposte.net>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2026-01-01 23:00:57 +01:00
Archit Jain 3cd1ff6ac4 Translated using Weblate (Hindi (Latin script))
Currently translated at 3.8% (11 of 288 strings)

Added translation using Weblate (Hindi (Latin script))

Co-authored-by: Archit Jain <archit.jain38@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hi_Latn/
Translation: Spoolman/Web Client
2026-01-01 23:00:56 +01:00
Giadej a41b1df76e Translated using Weblate (Italian)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Giadej <Giadej@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2026-01-01 23:00:56 +01:00
Marek Dvorný bed385b796 Translated using Weblate (Czech)
Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Marek Dvorný <lupickatec@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2026-01-01 23:00:55 +01:00
Dr_Perry_Coke 2508210469 Translated using Weblate (Russian)
Currently translated at 99.6% (287 of 288 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2026-01-01 23:00:54 +01:00
Cristian fae3a92f5e Translated using Weblate (Romanian)
Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Cristian <opreacristiann@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ro/
Translation: Spoolman/Web Client
2026-01-01 23:00:54 +01:00
Yücelen Gül 37fcad4a0a Translated using Weblate (Turkish)
Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Added translation using Weblate (Turkish)

Co-authored-by: Yücelen Gül <yuceleng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/tr/
Translation: Spoolman/Web Client
2026-01-01 23:00:53 +01:00
Jose 4d62eb24c1 Translated using Weblate (Spanish)
Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Jose <jcueria@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2026-01-01 23:00:52 +01:00
dimquea 05f0d7fd20 Translated using Weblate (Russian)
Currently translated at 99.6% (287 of 288 strings)

Co-authored-by: dimquea <dimquea@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2026-01-01 23:00:51 +01:00
KnightArch 3666c10117 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: KnightArch <knightarch@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client
2026-01-01 23:00:51 +01:00
rupefelo 902fbbaec2 Translated using Weblate (Portuguese)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: rupefelo <rupefelo@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2026-01-01 23:00:50 +01:00
Justus Kagerer b10c1e1aef Translated using Weblate (German)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Justus Kagerer <justus-kagerer@web.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2026-01-01 23:00:50 +01:00
derSchreiber a6c4e8c49b Translated using Weblate (German)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: derSchreiber <jan@machen-wir.net>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2026-01-01 23:00:49 +01:00
Donkie 975cd9c613 Merge pull request #800 from Donkie/dependabot/npm_and_yarn/client/multi-6d05d0e569
Bump qs, body-parser and express in /client
2026-01-01 23:00:40 +01:00
dependabot[bot] a124d73fae Bump qs, body-parser and express in /client
Bumps [qs](https://github.com/ljharb/qs), [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.13.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1)

Updates `body-parser` from 1.20.3 to 1.20.4
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.3...1.20.4)

Updates `express` from 4.21.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
- dependency-name: body-parser
  dependency-version: 1.20.4
  dependency-type: indirect
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 13:54:15 +00:00
nebelung 21f46427e5 Add support for Fedora to install.sh 2025-11-18 23:10:44 -05:00
dependabot[bot] 4d48fca237 Bump js-yaml from 3.14.1 to 3.14.2 in /client
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 23:55:28 +00:00
beikeland 967b9d5b31 Change short QR codes to be uppercase to allow for alphanumerical encodingm and make QR code scanner case insensitive 2025-11-04 16:42:33 +01:00
dependabot[bot] 19ccb7099a Bump vite from 6.3.5 to 6.4.1 in /client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 02:24:53 +00:00
dependabot[bot] 13b141dcdf Bump axios from 1.9.0 to 1.12.2 in /client
Bumps [axios](https://github.com/axios/axios) from 1.9.0 to 1.12.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.9.0...v1.12.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 23:02:19 +00:00
Philip Mayer 9df3f0c91b Add changes based on code review https://github.com/Donkie/Spoolman/issues/745 2025-09-22 21:53:45 +02:00
JakobGue e8ca2d55bc Merge branch 'master' into spool_count 2025-09-06 23:25:47 +02:00
Philip Mayer e49976bf8a Reduce docker container size by usage of slim bookworm #745 2025-08-21 00:04:56 +02:00
Donkie 3efb99f635 Merge pull request #732 from Donkie/dependabot/npm_and_yarn/client/form-data-4.0.4
Bump form-data from 4.0.0 to 4.0.4 in /client
2025-07-22 13:59:05 +02:00
dependabot[bot] a05113c454 Bump form-data from 4.0.0 to 4.0.4 in /client
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 00:16:11 +00:00
ajford 25f78e90e6 Fixes #477 by defaulting to spool values first 2025-06-27 00:29:23 -05:00
JakobGue d3181382ae add spool count in locations with a setting to enable / disable the display 2025-06-21 19:26:39 +02:00
Donkie 8af6013331 Add issue manager workflow for automated issue handling 2025-06-16 19:23:26 +02:00
Donkie a6607fb162 Merge pull request #712 from Donkie/weblate
Translations update
2025-06-16 19:10:54 +02:00
தமிழ்நேரம் e1923b8083 Translated using Weblate (Tamil)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ta/
Translation: Spoolman/Web Client
2025-06-16 00:02:04 +00:00
KnightArch 3e6750765f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: KnightArch <knightarch@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client
2025-06-16 00:02:03 +00:00
Julia 🌸🏳️‍⚧️ 13d320ab1d Translated using Weblate (Polish)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Julia 🌸🏳️‍⚧️ <daviteusz0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2025-06-16 00:02:02 +00:00
Miloslav Kos d11bc1a6bb Translated using Weblate (Czech)
Currently translated at 100.0% (288 of 288 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2025-06-16 00:02:01 +00:00
Donkie d27a967737 Fixed pt-BR json file 2025-06-10 22:08:45 +02:00
Donkie 8712ad4840 Fixed en lang json 2025-06-10 22:04:06 +02:00
Donkie 7b7d73224d Set github ci permissions 2025-06-08 12:23:37 +02:00
Donkie 25536233fc Bump server packages 2025-06-08 12:07:10 +02:00
Donkie 1962945c5d Bump client packages 2025-06-08 12:03:27 +02:00
Donkie 0b9b3e7391 Some style fixes 2025-06-08 12:00:10 +02:00
Donkie 77aa955766 Merge pull request #605 from ecarjat/master
Add SPOOLMAN_CORS_ORIGIN variable
2025-06-08 11:53:44 +02:00
Donkie 2766f32d4e Merge pull request #706 from Donkie/weblate
Translations update
2025-06-08 11:48:46 +02:00
Tobias Maschek ff473429c7 Translated using Weblate (German)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: Tobias Maschek <web@jemx.net>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2025-06-08 11:47:26 +02:00
Donkie 5ccd225ed6 Merge pull request #694 from opastorello/master
Remade translation into Brazilian Portuguese
2025-06-08 11:47:17 +02:00
ecarjat de7fccdc1a Merge branch 'master' into master 2025-06-06 09:22:44 +02:00
ecarjat 1ecf70773f Update .env.example 2025-06-06 09:21:57 +02:00
Donkie 8bfb263858 Merge branch 'master' into master 2025-05-31 19:52:01 +02:00
Donkie 488b37c5a0 Merge pull request #677 from Donkie/weblate
Translations update
2025-05-31 19:49:24 +02:00
rupefelo fb355945cb Translated using Weblate (Portuguese)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: rupefelo <rupefelo@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2025-05-31 19:44:51 +02:00
Ivo Grossen 5c8e8b9d11 Translated using Weblate (German)
Currently translated at 99.6% (285 of 286 strings)

Co-authored-by: Ivo Grossen <ivo.grossen95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2025-05-31 19:44:51 +02:00
தமிழ்நேரம் d975020260 Translated using Weblate (Tamil)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ta/
Translation: Spoolman/Web Client
2025-05-31 19:44:51 +02:00
Samuel Wang fdb34f5c5e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2025-05-31 19:44:51 +02:00
Donkie 37803e1029 Merge pull request #660 from spyder007/master
Add Measured Weight option to the filament adjustment modal
2025-05-31 19:44:46 +02:00
Nícolas Pastorello b1d061cf08 Update common.json 2025-05-05 16:44:26 -03:00
Nícolas Pastorello b6ed0cb6e9 Update index.tsx 2025-05-05 16:37:34 -03:00
Nícolas Pastorello 3e8b3526e2 Update common.json 2025-05-05 16:24:46 -03:00
Matt Gerega f31b40bec6 Merge branch 'Donkie:master' into master 2025-04-25 13:20:01 -04:00
Donkie 73a087fce0 Merge pull request #668 from sinclairpaul/mysql-recycle
🔨 Add pool_recycle for MySQL
2025-04-14 08:58:26 +02:00
Paul Sinclair eabb10a009 Merge branch 'Donkie:master' into mysql-recycle 2025-04-12 14:32:42 -04:00
Donkie ed500ef293 Merge pull request #658 from Donkie/weblate
Translations update
2025-04-12 19:52:51 +02:00
dimquea 8e8cf7eaaa Translated using Weblate (Russian)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: dimquea <dimquea@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2025-04-12 17:49:26 +00:00
Arvi Saluste 4c876d7aef Translated using Weblate (Estonian)
Currently translated at 17.4% (50 of 286 strings)

Co-authored-by: Arvi Saluste <fredi.at@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/
Translation: Spoolman/Web Client
2025-04-12 17:49:25 +00:00
KnightArch 97eddadb3b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: KnightArch <knightarch@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client
2025-04-12 17:49:25 +00:00
Donkie c3c2b70213 Updated client deps 2025-04-12 19:47:10 +02:00
Donkie 11a74e2197 Use env for ci ghcr_image since vars is not accessible in PRs 2025-04-12 19:42:53 +02:00
Paul Sinclair ea36a94327 🔨 Add pool_recycle for MySQL 2025-04-05 09:58:34 -04:00
Matt Gerega 4e10f1019d Add Measured Weight option to the filament adjustment modal #560 2025-03-20 16:52:14 -04:00
Donkie ec5173067e Add push to dockerhub 2025-03-19 19:05:17 +01:00
Donkie 4003c0aadd Merge pull request #626 from htsachakis/adjust-spool-button-on-show
Added adjust spool button
2025-03-19 09:03:30 +01:00
Charalampos Tsachakis dbdad84120 Merge branch 'master' into adjust-spool-button-on-show 2025-03-19 09:07:08 +02:00
Donkie 1888240044 Bump version to 0.22.1 2025-03-18 21:41:35 +01:00
Donkie 9e2d10a816 Merge pull request #651 from Donkie/weblate
Translations update
2025-03-18 21:38:31 +01:00
KnightArch db59fce383 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (285 of 286 strings)

Co-authored-by: KnightArch <knightarch@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client
2025-03-18 20:37:08 +00:00
Donkie dcb21f71cb Added auto focus to adjust spool input field
Resolves #585
2025-03-18 21:36:57 +01:00
Donkie aa839b08e5 Handle input number formatting better
Now it doesn't interfer with the user writing until the user leaves the element

Resolves #633
Resolves #632
2025-03-18 21:27:00 +01:00
Donkie 3d1bb70913 Fixed not being able to change color picker format
Resolves #610
2025-03-18 21:09:19 +01:00
Donkie 6f01f372d6 Remove debug logging from numberFormatter function 2025-03-18 21:03:19 +01:00
Donkie a775beb5bf Fixed wonky trailing zero removal
Resolves #623
2025-03-18 20:59:57 +01:00
Donkie a94163dc1a Fixed empty default values extra field range breaking page
Resolves #652
2025-03-18 20:51:26 +01:00
Donkie dcf043cb71 Fix uvicorn logging handler removal to prevent index error 2025-03-18 20:45:36 +01:00
Donkie 3fc2f712e3 Fixed a deprecation 2025-03-18 20:44:28 +01:00
Donkie f041207db8 Fixed broken antd elements
Resolves #655
2025-03-18 20:38:48 +01:00
Donkie dde468ed69 Add Japanese, Brazilian Portuguese, Tamil, and Thai locales to i18n.ts 2025-03-18 20:32:43 +01:00
Donkie e538f06483 Added script to find missing locales in i18n.ts file 2025-03-18 20:32:12 +01:00
Donkie 53d5fdf7e3 Remove country code flags in language selector 2025-03-18 20:31:53 +01:00
Donkie a5df70a803 Bump version to 0.22.0 2025-03-16 21:34:21 +01:00
Donkie 70ea7ad414 Update python dependencies and ruff/black versions 2025-03-16 21:34:02 +01:00
Donkie 92065fd8df Update python dependencies 2025-03-16 18:23:40 +01:00
Donkie 11b12ac2e7 Updated client packages 2025-03-16 17:49:20 +01:00
Donkie 0e4d0b3e14 Merge pull request #639 from tlanfer/master
Allow PATCH'ing partial updates of extra attributes
2025-03-16 17:47:22 +01:00
Donkie 15e3e3f187 Merge pull request #645 from Donkie/weblate
Translations update
2025-03-16 17:42:54 +01:00
Maurice Kevenaar 88c25d9fea Translated using Weblate (Dutch)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2025-03-16 17:42:05 +01:00
Yuta Imada 8cc485ac96 Translated using Weblate (Japanese)
Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Japanese)

Currently translated at 59.0% (169 of 286 strings)

Translated using Weblate (Japanese)

Currently translated at 34.9% (100 of 286 strings)

Added translation using Weblate (Japanese)

Co-authored-by: Yuta Imada <1@yutaimd.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ja/
Translation: Spoolman/Web Client
2025-03-16 17:42:04 +01:00
Donkie 3c2936bf5e Merge pull request #649 from pedrolamas/pedrolamas/fix-multi-color-reset
Reset multi-color when single color is selected
2025-03-16 17:41:56 +01:00
Pedro Lamas 3963ec6203 Reset multi-color when single color is selected 2025-03-12 18:10:26 +00:00
Thorsten Lanfer c8ab290f33 Merge branch 'master' into master 2025-03-05 19:55:44 +01:00
Thorsten Lanfer 55acc4993d reformat 2025-03-05 19:46:54 +01:00
Donkie d253391b63 Merge pull request #640 from Donkie/weblate
Translations update
2025-03-05 19:04:19 +01:00
தமிழ்நேரம் 2761829db8 Translated using Weblate (Tamil)
Currently translated at 100.0% (286 of 286 strings)

Added translation using Weblate (Tamil)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ta/
Translation: Spoolman/Web Client
2025-03-05 12:09:14 +01:00
Julia 🌸 4a1358a438 Translated using Weblate (Polish)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: Julia 🌸 <daviteusz0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2025-03-05 12:09:13 +01:00
Miloslav Kos e54608f139 Translated using Weblate (Czech)
Currently translated at 100.0% (286 of 286 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2025-03-05 12:09:13 +01:00
Thorsten Lanfer 4b8fd3aed3 Update spool.py 2025-02-24 22:21:10 +01:00
Thorsten Lanfer 28f6f8e916 Allow posting partial updates of extra attributes 2025-02-24 22:21:10 +01:00
Donkie cd274a9968 Updated client dependencies 2025-02-24 21:46:56 +01:00
Donkie 3a44a01467 Fixed style in externaldb.py 2025-02-24 21:20:27 +01:00
Donkie 4b08f27614 Merge pull request #624 from bdchik/fix
Bug Fix:  Ensure the QR code favicon.svg honors the base path, when configured.
2025-02-24 21:04:19 +01:00
Donkie d930217cd3 Merge pull request #604 from cboelsen/master
Allow disabling access log with uvicorn's `--no-access-log`
2025-02-24 21:02:17 +01:00
Donkie 80b869feec Merge pull request #595 from Donkie/dependabot/npm_and_yarn/client/vite-5.4.14
Bump vite from 5.4.11 to 5.4.14 in /client
2025-02-24 21:01:30 +01:00
Donkie cf0d458364 Merge pull request #591 from inukiwi/master
Do not ignore empty lists in database selects
2025-02-24 21:00:30 +01:00
Donkie 9d9f297065 Merge pull request #587 from fleaz/fix_disable_download
Properly build URLs with urljoin
2025-02-24 20:57:26 +01:00
Donkie 58a6233e7b Merge pull request #582 from ErikMichelson/feat/round-prices-setting
Add setting to toggle rounding of prices
2025-02-24 20:56:44 +01:00
Donkie 03815687ff Merge pull request #526 from Pantastisch/feature/hex-color-code-in-filament-page
Add Color Code Display for Single Color Filaments
2025-02-24 20:48:31 +01:00
Donkie c0af80d439 Merge pull request #581 from Donkie/weblate
Translations update
2025-02-24 20:46:42 +01:00
Charalampos Tsachakis 53da38e396 Added adjust spool button
Added adjust spool button to spools/show page
2025-02-12 11:31:11 +02:00
derSchreiber 4a914d7bce Translated using Weblate (German)
Currently translated at 99.6% (283 of 284 strings)

Translated using Weblate (German)

Currently translated at 99.6% (283 of 284 strings)

Translated using Weblate (German)

Currently translated at 99.6% (283 of 284 strings)

Co-authored-by: derSchreiber <jan@machen-wir.net>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Sascha Bürk d52826656d Translated using Weblate (German)
Currently translated at 99.6% (283 of 284 strings)

Translated using Weblate (German)

Currently translated at 99.6% (283 of 284 strings)

Co-authored-by: Sascha Bürk <macfly@german-bash.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Julia 🌸 5a32d140ef Translated using Weblate (Polish)
Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Julia 🌸 <daviteusz0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Icezaza Ch 834fd1ed18 Translated using Weblate (Thai)
Currently translated at 100.0% (284 of 284 strings)

Added translation using Weblate (Thai)

Co-authored-by: Icezaza Ch <icezaza.jar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Mathis Mensing 4fcd42a301 Translated using Weblate (German)
Currently translated at 99.6% (283 of 284 strings)

Co-authored-by: Mathis Mensing <github@matmen.dev>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Marcus Vechiato 07dbb09a5e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Marcus Vechiato <vechiato@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt_BR/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Dr_Perry_Coke 3f97be2e51 Translated using Weblate (Russian)
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
Samuel Wang 9efed6dea7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2025-02-11 22:28:28 +01:00
aurelien 67c2c89e2a Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: aurelien <a.ardjoune@etik.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2025-02-11 22:28:27 +01:00
Brett Chik c5878883a8 Link to print label from spool details page missing getBasePath() 2025-02-07 20:24:03 -08:00
Brett Chik 21befc2993 Ensure the QR code icon honors the base path, when configured. 2025-02-07 20:09:29 -08:00
ecarjat 108fdd2908 Added SPOOLMAN_CORS_ORIGIN variable 2025-01-26 15:41:58 +01:00
Christian Boelsen 418c8cf518 Allow disabling access log with uvicorn's --no-access-log 2025-01-25 16:10:07 +00:00
dependabot[bot] 2e67d7e6a3 Bump vite from 5.4.11 to 5.4.14 in /client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.11 to 5.4.14.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 04:46:52 +00:00
inukiwi 1f3c1c0ad5 Do not ignore empty lists in database selects 2025-01-15 23:14:09 +01:00
fleaz a2e6ad983a Properly build URLs with urljoin 2025-01-13 19:51:35 +01:00
Erik Michelson e93f89aa51 Add setting to toggle rounding of prices 2024-12-30 01:07:38 +01:00
Donkie 276fd1e733 Merge pull request #484 from Forest-Troll/master
Prometheus metrics improve
2024-12-28 22:04:14 +01:00
Donkie a14a520116 Merge pull request #557 from Donkie/weblate
Translations update
2024-12-28 22:03:27 +01:00
Maurice Kevenaar 524e0f8657 Translated using Weblate (Dutch)
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-12-28 21:01:32 +00:00
Kayz C c83ddbf5c5 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Kayz C <kayzed.x@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client
2024-12-28 21:01:32 +00:00
Diogo Resende e6cd503ebd Translated using Weblate (Portuguese)
Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (French)

Currently translated at 99.6% (283 of 284 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-12-28 21:01:31 +00:00
darkk-k1 291572ddc1 Translated using Weblate (Polish)
Currently translated at 98.9% (281 of 284 strings)

Translated using Weblate (Polish)

Currently translated at 85.9% (244 of 284 strings)

Co-authored-by: darkk-k1 <softdev@darkabo.eu>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-12-28 21:01:30 +00:00
Daniel Zając e2a1113033 Translated using Weblate (Polish)
Currently translated at 98.9% (281 of 284 strings)

Translated using Weblate (Polish)

Currently translated at 85.9% (244 of 284 strings)

Co-authored-by: Daniel Zając <zajac20033@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-12-28 21:01:30 +00:00
Donkie 6421f723ee Merge pull request #578 from Khaos66/master
Enable install script to be automated
2024-12-28 22:01:23 +01:00
Khaos 45ea68bbc0 Enable install script to be automated
Added a script parameter to control the systemd service installation step
2024-12-28 20:24:57 +01:00
Panagiotis Doulgeris 6b2dbda354 Add possibility to exclude margin in spool icon for overview sites 2024-12-22 15:38:05 +01:00
Dmitry Belousov b37c19eb9e diameter is missed 2024-12-12 22:42:22 +03:00
Dmitry Belousov 385d9bb3da Add spool initial weight. Change some descriptions. 2024-12-12 22:40:35 +03:00
Dmitry Belousov 4479da84ed Merge remote-tracking branch 'origin/master'
# Conflicts:
#	spoolman/prometheus/metrics.py
2024-12-12 22:39:24 +03:00
Dmitry Belousov 4ee34f67d1 Add spool initial weight. Change some descriptions. 2024-12-12 22:38:11 +03:00
Forest-Troll 9ceee4aa7a Merge branch 'Donkie:master' into master 2024-12-12 22:33:19 +03:00
Donkie 053b20f19c Bump version to 0.21.0 2024-12-01 14:54:31 +01:00
Donkie a3f506ce85 Bumped python dependencies 2024-12-01 14:53:58 +01:00
Donkie 0e6bdd764f Merge pull request #548 from Donkie/weblate
Translations update
2024-12-01 14:35:34 +01:00
gallegonovato d4ddb1f63e Translated using Weblate (Spanish)
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-11-25 21:36:36 +01:00
Miloslav Kos bb7b64669e Translated using Weblate (Czech)
Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-11-25 21:36:36 +01:00
Donkie 1372d75d90 Update README.md 2024-11-25 21:36:19 +01:00
Donkie 139d435cee Fixed manufacturer with spool weight 0 not being allowed
Resolves #521
2024-11-25 20:49:39 +01:00
Donkie a3be91b89c Fixed not being able to clear price field
Resolves #525
2024-11-25 20:39:24 +01:00
Donkie 649c074bb3 Fix uvicorn doing its own thing when logging
Resolves #532
2024-11-25 20:21:28 +01:00
Donkie 04920e463a Fixed number precision in locations page spool card 2024-11-25 19:07:24 +01:00
Donkie c57141254c Merge branch 'locationspage' into master 2024-11-24 15:05:01 +01:00
Donkie 46a9097491 Final locations updates 2024-11-24 15:04:20 +01:00
Donkie d142b0eaec Include the custom locations in spool edit page 2024-11-24 14:51:15 +01:00
Donkie 0827cc8a5a Improvements to default location 2024-11-24 14:45:16 +01:00
Donkie 755e06c989 Now goes back to loc page on edit submit 2024-11-24 12:31:38 +01:00
Donkie bb0b00b973 Merge pull request #542 from Donkie/weblate
Translations update
2024-11-24 09:56:44 +01:00
Donkie 1a55855ce9 Some i18n stuff 2024-11-23 23:16:41 +01:00
Donkie 67c5b34bf5 Final touches on locations page 2024-11-23 22:42:47 +01:00
Samuel Wang c68c32698a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-11-23 06:00:33 +01:00
Daniel Zając 289818ab06 Translated using Weblate (Polish)
Currently translated at 84.5% (235 of 278 strings)

Co-authored-by: Daniel Zając <zajac20033@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-11-23 06:00:31 +01:00
Donkie bf06408368 Added reading and using of spool orders in locations 2024-11-20 21:57:01 +01:00
Donkie ce42d687ed Some file rearranging 2024-11-20 21:08:43 +01:00
Donkie 37b6d1fcee Added ability to rename location 2024-11-20 21:00:39 +01:00
Donkie 5861190821 Fixed being able to create multiple locs of same name 2024-11-20 20:15:46 +01:00
Donkie 8d6f2636c5 Locations can now be drag-dropped around 2024-11-20 20:12:08 +01:00
Donkie 3dc0082f0e Added ability to add new location 2024-11-20 19:37:56 +01:00
Donkie e4175e26ec Added a locations setting so empty locations are still kept around 2024-11-19 22:32:05 +01:00
Donkie 1eca7b2503 Added buttons to view/edit spools on locations page 2024-11-19 22:04:06 +01:00
Donkie b568147884 Updated some dependencies 2024-11-19 21:56:22 +01:00
Donkie a4355967d0 Added basic locations page to easily move spools between locations 2024-11-19 21:49:29 +01:00
Donkie 502ea1ba8b Added Romanian, Persian and Traditional Chinese to list of select-able languages 2024-11-18 22:41:14 +01:00
Donkie b555f812ef Merge pull request #523 from Donkie/weblate
Translations update
2024-11-18 22:21:33 +01:00
Kayz C e0371192a7 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (278 of 278 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 73.7% (205 of 278 strings)

Added translation using Weblate (Chinese (Traditional Han script))

Co-authored-by: Kayz C <kayzed.x@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/
Translation: Spoolman/Web Client
2024-11-18 16:00:32 +01:00
darkk-k1 b129fd3cb7 Translated using Weblate (Polish)
Currently translated at 84.5% (235 of 278 strings)

Translated using Weblate (Polish)

Currently translated at 83.4% (232 of 278 strings)

Co-authored-by: darkk-k1 <softdev@darkabo.eu>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-11-18 16:00:31 +01:00
Bestia Nadir 0c8ec342ce Translated using Weblate (Romanian)
Currently translated at 57.9% (161 of 278 strings)

Co-authored-by: Bestia Nadir <nadirbestia@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ro/
Translation: Spoolman/Web Client
2024-11-18 16:00:29 +01:00
Donkie ba488655bf Update README.md 2024-11-07 21:07:15 +01:00
Donkie 0ef4649927 Update README.md 2024-11-06 22:24:02 +01:00
Donkie 281e6cbe44 Update README.md 2024-11-06 22:21:36 +01:00
Panagiotis Doulgeris fd256bab95 Show hex color code in filament page and improve styling 2024-11-01 21:36:17 +01:00
Donkie 39aea03a01 Merge pull request #517 from TomW1605/add-archive-un-archive-button-to-spool-view-page
add archive/un archive button to spool view page
2024-10-27 18:21:24 +01:00
Donkie 86f69d470f Added backend supported csv/json export functionality 2024-10-27 12:36:39 +01:00
TomW1605 d60b93349a fix spool weight < 0 check 2024-10-27 15:20:53 +08:00
Donkie a95ddcedd1 Merge pull request #506 from Donkie/weblate
Translations update
2024-10-26 22:51:53 +02:00
Oleksii Martyniuk dc93e5b725 Translated using Weblate (Ukrainian)
Currently translated at 96.4% (268 of 278 strings)

Co-authored-by: Oleksii Martyniuk <hohnabeer@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client
2024-10-26 22:31:48 +02:00
Miloslav Kos 7783718d44 Translated using Weblate (Czech)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-10-26 22:31:47 +02:00
AKTronics 5645b5358f Translated using Weblate (Persian)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: AKTronics <khazaei.abolfazl@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/
Translation: Spoolman/Web Client
2024-10-26 22:31:47 +02:00
Dr_Perry_Coke 9c0f1c489d Translated using Weblate (Russian)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-10-26 22:31:46 +02:00
Donkie 4a4a6c20e3 Merge pull request #519 from Donkie/dependabot/npm_and_yarn/client/rollup-4.24.0
Bump rollup from 4.21.3 to 4.24.0 in /client
2024-10-26 22:30:04 +02:00
dependabot[bot] 3bbfb1d92e Bump rollup from 4.21.3 to 4.24.0 in /client
Bumps [rollup](https://github.com/rollup/rollup) from 4.21.3 to 4.24.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.21.3...v4.24.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-26 20:27:48 +00:00
Donkie 25a33c6361 Merge pull request #513 from Donkie/dependabot/npm_and_yarn/client/multi-9f37c16f8f
Bump cookie and express in /client
2024-10-26 22:26:51 +02:00
Donkie 8a0370d74b Merge pull request #518 from TomW1605/add-external-id-to-filament-and-vendor-edit-pages
add external id to filament and vendor edit pages
2024-10-26 22:26:20 +02:00
TomW1605 761a4978b6 add external id to filament and vendor edit pages 2024-10-26 14:12:32 +08:00
TomW1605 efae0d8466 add archive/un archive button to spool view page 2024-10-26 13:48:27 +08:00
dependabot[bot] 6a5847c152 Bump cookie and express in /client
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-19 10:28:08 +00:00
Donkie b74b7e0205 Merge pull request #497 from Uro1/fix-line-endings 2024-09-30 17:32:23 +02:00
Uro ea215efdfc Update gitattributes to preserve lf line endings on the .env.example file 2024-09-30 12:19:20 +01:00
Donkie 590b0df191 Fixed release zip missing dot-files 2024-09-23 19:26:29 +02:00
Donkie 62705b590b Merge pull request #482 from Donkie/weblate 2024-09-23 11:31:30 +02:00
AKTronics 551e31a9c6 Translated using Weblate (Persian)
Currently translated at 14.7% (41 of 278 strings)

Added translation using Weblate (Persian)

Co-authored-by: AKTronics <khazaei.abolfazl@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/
Translation: Spoolman/Web Client
2024-09-23 11:16:21 +02:00
Diogo Resende 6f6cebb88e Translated using Weblate (Italian)
Currently translated at 100.0% (278 of 278 strings)

Added translation using Weblate (Portuguese (Brazil))

Translated using Weblate (Portuguese)

Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-09-23 11:16:20 +02:00
Vincent MALZIEU 00d4ae7c22 Translated using Weblate (French)
Currently translated at 100.0% (278 of 278 strings)

Translated using Weblate (French)

Currently translated at 98.9% (275 of 278 strings)

Co-authored-by: Vincent MALZIEU <vincent.malzieu@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-09-23 11:16:19 +02:00
Maurice Kevenaar 5b6151c4ac Translated using Weblate (Dutch)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-09-23 11:16:18 +02:00
gallegonovato d06f044b83 Translated using Weblate (Spanish)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-09-23 11:16:17 +02:00
Miloslav Kos 09f5cba082 Translated using Weblate (Czech)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-09-23 11:16:16 +02:00
Donkie 980809f1ad Merge pull request #488 from Donkie/dependabot/npm_and_yarn/client/multi-9423f4c335 2024-09-18 06:06:23 +02:00
dependabot[bot] 59c76c37cf Bump body-parser and express in /client
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 04:05:36 +00:00
Donkie 9cb1dfbf79 Merge pull request #487 from Donkie/dependabot/npm_and_yarn/client/vite-5.4.6 2024-09-18 06:04:37 +02:00
dependabot[bot] 3aa2f93cf2 Bump vite from 5.3.5 to 5.4.6 in /client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.5 to 5.4.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 20:03:48 +00:00
Dmitry Belousov 36b04ae242 Rename metrics with base units.
Add spool initial weight.
2024-09-15 23:48:25 +03:00
Donkie 4f8d5abcec Bump version to 0.20.0 2024-09-08 22:42:34 +02:00
Donkie a826db12ea Merge pull request #446 from TomW1605/allow-prefix-and-suffix-with-double-braces
allow prefix and suffix with double braces in label format
2024-09-08 22:38:39 +02:00
Donkie 608d88f6c5 Merge pull request #476 from Donkie/weblate
Translations update
2024-09-08 22:35:45 +02:00
Federico Di Lorenzo 533139b879 Translated using Weblate (Italian)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Federico Di Lorenzo <federico.dilo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-09-08 20:18:00 +00:00
Benedikt Fuchs df150553f7 Translated using Weblate (German)
Currently translated at 99.6% (277 of 278 strings)

Co-authored-by: Benedikt Fuchs <fuchs.bene@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-09-08 20:18:00 +00:00
Donkie 909d8c38bd Test stage now only downloads the relevant images 2024-09-08 22:17:46 +02:00
Donkie 13a038cb3e Updated all action versions 2024-09-08 13:29:27 +02:00
Donkie eeb1e53490 Merge pull request #475 from Donkie/dependabot/npm_and_yarn/client/axios-1.7.7
Bump axios from 1.7.3 to 1.7.7 in /client
2024-09-07 19:56:46 +02:00
Donkie 4469523650 Merge pull request #469 from Donkie/weblate
Translations update
2024-09-07 19:56:24 +02:00
dependabot[bot] a0e62e2fd5 Bump axios from 1.7.3 to 1.7.7 in /client
Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.7.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.7)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-07 17:55:05 +00:00
Donkie b70cfb0e79 Bump artifact action versions 2024-09-07 19:54:01 +02:00
Vincent MALZIEU b930c68479 Translated using Weblate (French)
Currently translated at 98.9% (275 of 278 strings)

Co-authored-by: Vincent MALZIEU <vincent.malzieu@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-09-06 23:09:14 +02:00
Maurice Kevenaar c9e8d1b80f Translated using Weblate (Dutch)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-09-06 23:09:13 +02:00
TomW1605 7b0e98827e add double {{}} explanation to help text and example 2024-08-31 08:16:38 +08:00
Donkie 5bd9d055d0 Merge pull request #460 from Donkie/weblate 2024-08-28 07:39:40 +02:00
gallegonovato 1f2e26b6f1 Translated using Weblate (Spanish)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-08-27 19:09:16 +00:00
Miloslav Kos 334a00054e Translated using Weblate (Czech)
Currently translated at 100.0% (278 of 278 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-08-27 19:09:15 +00:00
Giovanni Locatelli fbcaa81128 Translated using Weblate (Italian)
Currently translated at 100.0% (271 of 271 strings)

Co-authored-by: Giovanni Locatelli <giovi.loc98@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-27 19:09:14 +00:00
Zack zhou (WeAPoN.Z) 5f4bbe6a7b Translated using Weblate (Chinese (Simplified))
Currently translated at 95.9% (260 of 271 strings)

Co-authored-by: Zack zhou (WeAPoN.Z) <zszxzb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-08-27 19:09:13 +00:00
Donkie a6d271ac87 Merge pull request #461 from samturner3/master
Feature: Add option to use full URL in QR codes instead of web+spoolman: prefix
2024-08-26 20:23:25 +02:00
Donkie 93c9f46385 Merge pull request #458 from teoteo/teoteo-patch-1
Helps to identify the icon
2024-08-26 20:16:06 +02:00
samturner3 e65b87cc33 fix: Remove some en, ensure setting is default false 2024-08-26 18:34:00 +10:00
samturner3 c54e11ca11 feat: Add base_url and URB based QR code on labels 2024-08-26 18:08:13 +10:00
samturner3 a0e9f10127 Rename setting 'qr_code_url' to 'base_url' 2024-08-26 18:08:13 +10:00
samturner3 e3175723ea fix: Settings file 2024-08-26 18:08:13 +10:00
samturner3 2937d7d5bc Merge remote-tracking branch 'upstream/master' 2024-08-24 16:18:17 +10:00
teoteo 54ac546f78 Helps to identify the icon when there is low contrast between color and table background 2024-08-20 08:57:11 +02:00
Donkie 32c737fa98 Revert so commit now happens before notice, but add a catch-all to websocket notifications 2024-08-16 15:13:09 +02:00
Donkie 8edf6d2e20 Merge pull request #449 from Donkie/weblate
Translations update
2024-08-16 10:33:45 +02:00
Tobias Maschek 1a9b964fc2 Translated using Weblate (German)
Currently translated at 97.7% (265 of 271 strings)

Co-authored-by: Tobias Maschek <web@jemx.net>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-08-16 10:28:26 +02:00
Donkie e5f79493cc Fixed error on trying to send ws message to disconnected client
This could result in a major filament usage track failure since the Use http request would fail and Moonraker would keep retrying to save the used filament. However it was actually saved since the db.commit() happens before the websocket message fails.
2024-08-16 10:28:22 +02:00
Thomas White 813d299c8c allow prefix and suffix with double braces 2024-08-14 21:16:18 +08:00
Donkie 5c5bd4627b Bump version to 0.19.3 2024-08-10 11:50:16 +02:00
Donkie da89b80d07 Allow larger print preview scale size
Resolves #439
2024-08-10 11:45:21 +02:00
Donkie 14a22e072f Fixed being unable to change currency
Resolves #436
2024-08-10 11:44:11 +02:00
Donkie 2e414af9bb Bump version to 0.19.2 2024-08-09 17:16:08 +02:00
Donkie d7479d3cdf Merge pull request #432 from Donkie/weblate
Translations update
2024-08-09 17:15:21 +02:00
Maurice Kevenaar 4a65ff574c Translated using Weblate (Dutch)
Currently translated at 100.0% (271 of 271 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-08-09 15:15:06 +00:00
Ano10 ed7f09df3a Translated using Weblate (French)
Currently translated at 100.0% (271 of 271 strings)

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-08-09 15:15:05 +00:00
Diogo Resende 5829d24807 Translated using Weblate (Portuguese)
Currently translated at 99.2% (269 of 271 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-08-09 15:15:05 +00:00
Vegard Fladby 88661c08ad Translated using Weblate (Norwegian Bokmål)
Currently translated at 72.6% (197 of 271 strings)

Co-authored-by: Vegard Fladby <vegard@fladby.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nb_NO/
Translation: Spoolman/Web Client
2024-08-09 15:15:04 +00:00
Daniel Hultgren 225aa77d41 Translated using Weblate (Swedish)
Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Swedish)

Currently translated at 89.2% (242 of 271 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-08-09 15:15:04 +00:00
gallegonovato 322354a6c3 Translated using Weblate (Spanish)
Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (267 of 267 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-08-09 15:15:03 +00:00
Miloslav Kos ebbaa6ee12 Translated using Weblate (Czech)
Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (261 of 261 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-08-09 15:15:03 +00:00
Donkie b2f6227540 Merge pull request #435 from foxt/patch-1
Install script: Don't bother using sudo if user is running as root
2024-08-09 17:14:56 +02:00
Donkie 2fd80038e3 Bump version to 0.19.1 2024-08-09 17:07:13 +02:00
Donkie 9469f47f09 Fixed importing multi color filaments in spool create form 2024-08-09 17:04:40 +02:00
Donkie eb4e1a950a Fixed importing multi-color in create filament 2024-08-09 16:58:30 +02:00
foxt 208e2f67e3 Install script: Don't bother using sudo if user is running as root
On some systems, sudo isn't present by default, so allow the user to manually elevate the script bypassing the use of sudo.
2024-08-09 10:01:59 +01:00
Donkie eee36fad66 Fixed multi color picker not working 2024-08-08 23:02:11 +02:00
Donkie c93ca5d916 Bump version to 0.19.0 2024-08-07 15:44:24 +02:00
Donkie 902e98b059 Increased itest connection timeout 2024-08-07 15:27:22 +02:00
Donkie 3454002c09 Fixed conftest 2024-08-07 15:19:35 +02:00
Donkie 515722bf4f Attempt to make itest spoolman up check more robust 2024-08-07 15:12:42 +02:00
Donkie efa93615ec Updated dependencies in integration tests 2024-08-07 14:45:43 +02:00
Donkie ae26bf5733 Fixed label template error if invalid extra field 2024-08-07 14:26:50 +02:00
Donkie 10ad8cebcb Added button to save labels as png images
Resolves #80
Resolves #348
2024-08-07 13:58:48 +02:00
Donkie 7082c5206b Added ability to hide qr code entirely when printing
Resolves #231
2024-08-07 13:09:43 +02:00
Donkie 7108b0e7a1 Optimized and organized all clients imports 2024-08-07 12:46:37 +02:00
Donkie 8413e33895 Minor fix to ActionsColumn function
No longer uses hooks but instead takes in the title
2024-08-07 12:40:54 +02:00
Donkie 736e59d1eb Improve useLiveify a bit 2024-08-07 12:19:25 +02:00
Donkie 3bd93cb0af Removed React prefix to some use hooks 2024-08-06 14:28:09 +02:00
Donkie 701ed2fae6 Added button to adjust filament
Resolves #428
Resolves #30
2024-08-06 11:34:47 +02:00
Donkie 00e35584f4 Fixed item cloning not handling extra fields properly
Resolves #424
2024-08-05 19:56:42 +02:00
Donkie 48ef3a23f2 Fixed client showing wrong spool price if overridden to 0
Resolves #431
2024-08-05 19:43:05 +02:00
Donkie 93d27a96d3 Spool weight can now be set to 0
Fixes #422
2024-08-05 19:39:54 +02:00
Donkie 694e4b9f61 Update compose-action to try fix CI issue 2024-08-05 19:35:17 +02:00
Donkie f3711f3789 Formatting 2024-08-05 19:26:35 +02:00
Donkie 3b1a09752e Fixed AlignType import 2024-08-05 19:25:14 +02:00
Donkie 71b3ad8f7b Update pdm version in pre-commit 2024-08-05 19:20:29 +02:00
Donkie ae0dbc740b Fixed client build 2024-08-05 19:20:29 +02:00
Donkie 5afe53e05b Merge pull request #423 from tzahari/master
Optimized list views
2024-08-05 19:16:50 +02:00
Donkie 4f49481475 Update PDM lockfile
This updates all dependencies, as well as makes it compatible with latest PDM which now complains about the cross_platform strategy choice, we instead use inherit_metadata now.
2024-08-05 19:15:34 +02:00
Donkie 093d5e095c Merge remote-tracking branch 'weblate/master' into master 2024-08-05 18:57:49 +02:00
Ahti Nurme 6f1338caf0 Translated using Weblate (Estonian)
Currently translated at 12.4% (32 of 258 strings)

Added translation using Weblate (Estonian)

Co-authored-by: Ahti Nurme <ahtinurme@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/
Translation: Spoolman/Web Client
2024-08-05 16:49:51 +00:00
Diogo Resende 65b8762805 Translated using Weblate (Portuguese)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-08-05 16:49:48 +00:00
Mantasp Mants d32d6779c3 Added translation using Weblate (Lithuanian)
Co-authored-by: Mantasp Mants <mantasjucuus150@gmail.com>
2024-08-05 16:49:47 +00:00
Dr_Perry_Coke f7a5149c81 Translated using Weblate (Russian)
Currently translated at 94.1% (243 of 258 strings)

Translated using Weblate (Russian)

Currently translated at 93.7% (242 of 258 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-08-05 16:49:46 +00:00
Alessandro Schiavo b7d2e13b8f Translated using Weblate (Italian)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Alessandro Schiavo <schiavo.alessandro.as@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:44 +00:00
srbjessen 59089303eb Translated using Weblate (Danish)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client
2024-08-05 16:49:43 +00:00
Maurice Kevenaar f5cd2c8771 Translated using Weblate (Dutch)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-08-05 16:49:42 +00:00
Federico Di Lorenzo 1587b116db Translated using Weblate (Italian)
Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Federico Di Lorenzo <federico.dilo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:41 +00:00
gallegonovato 3df7d6ad3e Translated using Weblate (Spanish)
Currently translated at 100.0% (261 of 261 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (260 of 260 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (260 of 260 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-08-05 16:49:40 +00:00
Miloslav Kos c288dc3f9b Translated using Weblate (Czech)
Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (260 of 260 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-08-05 16:49:39 +00:00
Alessandro Schiavo 98105f7bab Translated using Weblate (Italian)
Currently translated at 98.4% (250 of 254 strings)

Co-authored-by: Alessandro Schiavo <schiavo.alessandro.as@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:38 +00:00
Donkie fc505c98f1 Merge pull request #399 from Donkie/weblate
Translations update
2024-08-05 18:49:32 +02:00
Donkie 92f70e73e0 Updated most node dependencies 2024-08-05 18:49:15 +02:00
Donkie 21b9f16531 Print preview area now sizes according to its sibling area 2024-08-05 18:39:41 +02:00
Donkie bf8cbc7a96 Fixed continue button in spool select page 2024-08-05 18:16:02 +02:00
Donkie f8c518e808 Added print page header and back functionality 2024-08-05 18:11:44 +02:00
Ahti Nurme 356946d8d3 Translated using Weblate (Estonian)
Currently translated at 12.4% (32 of 258 strings)

Added translation using Weblate (Estonian)

Co-authored-by: Ahti Nurme <ahtinurme@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Diogo Resende edbde651ed Translated using Weblate (Portuguese)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Mantasp Mants d90426b63d Added translation using Weblate (Lithuanian)
Co-authored-by: Mantasp Mants <mantasjucuus150@gmail.com>
2024-08-05 16:49:27 +02:00
Dr_Perry_Coke 87e1e4fb5d Translated using Weblate (Russian)
Currently translated at 94.1% (243 of 258 strings)

Translated using Weblate (Russian)

Currently translated at 93.7% (242 of 258 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Alessandro Schiavo 19af94efb9 Translated using Weblate (Italian)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Alessandro Schiavo <schiavo.alessandro.as@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
srbjessen c69035e707 Translated using Weblate (Danish)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Maurice Kevenaar 981d08ea65 Translated using Weblate (Dutch)
Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Federico Di Lorenzo d9cfc90060 Translated using Weblate (Italian)
Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (258 of 258 strings)

Co-authored-by: Federico Di Lorenzo <federico.dilo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
gallegonovato cbdb3ad221 Translated using Weblate (Spanish)
Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (260 of 260 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Miloslav Kos 69b856447d Translated using Weblate (Czech)
Currently translated at 100.0% (258 of 258 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (260 of 260 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Alessandro Schiavo e5a6bf6118 Translated using Weblate (Italian)
Currently translated at 98.4% (250 of 254 strings)

Co-authored-by: Alessandro Schiavo <schiavo.alessandro.as@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-08-05 16:49:27 +02:00
Donkie 38ec685356 Added preset duplicate button 2024-08-05 16:09:43 +02:00
Donkie 71f6ce55f5 Renamed print settings to presets instead, manual preset store button 2024-08-05 16:06:00 +02:00
Donkie b764e9ecd9 Spools to print are now stored using query parameters 2024-08-05 15:41:07 +02:00
Donkie 9318ef5c19 Label printing page updates
No longer a modal
Use flex divs instead of a table
2024-08-05 15:39:38 +02:00
Thomas Zahari 7e87751caf Added right alignment for NumberColumns and price
Removed seconds from date / times
Removed decimals from spool length and weight
2024-07-12 16:42:26 +02:00
Donkie 9aa9479a14 Updated most client deps 2024-06-19 21:31:12 +02:00
Donkie 052bdb453d Added default template 2024-06-19 21:18:48 +02:00
Donkie 20923deebd Added bold tags to template 2024-06-19 21:13:18 +02:00
Donkie 515aafac8f Fixed incorrect react key 2024-06-19 21:13:07 +02:00
Donkie 58e76b0ece Re-added missing translation text 2024-06-19 20:36:06 +02:00
Donkie b2e49aa270 Added new print label template system 2024-06-19 20:33:23 +02:00
Donkie 4100813f22 Stop settings spam in label settings 2024-06-18 19:37:17 +02:00
Donkie 0452b0be04 Added ability to select and create multiple label settings 2024-06-18 19:09:42 +02:00
Donkie 426496b4e5 Recoded label printing settings to store in DB instead of in browser 2024-06-15 14:07:48 +02:00
Donkie 4d0e76b64f Merge branch 'multicolor' 2024-06-11 18:30:45 +02:00
Donkie f1b27b441d New spool icon updates 2024-06-11 17:09:54 +02:00
Donkie e2df888e96 New spool color indication that supports multi colors 2024-06-09 19:23:43 +02:00
Donkie f7fce7f2cc Added printing dialog item copies
Resolves #398
2024-06-09 12:28:30 +02:00
Donkie 111eab1f24 Update README.md 2024-06-08 23:51:37 +02:00
Donkie 30098e0213 Abstracted spoolicon into its own component 2024-06-08 22:56:13 +02:00
Donkie f3d23ad9fe Added client edit support for multi colors 2024-06-08 22:56:13 +02:00
Donkie ac2829ef84 Added create filament page support for multi color 2024-06-08 22:56:13 +02:00
Donkie 4b155a93d2 Merge pull request #386 from Donkie/weblate
Translations update
2024-06-08 22:50:32 +02:00
Donkie cc7dceb2a3 Fixed backend not supporting editing multi color dir 2024-06-08 22:50:01 +02:00
Donkie c7d58a2938 Enabled Portuguese language
Resolves #390
2024-06-08 22:50:01 +02:00
Samuel Wang d9417655b4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-06-06 23:09:34 +02:00
Manuel 62573ecaff Translated using Weblate (German)
Currently translated at 98.0% (249 of 254 strings)

Co-authored-by: Manuel <mahr92@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-06-06 23:09:33 +02:00
luger16 06e04c9000 Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: luger16 <luis.gerlach@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-06-06 23:09:31 +02:00
Sascha Bürk 2d0d4ff425 Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Sascha Bürk <macfly@german-bash.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-06-06 23:09:30 +02:00
Diogo Resende 2ec56ec89f Translated using Weblate (Portuguese)
Currently translated at 100.0% (254 of 254 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-06-06 23:09:28 +02:00
gallegonovato 6e034eb345 Translated using Weblate (Spanish)
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-06-06 23:09:27 +02:00
Ano10 68e231e5d1 Translated using Weblate (French)
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-06-06 23:09:25 +02:00
Justus Kagerer 6b0c6218b7 Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Translated using Weblate (German)

Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Justus Kagerer <justus-kagerer@web.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-06-06 23:09:24 +02:00
Nick Träger 773f24921b Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Translated using Weblate (German)

Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Nick Träger <shyperchen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-06-06 23:09:22 +02:00
Michael Klečka 37aab5eb77 Translated using Weblate (Czech)
Currently translated at 99.6% (253 of 254 strings)

Co-authored-by: Michael Klečka <michael.klecka@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-06-06 23:09:20 +02:00
Donkie 8f2cc05a7c Merge pull request #392 from QuinnDamerell/octoeverywhere-docs 2024-05-31 06:33:16 +02:00
Quinn Damerell 9bc0a03582 Adding the OctoEverywhere integration to the readme.md. 2024-05-30 21:23:46 -07:00
Donkie 8c164ac55f Added backend support for multi-color filaments 2024-05-28 18:49:24 +02:00
Donkie 91f33ae9b2 Column filters now perform exact matches
Resolves #367
2024-05-25 21:06:36 +02:00
Donkie b00075ab87 Added backend support to perform exact matches 2024-05-25 20:56:22 +02:00
Donkie 65bfba89cf Changed docker base image to Python 3.12 bookworm
Previously we had alpine but tbh its stupid
2024-05-23 20:14:13 +02:00
Donkie c851950701 Tests fully pass again 2024-05-23 20:13:51 +02:00
Donkie 535fa40ad2 Updated most python dependencies
Primarily FastAPI and Pydantic to v2. Also ruff to latest.

Updated some code to support Pydantic v2
2024-05-23 19:42:27 +02:00
Donkie 306dbe40af Bump version to 0.18.1 2024-05-22 20:16:22 +02:00
Donkie 26f5eaa2f7 Fixed base path issues 2024-05-22 20:15:39 +02:00
Donkie 620febc3ab Moved hishel cache directory into spoolmans data directory
This is to prevent permission issues
2024-05-21 11:25:08 +02:00
Donkie 0ab6e92d65 Fixed postgres db type example 2024-05-21 11:21:00 +02:00
Donkie 59295a3e5a Added permission error catch for setting up hishel cache 2024-05-21 00:00:10 +02:00
Donkie 4eee6712c1 Bump version to 0.18.0 2024-05-20 22:55:22 +02:00
Donkie ca545b72b9 Fixed spool price sorting not working
Resolves #349
2024-05-20 22:42:44 +02:00
Donkie 52c52bcfd8 Updated external db sync to handle spool types and new color hex format 2024-05-20 22:39:25 +02:00
Donkie 5dcc28aa98 Merge pull request #378 from Donkie/weblate
Translations update
2024-05-20 22:36:18 +02:00
Maurice Kevenaar c834d3dd01 Translated using Weblate (Dutch)
Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-05-20 22:36:10 +02:00
Constanza Ortiz Simpson 2f1776cbb6 Translated using Weblate (Spanish)
Currently translated at 92.5% (235 of 254 strings)

Co-authored-by: Constanza Ortiz Simpson <constanza.ortiz@uc.cl>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-05-20 22:36:10 +02:00
Miloslav Kos cef3eba46c Translated using Weblate (Romanian)
Currently translated at 68.1% (173 of 254 strings)

Translated using Weblate (Polish)

Currently translated at 87.0% (221 of 254 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (254 of 254 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ro/
Translation: Spoolman/Web Client
2024-05-20 22:36:10 +02:00
Donkie 7bd2eac94b Merge pull request #381 from StuSerious/master
feat: lossless image optimization.
2024-05-20 22:36:05 +02:00
Stefano Serioli 6fea0b77b9 merge: lossless image optimization
using imgbot, for more see https://github.com/StuSerious/Spoolman/pull/1
2024-05-20 01:13:13 -07:00
ImgBotApp 835235257c [ImgBot] Optimize images
*Total -- 113.52kb -> 77.34kb (31.87%)

/client/public/icon512_maskable.png -- 20.26kb -> 10.24kb (49.45%)
/client/public/apple-touch-icon.png -- 20.26kb -> 10.24kb (49.45%)
/client/icons/spoolman_monochrome.svg -- 4.60kb -> 3.03kb (34.2%)
/client/public/icon512_rounded.png -- 30.93kb -> 20.97kb (32.2%)
/client/icons/spool.svg -- 4.66kb -> 3.27kb (29.83%)
/client/icons/spoolman.svg -- 8.86kb -> 7.44kb (16.11%)
/client/src/icon_spool.svg -- 3.92kb -> 3.41kb (12.93%)
/client/public/favicon.svg -- 8.56kb -> 7.93kb (7.41%)
/client/src/icon.svg -- 8.56kb -> 7.93kb (7.41%)
/client/public/kofi_s_logo_nolabel.png -- 2.89kb -> 2.88kb (0.51%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2024-05-16 12:27:52 +00:00
Donkie c4693d3f34 Implemented used_length spool sort 2024-05-15 20:18:24 +02:00
Donkie 51c3b46f1f Fixed wrong remaining weight and length spool sort 2024-05-15 20:17:00 +02:00
Donkie 090cbadf3b Fixed selected filament re-rendering too much 2024-05-15 19:40:34 +02:00
Donkie 69bb3cec77 Merge pull request #377 from zarthross/patch-1
Update OctoPrint plugin in README.md
2024-05-14 21:30:47 +02:00
Darren Gibson 98aa1fdf8b Update README.md 2024-05-14 14:29:52 -05:00
Darren Gibson c6520d336e Update OctoPrint plugin in README.md
original plugins repo was archived, putting in new link found from Octoprint plugin site
2024-05-13 16:16:18 -05:00
Donkie ccd4026279 Fixed typing error in filament create form 2024-05-13 22:11:31 +02:00
Donkie 5a8d71fe70 Added import external filament in filament create form 2024-05-13 21:55:18 +02:00
Donkie 46ab709f72 Added extruder and bed temp to materials external db 2024-05-12 22:12:13 +02:00
Donkie 55471062ab External filament select for edit spool form 2024-05-12 22:08:47 +02:00
Donkie 4598d8f520 Some cleanup in create spool page 2024-05-12 21:58:01 +02:00
Donkie 3de2c94098 Create internal filament from external flow done 2024-05-12 21:58:01 +02:00
Donkie e4d2ecf0dc Filament/vendor find by ext id + integration tests 2024-05-12 21:58:01 +02:00
Donkie 4af975d390 Removed 404 codes from find apis because they will never return that 2024-05-12 21:58:01 +02:00
Donkie c2f2f845f6 Removed hashtag from external db color 2024-05-12 21:58:01 +02:00
Donkie 7f8606050b Made external db density field required 2024-05-12 21:58:00 +02:00
Donkie 1e4c73138c Added external ID fields to filaments and vendors 2024-05-12 21:58:00 +02:00
Donkie 36945f449d Show external filaments in spool create 2024-05-12 21:58:00 +02:00
Donkie 8d03a53228 Formatted client files 2024-05-12 21:57:43 +02:00
Donkie eca8e32a55 Removed plural from new api to match others 2024-05-12 21:57:43 +02:00
Donkie 987f357f97 Merge pull request #373 from Donkie/weblate
Translations update
2024-05-12 20:11:37 +02:00
Donkie cbaa7b81df Added API for fetching external database, removed parameters structure, 2024-05-12 12:14:25 +02:00
Donkie 595fd82b56 Minor fix in readme 2024-05-12 10:49:09 +02:00
Donkie 21d541a680 Added script to download files from external db 2024-05-12 10:48:33 +02:00
Diogo Resende c7347cf512 Translated using Weblate (Portuguese)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Diogo Resende <dresende@thinkdigital.pt>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-05-11 19:03:17 +02:00
RR-Rico 8454825999 Translated using Weblate (German)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: RR-Rico <schmitt@raceresult.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-05-11 19:03:17 +02:00
Miloslav Kos 35a6e1cc09 Translated using Weblate (Czech)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-05-11 19:03:17 +02:00
Daniel Hultgren 78e91a199a Translated using Weblate (Swedish)
Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English)

Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/en/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-05-11 19:03:17 +02:00
Donkie 1962ffb43e Added install from source guide 2024-05-11 19:03:08 +02:00
Donkie 9040a55204 Merge pull request #368 from Donkie/weblate
Translations update
2024-05-10 13:51:40 +02:00
gallegonovato 1ed06912ba Translated using Weblate (Spanish)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-05-10 12:57:48 +02:00
Miloslav Kos a46188adca Translated using Weblate (Czech)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Miloslav Kos <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-05-10 12:57:48 +02:00
Mikolaj Ostrowski 986d06d912 Translated using Weblate (Polish)
Currently translated at 97.5% (241 of 247 strings)

Co-authored-by: Mikolaj Ostrowski <mikolaj.p.ostrowski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-05-10 12:57:47 +02:00
Maurice Kevenaar e1d5493f15 Translated using Weblate (Dutch)
Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English)

Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/en/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-05-10 12:57:47 +02:00
Ano10 172db393b8 Translated using Weblate (French)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-05-10 12:57:46 +02:00
Donkie cd9cca0c06 Fixed missing translation.json errors 2024-05-10 12:56:44 +02:00
Donkie 9ff457f2fd Updated packages 2024-05-10 12:52:41 +02:00
Donkie 88705a7f18 Readme updates, removed env var section in favour of the .env.example file 2024-05-10 12:00:46 +02:00
Donkie bc32f1e890 Now supports running under a sub path
Just set the SPOOLMAN_BASE_PATH environment variable and it should work.

Resolves #95
2024-05-10 11:37:09 +02:00
Donkie fc532ff697 Update bug_report.md 2024-05-06 19:30:37 +02:00
Donkie 4f93fdd4ea Bump version to 0.17.0 2024-05-04 20:35:35 +02:00
Donkie 1ddd9fb94a Merge pull request #335 from CooperGerman/master
Create install_arch.sh
2024-05-04 20:28:50 +02:00
Donkie 3db02717da Merge pull request #346 from Donkie/weblate
Translations update
2024-05-04 20:19:37 +02:00
Mick#6366 506b390b7f Translated using Weblate (Czech)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Mick#6366 <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-04-29 10:07:24 +02:00
gallegonovato d80a9b60b1 Translated using Weblate (Spanish)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-04-29 10:07:23 +02:00
Athanasios Papadimos e61b05344f Translated using Weblate (Greek)
Currently translated at 98.3% (243 of 247 strings)

Co-authored-by: Athanasios Papadimos <thanosp1993@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translation: Spoolman/Web Client
2024-04-29 10:07:22 +02:00
Dr_Perry_Coke ab98a9ca12 Translated using Weblate (Russian)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-04-29 10:07:21 +02:00
Benedikt Fuchs 3f82da9743 Translated using Weblate (German)
Currently translated at 100.0% (247 of 247 strings)

Co-authored-by: Benedikt Fuchs <fuchs.bene@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-04-29 10:07:19 +02:00
Habag111 210cda4369 Translated using Weblate (Polish)
Currently translated at 91.2% (220 of 241 strings)

Co-authored-by: Habag111 <spam.eimail@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pl/
Translation: Spoolman/Web Client
2024-04-29 10:07:18 +02:00
Constantin Sarbu bfec375a67 Translated using Weblate (Romanian)
Currently translated at 74.6% (180 of 241 strings)

Translated using Weblate (Romanian)

Currently translated at 69.7% (168 of 241 strings)

Co-authored-by: Constantin Sarbu <dinu.sarbu@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ro/
Translation: Spoolman/Web Client
2024-04-29 10:07:17 +02:00
Samuel Wang 5139c885be Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-04-29 10:07:16 +02:00
CooperGerman a6774dd054 Fix for https://github.com/Donkie/Spoolman/pull/335#discussion_r1578393582 2024-04-26 15:33:48 +02:00
CooperGerman c7f02f52e5 Fix for https://github.com/Donkie/Spoolman/pull/335#discussion_r1578391362 2024-04-26 15:33:48 +02:00
Donkie fa4905de0f Merge pull request #339 from spyder007/feature/spool_revisions
Add additional handling for spool weights
2024-04-24 20:38:24 +02:00
Matt Gerega 153b21408b Merge branch 'master' into feature/spool_revisions 2024-04-17 17:26:14 -04:00
Matt Gerega fdec032d19 Updated handling of higher measurements 2024-04-15 16:41:14 -04:00
Donkie 92a5868ccb Merge pull request #350 from Forest-Troll/master
Added prometheus integration
2024-04-15 21:10:27 +02:00
Matt Gerega 1ec16e64fb Updated calculation of default values during migration and added additional help 2024-04-15 10:44:03 -04:00
CooperGerman fd33446a59 Merge branch 'Donkie:master' into master 2024-04-15 10:29:20 +02:00
CooperGerman 047b0b286b Added os detection logging in terminal 2024-04-15 10:28:32 +02:00
Dmitry Belousov 6aef3a82f4 Changed .env.example 2024-04-15 00:18:10 +03:00
Dmitry Belousov 91d922d0b8 fixed version prometheus-client
added env variable SPOOLMAN_METRICS_ENABLED for enabled database collector

set metrics path as /metrics

checked Null values at vendors, init weight and price
removed some logs
2024-04-15 00:17:36 +03:00
Dmitry Belousov fc8df5346d Rollback pdm.lock to cadb707
Rollback pyproject.toml
2024-04-14 22:59:35 +03:00
Donkie 640ca8c430 Fixed spool clone showing white screen
Resolves #326
2024-04-14 10:36:51 +02:00
Dmitry Belousov 2aa0a4f1b0 WA for FastApi session manager 2024-04-12 01:20:35 +03:00
Matt Gerega c6f9abeced Changed initial_weight to be net weight, not gross weight 2024-04-10 11:00:46 -04:00
Dmitry Belousov 28b2bfec50 Added prometheus client
Added scheduler for compute current spool metrics and info
2024-04-09 15:25:09 +03:00
CooperGerman fc6ab9ee3a Merge branch 'Donkie:master' into master 2024-04-09 09:20:18 +02:00
CooperGerman 81ef9b115d Merged arch and debian install scripts for a generic one 2024-04-09 09:18:47 +02:00
Matt Gerega dad446621f Changed g to grams in API docs for clarity 2024-04-08 17:40:22 -04:00
Matt Gerega 23cfb42ba8 Updated based on PR requests 2024-04-08 17:38:18 -04:00
Matt Gerega 37804c14a8 modified default settings 2024-04-08 09:27:15 -04:00
Matt Gerega c77d0186ca Merge remote-tracking branch 'upstream/master' into feature/spool_revisions 2024-04-08 09:26:34 -04:00
CooperGerman 163c809fd5 Add .history to .gitignore 2024-04-08 11:27:52 +02:00
CooperGerman 1696759325 Update install script in readme 2024-04-08 11:27:44 +02:00
Donkie 29f54358d6 Merge pull request #345 from Donkie/dependabot/npm_and_yarn/client/vite-5.0.13 2024-04-03 20:00:31 +02:00
dependabot[bot] e391d041ac Bump vite from 5.0.12 to 5.0.13 in /client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.0.13.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.13/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:59:40 +00:00
Matt Gerega 80fbfdfe65 Merge remote-tracking branch 'upstream/master' into feature/spool_revisions 2024-04-01 18:57:47 -04:00
Donkie db18a5f19d Added libffi to fix armv7 builds
Thanks @spyder007
2024-04-01 22:15:27 +02:00
Donkie e6aa80347f Merge pull request #333 from Donkie/dependabot/npm_and_yarn/client/follow-redirects-1.15.6
Bump follow-redirects from 1.15.4 to 1.15.6 in /client
2024-04-01 22:13:37 +02:00
Donkie b86841d7fe Merge pull request #325 from Donkie/weblate
Translations update
2024-04-01 22:13:29 +02:00
Matt Gerega ba084b47b5 Merge remote-tracking branch 'upstream/master' into feature/spool_revisions 2024-04-01 14:00:38 -04:00
Matt Gerega 90795fea84 Add libffi-dev to python-builder 2024-04-01 11:41:13 -04:00
srbjessen 9dd87049d2 Translated using Weblate (Danish)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client
2024-03-29 07:55:41 +01:00
Karter87 a49f150560 Translated using Weblate (Dutch)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Karter87 <charlievdschoor@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-03-29 07:55:40 +01:00
Roberto Besser cc19079bd9 Translated using Weblate (Portuguese)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Roberto Besser <rmbesser@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-03-29 07:55:40 +01:00
Donkie 7808615b1d Merge pull request #341 from Donkie/dependabot/npm_and_yarn/client/express-4.19.2 2024-03-29 07:55:01 +01:00
dependabot[bot] 23f67b6739 Bump express from 4.18.2 to 4.19.2 in /client
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-29 02:53:53 +00:00
Matt Gerega 951592ab4c Add more integration tests for spool weights 2024-03-28 09:38:24 -04:00
Matt Gerega a6ae808389 Added additional integration tests for measure and add 2024-03-28 09:29:39 -04:00
Matt Gerega 19c414da4b Add integration tests for new measure endpoint 2024-03-27 15:44:54 -04:00
Matt Gerega 3ea4937f24 Updated spool UIs (create/edit) 2024-03-27 14:47:05 -04:00
Matt Gerega 3aed735cea Updated vendor API to correct issues - Implement new UI 2024-03-26 21:26:41 -04:00
Matt Gerega e68f88da2a Added empty spool weight as an option field for the vendor 2024-03-26 20:35:50 -04:00
Matt Gerega 83fd3073ec Fixed formatting 2024-03-26 14:02:17 -04:00
Matt Gerega 1d6830d769 Added initial_weight and empty_weight to spool 2024-03-26 13:55:44 -04:00
CooperGerman 2a723da910 Create install_arch.sh 2024-03-21 16:37:03 +01:00
dependabot[bot] 6d72dd69fd Bump follow-redirects from 1.15.4 to 1.15.6 in /client
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 22:59:27 +00:00
Michal Kunc 06e546f3fa Add more description to help text, fix cs locale. 2024-03-11 10:25:28 +01:00
Michal Kunc 8c19efa2c4 Add QR Code base URL setting 2024-03-04 01:41:26 +01:00
Donkie a68e0d59bd Merge pull request #301 from Donkie/weblate
Translations update
2024-03-02 19:30:16 +01:00
Athanasios Papadimos 7e8611668e Translated using Weblate (Greek)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Athanasios Papadimos <thanosp1993@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Donkó Márk 1109474ee2 Translated using Weblate (Hungarian)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Donkó Márk <donkomark@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
luger16 3bff50871f Translated using Weblate (German)
Currently translated at 96.2% (232 of 241 strings)

Co-authored-by: luger16 <luis.gerlach@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Kev 65dcc5cc58 Translated using Weblate (German)
Currently translated at 96.2% (232 of 241 strings)

Co-authored-by: Kev <leadustin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Jaccob Hui ce1d38ea94 Translated using Weblate (Chinese (Simplified))
Currently translated at 90.8% (219 of 241 strings)

Co-authored-by: Jaccob Hui <783739330@qq.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Roberto Besser dabf3f5a51 Translated using Weblate (Portuguese)
Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Roberto Besser <rmbesser@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/pt/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Daniel Oliver bd1d1a6d7c Translated using Weblate (Spanish)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Daniel Oliver <d.oliver13@me.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Giadej 37a65e954c Translated using Weblate (Italian)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Giadej <Giadej@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
Dr_Perry_Coke 6149e75332 Translated using Weblate (Russian)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Dr_Perry_Coke <dr.perrycoke@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-03-02 19:30:02 +01:00
gallegonovato a177d561e1 Translated using Weblate (Spanish)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
A S 8fd4147b5d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: A S <good.win.alexs@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
Guillaume Guerin 7f3df034af Translated using Weblate (French)
Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (French)

Currently translated at 95.4% (230 of 241 strings)

Co-authored-by: Guillaume Guerin <guerin.dev@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
Maurice Kevenaar eae391f34d Translated using Weblate (Dutch)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
Daniel Hultgren e3b2bed573 Translated using Weblate (Swedish)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
Mick#6366 aee0b477d6 Translated using Weblate (Czech)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Mick#6366 <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-03-02 19:30:01 +01:00
Donkie 739e982215 Merge pull request #324 from Donkie/dependabot/npm_and_yarn/client/sanitize-html-2.12.1
Bump sanitize-html from 2.11.0 to 2.12.1 in /client
2024-03-02 19:29:56 +01:00
dependabot[bot] d1598e0407 Bump sanitize-html from 2.11.0 to 2.12.1 in /client
Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.11.0 to 2.12.1.
- [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.11.0...2.12.1)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 17:31:16 +00:00
Michal Kunc 30ee5b7cae Add option to use full URL in QR codes instead of web+spoolman: prefix 2024-02-10 00:09:39 +01:00
Donkie babb95d086 Bump version to 0.16.1 2024-02-04 10:02:52 +00:00
Donkie c6c75e6271 Prefix ExecStart with bash to circumvent execute permission issue
No matter how many ways we try to add execute permissions to these scripts they always seem to get lost in one way or another. Now for example Moonraker doesn't run the install_debian.sh script, so the chmod never gets run. A .zip file doesn't seem to properly store the execute permission, so this would never work.
2024-02-02 23:14:54 +01:00
Donkie c65bc4201b Currency setting is now utilized in filament/spool lists, edit pages etc 2024-02-02 23:06:06 +01:00
Donkie 220457c1ac Merge pull request #297 from Donkie/weblate
Translations update
2024-02-02 22:23:51 +01:00
Daniel Hultgren 126ba45bff Translated using Weblate (Swedish)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-02-02 22:22:10 +01:00
Алексей b6756e76e0 Translated using Weblate (Russian)
Currently translated at 100.0% (240 of 240 strings)

Co-authored-by: Алексей <follko87@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ru/
Translation: Spoolman/Web Client
2024-02-02 22:22:10 +01:00
Giadej 4fb4e58d3f Translated using Weblate (Italian)
Currently translated at 84.5% (203 of 240 strings)

Co-authored-by: Giadej <Giadej@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/it/
Translation: Spoolman/Web Client
2024-02-02 22:22:10 +01:00
Mick#6366 a7783812aa Translated using Weblate (Czech)
Currently translated at 100.0% (240 of 240 strings)

Co-authored-by: Mick#6366 <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-02-02 22:22:10 +01:00
Donkie f969e11d28 Minor translation fixes 2024-02-02 22:13:59 +01:00
Donkie c2335366bb Fixed sorting by remaining weight/length 2024-02-02 22:10:11 +01:00
Donkie b6b6fcf6bd Remove .venv in CI build job 2024-02-01 20:04:00 +01:00
Donkie 6d63519bee Bump version to 0.16.0 2024-02-01 17:27:26 +00:00
Donkie ad177a4cba Merge pull request #293 from Donkie/weblate
Translations update
2024-02-01 18:25:33 +01:00
Donkie cccb0de982 Merge pull request #279 from hcooper/dynamic-port
Support dynamic ports
2024-02-01 18:25:04 +01:00
Maurice Kevenaar d3511b232c Translated using Weblate (Dutch)
Currently translated at 100.0% (240 of 240 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-01-31 21:01:45 +01:00
Mick#6366 de9515f6ce Translated using Weblate (Czech)
Currently translated at 100.0% (240 of 240 strings)

Co-authored-by: Mick#6366 <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translation: Spoolman/Web Client
2024-01-31 21:01:45 +01:00
Hereward Cooper c0714e053d change test var name 2024-01-30 14:09:32 -08:00
Donkie 88fa4b636a Create .gitattributes 2024-01-30 20:12:16 +01:00
Donkie 23f383dd63 Fixed edit forms refreshing after you start editing 2024-01-30 20:02:54 +01:00
Donkie 41a40d9fa3 Minor spool edit cleanup, added enum 2024-01-30 19:29:17 +01:00
Donkie 81b7a9b4ec Added tooltip in filament filter menu
To differentiate between different filaments that may be named the same.
Resolves #276
2024-01-30 19:21:38 +01:00
Donkie 36926c9ccf Added "Add Spool" button on filament list page
Resolves #281
2024-01-30 18:57:23 +01:00
Donkie 4d65dea1e9 Fixed sorting by filament in spool page
Previously it sorted by filament ID which gave wonky results, now it properly sorts by filament combined name instead

Resolves #219
2024-01-30 18:20:53 +01:00
Donkie 290eb5cb81 Added backend ability to sort by combined_name 2024-01-30 18:20:53 +01:00
Donkie 414c11cd7f Merge pull request #270 from Donkie/weblate
Translations update
2024-01-29 22:29:23 +01:00
luger16 c8b8dab906 Translated using Weblate (German)
Currently translated at 93.3% (223 of 239 strings)

Co-authored-by: luger16 <luis.gerlach@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/de/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
my_mail a99530391b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (239 of 239 strings)

Co-authored-by: my_mail <egor6508@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
srbjessen a034e96939 Translated using Weblate (Danish)
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: srbjessen <srbjessen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Сергій 445331369b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Gregory Fontaine 7a7d43b548 Translated using Weblate (French)
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Gregory Fontaine <gregory.fontaine35@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Samuel Wang b854f936c5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Samuel Wang <imhsaw@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Maurice Kevenaar 684b17619c Translated using Weblate (Dutch)
Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Maurice Kevenaar <maurice@kevenaar.name>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Daniel Hultgren a6cb08a587 Translated using Weblate (Swedish)
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
bittin1ddc447d824349b2 e4b25d1eda Translated using Weblate (Swedish)
Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
gallegonovato 30c5891c11 Translated using Weblate (Spanish)
Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/es/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Mick#6366 9b5d8969e5 Translated using Weblate (Czech)
Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Romanian)

Currently translated at 56.4% (118 of 209 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (209 of 209 strings)

Co-authored-by: Mick#6366 <kos.m@post.cz>
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/
Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ro/
Translation: Spoolman/Web Client
2024-01-29 21:01:46 +01:00
Donkie 9d3fb02231 Fixed spool edit page being in render loop 2024-01-26 17:30:34 +01:00
Donkie d0a958decb Removed camera constraints from QR scanner
By default camera needs to be at least 640x640 but I don't see why that's necessary.

Fixes #285
2024-01-26 17:14:45 +01:00
Donkie f7c9c44ea7 Write build info earlier when we still have git 2024-01-25 22:58:25 +01:00
Donkie 721733551f Fix git commit in ci.yml 2024-01-25 22:54:26 +01:00
Donkie 46c95eb5dd Fixed valueerror on invalid build date 2024-01-25 22:14:31 +01:00
Donkie b40da6e2ee Updated commit and build date capturing
It's now written to a build.txt file instead of using environment variables internally. Standalone installs now has this properly as well.
2024-01-25 22:03:18 +01:00
Donkie c0b4951a68 Read Spoolman version directly from pyproject.toml instead 2024-01-25 21:54:10 +01:00
Donkie e77623bab0 Fixed start script 2024-01-25 21:53:48 +01:00
Donkie 1479e13d94 Some script permission fixes 2024-01-25 20:49:16 +01:00
Donkie 2786eff3da Fixed release publishing happening outside tag push 2024-01-25 20:21:42 +01:00
Donkie 52cdb01110 Updated scripts to not use PDM anymore 2024-01-25 20:13:24 +01:00
Donkie 44d3ed72b0 CI job now builds a requirement.txt into the spoolman.zip 2024-01-25 20:11:12 +01:00
hcooper 1273b20224 Support dynamic ports 2024-01-23 11:16:57 -08:00
Donkie f294561d57 Merge pull request #256 from Donkie/custom-fields
New backend custom fields system
2024-01-23 19:37:33 +01:00
566 changed files with 105294 additions and 10063 deletions

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
client/node_modules
client_v2/node_modules
*.egg-info
*.egg/
+78 -1
View File
@@ -1,17 +1,25 @@
# DB Type: sqlite, mysql, postgresql, cockroachdb
# DB Type: sqlite, mysql, postgres, cockroachdb
# Default if not set: sqlite
#SPOOLMAN_DB_TYPE=sqlite
# DB Setup, if not using sqlite
# Database hostname
#SPOOLMAN_DB_HOST=
# Database port
#SPOOLMAN_DB_PORT=
# Database name
#SPOOLMAN_DB_NAME=
# Database login username
#SPOOLMAN_DB_USERNAME=
# Path of file which contains the database password. Can be used instead of SPOOLMAN_DB_PASSWORD if desired.
#SPOOLMAN_DB_PASSWORD_FILE=
# Database login password
#SPOOLMAN_DB_PASSWORD=
# Query parameters for the database connection, e.g. set to `unix_socket=/path/to/mysql.sock` to connect using a MySQL socket.
#SPOOLMAN_DB_QUERY=
# Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
# Logs will only be reported if the level is higher than the level set here
# Default if not set: INFO
#SPOOLMAN_LOGGING_LEVEL=INFO
@@ -34,3 +42,72 @@
# Host and port to listen on
SPOOLMAN_HOST=0.0.0.0
SPOOLMAN_PORT=7912
# Change base path
# Set this if you want to host Spoolman at a sub-path
# If you want the root to be e.g. myhost.com/spoolman
# Then set this to /spoolman
#SPOOLMAN_BASE_PATH=
# Enable Collect Prometheus metrics at database
# Default: FALSE
#SPOOLMAN_METRICS_ENABLED=TRUE
# Periodically check GitHub for a newer Spoolman release, shown in the UI footer and server log
# Default: TRUE
#SPOOLMAN_UPDATE_CHECK=FALSE
# Collect items (filaments, materials, etc.) from an external database
# Set this to a URL of an external database. Set to an empty string to disable
# Default: https://donkie.github.io/SpoolmanDB/
#EXTERNAL_DB_URL=https://myhost.com/spoolmandb/
# Sync interval in seconds, set to 0 to disable automatic sync. It will only sync on start-up then.
# Default: 3600
#EXTERNAL_DB_SYNC_INTERVAL=3600
# Display name for the external database, shown in the UI.
# Set this to match your own catalog if you changed EXTERNAL_DB_URL above.
# Default: SpoolmanDB
#EXTERNAL_DB_NAME=SpoolmanDB
# Serve the previous (React) web client instead of the current one
# Both clients ship in the image and talk to the same API, so this is a safe
# rollback if the current client misbehaves on your setup. Your data is untouched
# either way. Please report the problem so it can be fixed.
# Default: FALSE
#SPOOLMAN_LEGACY_CLIENT=TRUE
# Enable debug mode
# If enabled, the client will accept requests from any host
# This can be useful when developing, but is also a security risk
# Default: FALSE
#SPOOLMAN_DEBUG_MODE=TRUE
# UID and GID of the user in the docker container
# These only make sense if you are running Spoolman in a docker container
# Default if not set: 1000
#PUID=1000
#PGID=1000
# Extra browser origins allowed to talk to Spoolman, separated by commas.
# Only needed for a web UI served from a different address than Spoolman, such as a Fluidd or
# Mainsail instance on another host. Spoolman's own web UI does not need it, and neither does
# Moonraker, OctoPrint, Home Assistant or anything else that is not a browser.
# Each entry must include the scheme -- a bare "host:port" is not an origin and will never match.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin
#SPOOLMAN_CORS_ORIGIN=https://fluidd.local,http://mainsail.local:8080
# To allow every origin. WARNING: this turns the origin checks off entirely, so any website you
# visit can read and modify your Spoolman data in the background. Only on a trusted network.
#SPOOLMAN_CORS_ORIGIN=*
# Hostnames this Spoolman answers to, separated by commas. Off unless set.
# Protects against DNS rebinding, where a domain an attacker owns is pointed at your Spoolman's
# local address so that their web page can talk to it. Hostnames, not origins: no scheme, no port.
# "*.mydomain.com" covers the domain and its subdomains.
# IP addresses, single-word names, .local/.lan/.home/.home.arpa/.internal names and any hostname
# already in SPOOLMAN_CORS_ORIGIN keep working without being listed.
# If you reach Spoolman through a reverse proxy on a real domain, that domain must be listed here
# once this is set, or every request is refused with a 400.
#SPOOLMAN_ALLOWED_HOSTS=spoolman.mydomain.com
# See the Security page in the wiki for how these two fit together:
# https://github.com/Donkie/Spoolman/wiki/Security
+7
View File
@@ -0,0 +1,7 @@
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
# Never modify line endings of our bash scripts
*.sh -crlf
.env.example text eol=lf
+2 -2
View File
@@ -1,9 +1,9 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: Donkie # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: donkie
ko_fi:
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
+8 -9
View File
@@ -23,16 +23,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Spoolman Host (please complete the following information):**
- Installation type: [Docker or Standalone]
- Installed version: [e.g. 0.17.0, can be found at the bottom of your spoolman website if you don't know]
- OS and Distro: [e.g. Ubuntu, Raspbian]
- Database type: [SQLite, PostgreSQL, etc]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Desktop (please complete the following information):**
- OS: [e.g. Windows, Mac, iOS, Android, ...]
- Browser [e.g. Chrome, Firefox, Safari, ...]
**Additional context**
Add any other context about the problem here.
Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+67
View File
@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 570" width="800" height="570">
<!-- Generated by https://github.com/antfu-collective/sponsorkit -->
<style>
text {
font-weight: 300;
font-size: 14px;
fill: #777777;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.sponsorkit-link {
cursor: pointer;
}
.sponsorkit-tier-title {
font-weight: 500;
font-size: 20px;
}
</style>
<text x="400" y="40" text-anchor="middle" class="sponsorkit-tier-title">Gold Sponsors</text><a href="https://joshix.com" class="sponsorkit-link" target="_blank" id="joshix">
<text x="400" y="203" text-anchor="middle" class="sponsorkit-name" fill="currentColor">Josh Wood</text>
<clipPath id="c0">
<rect x="340" y="65" width="120" height="120" rx="60" ry="60" />
</clipPath>
<image x="340" y="65" width="120" height="120" href="data:image/webp;base64,UklGRsgGAABXRUJQVlA4ILwGAACwJACdASp4AHgAPm0skkYkIqGhL5ENkIANiUfPo+yShwQpAbER6gG8GTCe9OgMo/Ux8Mb6gcyqPI8xGY/5PrE+/PpB5ypAjRkDFzrivwF62s/DK34rzvVhAPgCBtQjb6uCuPF1IciZ/eOsAm2YcRY0JTDqwAeM+a8G/bPnitwjyrcNv4JeKAdm+2kIGNTUplFYHTh5H/6cvX/J2boEqumFpsZ4H+BTtbcCJ1umGOvIdIZ7mJJ4wKAIqBezjekx2kJcEuJv1TQjTzvFltO1PQyGeQS85A0kZIQo/eL9EMzL7yHbRNB95Za7xMB23tgYLYRGioR+cObcTdJh1itU3qSDjOx5GX2qxaVkPqMDgaM/dJltCbRr48Uj9txgtIhRKvgNxLd0fZ2iU1+9n5MIMAD+/PQBSjyc0vbGv1rJpHHondsozJ5is5nynZ+bxtrKmbuULzqkfkHTd2EZMZR4pKqYe4vgUljlur1epNBEkArOAPbtmGjmkwqXRkZtDettTONSDKtn7bi5F52h+cnz5FvLoE7c+pK+ZkBlpZy9Y7Wzx9bvKE/Org0ItinAq7f9TmihQ1eizXWPtgBKFd+Qu7BF+wqFv/68qGwtXD/9iMyD9G0NFBxF/YHjCzsUt0rvHlkPkG3u0s43UFSi+tFMWXfJVDMX9bqV1Y7dvCwhQ7NY+SZCNaoq487tNhvEhIlUb1mqdXv3rxhJ8QLM0iZ5muNzgAZH8rnxR/JlNXsfZhe4ZvsIgsa8/f3eUhXSZ/acnJ1dh8SayPmOJysvTMy4SmXCvIWvFZrAdbXjfUKSV2SDj8CFjFa3S/U+VcTXqTm5RTpQy2R6citHAtZ/FAo0R2l+ubQvHAZbG1naXrdCmvx9IQxQE9DrQrFccqLpAroykRLrIPEyTcvzXPoy6DcrqpzWpVsskaKDksL8HIeM6LvYhAMaz7kmdYfHfQhxnTzQEV3bii5mXAh7RLbsvuml5HPVj3hHYPeWcyqXuwdbKs1ei9Is2LWps/OYoY1/S8HkS7yWsH3lJGIp3YRtJrl0CBj80B5Kc7FCt1kLT7l02XMn2sp4G/Tmc2PTOSToCkOEzWm60rUaX7gU/NBUAHcFJzcveQBzWbtm/QvAzmyYRv27bSozA8jcjTB11XwBz7aRW6bOdhZevq1oNodI75fAQ9SjxsqijU6icL/C17hatpRfNDRK5339AsbtAKnhUpcmbBrYXmYrjfmcXLsTjT9m4yYPLzmAr38FhZbZYTXEwcQ611OLliezqUGH63rwrrD/Y1D7DP5TeGiQ6fSqUs5MbV+bW1zQlCU4ZZ6GFhrOnXhlH6oN2N9aO/Eu35SHfnCxZk2cch4YgaPmOGn8R7/6u2il6/EgXYMDeK7HcSEmss7n1CREawmfINYUCUxLB4s3z4pQQkVOTAy0PM3uylOfQt6LDpQ7ZjnhwGGNbVqPRwsClzPpxRnIAPXaDHJW0tFtNHZSNOmo3m9VzNkVN0wUOHb+/i1tW3RUHhuE+6p8D8C4px/D5MTU4l9INiryqSPO8KR9Pauk1PT7p4Iuf1GbZ9pYXVqIQcpIkYNKRyyJObLYCEVJYyUQ9s1sHx3kWGY8BVNVsZ45IfByrst+0iJywhlRMZLvY719IU2ssAyx9bQyJ+FOZOH11nib/g5i86gKhw1inytSYr7KSrOZ3vEZK0d131xF8D4Tqq5fmEuoaR7R9e/DvkPRxiSPwp6caZYHNjW0vw28jfBIeuc2Hu86STZP0dSWeaIOiTXK74vJEmLdMAUbYCX3jw2eTZJxYSuEI9YivnsnsJSPF9gk8RFk5dx+BVE3a+ypXgbjVO8Bg1tuNmJDEAH22PMgOxKSCbOhQ/ox19PW73FJ4ybJojgF3dPjPKzue/4Hf429x0Jsn2/tlNCrJf0T7XyE1cIgKvt5DbNFfh5Ss0cZe+I/9HUA7JwZCQ5LF2GqEB21XI1qJZor9y46SIjj2HxWJnJPdqHCEe6Tb7D+NLFsjfl+ZJGZu/HX6vjuORb0RIkmbq0aEVzHGjIawLzSZRKs+q7uRCXS2erdaj3ZPTqTV4YfqwFYAW3uYbwD+3Ktu9mIXLpFMsw8IiQNZopAHBbVQo+/Qk+1LBLeHztLhFDya0zWaexlFWshmq4c3J6RepzwNd/yweyeESecwL9eXf8fanxvJ2hOMKKU7Rh1g1ZBdGFJYXrZzN9PgAoBk/w77D0swqmrjebS9hmnON+C9owqx15EgS8f/BGWnupWZs+frjmVzRfc881ei87vK+ccntgAq2JzNWMLH1DB2R+Y8pkBzwAAAA==" clip-path="url(#c0)"/>
</a><text x="400" y="255" text-anchor="middle" class="sponsorkit-tier-title">Supporters</text><a href="https://github.com/swtaylor" class="sponsorkit-link" target="_blank" id="swtaylor">
<text x="360" y="348" text-anchor="middle" class="sponsorkit-name" fill="currentColor">Scott</text>
<clipPath id="c1">
<rect x="335" y="280" width="50" height="50" rx="25" ry="25" />
</clipPath>
<image x="335" y="280" width="50" height="50" href="data:image/webp;base64,UklGRpoEAABXRUJQVlA4II4EAABQFgCdASpQAFAAPmkqkUWkIqGXrAdgQAaEsYBhWgnHTNnEshj28rNZ+GmOXmp93KFPcld/L/07TSyJCfZvbGHb5moRzw0e45eO2MkuhDHxKdpqpve1uYdG0BsLtaR1qQkiaMCv1f1r0sWQvsyKv+yTj4NBtLITGsATKV0IENN3Bl8f6aAHqFPbX/IIwHqJ5dhsp8hpZiO3hdsJnuJjXBSNoo/ohDiJMtm1nZDTgJRCWMgANxShzJ+lEFPAAP79QB9OMqjqK/Wb/XKJUhuVyjCLtjHwBkqY5p46HUIIHw/VhGYrttpwJLsPtJ3UJc+EdV/S8DwYN/yByU6U0rMLIGZSGqa2ErEr7V4GGC9IxFbTvXtQDpVVaddvvt0O1HhsxCFwCfsVJwpY1+j6FX0BkGY97/sXD6e4FiqF4u1+mgpFo5AvXaoOAnyAXGYvs9mrbX7kY2vf2rzqVsTsXUDRBIdM9zf8QYGvyqij+qJIWtiJDJtziH6+PIJPNyYaF3dIun0bWAOTdHDJDIxzmdua4QIZ3+SGIdZ6zj9igFRf/D/OkJzQCGsrZMNHiAul/RsCjH5bv2NQCrlcm8Tek/Bol07kSj6qBCsK/tnGmvsT5mrYoWbQsc4tflV2jYurFzbXEjfmMJTron7S5k+b88/hOxLGsnpY7GN+cajmuqSNIAdZ5Es22QFpRJOcqh4DbtElMbp4JmiIP45o+v5aoyEyIsS6tJ/LWKnofIE+2BdiITSXHMzWWzLZ4yBVsG5jcvHbzsqFp9QFsEMe6GYubnNwvK+3uPptk8jnZN/0geWJiLFMxprrPEhv3N3VEKZ1n4yf3EoOYfywMYJ8syuqMKLLH7PB7tDql0Qxsl77fR4fURVBPGgoHbDUjrhRmEeoMSuuWD6FfZ2sx4D8aLlWKdTcRjMO7i2yNb+apfklcWh9CkNjigw1K6OlcaslrUAguez3uDHP3/e0RlxlD9oXf2i7bPFmeW32p6/krBex+5LE4YroYFb6azBBWEixY90qpd5zwSzarPhNoxV4gRRgKTbMmmIQBTtNa7l3sPIIf0znFnxX+RXwIzLmfRPY66VGkoxV5zEcJwTYngDDe8zfqp4uBhCEIq76aOUn/Kf6ydnnBwkv9EvRuTQ5wEh7jIfn74wudig9529et3sKwVIC/3DyAKgOvwkmC2wiuE/MdSRyVEgJfYBkOWSn8IJEl+ZBUelXSxrKGBeAn3CJUN7vn0hBYCKhrVCuYqCQxB4RtM9ewors7V6olzJUbu6E8WazDVYTU69/pqNi1eOfeYqbv3/il9BdKqx1InGxSUo06kUmrmTyn6cmrDkyIUYrAOttFM6Sy9SKCxPlXgDG3YV1NWmkW8vZQli8jp71xHO/80aXNIOPq4kWo3h4dKuH3JF6yDQVpAN3PJb6rD5ZE8m6JN4hbYWXPjH5YnM0xk8X7xJCgZyaSgFgvc5CeJSohiXsOdwanYvLqgpgjznJYJw+YLcVlbPsLGeg0I1HVpaxWt8Sds1iBtQ+I5ZF2VWTAbdhn9vK+oC8c498LHPQiREbCHMgAA==" clip-path="url(#c1)"/>
</a>
<a href="https://github.com/tlangkabel" class="sponsorkit-link" target="_blank" id="tlangkabel">
<text x="440" y="348" text-anchor="middle" class="sponsorkit-name" fill="currentColor">tlangkabel</text>
<clipPath id="c2">
<rect x="415" y="280" width="50" height="50" rx="25" ry="25" />
</clipPath>
<image x="415" y="280" width="50" height="50" href="data:image/webp;base64,UklGRlADAABXRUJQVlA4IEQDAACQEQCdASpQAFAAPm0wlEikIqGhJnM6aIANiWVfalewGPbCULeZf9lVKyeAFRcq/sb9iNrYjtrl20Tl9Brpxl6pzt/hrSsHFFGzOJtddJg/shyIiZ1LzVI7175shK6DAWErqJbPidMLUWg3ubKTpxfjBC+/K7Eh9dbQf4YG/eS9ejJPeMpsfu0N2Ta9ObSG201Sy7FkAAD+/lcRS9KSg1a5BZKoJe3h3qj9X3MvB+vnUNlwpci/lOuX9adPeFnDrws/r9sfemPaCgBt/l/Us8nItNrXxn5gmJbR+d1kXCM15yJi87WEtT4HCgIDAlIcW7CNJpCpFzXlmUiZuTpXY9h0L3P1/9DlXWJ6UxR6Eka9lGj1h3gbBdw5XJNayBK6l5knJ75c3VxeMfA41IqIUfRBLqAJxTUAFirrc8GjSidiV2PrpxHD/vIDyqxi0CwSZHZneIXXKdQgElMsiWY7lv2ZYJRbz852fWqChfw8V7O8CmRHJeflh1n6bsJZJW15B0GdmNkd9rEcEEN9c02fOga7TRSa5J9qqm5mQK6ttlG3o20FKndVI0RGhhZ0QoJZiWB2E6EVN8GReTD++Q4tmgx+anuuTQYLlqIbPj0fiNMWruZYNaWBR7lQfGtCltKKoXpGrdsEsb78iI+dXE+PByx8SYCDE7YMosWquKmh38fWmtCWRExHFt6J+fOK7878sckHHUxk7dOZiMG6VJPl1M6s7RVupY4pG4KbEsXrIVmKJ2PMgZqR81XYynE68U6nfMgqjmpQKS+5rFiSdo9GtJr0g9EWmfCGX21fjpknlYAQ4LQMZxVpxCGUkeedmYL/GqnCnxtjZJ4m7M8/y8rkFZZ1HEy8m7h9/qg9sTYZEd+SuGny1fVkAXSsJO6xrQFyB4W5X+QY+dKANG78hoe1ZJqb7LqT4nlWM/49WvYLYLjxLvUEcDmySZFru6bT8XZangg+KyZQR73z0+f7IiRj/PuASJHhqcqirbnHPTDWfKrQu6dWrKHR5kcRlz5Dpnb0gBEPKIQrv5DXBVJRILmF23xJK0rUzUkwQ/L6pkq0Ty8GGXnfzba9DbTT4HHDUwuMOzyNOyuL3/r/ySX1m8qwdpWmnnoAAA==" clip-path="url(#c2)"/>
</a><text x="400" y="400" text-anchor="middle" class="sponsorkit-tier-title">Backers</text><a href="https://polymorphic.dk" class="sponsorkit-link" target="_blank" id="polymorphicdk">
<clipPath id="c3">
<rect x="372.5" y="425" width="25" height="25" rx="12.5" ry="12.5" />
</clipPath>
<image x="372.5" y="425" width="25" height="25" href="data:image/webp;base64,UklGRjYCAABXRUJQVlA4ICoCAADwCwCdASoyADIAPmUqkEWkIquZ/Z4AuAZEsYBfefuIopNZ6c4YXlAnC2/DzVHzOazQULy+IzbYtxgfWvdvZMf3fvpN64RGUtlBqaalSwhyxpsxwa2VBwP9czxhfRUkgHDC3n7wM35VAAD++cjP0J212mopM4/Rh0hOscBf9Z3H09zvnh+baFS5+i/jyZ8i0WlzZRzLr9W56mlBTdHVe7poj6YN27piHQHv4HFq8LNPFIaEo2UCdds7haNciUEC46JgnRrnv7x+SK2UmmeRCGWLE0tMvr9Lj7Msgwnee0YpKdP2gtm7UAQ6hAMR+/hIP28TYk/3Jc3oOFFIcOOAwbvtVofjEaORNOQj5vpSLYgA1YmU3MvsWKJiHK3wAxZSAEODjaEXSgWuFQDo/u4k0dv0NcRqX5vjk4COQg+LoZEOWWOnfY3J0rqmkONX0GtvUXQ1vhgSv0fst67tyvesub67hOcUmsPZCS2W/NVUshMelGBQ6+shQOlHXxNysIkuO9ciE/I5vxamqTWlG92zlO5w2IZIbq2x10b5KfSCjkFr/Lspe+4EXhtwhfaZOKBx8eESeBLmBmMMDoK+v1nSYACGfyvQ7ii2bxvOkchviukeoj4B7Q3i8cHNE2d+he9657d5nwMMJ1cubwm7umN5MGKk5B731r3aX3uCbazwf6lY5MQLnw7xU4XxGtQNkQEHQqsG5ajWgOeLm5upXvEKFTLc8trHi1L8A4pAAA==" clip-path="url(#c3)"/>
</a>
<a href="https://github.com/Kreism" class="sponsorkit-link" target="_blank" id="Kreism">
<clipPath id="c4">
<rect x="402.5" y="425" width="25" height="25" rx="12.5" ry="12.5" />
</clipPath>
<image x="402.5" y="425" width="25" height="25" href="data:image/webp;base64,UklGRlAEAABXRUJQVlA4IEQEAAAQEwCdASoyADIAPm0wkkakIyGhKhzK6IANiUATpygqc9AuwDC76jadzy/+jFdrvoARipqP9l4STP6Jkfek45SHQI9T+wT+s2+cmrUBCJ9Mic9iBdMAlBmtbr3k3fO+bnxwdUPe9YN0drh6w7q4kO+jqNk0uEzUpkgTMYiFhrnVc9qKcB0G35IUcNmQFww80vxGnlkoIEV/s7YNLN7cVA0xQAD++ScfQpSJONSUqCMK+Ol5ZCh0b26aEtYQ7wo7d/iK4GFJ/Oln8/RBtU+uqnwhlmNv/XvVpC/PS5bd8Mbw8mdK7EK/bTtH2GXbGoqm+fSThwv9egzABKWJmjoyNIybs62ApVKMR1syEfaHs9rrVJa4Ku+3KznIaFb3nP3rKOJTl0JxjplCTw0qEoyER2/l8s9Fx7Wj27E7je+BerqTiLU2WKIiK6oWo6qxLj7DM4pmLTmy2HRcdZW4HbJKEvw2qtft+aVqnIpmkpHQ7gIUZB8YqXVLOuf479mRmJSRWUM8XDM+/ZHsiRjQLB8/63zHaV36axymJybL7DPubsyezxXHl3gW1oAHTPcn4dG6tghnVGt61bBjLL4xKpu+jhr9KGulx5hg9zz03dwj6wQGMzLoAS0KS1ootiTv7tSxN8h8BQKdViSfbkmzM/WQZgxko9b7T0mJ3RPfTsolRpzwAOjDjSkxnIA6VC2hoKAHSGYvW/ug0PznrxyLQlqs0eNgZM46ou98sNg2+VDv3Lc+ke9E7g3i9yt7ws/XeGzfyvfyZZlBCDKPStuncmbbhTf808x5SIglbQFAZGxHtRnUaqkoa8E4PlA6mlfzqRey1HGPP/oBPQe25l86Y71vr3eeZn2GcLFzKwjIaDNA9OMteOhej30/TXc+yBzpdTsWtQPyLo3IqXfdT5c8eyWgB4lmNJaOIgcTBCsZW/E/js1Iww91n/KKNlN9puBaomIKqdBj9FbL1nVpR97y+x9x7Vuw2LRI8xu8jNj73/Guv+NyuTRu6t7+QlUXob0HmqrdNuLf8HmZoRE6bv4f6MTQWWfGBSeT6vkpw9I2Qpz3NyC7sC4HMdPjh5JvW4J93BcKqA1N6Nw+bVRvl2ew47w+Kc2vN3IAj5IngbnUvDKEqQZgN+zQ/7BqM5d4YIk6vkyUxeXcAZ0pwBuwstcAJh0sx2PbQrT71ohYigAPz7xq1HAZ/mYsBogBIa5gO2HGgEFavUP9k+jD/GJZvLziNZzsbcX7YdjCdKTH0xwuEqiuufKzOaECClx4DsZuvipljXph50HXFc5XAfZgS+sQOZbpmb6Kf3mMjtfCCDW7efAAxeoPhCCHRcVOV3vJgGILkvW4p2k4VUrVyfDLJ3k9+AriK2tAZroSkIBkalglP1yJ4tYR22YS5slOQ3/CLnU0an8lFGOhWHQVw1CN0JCygQkClgJR4q5+ghh23OXWA/3HEAKNDvo3oAA=" clip-path="url(#c4)"/>
</a><text x="400" y="485" text-anchor="middle" class="sponsorkit-tier-title">Past Sponsors</text><a href="https://github.com/marko-p" class="sponsorkit-link" target="_blank" id="marko-p">
<clipPath id="c5">
<rect x="372.5" y="510" width="25" height="25" rx="12.5" ry="12.5" />
</clipPath>
<image x="372.5" y="510" width="25" height="25" href="data:image/webp;base64,UklGRl4BAABXRUJQVlA4IFIBAAAwCQCdASoyADIAPm00lEakIyIhLhkoAIANiUAYoZG41zSo4e3a0MDqVd5hvyUF1IEn9JgXObpn2Mt9prYfftYf4GRNRZ7hZ/fWD/6kU0akoeFYAP7mh/5A2Gfo2763tH9O1yC1D3TebcFfF/bO2z76334lot7jYxzsHcClMluiJtad5Z2A9ZtsHn0CgO1qVt/DcFFWwoCom7DWqiX/JeWTPmV7+u5ecLmedHJQ6jheSaRadpREwA9UiSGzpAggcCbnuZdKF5py6aWBIPoYdwJPwQTuDBoyC3glR90OjgWIADT7RZMYVBOeJZ0jA7g8++YxIx+GNqekY+gLy77edX+cDhTWBlwoSyzAKTVFGooGOFJwHGufQopr/cr1nMvbodaPTkzq9FZtdY+eVmTADFYRaCF0QDCX+qURMnt+yeGaWDzHEhEsTMeIJbGyRhUdMgAAAA==" clip-path="url(#c5)"/>
</a>
<a href="https://github.com/mrgirosky" class="sponsorkit-link" target="_blank" id="mrgirosky">
<clipPath id="c6">
<rect x="402.5" y="510" width="25" height="25" rx="12.5" ry="12.5" />
</clipPath>
<image x="402.5" y="510" width="25" height="25" href="data:image/webp;base64,UklGRqoBAABXRUJQVlA4IJ4BAADwCwCdASoyADIAPm0wlUYkIyIhMBYJAIANiWYAy5x8BkkfdwjET0C9gJwHmAfqB0gPMB0EPOg6lXn2PaFvyUJKOZTATBzOiskTHqIVkrjKyrKQ96FyqvM1Gv1jQBXFMKi+U5hNm1EI8AD+8QHHdvGR1JUV2sahGaCbtFiJaByUNdMrLaM575u+kR44ggAKv3SXhizdVAvYYtYu0xpoP1QuZzHV05ERx1UDN3JJOZQA2eCKoCPnSiD3aW7Cp+XxYc07Q+Xfur3IkZX89Dy0NN7ogFFINEj83SaUWhjo7BEkmq5OUe8vjhUSout2T6n2+fwYwcD6/3bekvfYqCCWM1HLHyjGroEjvI40NzvNVty9uCAfSiHRL48OpEWqqoi85QWoAQLMK4WoSLcWsXpVCQCI88AiLHFkRtMAV4H+PUfk2ZsEzGSka1EFOtnn3npbU1/aitYgLuS4rcly4D0v9dMawDumQfY5uMacYPo6dkjOTJAdPQDXloB9LbiSBWrbcdnYyZZ/WOaAySoDhCCl3eQFOdBCSqnKG0gHZXAAAAA=" clip-path="url(#c6)"/>
</a>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
node_modules
.env
.cache.json
+852
View File
@@ -0,0 +1,852 @@
{
"name": "spoolman-sponsorkit",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "spoolman-sponsorkit",
"devDependencies": {
"sponsorkit": "18.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
"integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
"integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.4"
}
},
"node_modules/@img/sharp-freebsd-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
"integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
"integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
"integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
"integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
"integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
"integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
"integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
"integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
"integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
"integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
"integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
"integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
"integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
"integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
"integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
"integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
"integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
"integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
"integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
"integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.11.3"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-webcontainers-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
"integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
"integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
"integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
"integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@quansync/fs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz",
"integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"quansync": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/ansis": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz",
"integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
}
},
"node_modules/cac": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz",
"integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"dev": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-9.0.1.tgz",
"integrity": "sha512-ARftfC5HdUNu9jJeL8pHj8debUIHA2b91FizCoMzY4lG6dDX13jdvTK0TBe24IBDRf2HvJSzzwEPvmbkQWHRSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ofetch": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz",
"integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"destr": "^2.0.5",
"node-fetch-native": "^1.6.7",
"ufo": "^1.6.1"
}
},
"node_modules/p-limit": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz",
"integrity": "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.2.1"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quansync": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz",
"integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
"semver": "^7.8.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.35.5",
"@img/sharp-darwin-x64": "0.35.5",
"@img/sharp-freebsd-wasm32": "0.35.5",
"@img/sharp-libvips-darwin-arm64": "1.3.4",
"@img/sharp-libvips-darwin-x64": "1.3.4",
"@img/sharp-libvips-linux-arm": "1.3.4",
"@img/sharp-libvips-linux-arm64": "1.3.4",
"@img/sharp-libvips-linux-ppc64": "1.3.4",
"@img/sharp-libvips-linux-riscv64": "1.3.4",
"@img/sharp-libvips-linux-s390x": "1.3.4",
"@img/sharp-libvips-linux-x64": "1.3.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
"@img/sharp-libvips-linuxmusl-x64": "1.3.4",
"@img/sharp-linux-arm": "0.35.5",
"@img/sharp-linux-arm64": "0.35.5",
"@img/sharp-linux-ppc64": "0.35.5",
"@img/sharp-linux-riscv64": "0.35.5",
"@img/sharp-linux-s390x": "0.35.5",
"@img/sharp-linux-x64": "0.35.5",
"@img/sharp-linuxmusl-arm64": "0.35.5",
"@img/sharp-linuxmusl-x64": "0.35.5",
"@img/sharp-webcontainers-wasm32": "0.35.5",
"@img/sharp-win32-arm64": "0.35.5",
"@img/sharp-win32-ia32": "0.35.5",
"@img/sharp-win32-x64": "0.35.5"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/sponsorkit": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/sponsorkit/-/sponsorkit-18.0.0.tgz",
"integrity": "sha512-9Jr+n4SjU8Qtldq8i0UlwwqBEAVy5JFbIQNXRUIs1N+LPTCnX2/of5oyHYftCLi3KxAODVMmyEdWqkJMTagR6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansis": "^4.3.1",
"cac": "^7.0.0",
"consola": "^3.4.2",
"d3-hierarchy": "^3.1.2",
"normalize-url": "^9.0.1",
"ofetch": "^1.5.1",
"p-limit": "^7.3.1",
"sharp": "^0.35.3",
"unconfig": "^7.5.0"
},
"bin": {
"sponsorkit": "dist/cli.mjs"
},
"engines": {
"node": "^22.18.0 || ^24.11.0 || >=26.0.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"dev": true,
"license": "MIT"
},
"node_modules/unconfig": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz",
"integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@quansync/fs": "^1.0.0",
"defu": "^6.1.4",
"jiti": "^2.6.1",
"quansync": "^1.0.0",
"unconfig-core": "7.5.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/unconfig-core": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz",
"integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@quansync/fs": "^1.0.0",
"quansync": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "spoolman-sponsorkit",
"private": true,
"description": "Generates the sponsors image rendered in the README. Run by .github/workflows/sponsors.yml.",
"type": "module",
"devDependencies": {
"sponsorkit": "18.0.0"
}
}
+112
View File
@@ -0,0 +1,112 @@
import { defineConfig, tierPresets } from 'sponsorkit'
import type { Provider, Sponsorship } from 'sponsorkit'
/**
* People who tipped on Ko-fi and asked to be credited on the sponsors board.
* Ko-fi has no API we can query, so these are transcribed by hand from the
* Ko-fi dashboard. A tip counts toward a tier for KOFI_TIP_EFFECTIVITY_DAYS,
* after which the entry drops to "Past Sponsors" on its own - the same way a
* one-time GitHub sponsorship ages out, so nothing here needs pruning.
*/
const kofiTips: KofiTip[] = [
{ login: 'marko-p', dollars: 10, date: '2026-09-01' },
]
/** Days a one-time tip keeps counting. Matches sponsorkit's own Ko-fi provider. */
const KOFI_TIP_EFFECTIVITY_DAYS = 30
interface KofiTip {
/** GitHub login. Drives the avatar and the link on the board. */
login: string
/** Display name, defaults to the login. */
name?: string
/** Tip amount in USD. Decides the tier, see the `tiers` list below. */
dollars: number
/** Date of the tip, YYYY-MM-DD. */
date: string
}
function kofiTipToSponsorship(tip: KofiTip): Sponsorship {
const expireAt = new Date(Date.parse(tip.date) + KOFI_TIP_EFFECTIVITY_DAYS * 24 * 60 * 60 * 1000)
return {
sponsor: {
type: 'User',
login: tip.login,
name: tip.name ?? tip.login,
avatarUrl: `https://github.com/${tip.login}.png`,
linkUrl: `https://github.com/${tip.login}`,
},
// -1 is sponsorkit's marker for a sponsorship that has run out.
monthlyDollars: expireAt > new Date() ? tip.dollars : -1,
isOneTime: true,
privacyLevel: 'PUBLIC',
tierName: 'Ko-fi',
createdAt: new Date(tip.date).toISOString(),
expireAt: expireAt.toISOString(),
}
}
const KofiManualProvider: Provider = {
name: 'kofi-manual',
async fetchSponsors() {
return kofiTips.map(kofiTipToSponsorship)
},
}
export default defineConfig({
github: {
login: 'Donkie',
type: 'user',
},
// Listing providers explicitly disables auto-detection, so GitHub has to be
// named here alongside the hand-maintained Ko-fi list.
providers: ['github', KofiManualProvider],
width: 800,
renderer: 'tiers',
formats: ['svg', 'png'],
// Relative to this directory. Lands next to the other README images.
outputDir: '../media',
name: 'sponsors',
// Resolved against outputDir; keep the fetch cache out of the media folder.
cacheFile: '../sponsorkit/.cache.json',
tiers: [
{
title: 'Backers',
preset: tierPresets.xs,
},
{
title: 'Supporters',
monthlyDollars: 10,
preset: tierPresets.medium,
},
{
title: 'Sponsors',
monthlyDollars: 25,
preset: tierPresets.large,
},
{
title: 'Silver Sponsors',
monthlyDollars: 50,
preset: tierPresets.xl,
},
{
title: 'Gold Sponsors',
monthlyDollars: 100,
preset: {
...tierPresets.xl,
avatar: { size: 120 },
boxWidth: 150,
boxHeight: 160,
name: { maxLength: 25 },
},
},
{
title: 'Past Sponsors',
monthlyDollars: -1,
preset: tierPresets.xs,
},
],
})
+15 -9
View File
@@ -25,19 +25,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PDM
uses: pdm-project/setup-pdm@v2
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.9"
- run: pdm sync
- run: pdm run docs
version: "0.9.26"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install Poe the Poet
run: uv tool install poethepoet
- run: uv sync
- run: poe generate-docs
- name: Setup Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v5.0.0
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3.0.1
with:
path: "docs/"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4.0.5
+533 -94
View File
@@ -3,53 +3,111 @@ name: CI
on:
push:
branches:
- master
# Every branch builds, so a fix can be handed to a bug reporter as a Docker
# image before it merges (see publish-images). Weblate pushes locale-only
# commits constantly and dependabot's branches arrive as PRs anyway, so
# bot branches skip push runs and are built by the pull_request run instead.
- "**"
- "!weblate*"
- "!dependabot/**"
tags:
- "v*"
pull_request:
branches:
- master
permissions:
contents: read
packages: read
# A new push makes the previous run's result irrelevant, so cancel it and let the
# new one have the runners. Master and tags are exempt: both publish images, and a
# cancelled run there is a half-published release.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/') }}
env:
REGISTRY: ghcr.io
IMAGE_NAME: donkie/spoolman
GHCR_IMAGE: donkie/spoolman
jobs:
#
# Linting
#
style:
# Same-repo branches already got a full run from the push trigger, so skip the
# duplicate pull_request run. Bot branches excluded above still need it, as do
# forks, whose pushes fire no event here. Skipping the three jobs with no `needs`
# cascades to every other job, since a skipped dependency skips its dependents.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'weblate') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v4
- name: Install node
uses: actions/setup-node@v4.0.3
with:
python-version: "3.9"
node-version: '24'
- name: Install pre-commit
run: pip install pre-commit==3.3.3
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.26"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install lefthook
run: uv add --dev lefthook
- name: Install client dependencies
run: |
cd client
npm ci
# lefthook's `ci` hook lints/formats/type-checks client_v2 too, via
# hardcoded ./node_modules/.bin/* paths, so its deps must be present.
- name: Install client_v2 dependencies
run: |
cd client_v2
npm ci
- name: Run pre-commit
run: pre-commit run --all-files
run: uv run lefthook run ci
#
# Build the Spoolman client for baking into the docker images
# This is done on native hardware to speed things up, vite is very slow on emulated hardware
#
build-client:
# Same-repo branches already got a full run from the push trigger, so skip the
# duplicate pull_request run. Bot branches excluded above still need it, as do
# forks, whose pushes fire no event here. Skipping the three jobs with no `needs`
# cascades to every other job, since a skipped dependency skips its dependents.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'weblate') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4.0.3
with:
node-version: '20'
node-version: '24'
- name: Install dependencies
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.26"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install client dependencies
run: |
cd client
npm ci
@@ -60,24 +118,57 @@ jobs:
rm -f .env && echo "VITE_APIURL=/api/v1" > .env.production
npm run build
# Remove client source and keep only the built dist
# Build the new Svelte client (client_v2), served by default at runtime.
# No VITE_APIURL is set: the client resolves the API relative to its base path.
- name: Install client_v2 dependencies
run: |
cd client_v2
npm ci
- name: Build client_v2
run: |
cd client_v2
npm run build
- name: Write build info
run: |
echo "GIT_COMMIT=$(git rev-parse --short HEAD)" > build.txt
echo "BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> build.txt
# Remove client sources and keep only the built output for each client
- name: Clean
run: |
mv client/dist dist
rm -rf client
mkdir client
mv dist client/dist
mv client_v2/build build_v2
rm -rf client_v2
mkdir client_v2
mv build_v2 client_v2/build
rm -rf .git
rm -rf .venv
- name: Fix permissions
run: |
chmod +x scripts/*.sh
- name: Upload client Spoolman artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.4.0
with:
name: client
path: client/dist
- name: Upload full Spoolman artifact
uses: actions/upload-artifact@v3
- name: Upload client_v2 Spoolman artifact
uses: actions/upload-artifact@v4.4.0
with:
name: client_v2
path: client_v2/build
- name: Upload full Spoolman artifact
uses: actions/upload-artifact@v4.4.0
with:
include-hidden-files: true
name: spoolman
path: .
#
@@ -86,48 +177,56 @@ jobs:
build-amd64:
needs: [build-client]
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Download client
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
name: client
path: client/dist
- name: Download client_v2
uses: actions/download-artifact@v4.1.7
with:
name: client_v2
path: client_v2/build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3.6.1
- name: Log in to the Github Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Spoolman
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6.7.0
with:
context: .
tags: ${{ env.IMAGE_NAME }}:test
tags: ${{ env.GHCR_IMAGE }}:test
outputs: type=docker,dest=/tmp/spoolman.tar
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64,mode=max
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-amd64
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-amd64,mode=max
# Separate build for pull requests since PRs dont have access to store cache
- name: Build Spoolman (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6.7.0
with:
context: .
tags: ${{ env.IMAGE_NAME }}:test
tags: ${{ env.GHCR_IMAGE }}:test
outputs: type=docker,dest=/tmp/spoolman.tar
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-amd64
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.4.0
with:
name: spoolman-image
path: /tmp/spoolman.tar
@@ -135,43 +234,50 @@ jobs:
# Build tester image for integration tests
#
build-tester:
# Same-repo branches already got a full run from the push trigger, so skip the
# duplicate pull_request run. Bot branches excluded above still need it, as do
# forks, whose pushes fire no event here. Skipping the three jobs with no `needs`
# cascades to every other job, since a skipped dependency skips its dependents.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'weblate') }}
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3.6.1
- name: Log in to the Github Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Spoolman Tester
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6.7.0
with:
context: ./tests_integration
tags: ${{ env.IMAGE_NAME }}-tester:latest
tags: ${{ env.GHCR_IMAGE }}-tester:latest
outputs: type=docker,dest=/tmp/spoolman-tester.tar
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-tester
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-tester,mode=max
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-tester
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-tester,mode=max
# Separate build for pull requests since PRs dont have access to store cache
- name: Build Spoolman Tester (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6.7.0
with:
context: ./tests_integration
tags: ${{ env.IMAGE_NAME }}-tester:latest
tags: ${{ env.GHCR_IMAGE }}-tester:latest
outputs: type=docker,dest=/tmp/spoolman-tester.tar
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-tester
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-tester
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.4.0
with:
name: spoolman-tester-image
path: /tmp/spoolman-tester.tar
@@ -186,11 +292,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Download built images
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
pattern: spoolman*-image
path: /tmp
- name: Load built images
@@ -199,162 +306,495 @@ jobs:
docker load --input /tmp/spoolman-tester-image/spoolman-tester.tar
- name: Perform integration tests
uses: isbang/compose-action@v1.4.1
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./tests_integration/docker-compose-${{ matrix.dbtype }}.yml"
up-flags: "--abort-on-container-exit"
down-flags: "--volumes"
#
# Build arm64 image
# Don't run this for pull requests
# Only push to buildcache and let release job push to registry
# Perform frontend (browser) integration tests against the real image
#
build-arm64:
if: ${{ github.event_name != 'pull_request' }}
needs: [build-client]
test-frontend:
needs: [build-amd64]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Download built image
uses: actions/download-artifact@v4.1.7
with:
name: spoolman-image
path: /tmp/spoolman-image
- name: Load built image
run: docker load --input /tmp/spoolman-image/spoolman.tar
- name: Install node
uses: actions/setup-node@v4.0.3
with:
node-version: '24'
- name: Install Playwright
run: |
cd tests_frontend
npm ci
npx playwright install --with-deps chromium
- name: Start Spoolman
run: docker compose -f tests_frontend/docker-compose.yml up -d --wait
- name: Run frontend integration tests
run: |
cd tests_frontend
npx playwright test
- name: Dump Spoolman logs on failure
if: failure()
run: docker compose -f tests_frontend/docker-compose.yml logs spoolman
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4.4.0
with:
name: playwright-report
path: tests_frontend/playwright-report
retention-days: 7
- name: Stop Spoolman
if: always()
run: docker compose -f tests_frontend/docker-compose.yml down -v
#
# Perform frontend (browser) integration tests for client_v2 against the real image
#
test-frontend-v2:
needs: [build-amd64]
runs-on: ubuntu-latest
permissions:
contents: read
# Argos authenticates via OIDC; fork PRs get no id-token and fall back to tokenless upload.
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download built image
uses: actions/download-artifact@v4.1.7
with:
name: spoolman-image
path: /tmp/spoolman-image
- name: Load built image
run: docker load --input /tmp/spoolman-image/spoolman.tar
- name: Install node
uses: actions/setup-node@v4.0.3
with:
node-version: '24'
- name: Install Playwright
run: |
cd tests_frontend_v2
npm ci
npx playwright install --with-deps chromium
- name: Start Spoolman
run: docker compose -f tests_frontend_v2/docker-compose.yml up -d --wait
# Screenshots first: they seed a fixed dataset and need the database empty.
# The Argos reporter uploads them and posts the visual diff on the PR.
- name: Take client_v2 screenshots
env:
ARGOS_UPLOAD: "1"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd tests_frontend_v2
npx playwright test --project screenshots
- name: Run client_v2 frontend integration tests
run: |
cd tests_frontend_v2
npx playwright test --project chromium
- name: Dump Spoolman logs on failure
if: failure()
run: docker compose -f tests_frontend_v2/docker-compose.yml logs spoolman
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4.4.0
with:
name: playwright-report-v2
path: tests_frontend_v2/playwright-report
retention-days: 7
- name: Stop Spoolman
if: always()
run: docker compose -f tests_frontend_v2/docker-compose.yml down -v
#
# Build arm64 image
# Also exported to a tar so test-multiarch-smoke can boot it under QEMU.
# On pull requests we can't push to buildcache (forks don't have registry
# write access), so cache-to is skipped and we only read from it.
#
build-arm64:
needs: [build-client]
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download client
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
name: client
path: client/dist
- name: Download client_v2
uses: actions/download-artifact@v4.1.7
with:
name: client_v2
path: client_v2/build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3.6.1
- name: Log in to the Github Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and cache Docker image
uses: docker/build-push-action@v4
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6.7.0
with:
context: .
platforms: linux/arm64
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64,mode=max
tags: ${{ env.GHCR_IMAGE }}:test-arm64
outputs: type=docker,dest=/tmp/spoolman-arm64.tar
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-arm64
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-arm64,mode=max
# Separate build for pull requests since PRs dont have access to store cache
- name: Build and cache Docker image (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v6.7.0
with:
context: .
platforms: linux/arm64
tags: ${{ env.GHCR_IMAGE }}:test-arm64
outputs: type=docker,dest=/tmp/spoolman-arm64.tar
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-arm64
- name: Upload arm64 image artifact
uses: actions/upload-artifact@v4.4.0
with:
name: spoolman-image-arm64
path: /tmp/spoolman-arm64.tar
#
# Build arm/v7 image
# Don't run this for pull requests
# Only push to buildcache and let release job push to registry
# Also exported to a tar so test-multiarch-smoke can boot it under QEMU.
# On pull requests we can't push to buildcache (forks don't have registry
# write access), so cache-to is skipped and we only read from it.
#
build-armv7:
if: ${{ github.event_name != 'pull_request' }}
needs: [build-client]
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Download client
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
name: client
path: client/dist
- name: Download client_v2
uses: actions/download-artifact@v4.1.7
with:
name: client_v2
path: client_v2/build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
id: buildx
uses: docker/setup-buildx-action@v3.6.1
# Keep uv's cache of wheels built from source (asyncpg, greenlet, ...) across
# runs. They have no armv7 wheels, and compiling them under QEMU is ~9 of the
# ~11 minutes of a build that misses the layer cache (lockfile or base image
# changed). The Dockerfile prunes the cache down to just those wheels, so it's
# small enough to save on every run and always restore the newest one.
- name: Cache uv wheels
uses: actions/cache@v4
with:
path: uv-cache
key: uv-cache-armv7-${{ github.run_id }}
restore-keys: uv-cache-armv7-
- name: Inject uv cache into BuildKit
uses: reproducible-containers/buildkit-cache-dance@v3.4.0
with:
builder: ${{ steps.buildx.outputs.name }}
cache-map: |
{ "uv-cache": "/root/.cache/uv" }
- name: Log in to the Github Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and cache Docker image
uses: docker/build-push-action@v4
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6.7.0
with:
context: .
platforms: linux/arm/v7
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-armv7
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-armv7,mode=max
tags: ${{ env.GHCR_IMAGE }}:test-armv7
outputs: type=docker,dest=/tmp/spoolman-armv7.tar
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-armv7
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-armv7,mode=max
# Separate build for pull requests since PRs dont have access to store cache
- name: Build and cache Docker image (PR)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v6.7.0
with:
context: .
platforms: linux/arm/v7
tags: ${{ env.GHCR_IMAGE }}:test-armv7
outputs: type=docker,dest=/tmp/spoolman-armv7.tar
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-armv7
- name: Upload armv7 image artifact
uses: actions/upload-artifact@v4.4.0
with:
name: spoolman-image-armv7
path: /tmp/spoolman-armv7.tar
#
# Release image if tests pass
# Smoke test the arm64 and armv7 images under QEMU emulation: boot the
# container with no config (defaults to SQLite, no external DB needed) and
# confirm /api/v1/health comes up. Emulated boot (interpreter start +
# alembic migrations) is slow, hence the generous retry budget.
#
test-multiarch-smoke:
needs: [build-arm64, build-armv7]
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
platform: linux/arm64
artifact: spoolman-image-arm64
tarball: spoolman-arm64.tar
- arch: armv7
platform: linux/arm/v7
artifact: spoolman-image-armv7
tarball: spoolman-armv7.tar
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
- name: Download built image
uses: actions/download-artifact@v4.1.7
with:
name: ${{ matrix.artifact }}
path: /tmp/spoolman-image
- name: Load built image
run: docker load --input /tmp/spoolman-image/${{ matrix.tarball }}
- name: Start Spoolman (${{ matrix.arch }}, emulated via QEMU)
run: |
docker run -d --name spoolman-smoke \
--platform ${{ matrix.platform }} \
-p 8000:8000 \
${{ env.GHCR_IMAGE }}:test-${{ matrix.arch }}
- name: Wait for /health endpoint
run: |
for i in $(seq 1 90); do
if curl -fsS http://localhost:8000/api/v1/health; then
echo
echo "Spoolman (${{ matrix.arch }}) is healthy"
exit 0
fi
sleep 5
done
echo "Spoolman (${{ matrix.arch }}) did not become healthy in time" >&2
exit 1
- name: Dump Spoolman logs on failure
if: failure()
run: docker logs spoolman-smoke
- name: Stop Spoolman
if: always()
run: docker rm -f spoolman-smoke
#
# Release images GHCR and Docker Hub if tests pass
# Don't run this for pull requests
#
publish-images:
if: ${{ github.event_name != 'pull_request' }}
needs: [test, build-amd64, build-arm64, build-armv7, style, build-client]
needs:
[
test,
test-frontend,
test-frontend-v2,
test-multiarch-smoke,
build-amd64,
build-arm64,
build-armv7,
style,
build-client,
]
runs-on: ubuntu-latest
permissions:
packages: write
# Lets cosign fetch an OIDC token from Github and sign without a private key.
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Download client
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
name: client
path: client/dist
- name: Download client_v2
uses: actions/download-artifact@v4.1.7
with:
name: client_v2
path: client_v2/build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3.6.1
- name: Log in to the Github Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
# The trailing type=ref,event=branch is what gives an unmerged branch a
# pullable image (ghcr.io/donkie/spoolman:my-branch) for a bug reporter to
# test. It is GHCR-only and skips master (already published as :edge), so
# Docker Hub keeps carrying nothing but :edge and tagged releases.
- name: Extract metadata (tags, labels) for GHCR
id: meta-ghcr
uses: docker/metadata-action@v5.5.1
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
ghcr.io/${{ env.GHCR_IMAGE }}
tags: |
type=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch,enable=${{ github.ref_name != 'master' }}
- name: Extract metadata (tags, labels) for Docker Hub
id: meta-dh
uses: docker/metadata-action@v5.5.1
with:
images: |
${{ vars.REMOTE_REGISTRY_URL }}/${{ vars.REMOTE_REGISTRY_NAMESPACE }}/${{ vars.REMOTE_REGISTRY_REPOSITORY }}
tags: |
type=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Log in to DockerHub
uses: docker/login-action@v3.3.0
with:
registry: ${{ vars.REMOTE_REGISTRY_URL }}
username: ${{ secrets.REMOTE_REGISTRY_USERNAME }}
password: ${{ secrets.REMOTE_REGISTRY_PASSWORD }}
- name: Store git commit and build date
run: |
echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
echo "BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV"
- name: Install cosign
uses: sigstore/cosign-installer@v4.1.2
with:
# Installer v4+ verifies cosign v3 downloads via the Sigstore bundle
# format; pin an explicit, current cosign release for reproducibility.
cosign-release: v3.1.1
- name: Build and push Docker images
uses: docker/build-push-action@v4
id: build
uses: docker/build-push-action@v6.7.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: |
${{ steps.meta-ghcr.outputs.tags }}
${{ steps.meta-dh.outputs.tags }}
labels: ${{ steps.meta-ghcr.outputs.labels }}
build-args: |
GIT_COMMIT
BUILD_DATE
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-armv7
type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-amd64
type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-arm64
type=registry,ref=ghcr.io/${{ env.GHCR_IMAGE }}:buildcache-armv7
# Sign the manifest by digest rather than by tag: all the tags above point
# at the same digest, and the same digest is valid in both registries, so
# signing every tag would just repeat the same signature.
- name: Sign the published images
env:
DIGEST: ${{ steps.build.outputs.digest }}
TAGS: |
${{ steps.meta-ghcr.outputs.tags }}
${{ steps.meta-dh.outputs.tags }}
run: |
echo "${TAGS}" | while read -r tag; do
[ -n "${tag}" ] || continue
echo "${tag%:*}"
done | sort --unique | while read -r repository; do
echo "Signing ${repository}@${DIGEST}"
cosign sign --yes "${repository}@${DIGEST}"
done
#
# Create Github Release if CI was triggered by a tag
#
publish-release:
if: ${{ github.event_name != 'pull_request' }} && startsWith(github.event.ref, 'refs/tags/v')
if: ${{ (github.event_name != 'pull_request') && startsWith(github.event.ref, 'refs/tags/v') }}
needs: [publish-images]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download built spoolman
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4.1.7
with:
name: spoolman
path: /tmp/spoolman
@@ -383,4 +823,3 @@ jobs:
files: /tmp/spoolman/spoolman.zip
token: ${{ secrets.GITHUB_TOKEN }}
body: "⚠️ TODO ⚠️"
+149
View File
@@ -0,0 +1,149 @@
name: Guard Translations
# Translations are managed exclusively through Weblate (https://hosted.weblate.org/projects/spoolman/).
# Only the "en" source locale is edited by hand in this repo; every other language is written back
# by the Weblate bot. This workflow fails any PR (except Weblate's own) that hand-edits a non-English
# translation file, so manual edits don't get overwritten and lost on the next Weblate sync.
#
# Two clients, two different rules:
#
# client_v2/locales/ — the ACTIVE translation target. Weblate translates this one.
# MODIFY/DELETE of an existing non-English file is blocked; ADDING a
# brand-new language file is allowed, because bootstrapping a language
# also needs two hand-written code entries (see NEW_LANGUAGE_HOWTO
# below) that only a human PR can add.
#
# client/public/locales/ — the LEGACY React client, CLOSED to translation work. Its Weblate
# component is locked, so nothing new arrives from translators and
# the files here are frozen at whatever shipped. Any change to a
# non-English file is blocked, including adding a new language:
# effort spent there is effort not spent on the client users get.
on:
pull_request:
types:
- opened
- synchronize
- reopened
# NOTE: no `paths:` filter on purpose. If this check is marked "Required" in branch
# protection, a paths filter would leave PRs that don't touch locales stuck "pending"
# forever (the check would never post a status). Instead we always run and pass fast
# when nothing offending changed.
permissions:
contents: read
jobs:
guard-translations:
runs-on: ubuntu-latest
# Skip the guard for Weblate's own translation-sync PRs.
if: github.event.pull_request.user.login != 'weblate'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for manual non-English translation changes
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# --no-renames turns any rename into a delete + add pair, so a "renamed" (i.e. rewritten)
# existing file still trips the delete rule and can't be smuggled through as a fresh add.
#
# --diff-filter=MD keeps only Modified and Deleted paths: edits to files that ALREADY exist
# on the base branch. Added (A) files are handled separately per client below.
# Three dots, not two. `git diff A B` compares the two trees as they stand, so every
# commit that landed on the base branch after this PR forked shows up as if the PR had
# changed it -- a Weblate sync merging to master made unrelated PRs fail with
# "client_v2/locales/sv/common.json" in the offending list. `git diff A...B` diffs from
# the MERGE BASE instead, which is exactly "what this PR changed" and stays correct no
# matter how far master has moved on.
edited="$(git diff --no-renames --diff-filter=MD --name-only "$BASE_SHA...$HEAD_SHA" || true)"
added="$(git diff --no-renames --diff-filter=A --name-only "$BASE_SHA...$HEAD_SHA" || true)"
# Active client: edits to existing non-English files are blocked, new languages are fine.
offending_v2="$(printf '%s\n' "$edited" \
| grep -E '^client_v2/locales/[^/]+/' \
| grep -vE '^client_v2/locales/en/' \
|| true)"
# Legacy client: every non-English change is blocked, adds included.
offending_legacy="$(printf '%s\n' "$edited" "$added" \
| grep -E '^client/public/locales/[^/]+/' \
| grep -vE '^client/public/locales/en/' \
|| true)"
if [ -z "$offending_v2" ] && [ -z "$offending_legacy" ]; then
echo "No manual edits to non-English translation files detected. ✅"
exit 0
fi
echo "::error title=Manual translation edits are not allowed::This PR hand-edits non-English translation files. See the job summary for details."
{
echo "## ❌ Manual translation edits detected"
echo ""
if [ -n "$offending_v2" ]; then
echo "### Existing translations edited by hand"
echo ""
echo "This PR modifies or deletes existing non-English translation files:"
echo ""
echo '```'
echo "$offending_v2"
echo '```'
echo ""
echo "Every language except the English source (\`client_v2/locales/en/common.json\`) is"
echo "managed **exclusively through Weblate**. A manual edit here will be **silently"
echo "overwritten and lost** the next time Weblate syncs — so we don't accept them."
echo ""
echo "**What to do instead:**"
echo ""
echo "- **To fix or improve a translation:** contribute it through Weblate at"
echo " <https://hosted.weblate.org/projects/spoolman/>. It's free, requires no coding,"
echo " and your change flows back into the repo automatically."
echo "- **To add or change a user-facing string:** edit **only**"
echo " \`client_v2/locales/en/common.json\`. Weblate picks up the new keys and exposes"
echo " them to translators. Revert your changes to every other language and push again."
echo "- **To add a brand-new language:** that IS allowed here — new files aren't blocked,"
echo " only edits to existing ones. A new language needs two code entries as well:"
echo " the locale code in \`client_v2/project.inlang/settings.json\` (so paraglide compiles"
echo " it) and an endonym in \`client_v2/src/lib/i18n/languages.ts\` (so it appears in the"
echo " language picker). Without both, the translation file is built but unreachable."
echo ""
fi
if [ -n "$offending_legacy" ]; then
echo "### Legacy client translations are closed"
echo ""
echo "This PR changes non-English translation files of the **legacy React client**:"
echo ""
echo '```'
echo "$offending_legacy"
echo '```'
echo ""
echo "\`client/public/locales/\` belongs to the old client, which is only kept around for"
echo "the \`SPOOLMAN_LEGACY_CLIENT\` fallback. Its Weblate component is **locked** and its"
echo "translations are frozen — nothing here reaches the UI that users get by default."
echo ""
echo "**Translate \`client_v2\` instead:** the same strings, in the client everyone actually"
echo "sees, at <https://hosted.weblate.org/projects/spoolman/>."
echo ""
fi
echo "> If you believe a file genuinely needs a manual change (e.g. a structural fix),"
echo "> please explain it in a PR comment so a maintainer can review."
} >> "$GITHUB_STEP_SUMMARY"
echo ""
echo "Offending files:"
# Plain `if` rather than `[ -n … ] && echo …`: under `set -e` a false test makes the
# whole && list return non-zero and abort the step, which would skip the second echo.
if [ -n "$offending_v2" ]; then echo "$offending_v2"; fi
if [ -n "$offending_legacy" ]; then echo "$offending_legacy"; fi
exit 1
+38
View File
@@ -0,0 +1,38 @@
name: Issue Manager
on:
schedule:
- cron: "0 0 * * *"
issue_comment:
types:
- created
issues:
types:
- labeled
pull_request_target:
types:
- labeled
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
issue-manager:
runs-on: ubuntu-latest
steps:
- uses: tiangolo/issue-manager@0.8.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
config: >
{
"answered": {
"delay": 604800,
"message": "It seems the issue was answered, closing this now."
},
"waiting": {
"delay": 604800,
"message": "Closing after 7 days of waiting for the additional info requested."
}
}
+77
View File
@@ -0,0 +1,77 @@
name: Label sponsor issues
on:
issues:
types: [opened]
permissions:
issues: write
concurrency:
group: sponsor-label-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Label the issue if its author is a sponsor
uses: actions/github-script@v9
with:
# The sponsorship lookup is viewer-relative: it answers "is this person
# sponsoring *me*" for whoever owns the token. GITHUB_TOKEN's viewer is
# the Actions bot, which nobody sponsors, so it always comes back empty.
# It has to be the PAT, and reading privacyLevel needs its read:user scope.
github-token: ${{ secrets.PAT }}
script: |
// The Supporter tier and up. Below this, no tag was promised.
const MIN_MONTHLY_DOLLARS = 10
const LABEL = 'sponsor'
const issue = context.payload.issue
const author = issue.user.login
if (author.endsWith('[bot]')) return
const query = `query($login: String!) {
user(login: $login) {
sponsorshipForViewerAsSponsorable(activeOnly: true) {
privacyLevel
tier { monthlyPriceInDollars }
}
}
}`
let sponsorship
try {
const data = await github.graphql(query, { login: author })
sponsorship = data.user?.sponsorshipForViewerAsSponsorable
} catch (error) {
// Deleted accounts and app logins never resolve to a user.
core.info(`Could not look up ${author}: ${error.message}`)
return
}
if (!sponsorship) {
core.info(`${author} is not an active sponsor`)
return
}
const dollars = sponsorship.tier.monthlyPriceInDollars
if (dollars < MIN_MONTHLY_DOLLARS) {
core.info(`${author} sponsors at $${dollars}, below the $${MIN_MONTHLY_DOLLARS} tier`)
return
}
// A private sponsor chose not to be listed publicly. This label is
// public, so applying it would out them; skip rather than disclose.
if (sponsorship.privacyLevel === 'PRIVATE') {
core.info(`${author} sponsors privately, leaving the issue unlabelled`)
return
}
await github.rest.issues.addLabels({
...context.repo,
issue_number: issue.number,
labels: [LABEL],
})
core.info(`Labelled #${issue.number}, opened by $${dollars}/month sponsor ${author}`)
+51
View File
@@ -0,0 +1,51 @@
name: Generate sponsors image
on:
schedule:
- cron: '0 3 * * *' # daily
workflow_dispatch:
permissions:
contents: write
concurrency:
group: sponsors
cancel-in-progress: false
jobs:
sponsorkit:
runs-on: ubuntu-latest
steps:
# Checks out with the PAT rather than the default GITHUB_TOKEN: the commit
# step below pushes straight to master, and the ruleset there only lets a
# bypass-capable actor through.
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: true
token: ${{ secrets.PAT }}
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
cache-dependency-path: .github/sponsorkit/package-lock.json
- name: Install sponsorkit
working-directory: .github/sponsorkit
run: npm ci
- name: Generate sponsors image
working-directory: .github/sponsorkit
run: npx sponsorkit
env:
SPONSORKIT_GITHUB_TOKEN: ${{ secrets.PAT }}
# Only commits when the rendered image actually differs from what is checked in.
- name: Commit sponsors image
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: 'chore: update sponsors image'
file_pattern: '.github/media/sponsors.svg .github/media/sponsors.png'
skip_dirty_check: false
+17 -13
View File
@@ -13,34 +13,38 @@ on:
- minor
- major
permissions:
contents: write
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.PAT }}
- name: Set up Python
uses: actions/setup-python@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.9"
version: "0.9.26"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4.0.3
with:
node-version: '20'
node-version: '24'
- name: Setup PDM
run: |
pip install pdm pdm-bump
- name: Bump version
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
pdm run bump ${{ github.event.inputs.bump }}
uv run poe bump ${{ github.event.inputs.bump }}
- name: Push
run: |
+136
View File
@@ -0,0 +1,136 @@
name: Auto-merge Weblate PRs
# Translations are managed exclusively through Weblate (https://hosted.weblate.org/projects/spoolman/),
# which opens a pull request for every sync back to this repo (see guard-translations.yml). Those PRs
# are machine-generated, always scoped to a locale tree (client_v2/locales/ today), and gated by the
# full CI suite just like any other PR. Rather than hand-merging each one, this workflow turns on GitHub's native
# auto-merge once a week for Weblate-authored PRs so they merge themselves once all required status checks pass.
#
# Why auto-merge instead of giving Weblate direct push access to master:
# - Every sync still runs through style/tests/build before it can land; nothing merges unvalidated.
# - No actor gains unrestricted, un-CI'd direct write to master. A compromised Weblate token can only
# open a PR; auto-merge fires solely on green CI for Weblate-authored PRs.
# - GitHub write/bypass can't be scoped to locale files, so direct write would grant all-file access.
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
# contents+pull-requests write let the built-in GITHUB_TOKEN enable auto-merge and perform the merge.
# The token is not a ruleset bypass actor and doesn't need to be: with 0 required approvals it
# satisfies the branch requirements once the required checks go green, exactly like a manual merge.
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
# Only ever act on Weblate's own translation-sync PRs. Human PRs are never touched.
# Scheduled runs use the default-branch workflow. This job never checks out or executes
# PR code; it only calls the GitHub API to inspect PRs and enable auto-merge or merge them.
steps:
# Defence in depth: don't trust the author alone, trust the diff. Even a compromised Weblate
# token can only ever get auto-merged if the PR is confined to locale JSON files. Anything
# else (a code file, a workflow, a non-.json file, a file outside the two locale trees)
# fails the check, auto-merge is never enabled, and the PR waits for a human. The file list is
# read from the API; this step never checks out or runs PR code.
#
# Both locale trees are allowed. client_v2/locales/ is where translations go now; the legacy
# client/public/locales/ tree stays on the list only so that a sync opened before its Weblate
# component was locked isn't stranded half-merged. Locking the component, not this allowlist,
# is what stops new legacy translations (see guard-translations.yml).
- name: Verify and merge open Weblate PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
# Merge commit, NOT squash. This is load-bearing for Weblate, not a style preference.
#
# Weblate keeps its own clone of this repo and rebases it onto master to pick up our changes.
# A squash merge replaces the Weblate commits with one brand-new commit that has a different
# hash AND, when the PR held more than one commit, a different patch-id than any of them. The
# rebase therefore can't tell that the work already landed, replays each original commit on top
# of a master that already contains those very strings, and dies on a content conflict:
#
# Rebasing (1/3)
# CONFLICT (content): Merge conflict in client_v2/locales/ru/common.json
#
# From then on Weblate can't merge upstream and stops syncing until someone resets it by hand
# (this happened with PR #1004, which squashed two Russian commits into one). Weblate's own
# docs are explicit: "Do not squash merge it. Squash merging creates a new commit instead of
# preserving the Weblate commits, so Weblate might not recognize that its local commits are
# already included upstream and might need a repository reset to recover."
#
# A merge commit keeps every Weblate commit reachable with its original hash, so the rebase
# sees them as already upstream and drops them silently. Nothing else forces squash here:
# the repo has allow_merge_commit enabled and the master ruleset carries no linear-history
# rule, and human PRs are merge-committed too.
run: |
set -euo pipefail
prs="$(gh pr list --repo "$REPO" --author weblate --state open --limit 10000 --json number,url)"
rows="$(printf '%s\n' "$prs" | jq -r '.[] | [.number, .url] | @tsv')"
failed=0
while IFS=$'\t' read -r PR_NUMBER PR_URL; do
[ -n "$PR_NUMBER" ] || continue
if ! files="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename')"; then
echo "::error::Could not read changed files for $PR_URL; refusing to auto-merge."
echo "## ❌ Auto-merge blocked: could not read files for $PR_URL" >> "$GITHUB_STEP_SUMMARY"
failed=1
continue
fi
if [ -z "$files" ]; then
echo "::error title=Empty diff::Weblate PR $PR_URL reports no changed files; refusing to auto-merge."
echo "## ❌ Auto-merge blocked: empty diff for $PR_URL" >> "$GITHUB_STEP_SUMMARY"
failed=1
continue
fi
# Every changed path must be a .json file under client_v2/locales/<lang>/ (active) or
# client/public/locales/<lang>/ (legacy, being wound down).
offending="$(printf '%s\n' "$files" \
| grep -vE '^(client_v2/locales|client/public/locales)/[^/]+/.+\.json$' || true)"
if [ -n "$offending" ]; then
echo "::error title=Out-of-scope Weblate PR::$PR_URL touches files outside the locale trees; not auto-merging."
{
echo "## ❌ Auto-merge blocked: out-of-scope changes"
echo ""
echo "Weblate PR: $PR_URL"
echo ""
echo "This Weblate PR changes files that are **not** locale JSON under"
echo "\`client_v2/locales/\` or \`client/public/locales/\`:"
echo ""
echo '```'
echo "$offending"
echo '```'
echo ""
echo "Weblate sync PRs are only auto-merged when every changed file is a translation JSON"
echo "file. This PR isn't, so it has been left for a maintainer to review and merge by hand."
} >> "$GITHUB_STEP_SUMMARY"
failed=1
continue
fi
echo "$PR_URL: all $(printf '%s\n' "$files" | wc -l) changed file(s) are locale JSON. Safe to auto-merge."
if merge_output="$(gh pr merge --repo "$REPO" --auto --merge "$PR_URL" 2>&1)"; then
echo "$merge_output"
continue
fi
echo "$merge_output"
# GitHub can refuse auto-merge when the PR is already ready to merge.
if [[ "$merge_output" == *"clean status"* || "$merge_output" == *"already mergeable"* ]]; then
if gh pr merge --repo "$REPO" --merge "$PR_URL"; then
continue
fi
fi
echo "::error::Could not merge $PR_URL."
echo "## ❌ Auto-merge blocked: merge failed for $PR_URL" >> "$GITHUB_STEP_SUMMARY"
failed=1
done <<< "$rows"
exit "$failed"
+8 -3
View File
@@ -15,7 +15,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -26,6 +25,8 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
# requirements.txt is committed (generated from uv on release) for bare-metal /
# Moonraker update_manager consumers. See spoolman/bump.py.
# PyInstaller
# Usually these files are written by a python script from a template
@@ -200,6 +201,10 @@ $RECYCLE.BIN/
### VisualStudioCode ###
.vscode/
data/
devdata
.pdm-python
.history
.claude
# playwright-cli debug artifacts
.playwright-cli/
-24
View File
@@ -1,24 +0,0 @@
default_language_version:
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-yaml
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
args: ["--check"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.286
hooks:
- id: ruff
args: ["--target-version", "py39"]
- repo: https://github.com/pdm-project/pdm
rev: 2.7.4
hooks:
- id: pdm-lock-check
+64 -27
View File
@@ -1,47 +1,82 @@
FROM python:3.11-alpine as python-builder
FROM python:3.14-slim-bookworm AS python-builder
RUN apk add --no-cache g++ python3-dev libpq-dev libstdc++ shadow
# A multi-platform build runs this stage once per platform in parallel. Each gets
# its own uv cache: wheels are per-platform anyway, and one shared cache made the
# platforms queue on uv's cache lock until the slow emulated armv7 sync pushed the
# others past uv's 300 s lock timeout whenever uv.lock changed.
ARG TARGETARCH
ARG TARGETVARIANT
# Add local user so we don't run as root
RUN groupmod -g 1000 users \
&& useradd -u 911 -U app \
&& usermod -G users app
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_NO_DEV=1
ENV UV_PYTHON_DOWNLOADS=0
ENV PATH="/home/app/.local/bin:${PATH}"
# Install system dependencies
# No python3-dev: the base image already ships the 3.14 headers, and python3-dev
# would pull in Debian's whole python3.11 stack. patchelf is for armv7, see below.
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ \
libpq-dev \
libffi-dev \
patchelf \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install PDM
RUN pip install pip setuptools wheel\
&& pip install pdm
# Install UV
RUN pip install --no-cache-dir uv
# Copy and install dependencies
COPY --chown=app:app pyproject.toml /home/app/spoolman/
COPY --chown=app:app pdm.lock /home/app/spoolman/
# Install dependencies
WORKDIR /home/app/spoolman
RUN pdm sync --prod --no-editable
RUN --mount=type=cache,id=uv-${TARGETARCH}${TARGETVARIANT},target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project \
&& uv cache prune --ci
# Copy and install app
COPY --chown=app:app migrations /home/app/spoolman/migrations
COPY --chown=app:app spoolman /home/app/spoolman/spoolman
COPY --chown=app:app alembic.ini /home/app/spoolman/
COPY --chown=app:app README.md /home/app/spoolman/
# greenlet ships no 32-bit ARM wheel, so on armv7 it is compiled from source.
# setuptools links the C++ extension with gcc, which leaves libstdc++ out of the
# .so's NEEDED list, and it then crashes at import with an undefined libstdc++
# typeinfo symbol, taking startup down during the DB migration. Add libstdc++ to
# the NEEDED list so the loader pulls it in. Only armv7 is affected; amd64 and
# arm64 use a correctly linked prebuilt wheel. Done before the app is copied in so
# the layer stays cached across source-only changes.
RUN if [ "$(uname -m)" = "armv7l" ]; then \
patchelf --add-needed libstdc++.so.6 \
"$(find /home/app/spoolman/.venv -name '_greenlet*.so')"; \
fi
FROM python:3.11-alpine as python-runner
# Copy and install app. No --chown here: the "app" user only exists in the
# runner stage, and Podman (unlike Docker/BuildKit) refuses to resolve it. Final
# ownership is set when these files are copied into the runner stage below.
COPY migrations /home/app/spoolman/migrations
COPY spoolman /home/app/spoolman/spoolman
COPY alembic.ini README.md uv.lock pyproject.toml /home/app/spoolman/
RUN --mount=type=cache,id=uv-${TARGETARCH}${TARGETVARIANT},target=/root/.cache/uv \
uv sync --locked \
&& uv cache prune --ci
FROM python:3.14-slim-bookworm AS python-runner
LABEL org.opencontainers.image.source=https://github.com/Donkie/Spoolman
LABEL org.opencontainers.image.description="Keep track of your inventory of 3D-printer filament spools."
LABEL org.opencontainers.image.licenses=MIT
RUN apk add --no-cache libstdc++ su-exec shadow
# Install gosu for privilege dropping
RUN apt-get update && apt-get install -y \
gosu \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Add local user so we don't run as root
RUN groupmod -g 1000 users \
&& useradd -u 1000 -U app \
&& usermod -G users app \
RUN useradd -u 1000 -U app \
&& mkdir -p /home/app/.local/share/spoolman \
&& chown -R app:app /home/app/.local/share/spoolman
# Copy built client
# Copy built clients. The new Svelte client (client_v2) is served by default; the
# legacy React client (client/dist) is kept for the SPOOLMAN_LEGACY_CLIENT fallback.
COPY --chown=app:app ./client/dist /home/app/spoolman/client/dist
COPY --chown=app:app ./client_v2/build /home/app/spoolman/client_v2/build
# Copy built app
COPY --chown=app:app --from=python-builder /home/app/spoolman /home/app/spoolman
@@ -52,14 +87,16 @@ RUN chmod +x /home/app/spoolman/entrypoint.sh
WORKDIR /home/app/spoolman
ENV PATH="/home/app/spoolman/.venv/bin:${PATH}"
ENV PYTHONPATH="/home/app/spoolman:${PYTHONPATH}"
ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown
ENV GIT_COMMIT=${GIT_COMMIT}
ENV BUILD_DATE=${BUILD_DATE}
# Write GIT_COMMIT and BUILD_DATE to a build.txt file
RUN echo "GIT_COMMIT=${GIT_COMMIT}" > build.txt \
&& echo "BUILD_DATE=${BUILD_DATE}" >> build.txt
# Run command
EXPOSE 8000
ENTRYPOINT ["/home/app/spoolman/entrypoint.sh"]
CMD ["--host", "0.0.0.0", "--port", "8000"]
+47 -168
View File
@@ -4,184 +4,63 @@
<img alt="Icon of a filament spool" src="https://github.com/Donkie/Spoolman/assets/2332094/3c120b3a-1422-42f6-a16b-8d5a07c33000">
</picture>
<br/><br/>
<br/>
_Keep track of your inventory of 3D-printer filament spools._
Spoolman is a web service that helps you keep track of your filament spools and how they are being used.
Spoolman is a self-hosted web service designed to help you efficiently manage your 3D printer filament spools and monitor their usage. It acts as a centralized database that seamlessly integrates with popular 3D printing software like [OctoPrint](https://octoprint.org/) and [Klipper](https://www.klipper3d.org/)/[Moonraker](https://moonraker.readthedocs.io/en/latest/). When connected, it automatically updates spool weights as printing progresses, giving you real-time insights into filament usage.
It acts as a database, where other printer software such as Octoprint and Moonraker can interact with to have a centralized place for spool information.
For example, if used together with Moonraker, your spool weight will automatically be reduced as your print is progressing.
[![Static Badge](https://img.shields.io/badge/Spoolman%20Wiki-blue?link=https%3A%2F%2Fgithub.com%2FDonkie%2FSpoolman%2Fwiki)](https://github.com/Donkie/Spoolman/wiki)
[![GitHub Release](https://img.shields.io/github/v/release/Donkie/Spoolman)](https://github.com/Donkie/Spoolman/releases)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/Donkie?logo=githubsponsors&logoColor=EA4AAA&color=EA4AAA)](https://github.com/sponsors/Donkie)
It exposes a HTTP API which services can interact with. See the [OpenAPI description](https://donkie.github.io/Spoolman/) for more information.
### Features
* **Filament Management**: Keep comprehensive records of filament types, manufacturers, and individual spools.
* **API Integration**: The [REST API](https://donkie.github.io/Spoolman/) allows easy integration with other software, facilitating automated workflows and data exchange.
* **Real-Time Updates**: Stay informed with live spool updates through Websockets, providing immediate feedback during printing operations.
* **Central Filament Database**: A community-supported database of manufacturers and filaments simplify adding new spools to your inventory. Contribute by heading to [SpoolmanDB](https://github.com/Donkie/SpoolmanDB).
* **Web-Based Client**: Spoolman includes a built-in web client that lets you manage data effortlessly:
* View, create, edit, and delete filament data.
* Search, group and filter your inventory by manufacturer, material, location and more.
* Add custom fields to tailor information to your specific needs.
* Design and print labels with QR codes for easy spool identification and tracking.
* Contribute to its translation into 18 languages via [Weblate](https://hosted.weblate.org/projects/spoolman/).
* **Database Support**: SQLite, PostgreSQL, MySQL, and CockroachDB.
* **Multi-Printer Management**: Handles spool updates from several printers simultaneously.
* **Advanced Monitoring**: Integrate with [Prometheus](https://prometheus.io/) for detailed historical analysis of filament usage, helping you track and optimize your printing processes. See the [Wiki](https://github.com/Donkie/Spoolman/wiki/Filament-Usage-History) for instructions on how to set it up.
## Client
Spoolman includes a web-client that lets you directly manipulate all the data. It also has a few additional nice features such as label printing.
**Spoolman integrates with:**
* [Moonraker](https://moonraker.readthedocs.io/en/latest/configuration/#spoolman) and most front-ends (Fluidd, KlipperScreen, Mainsail, ...)
* [OctoPrint](https://github.com/mdziekon/octoprint-spoolman)
* [OctoEverywhere](https://octoeverywhere.com/spoolman?source=github_spoolman)
* [Home Assistant](https://github.com/Disane87/spoolman-homeassistant)
* [MCP Server](https://github.com/Disane87/spoolman-mcp) - Manage your filament inventory through AI assistants like Claude using the Model Context Protocol
![image](https://github.com/Donkie/Spoolman/assets/2332094/33928d5e-440f-4445-aca9-456c4370ad0d)
**Web client preview:**
![The Spoolman web client, showing the spool library with a spool's details open alongside it](.github/media/client-screenshot.png)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://hosted.weblate.org/widget/spoolman/287x66-black.png">
<source media="(prefers-color-scheme: light)" srcset="https://hosted.weblate.org/widget/spoolman/287x66-white.png">
<img alt="Icon of a filament spool" src="https://hosted.weblate.org/widget/spoolman/287x66-white.png">
</picture>
_The web client is translated by the community using [Weblate](https://hosted.weblate.org/projects/spoolman/)._
## Integration status
Spoolman doesn't depend on any specific printer software, but to make the most out of it, you should use it together with a frontend that supports it.
It is currently only supported in the Klipper ecosystem, with official support for the major frontends. Support for other ecosystems like Octoprint is ongoing.
* ✔️ Moonraker - See the [Moonraker Documentation](https://moonraker.readthedocs.io/en/latest/configuration/#spoolman)
* ✔️ Fluidd
* ✔️ KlipperScreen
* ✔️ Mainsail
* ✖️ Octoprint - A plugin is in progress: [OctoPrint-Spoolman](https://github.com/mkevenaar/OctoPrint-Spoolman)
* ✔️ Home Assistant integration (https://github.com/Disane87/spoolman-homeassistant)
<table>
<tr>
<td width="50%"><img alt="The Spoolman dashboard, showing spools as draggable cards grouped by storage location" src=".github/media/dashboard-screenshot.png"></td>
<td width="50%"><img alt="The Spoolman label designer, showing a 50 by 25 mm spool label with a QR code, filament name and colour swatch" src=".github/media/label-designer-screenshot.png"></td>
</tr>
<tr>
<td align="center"><sub>Dashboard — spools as cards, grouped by location or any other field. Drag one to move it.</sub></td>
<td align="center"><sub>Label designer — QR labels built from any spool, filament or vendor field.</sub></td>
</tr>
</table>
## Installation
Spoolman can interact with any of the following databases: SQLite, PostgreSQL, MySQL, MariaDB, CockroachDB.
By default, SQLite is used which is a simple no-install database solution that saves to a single .db file located in the server's user directory.
Please see the [Installation page on the Wiki](https://github.com/Donkie/Spoolman/wiki/Installation) for details how to install Spoolman.
Spoolman can be installed in two ways, either standalone on your machine or using Docker. If you already have Docker installed, it's recommended to use that.
If the new client misbehaves on your setup, set `SPOOLMAN_LEGACY_CLIENT=TRUE` to go back to
the previous one. Both ship in every release and talk to the same API, so your data is
untouched either way — but please [report the problem](https://github.com/Donkie/Spoolman/issues)
so it can be fixed.
### Standalone
This installation guide assumes you are using a Debian-based Linux distribution such as Ubuntu or Raspberry Pi OS. If you are using another distribution, please look inside the bash scripts to see what commands are being run and adapt them to your distribution.
## Sponsors
Spoolman is developed and maintained in my spare time. If it is useful to you, consider [sponsoring the project](https://github.com/sponsors/Donkie).
Copy-paste the entire below command and run it on your machine to install Spoolman.
```bash
sudo apt-get update && \
sudo apt-get install -y curl jq && \
mkdir -p ./Spoolman && \
source_url=$(curl -s https://api.github.com/repos/Donkie/Spoolman/releases/latest | jq -r '.assets[] | select(.name == "spoolman.zip").browser_download_url') && \
curl -sSL $source_url -o temp.zip && unzip temp.zip -d ./Spoolman && rm temp.zip && \
cd ./Spoolman && \
bash ./scripts/install_debian.sh
```
#### Updating
Updating Spoolman is quite simple. If you use the default database type, SQLite, it is stored outside of the installation folder (in `~/.local/share/spoolman`), so you will not lose any data by moving to a new installation folder.
Copy-paste the entire below commands and run it on your machine to update Spoolman to the latest version. The command assumes your existing Spoolman folder is named `Spoolman` and is located in your current directory.
```bash
# Stop and disable the old Spoolman service
sudo systemctl stop Spoolman
sudo systemctl disable Spoolman
systemctl --user stop Spoolman
systemctl --user disable Spoolman
# Download and install the new version
mv Spoolman Spoolman_old && \
mkdir -p ./Spoolman && \
source_url=$(curl -s https://api.github.com/repos/Donkie/Spoolman/releases/latest | jq -r '.assets[] | select(.name == "spoolman.zip").browser_download_url') && \
curl -sSL $source_url -o temp.zip && unzip temp.zip -d ./Spoolman && rm temp.zip && \
cp Spoolman_old/.env Spoolman/.env && \
cd ./Spoolman && \
bash ./scripts/install_debian.sh && \
rm -rf ../Spoolman_old
```
### Using Docker
You can also run Spoolman using Docker. Docker is a platform for developing, shipping, and running applications in containers. Containers are lightweight, portable, and self-contained environments that can run on any machine with Docker installed.
To install Docker on your machine, follow the instructions for your operating system on the [Docker website](https://docs.docker.com/engine/install/). Docker also includes the docker-compose tool which lets you configure the container deployment in a simple yaml file, without having to remember all the command line options. Note: older versions of docker-compose require you to have a dash (`-`) in the following commands, like `docker-compose` instead of `docker compose`.
Here is a sample docker-compose config to get you started. Copy-paste it into a file called `docker-compose.yml` and run `docker compose up -d` to start it. If you want to use the SQLite database as in this sample, you must first create a folder called `data` in the same directory as the `docker-compose.yml`, then you should run `chown 1000:1000 data` on it in order to give it the correct permissions for the user inside the docker container.
```yaml
version: '3.8'
services:
spoolman:
image: ghcr.io/donkie/spoolman:latest
restart: unless-stopped
volumes:
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
- type: bind
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
target: /home/app/.local/share/spoolman # Do NOT change this line
ports:
# Map the host machine's port 7912 to the container's port 8000
- "7912:8000"
environment:
- TZ=Europe/Stockholm # Optional, defaults to UTC
```
Once you have it up and running, you can access the web UI by browsing to `http://your.ip:7912`. Make sure that the data folder you created now contains a `spoolman.db` file. If you cannot find this file in your machine, then **your data will be lost** every time you update Spoolman.
#### Updating
If a new version of Spoolman has been released, you can update to it by first browsing to the directory where you have the `docker-compose.yml` file and then running `docker compose pull && docker compose up -d`.
### Environment variables
These are either set in the .env file if you use the standalone installation, or in the docker-compose.yml if you use Docker.
If you want to connect with an external database instead, specify the `SPOOLMAN_DB_*` environment variables from the table below.
| Variable | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| SPOOLMAN_DB_TYPE | Type of database, any of: `postgres`, `mysql`, `sqlite`, `cockroachdb` |
| SPOOLMAN_DB_HOST | Database hostname |
| SPOOLMAN_DB_PORT | Database port |
| SPOOLMAN_DB_NAME | Database name |
| SPOOLMAN_DB_USERNAME | Database username |
| SPOOLMAN_DB_PASSWORD_FILE | Path of file which contains the database password. Can be used instead of SPOOLMAN_DB_PASSWORD if desired. |
| SPOOLMAN_DB_PASSWORD | Database password |
| SPOOLMAN_DB_QUERY | Query parameters for the database connection, e.g. set to `unix_socket=/path/to/mysql.sock` to connect using a MySQL socket. |
| SPOOLMAN_LOGGING_LEVEL | Logging level, any of: `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`, defaults to `INFO`. |
| SPOOLMAN_AUTOMATIC_BACKUP | Automatic nightly DB backups for SQLite databases. Enabled by default, set to `FALSE` to disable. |
| PUID | (*docker only*) Set the UID of the user in the docker container. Default is 1000. |
| PGID | (*docker only*) Set the GID of the user in the docker container. Default is 1000. |
## Frequently Asked Questions (FAQs)
### QR Code Does not work on HTTP / The page is not served over HTTPS
This is a limitation of the browsers. Browsers require a secure connection to the server to enable HTTPS. This is not a limitation of Spoolman. For more information read this [blog](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/) from Mozilla.
You can put Spoolman behind a reverse proxy like Caddy or Nginx to enable HTTPS. See for example [this guide](https://caddyserver.com/docs/quick-starts/reverse-proxy) for Caddy.
### Can Spoolman be translated into my language?
Yes, head over to [Weblate](https://hosted.weblate.org/projects/spoolman/) to start the Translation
## Development
### Server/Backend (Python)
The Python backend runs on Python 3.9. It's built on FastAPI for the REST API, and SQLAlchemy to handle the databases.
To setup yourself for Python development, do the following:
1. Clone this repo
2. CD into the repo
3. Install PDM: `pip install --user pdm`
4. Install Spoolman dependencies: `pdm sync`
And you should be all setup. Read the Style and Integration Testing sections below as well.
#### Style
[Black](https://black.readthedocs.io/en/stable/) and [Ruff](https://docs.astral.sh/ruff/) is used to ensure a consistent style and good code quality. You can install extensions in your editor to make them run automatically.
[Pre-commit](https://pre-commit.com/) is used to ensure the style is maintained for each commit. You can setup pre-commit by simply running the following in the Spoolman root directory:
```
pip install pre-commit
pre-commit install
```
#### Integration Testing
The entire REST API is integration tested using an isolated docker container, with all 4 database types that we support (Postgres, MySQL, SQLite and CockroachDB). These integration tests live in `tests_integration/`. They are designed to "use" the REST API in the same way that a client would, and ensures that everything remains consistent between updates. The databases are created as part of the integration testing, so no external database is needed to run them.
If you have docker installed, you can run the integration tests using `pdm run itest` for all databases, or e.g. `pdm run itest postgres` for a single database.
### Client (Node/React/Typescript)
The client is a React-based web client, built using the [refine.dev](https://refine.dev) framework, with Ant Design as the components.
To test out changes to the web client, the best way is to run it in development mode.
Prerequisites:
* NodeJS 20 or above installed, along with NPM. Running `node --version` should print a correct version.
* A running Spoolman server, with the following two environment variables added in the `docker-compose.yml`:
```yaml
environment:
- FORWARDED_ALLOW_IPS=*
- SPOOLMAN_DEBUG_MODE=TRUE
```
Instructions:
1. Open a terminal and CD to the `client` subdirectory
2. Run `npm install`. If it doesn't succeed, you probably have an incorrect node version. Spoolman is only tested on NodeJS 20.
3. Run `echo "VITE_APIURL=http://192.168.0.123:7901/api/v1" > .env`, where the ip:port is the address of the running Spoolman server. This should create a `.env` file in the `client` directory. If you don't already have one running on your network, you can start one up using the `docker-compose.yml` showed above.
4. Run `npm run dev`. The terminal will print a "Local: xxxx" URL, open that in your browser and the web client should show up. Your existing spools etc in your Spoolman database should be loaded in.
5. Any edits in .ts/.tsx files will be automatically reloaded in your browser. If you make any change to .json files you will need to F5 in your browser.
<a href="https://github.com/sponsors/Donkie">
<img alt="The people currently sponsoring Spoolman" src=".github/media/sponsors.svg">
</a>
-16
View File
@@ -1,16 +0,0 @@
/* eslint-env node */
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
},
};
+3
View File
@@ -7,6 +7,7 @@
# testing
/coverage
/dev-dist
# production
/build
@@ -17,6 +18,8 @@
.env.development.local
.env.test.local
.env.production.local
# Generated at build time (CI and tests_frontend/run.py) — never committed
.env.production
npm-debug.log*
yarn-debug.log*
+6
View File
@@ -0,0 +1,6 @@
public/locales
dist
dev-dist
node_modules
.vscode
.vite
+1
View File
@@ -1,5 +1,6 @@
{
"printWidth": 120,
"endOfLine": "auto",
"overrides": [
{
"files": ["public/locales/**/*.json"],
+23
View File
@@ -0,0 +1,23 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import reactPlugin from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
export default defineConfig([
globalIgnores(["node_modules/", "dist/", "dev-dist/"]),
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js, reactHooks, reactRefresh },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
settings: { react: { version: "detect" } },
},
tseslint.configs.recommended,
reactPlugin.configs.flat.recommended,
reactPlugin.configs.flat["jsx-runtime"],
eslintConfigPrettier,
]);
+1 -93
View File
@@ -1,93 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="235"
height="500"
viewBox="0 0 235 500"
xml:space="preserve"
id="svg5"
sodipodi:docname="spool.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview5"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6588725"
inkscape:cx="132.92161"
inkscape:cy="209.47963"
inkscape:window-width="1350"
inkscape:window-height="1237"
inkscape:window-x="1189"
inkscape:window-y="48"
inkscape:window-maximized="0"
inkscape:current-layer="svg5" />&#10;<desc
id="desc1">Created with Fabric.js 5.3.0</desc>&#10;<defs
id="defs1">&#10;</defs>&#10;&#10;&#10;&#10;&#10;<path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(52, 52, 52);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.58757483,0,0,3.9476902,197.13549,250.04675)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path1"
inkscape:label="rightside" /><path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(255, 0, 0);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.38157612,0,0,3.4623174,197.13549,250.04674)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path2"
inkscape:label="filament_right" /><path
style="stroke:rgb(0, 0, 0);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(255, 0, 0);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(2.073642,0,0,3.3577086,117.29471,250.04674)"
d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z"
stroke-linecap="round"
id="path3"
inkscape:label="filament_main" /><g
transform="matrix(0.58757483,0,0,3.9476902,37.453957,250.04674)"
id="OvgM38-6CZm9R47a1TDIX"
inkscape:label="fg">&#10;<filter
id="SVGID_1"
y="-0.056899467"
height="1.1137989"
x="-0.056899467"
width="1.271853">&#10; <feGaussianBlur
in="SourceAlpha"
stdDeviation="3"
id="feGaussianBlur3" />&#10; <feOffset
dx="20"
dy="0"
result="oBlur"
id="feOffset3" />&#10; <feFlood
flood-color="rgb(0,0,0)"
flood-opacity="0.67"
id="feFlood3" />&#10; <feComposite
in2="oBlur"
operator="in"
id="feComposite3" />&#10; <feMerge
id="feMerge4">&#10; <feMergeNode
id="feMergeNode3" />&#10; <feMergeNode
in="SourceGraphic"
id="feMergeNode4" />&#10; </feMerge>&#10;</filter>&#10;&#10;<path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(52, 52, 52);fill-rule:nonzero;opacity:1;filter:url(#SVGID_1)"
vector-effect="non-scaling-stroke"
transform="translate(8.4959392e-7)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path4"
inkscape:label="leftside" /><path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(17, 17, 17);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="scale(0.41451706)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path5"
inkscape:label="hole" /></g>&#10;</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg5" width="235" height="500" version="1.1" viewBox="0 0 235 500" xml:space="preserve"><desc id="desc1">Created with Fabric.js 5.3.0</desc><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1" id="path1" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.58757483,0,0,3.9476902,197.13549,250.04675)" vector-effect="non-scaling-stroke"/><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:red;fill-rule:nonzero;opacity:1" id="path2" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.38157612,0,0,3.4623174,197.13549,250.04674)" vector-effect="non-scaling-stroke"/><path style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:red;fill-rule:nonzero;opacity:1" id="path3" stroke-linecap="round" d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z" transform="matrix(2.073642,0,0,3.3577086,117.29471,250.04674)" vector-effect="non-scaling-stroke"/><g id="OvgM38-6CZm9R47a1TDIX" transform="matrix(0.58757483,0,0,3.9476902,37.453957,250.04674)"><filter id="SVGID_1" width="1.272" height="1.114" x="-.057" y="-.057"><feGaussianBlur id="feGaussianBlur3" in="SourceAlpha" stdDeviation="3"/><feOffset id="feOffset3" dx="20" dy="0" result="oBlur"/><feFlood id="feFlood3" flood-color="#000" flood-opacity=".67"/><feComposite id="feComposite3" in2="oBlur" operator="in"/><feMerge id="feMerge4"><feMergeNode id="feMergeNode3"/><feMergeNode id="feMergeNode4" in="SourceGraphic"/></feMerge></filter><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1;filter:url(#SVGID_1)" id="path4" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="translate(8.4959392e-7)" vector-effect="non-scaling-stroke"/><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" id="path5" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="scale(0.41451706)" vector-effect="non-scaling-stroke"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

+1 -89
View File
@@ -1,89 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="220"
height="220"
viewBox="0 0 220 220"
xml:space="preserve"
id="svg8"
sodipodi:docname="spoolman.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="5.3318182"
inkscape:cx="110"
inkscape:cy="106.53026"
inkscape:window-width="1898"
inkscape:window-height="1360"
inkscape:window-x="868"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg8" />&#10;<desc
id="desc1">Created with Fabric.js 5.3.0</desc>&#10;<defs
id="defs1">&#10;</defs>&#10;&#10;&#10;&#10;&#10;&#10;&#10;&#10;<path
style="stroke:rgb(0, 0, 0);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(220, 119, 52);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.941963,0,0,0.941963,110,110)"
d="M -116.77741 -67.23295 C -116.77741 -94.5956 -94.5956 -116.77741 -67.23295 -116.77741 L 67.23295 -116.77741 L 67.23295 -116.77741 C 94.5956 -116.77741 116.77741 -94.5956 116.77741 -67.23295 L 116.77741 67.23295 L 116.77741 67.23295 C 116.77741 94.5956 94.5956 116.77741 67.23295 116.77741 L -67.23295 116.77741 L -67.23295 116.77741 C -94.5956 116.77741 -116.77741 94.5956 -116.77741 67.23295 z"
stroke-linecap="round"
id="path1"
inkscape:label="iconbg" /><path
style="stroke:rgb(89, 142, 199);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(52, 52, 52);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(4.0068,0,0,4.0068,110,110)"
d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z"
stroke-linecap="round"
id="path2"
inkscape:label="spoolface" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(17, 17, 17);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(-0.65629473,0.65629473,-0.65629473,-0.65629473,169.99999,109.99999)"
d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z"
stroke-linecap="round"
id="path3"
inkscape:label="bean3_bg" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(190, 190, 190);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(-0.65629473,0.65629473,-0.65629473,-0.65629473,158.49213,110.08398)"
d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z"
stroke-linecap="round"
id="path4"
inkscape:label="bean3_fg" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(17, 17, 17);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(-0.24022054,-0.89651527,0.89651527,-0.24022054,81.095899,162.73972)"
d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z"
stroke-linecap="round"
id="path5"
inkscape:label="bean2_bg" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(190, 190, 190);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(-0.24022054,-0.89651527,0.89651527,-0.24022054,86.7771,152.73163)"
d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z"
stroke-linecap="round"
id="path6"
inkscape:label="bean2_fg" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(17, 17, 17);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.89651527,0.24022054,-0.24022054,0.89651527,80.684929,58.484107)"
d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z"
stroke-linecap="round"
id="path7"
inkscape:label="bean1_bg" /><path
style="stroke:rgb(255, 255, 255);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(190, 190, 190);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.89651527,0.24022054,-0.24022054,0.89651527,86.511593,68.408219)"
d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z"
stroke-linecap="round"
id="path8"
inkscape:label="bean1_fg" />&#10;</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg8" width="220" height="220" version="1.1" viewBox="0 0 220 220" xml:space="preserve"><desc id="desc1">Created with Fabric.js 5.3.0</desc><path style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#dc7734;fill-rule:nonzero;opacity:1" id="path1" stroke-linecap="round" d="M -116.77741 -67.23295 C -116.77741 -94.5956 -94.5956 -116.77741 -67.23295 -116.77741 L 67.23295 -116.77741 L 67.23295 -116.77741 C 94.5956 -116.77741 116.77741 -94.5956 116.77741 -67.23295 L 116.77741 67.23295 L 116.77741 67.23295 C 116.77741 94.5956 94.5956 116.77741 67.23295 116.77741 L -67.23295 116.77741 L -67.23295 116.77741 C -94.5956 116.77741 -116.77741 94.5956 -116.77741 67.23295 z" transform="matrix(0.941963,0,0,0.941963,110,110)" vector-effect="non-scaling-stroke"/><path style="stroke:#598ec7;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1" id="path2" stroke-linecap="round" d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z" transform="matrix(4.0068,0,0,4.0068,110,110)" vector-effect="non-scaling-stroke"/><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" id="path3" stroke-linecap="round" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" transform="matrix(-0.65629473,0.65629473,-0.65629473,-0.65629473,169.99999,109.99999)" vector-effect="non-scaling-stroke"/><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#bebebe;fill-rule:nonzero;opacity:1" id="path4" stroke-linecap="round" d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z" transform="matrix(-0.65629473,0.65629473,-0.65629473,-0.65629473,158.49213,110.08398)" vector-effect="non-scaling-stroke"/><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" id="path5" stroke-linecap="round" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" transform="matrix(-0.24022054,-0.89651527,0.89651527,-0.24022054,81.095899,162.73972)" vector-effect="non-scaling-stroke"/><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#bebebe;fill-rule:nonzero;opacity:1" id="path6" stroke-linecap="round" d="M -29.98183 19.80196 L -Line truncated

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

+1 -61
View File
@@ -1,61 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="220"
height="220"
viewBox="0 0 220 220"
xml:space="preserve"
id="svg8"
sodipodi:docname="spoolman_monochrome.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="2.6659091"
inkscape:cx="73.333333"
inkscape:cy="97.1526"
inkscape:window-width="1898"
inkscape:window-height="1360"
inkscape:window-x="684"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg8" />&#10;<desc
id="desc1">Created with Fabric.js 5.3.0</desc>&#10;<defs
id="defs1"><inkscape:path-effect
effect="powerclip"
id="path-effect13"
is_visible="true"
lpeversion="1"
inverse="true"
flatten="false"
hide_clip="false"
message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />&#10;<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath13"><path
id="path13"
style="display:none;opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
inkscape:label="bean1_fg"
d="m -13.757715,-7.7471481 0.343537,-0.55719 v 0 c 2.876446,-4.6653799 7.9428216,-7.5315019 13.42331996,-7.5937639 l 0.37756065,-0.0043 0.0795148,8.0507248 v 0 c 0.0172956,1.7514341 -0.91182739,3.3759065 -2.43022861,4.2489907 -1.5184012,0.8730843 -3.3897584,0.8588954 -4.8947484,-0.037114 z M 0.4322926,16.185974 -0.22201757,16.167056 v 0 C -5.7005765,16.00867 -10.715899,13.054123 -13.510068,8.3390037 l -0.192495,-0.3248328 6.9323756,-4.0942248 v 0 c 1.5081376,-0.8906958 3.3795333,-0.8982879 4.894848,-0.019866 1.51531394,0.8784316 2.43870418,2.5061695 2.41523158,4.2575347 z m 13.3375784,-24.319362 0.310774,0.5761074 v 0 c 2.602114,4.8237643 2.551061,10.6444349 -0.135267,15.4218159 L 13.76031,8.1936583 6.7484202,4.2371568 v 0 C 5.2229859,3.3764201 4.2807128,1.7595388 4.2838,0.00802386 4.2868948,-1.7434936 5.2348557,-3.3570423 6.7633174,-4.2123972 Z" /><path
id="lpe_path-effect13"
style="opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
class="powerclip"
d="m -27.46181,-27.46181 h 54.92362 v 54.92362 h -54.92362 z m 13.704095,19.7146619 6.8989554,4.1073476 c 1.50499,0.8960094 3.3763472,0.9101983 4.8947484,0.037114 1.51840122,-0.8730842 2.44752421,-2.4975566 2.43022861,-4.2489907 v 0 l -0.0795148,-8.0507248 -0.37756065,0.0043 c -5.48049836,0.06226 -10.54687396,2.928384 -13.42331996,7.5937639 v 0 z M 0.4322926,16.185974 0.53989218,8.1576148 C 0.56336478,6.4062496 -0.36002546,4.7785117 -1.8753394,3.9000801 -3.3906541,3.0216582 -5.2620498,3.0292503 -6.7701874,3.9199461 v 0 l -6.9323756,4.0942248 0.192495,0.3248328 c 2.794169,4.7151193 7.8094915,7.6696663 13.28805043,7.8280523 v 0 z M 13.769871,-8.133388 6.7633174,-4.2123972 C 5.2348557,-3.3570423 4.2868948,-1.7434936 4.2838,0.00802386 4.2807128,1.7595388 5.2229859,3.3764201 6.7484202,4.2371568 v 0 l 7.0118898,3.9565015 0.185068,-0.329123 c 2.686328,-4.777381 2.737381,-10.5980516 0.135267,-15.4218159 v 0 z" /></clipPath></defs>&#10;&#10;&#10;&#10;&#10;&#10;&#10;&#10;<path
style="stroke:rgb(89, 142, 199);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000000;fill-rule:nonzero;opacity:1;fill-opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(4.0068,0,0,4.0068,110,110)"
d="M 0,-22.46181 C 12.39892,-22.46181 22.46181,-12.39892 22.46181,0 22.46181,12.39892 12.39892,22.46181 0,22.46181 -12.39892,22.46181 -22.46181,12.39892 -22.46181,0 -22.46181,-12.39892 -12.39892,-22.46181 0,-22.46181 Z"
stroke-linecap="round"
id="path2"
inkscape:label="spoolface"
clip-path="url(#clipPath13)"
inkscape:path-effect="#path-effect13"
inkscape:original-d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z" />&#10;</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg8" width="220" height="220" version="1.1" viewBox="0 0 220 220" xml:space="preserve"><desc id="desc1">Created with Fabric.js 5.3.0</desc><defs id="defs1"><clipPath id="clipPath13" clipPathUnits="userSpaceOnUse"><path id="path13" style="display:none;opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#fff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" d="m -13.757715,-7.7471481 0.343537,-0.55719 v 0 c 2.876446,-4.6653799 7.9428216,-7.5315019 13.42331996,-7.5937639 l 0.37756065,-0.0043 0.0795148,8.0507248 v 0 c 0.0172956,1.7514341 -0.91182739,3.3759065 -2.43022861,4.2489907 -1.5184012,0.8730843 -3.3897584,0.8588954 -4.8947484,-0.037114 z M 0.4322926,16.185974 -0.22201757,16.167056 v 0 C -5.7005765,16.00867 -10.715899,13.054123 -13.510068,8.3390037 l -0.192495,-0.3248328 6.9323756,-4.0942248 v 0 c 1.5081376,-0.8906958 3.3795333,-0.8982879 4.894848,-0.019866 1.51531394,0.8784316 2.43870418,2.5061695 2.41523158,4.2575347 z m 13.3375784,-24.319362 0.310774,0.5761074 v 0 c 2.602114,4.8237643 2.551061,10.6444349 -0.135267,15.4218159 L 13.76031,8.1936583 6.7484202,4.2371568 v 0 C 5.2229859,3.3764201 4.2807128,1.7595388 4.2838,0.00802386 4.2868948,-1.7434936 5.2348557,-3.3570423 6.7633174,-4.2123972 Z"/><path id="lpe_path-effect13" style="opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#fff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" d="m -27.46181,-27.46181 h 54.92362 v 54.92362 h -54.92362 z m 13.704095,19.7146619 6.8989554,4.1073476 c 1.50499,0.8960094 3.3763472,0.9101983 4.8947484,0.037114 1.51840122,-0.8730842 2.44752421,-2.4975566 2.43022861,-4.2489907 v 0 l -0.0795148,-8.0507248 -0.37756065,0.0043 c -5.48049836,0.06226 -10.54687396,2.928384 -13.42331996,7.5937639 v 0 z M 0.4322926,16.185974 0.53989218,8.1576148 C 0.56336478,6.4062496 -0.36002546,4.7785117 -1.8753394,3.9000801 -3.3906541,3.0216582 -5.2620498,3.0292503 -6.7701874,3.9199461 v 0 l -6.9323756,4.0942248 0.192495,0.3248328 c 2.794169,4.7151193 7.8094915,7.6696663 13.28805043,7.8280523 v 0 z M 13.769871,-8.133388 6.7633174,-4.2123972 C 5.2348557,-3.3570423 4.2868948,-1.7434936 4.2838,0.00802386 4.2807128,1.7595388 5.2229859,3.3764201 6.7484202,4.2371568 v 0 l 7.0118898,3.9565015 0.185068,-0.329123 c 2.686328,-4.777381 2.737381,-10.5980516 0.135267,-15.4218159 v 0 z" class="powerclip"/></clipPath></defs><path style="stroke:#598ec7;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:1;fill-opacity:1" id="path2" stroke-linecap="round" d="M 0,-22.46181 C 12.39892,-22.46181 22.46181,-12.39892 22.46181,0 22.46181,12.39892 12.39892,22.46181 0,22.46181 -12.39892,22.46181 -22.46181,12.39892 -22.46181,0 -22.46181,-12.39892 -12.39892,-22.46181 0,-22.46181 Z" clip-path="url(#clipPath13)" transform="matrix(4.0068,0,0,4.0068,110,110)" vector-effect="non-scaling-stroke"/></svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+2 -2
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#DC7734" />
<meta name="description" content="Spoolman - Keep track of your inventory of 3D-printer filament spools." />
<meta name="robots" content="noindex,nofollow" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<script type="text/javascript" src="./config.js"></script>
<title>Spoolman</title>
</head>
+8771 -4326
View File
File diff suppressed because it is too large. Load diff
+59 -40
View File
@@ -1,57 +1,76 @@
{
"name": "spoolman-ui",
"version": "0.15.0",
"version": "0.27.0",
"engines": {
"node": "20.x"
"node": "^20.19 || ^22.12 || >=24"
},
"private": true,
"type": "module",
"dependencies": {
"axios": "^1.6.2",
"@ant-design/icons": "^5.2.6",
"@loadable/component": "^5.16.2",
"@refinedev/antd": "^5.37.0",
"@refinedev/core": "^4.46.0",
"@refinedev/kbar": "^1.3.4",
"@refinedev/react-router-v6": "^4.5.4",
"@refinedev/simple-rest": "^5.0.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"@types/loadable__component": "^5.13.8",
"@yudiel/react-qr-scanner": "^1.2.4",
"antd": "^5.12.2",
"flag-icons": "^7.1.0",
"i18next": "^23.7.11",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-xhr-backend": "^3.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.21.0",
"react-to-print": "^2.14.15",
"vite-plugin-svgr": "^4.2.0"
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@loadable/component": "^5.16.7",
"@refinedev/antd": "^6.0.3",
"@refinedev/core": "^5.0.7",
"@refinedev/kbar": "^2.0.1",
"@refinedev/react-router": "^2.0.3",
"@refinedev/simple-rest": "^6.0.1",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-query-devtools": "^5.91.2",
"@yudiel/react-qr-scanner": "^2.5.0",
"axios": "^1.13.2",
"dayjs": "^1.11.10",
"html-to-image": "^1.11.13",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^4.0.2",
"react": "^19.2.3",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.1",
"react-router": "^7.18.3",
"react-to-print": "^3.2.0",
"uuid": "^14.0.0",
"vite-plugin-svgr": "^4.5.0",
"zustand": "^5.0.9"
},
"overrides": {
"@ant-design/pro-layout": {
"path-to-regexp": "^8.4.0"
}
},
"devDependencies": {
"@refinedev/cli": "^2.16.20",
"@types/node": "^20.10.4",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-mkcert": "^1.17.1"
"@eslint/js": "^9.39.2",
"@refinedev/cli": "^2.16.50",
"@types/loadable__component": "^5.13.10",
"@types/node": "^25.0.3",
"@types/react-dom": "^19.2.3",
"@types/react": "^19.2.7",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.1.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react": "^7.37.5",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"prettier": "3.7.4",
"typescript-eslint": "^8.52.0",
"typescript": "^5.9.3",
"vite": "^7.3.0",
"vite-plugin-mkcert": "^1.17.9",
"vite-plugin-pwa": "^1.2.0"
},
"scripts": {
"dev": "refine dev",
"build": "tsc && refine build",
"build.zip": "cd ./dist && zip -r spoolman-client.zip ./ ./ && cd ..",
"preview": "refine start",
"refine": "refine"
"refine": "refine",
"check-i18n": "node scripts/check-i18n.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"format": "prettier --write .",
"format-check": "prettier --check ."
},
"browserslist": {
"production": [
@@ -65,4 +84,4 @@
"last 1 safari version"
]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 768 B

+1 -32
View File
@@ -1,32 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
width="100%" height="100%" viewBox="0 0 220 220" xml:space="preserve">
<desc>Created with Fabric.js 5.3.0</desc>
<defs>
</defs>
<g transform="matrix(0.9419630011 0 0 0.9419630011 110 110)" id="97_aBJ1JQMj7gfw9xgqJk" >
<path style="stroke: rgb(0,0,0); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(220,119,52); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M -116.77741 -67.23295 C -116.77741 -94.5956 -94.5956 -116.77741 -67.23295 -116.77741 L 67.23295 -116.77741 L 67.23295 -116.77741 C 94.5956 -116.77741 116.77741 -94.5956 116.77741 -67.23295 L 116.77741 67.23295 L 116.77741 67.23295 C 116.77741 94.5956 94.5956 116.77741 67.23295 116.77741 L -67.23295 116.77741 L -67.23295 116.77741 C -94.5956 116.77741 -116.77741 94.5956 -116.77741 67.23295 z" stroke-linecap="round" />
</g>
<g transform="matrix(4.0068 0 0 4.0068 110 110)" id="bajrpvpoPMPegFQ9fLbAW" >
<path style="stroke: rgb(89,142,199); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(52,52,52); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z" stroke-linecap="round" />
</g>
<g transform="matrix(-0.6562947252 0.6562947252 -0.6562947252 -0.6562947252 170 110)" id="uzP7ffSaxXnc8ieJ8GKAi" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(17,17,17); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-7.913e-7, 0.0000091739)" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" stroke-linecap="round" />
</g>
<g transform="matrix(-0.6562947251 0.6562947251 -0.6562947251 -0.6562947251 158.4921266728 110.0839790702)" id="sLvjm8ckG9Tn4B020NjmX" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(190,190,190); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-0.0000037498, -3.945e-7)" d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z" stroke-linecap="round" />
</g>
<g transform="matrix(-0.2402205418 -0.8965152669 0.8965152669 -0.2402205418 81.095890411 162.7397260274)" id="itojFe0oOsJTunxHV6_ba" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(17,17,17); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-7.913e-7, 0.0000091739)" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" stroke-linecap="round" />
</g>
<g transform="matrix(-0.2402205418 -0.8965152669 0.8965152669 -0.2402205418 86.7770990664 152.7316258474)" id="-SRJJg6dkRVX31Lo1Y4pF" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(190,190,190); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-0.0000037498, -3.945e-7)" d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.8965152669 0.2402205418 -0.2402205418 0.8965152669 80.6849315069 58.4840992287)" id="Okowo2lxooMVge1Zf9juM" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(17,17,17); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-7.913e-7, 0.0000091739)" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.8965152669 0.2402205418 -0.2402205418 0.8965152669 86.5115961787 68.4082203385)" id="9DKPKJNmrFA1_5WNxgrse" >
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(190,190,190); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-0.0000037498, -3.945e-7)" d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z" stroke-linecap="round" />
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" version="1.1" viewBox="0 0 220 220" xml:space="preserve"><desc>Created with Fabric.js 5.3.0</desc><g id="97_aBJ1JQMj7gfw9xgqJk"><path style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#dc7734;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M -116.77741 -67.23295 C -116.77741 -94.5956 -94.5956 -116.77741 -67.23295 -116.77741 L 67.23295 -116.77741 L 67.23295 -116.77741 C 94.5956 -116.77741 116.77741 -94.5956 116.77741 -67.23295 L 116.77741 67.23295 L 116.77741 67.23295 C 116.77741 94.5956 94.5956 116.77741 67.23295 116.77741 L -67.23295 116.77741 L -67.23295 116.77741 C -94.5956 116.77741 -116.77741 94.5956 -116.77741 67.23295 z" transform="matrix(0.9419630011 0 0 0.9419630011 110 110) translate(0, 0)" vector-effect="non-scaling-stroke"/></g><g id="bajrpvpoPMPegFQ9fLbAW"><path style="stroke:#598ec7;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z" transform="matrix(4.0068 0 0 4.0068 110 110) translate(0, 0)" vector-effect="non-scaling-stroke"/></g><g id="uzP7ffSaxXnc8ieJ8GKAi"><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" transform="matrix(-0.6562947252 0.6562947252 -0.6562947252 -0.6562947252 170 110) translate(-7.913e-7, 0.0000091739)" vector-effect="non-scaling-stroke"/></g><g id="sLvjm8ckG9Tn4B020NjmX"><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#bebebe;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M -29.98183 19.80196 L -29.17187 17.09468 L -29.17187 17.09468 C -22.390079999999998 -5.573499999999999 -4.466099999999997 -23.18579 18.31756 -29.568920000000002 L 19.88717 -30.008660000000003 L 29.21402 3.473359999999996 L 29.21402 3.473359999999996 C 31.243070000000003 10.757369999999996 29.183770000000003 18.569419999999994 23.82768 23.906659999999995 C 18.47159 29.243899999999996 10.652330000000001 31.275649999999995 3.375510000000002 29.220929999999996 z" transform="matrix(-0.6562947251 0.6562947251 -0.6562947251 -0.6562947251 158.4921266728 110.0839790702) translate(-0.0000037498, -3.945e-7)" vector-effect="non-scaling-stroke"/></g><g id="itojFe0oOsJTunxHV6_ba"><path style="stroke:#fff;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M -33.16071 25.30347 C -35.26675 24.713710000000003 -37.021860000000004 23.25476 -37.98653 21.29195 C -38.95121 19.32914 -39.0341 17.04834 -38.21447 15.020669999999999 L -38.21447 15.020669999999999 L -38.21447 15.020669999999999 C -28.42178 -9.205020000000001 -9.211359999999999 -28.41564 15.014230000000005 -38.20859 L 15.047580000000005 -38.22207 L 15.047580000000005 -38.22207 C 17.067020000000007 -39.03841 19.338600000000007 -38.95586 21.293460000000003 -37.995110000000004 C 23.248330000000003 -37.03435 24.701380000000004 -35.286350000000006 25.288750000000004 -33.188840000000006 L 37.998650000000005 12.198379999999993 L 37.998650000000005 12.198379999999993 C 40.03828000000001 19.481909999999992 37.990640000000006 27.299259999999993 32.64228000000001 32.64761999999999 C 27.293920000000007 37.99597999999999 19.476570000000006 40.04361999999999 12.193040000000007 38.00398999999999 z" transform="matrix(-0.2402205418 -0.8965152669 0.8965152669 -0.2402205418 81.095890411 162.7397260274) translate(-7.913e-7, 0.0000091739)" vector-effeLine truncated

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

+167 -35
View File
@@ -12,13 +12,13 @@
"logout": "Odhlášení",
"delete": "Smazat",
"edit": "Upravit",
"cancel": "Storno",
"confirm": "Jste si jistý?",
"cancel": "Zrušit",
"confirm": "Jste si jisti?",
"filter": "Filtr",
"clear": "Přehled",
"refresh": "Obnovit",
"clear": "Vymazat",
"refresh": "Znovu načíst",
"show": "Zobrazit",
"undo": "Zpět",
"undo": "Vrátit zpět",
"import": "Import",
"clone": "Duplikovat",
"archive": "Archivovat",
@@ -27,13 +27,14 @@
"showArchived": "Zobrazit archivované",
"notAccessTitle": "Nemáte oprávnění k přístupu",
"hideColumns": "Skrýt sloupce",
"clearFilters": "Zrušit filtry",
"saveAndAdd": "Uložit a přidat"
"clearFilters": "Vypnout filtry",
"saveAndAdd": "Uložit a přidat",
"continue": "Pokračovat"
},
"warnWhenUnsavedChanges": "Jste si jisti, že chcete odejít? Máte neuložené změny.",
"notifications": {
"success": "Úspěšné",
"error": "Chyba (status code: {{statusCode}})",
"error": "Chyba (kód chyby: {{statusCode}})",
"undoable": "Máte {{seconds}} sekund na zrušení.",
"createSuccess": "Úspěšně vytvořeno {{resource}}",
"createError": "Při vytváření došlo k chybě {{resource}} (status code: {{statusCode}})",
@@ -41,7 +42,9 @@
"deleteError": "Chyba při odstranění {{resource}} (status code: {{statusCode}})",
"editSuccess": "Úspěšně upraveno {{resource}}",
"editError": "Chyba při úpravě {{resource}} (status code: {{statusCode}})",
"importProgress": "Importování: {{processed}}/{{total}}"
"importProgress": "Importování: {{processed}}/{{total}}",
"saveSuccessful": "Úspěšně uloženo!",
"validationError": "Chyba ověření: {{error}}"
},
"loading": "Načítání",
"version": "Verze",
@@ -56,9 +59,9 @@
},
"printing": {
"generic": {
"title": "Tisk",
"description": "Níže uvedeným nastavením dosáhnete požadovaného rozložení tisku. Mějte na paměti, že tiskárny a váš operační systém mohou použít vlastní okraje a měřítka, takže možná budete muset provést několik pokusů a omylů, než bude vše správně. Před tiskem na skutečné štítky si to vyzkoušejte na kousku běžného papíru.",
"helpMargin": "Okraje by měly být nastaveny tak, aby odpovídaly papíru na štítky a tiskárně, jejich změna ovlivní velikost celé mřížky.",
"title": "Probíhá tisk",
"description": "Níže uvedeným nastavením dosáhnete požadovaného rozložení tisku. Mějte na paměti, že tiskárny a váš operační systém mohou použít vlastní okraje a měřítka, takže možná budete muset provést několik pokusů a omylů, než bude vše správně nastaveno. Před tiskem na skutečné etikety si to vyzkoušejte na listu běžného papíru.",
"helpMargin": "Okraje by měly být nastaveny tak, aby odpovídaly listu etiket a tiskárně, jejich změna ovlivní velikost celé mřížky.",
"helpPrinterMargin": "Bezpečná zóna by měla být nastavena tak, jak blízko okraje papíru může tiskárna tisknout, jejich změna neovlivní celou mřížku.",
"print": "Tisk",
"columns": "Sloupce",
@@ -85,35 +88,63 @@
"none": "Žádný",
"border": "Ohraničení",
"grid": "Mřížka"
}
},
"itemCopies": "Kopie položky",
"defaultSettings": "Výchozí",
"addSettings": "Přidat novou předvolbu",
"newSetting": "Nový",
"deleteSettings": "Smazat aktuální předvolbu",
"settingsName": "Název předvolby",
"settings": "Předvolby",
"deleteSettingsConfirm": "Opravdu chcete tuto předvolbu smazat?",
"duplicateSettings": "Duplikát",
"saveSetting": "Uložit předvolby",
"saveAsImage": "Uložit jako obrázek"
},
"qrcode": {
"button": "Tisk QR kódů",
"title": "Tisk QR kódů",
"button": "Tisk etiket",
"title": "Tisk etikety",
"spoolWeight": "Hmotnost cívky: {{weight}}",
"lotNr": "Číslo šarže: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Velikost textu obsahu",
"textSize": "Velikost textu etikety",
"showSpoolmanIcon": "Zobrazit ikonu Spoolman",
"showVendor": "Výrobce",
"showContent": "Tisk štítku",
"showContent": "Tisk etikety",
"showLotNr": "Číslo šarže",
"showSpoolWeight": "Hmotnost cívky",
"showTemperatures": "Teploty",
"showSpoolComment": "Poznámka k cívce",
"showFilamentComment": "Poznámka k filamentu",
"showVendorComment": "Poznámka k výrobci"
"showVendorComment": "Poznámka k výrobci",
"useHTTPUrl": {
"label": "Odkaz na QR kód",
"tooltip": "Použije proprietární odkaz, který bude fungovat pouze v případě, že bude skenován pomocí funkce skenování Spoolman (výchozí). Adresa URL používá buď základní adresu URL zadanou v nastavení, nebo adresu URL aktuální stránky, pokud není nastavena.",
"options": {
"default": "Výchozí",
"url": "URL"
},
"preview": "Náhled:"
},
"template": "Šablona etikety",
"templateHelp": "Pomocí {} vložíte hodnoty objektu cívky jako text. Například, {id} bude nahrazeno ID cívky nebo {filament.material} bude nahrazeno materiálem cívky. Pokud hodnota chybí, bude nahrazena znakem „?“. K odstranění této hodnoty lze použít druhou sadu {}. Kromě toho bude odstraněn jakýkoli text mezi sadami {}, pokud hodnota chybí. Například {Lot Nr: {lot_nr}} zobrazí popisek pouze v případě, že cívka má číslo šarže. Text uzavřete dvojitou hvězdičkou **, aby byl tučný. Kliknutím na tlačítko zobrazíte seznam všech dostupných značek.",
"showQRCode": "Tisk QR kódu",
"showQRCodeMode": {
"no": "Ne",
"simple": "Jednoduchý",
"withIcon": "S ikonou"
}
},
"spoolSelect": {
"title": "Vybrat cívky",
"description": "Vyberte cívky pro tisk QR kódů.",
"description": "Vyberte cívky, pro které chcete tisknout etikety.",
"showArchived": "Zobrazit archivované",
"noSpoolsSelected": "Nevybrali jste žádné cívky.",
"selectAll": "Vybrat/odebrat vše",
"selectedTotal_one": "{{count}} vybraná cívka",
"selectedTotal_few": "{{count}} vybraných cívek",
"selectedTotal_other": "{{count}} vybrané cívky"
"selectedTotal_few": "{{count}} vybrané cívky",
"selectedTotal_other": "{{count}} vybraných cívek"
}
},
"scanner": {
@@ -142,13 +173,17 @@
"used_length": "Použitá délka",
"remaining_length": "Zbývající délka",
"location": "Umístění",
"lot_nr": "Číslo umístění",
"lot_nr": "Číslo šarže",
"first_used": "První použití",
"last_used": "Naposledy použitá",
"registered": "Registrováno",
"comment": "Komentář",
"archived": "Archivováno",
"price": ""
"price": "Cena",
"spool_weight": "Prázdná hmotnost",
"initial_weight": "Výchozí hmotnost",
"filament_internal": "Interní",
"filament_external": "Externí"
},
"fields_help": {
"weight_to_use": "Vyberte, jakou hodnotu hmotnosti chcete zadat. Naměřená hmotnost je k dispozici pouze v případě, že je pro vybraný filament nastavena hmotnost cívky.",
@@ -157,7 +192,10 @@
"measured_weight": "Jaká je hmotnost filamentu a cívky.",
"location": "Kde se cívka nachází, pokud máte více míst, kde cívky skladujete.",
"lot_nr": "Číslo šarže výrobce. Lze použít k zajištění jednotné barvy tisku v případě použití více cívek.",
"price": ""
"price": "Cena celé cívky. Pokud není nastavena, předpokládá se cena filamentu.",
"spool_weight": "Hmotnost prázdné cívky. Nechte prázdné, chcete-li použít hodnotu z filamentu nebo od výrobce.",
"initial_weight": "Počáteční hmotnost vlákna na cívce (čistá hmotnost). Pokud není nastavena, použije se hmotnost z objektu filament.",
"external_filament": "Vybrali jste filament z externí databáze. Objekt filamentu (a případně i objekt výrobce) se vytvoří automaticky při vytvoření této cívky. Tím se mohou vytvořit duplicitní objekty filamentů, pokud jste již objekt filamentu pro toto vlákno vytvořili."
},
"titles": {
"create": "Vytvořit cívku",
@@ -166,14 +204,25 @@
"list": "Cívky",
"show": "Zobrazit cívku",
"archive": "Archivní cívka",
"show_title": "[Cívka #{{id}}] {{name}}"
"show_title": "[Cívka #{{id}}] {{name}}",
"adjust": "Nastavení cívky filamentu"
},
"messages": {
"archive": "Opravdu chcete tuto cívku archivovat?"
},
"form": {
"new_location_prompt": "Zadejte nové umístění",
"spool_updated": "Od otevření této stránky byla tato cívka někým/něčím aktualizována. Uložením se tyto změny přepíší!"
"spool_updated": "Od otevření této stránky byla tato cívka někým/něčím aktualizována. Uložením se tyto změny přepíší!",
"measurement_type": {
"length": "Délka",
"weight": "Hmotnost"
},
"measurement_type_label": "Typ měření",
"adjust_filament_help": "Zde můžete přímo přidávat nebo odebírat filament z cívky. Kladná hodnota filament odebere, záporná hodnota ho přidá.",
"adjust_filament_value": "Spotřebované množství"
},
"formats": {
"last_used": "Naposledy použitý {{date}}"
}
},
"filament": {
@@ -195,15 +244,21 @@
"settings_extruder_temp": "Teplota extruderu",
"settings_bed_temp": "Teplota podložky",
"color_hex": "Barva",
"spools": "Zobrazit cívky"
"spools": "Zobrazit cívky",
"external_id": "Externí ID",
"single_color": "Jediný",
"coaxial": "Koextrudovaný",
"longitudinal": "Podélná",
"multi_color": "Více"
},
"fields_help": {
"name": "Název filamentu, aby se tento typ filamentu odlišil od ostatních filamentů od stejného dodavatele. Měl by obsahovat například barvu.",
"name": "Název filamentu, aby se tento typ vlákna odlišil od ostatních filamentů od stejného výrobce. Měl by obsahovat například barvu.",
"material": "Např. PLA, ABS, ASA, PETG, atd.",
"price": "Cena celé cívky v měně nastavené v systému.",
"price": "Cena celé cívky.",
"weight": "Hmotnost plného cívky filamentu (čistá hmotnost). Tato hmotnost by neměla zahrnovat hmotnost samotné cívky, ale pouze filamentu. Obvykle se uvádí na obalu.",
"spool_weight": "Hmotnost prázdné cívky. Slouží k určení naměřené hmotnosti cívky.",
"article_number": "Např. EAN, UPC, atd."
"article_number": "Např. EAN, UPC, atd.",
"multi_color_direction": "Filamenty mohou mít více barev dvěma způsoby: buď koextruzí jako dvoubarevná vlákna s konzistentními více barvami, nebo podélnými změnami barev jako gradientní vlákna, která mění barvy podél cívky."
},
"titles": {
"create": "Vytvořit filament",
@@ -214,7 +269,12 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Toto vlákno bylo od doby, kdy jste otevřeli tuto stránku, někým/něčím aktualizováno. Uložením se tyto změny přepíší!"
"filament_updated": "Toto vlákno bylo od doby, kdy jste otevřeli tuto stránku, někým/něčím aktualizováno. Uložením se tyto změny přepíší!",
"import_external": "Import z externího zdroje",
"import_external_description": "Výběrem filamentu v seznamu se automaticky vyplní jeho údaje ve formuláři pro vytvoření filamentu. Tím se přepíší všechny údaje, které jste do formuláře zadali.<br><br>Po kliknutí na tlačítko OK se v případě potřeby automaticky vytvoří objekt Výrobce."
},
"buttons": {
"add_spool": "Přidat cívku"
}
},
"vendor": {
@@ -223,7 +283,9 @@
"id": "Id",
"name": "Název",
"registered": "Registrováno",
"comment": "Komentář"
"comment": "Komentář",
"empty_spool_weight": "Hmotnost prázdné cívky",
"external_id": "Externí ID"
},
"titles": {
"create": "Vytvořit výrobce",
@@ -235,6 +297,9 @@
},
"form": {
"vendor_updated": "Od otevření této stránky byl tento prodejce někým/něčím aktualizován. Uložením se tyto změny přepíší!"
},
"fields_help": {
"empty_spool_weight": "Hmotnost prázdné cívky od tohoto výrobce."
}
},
"table": {
@@ -269,6 +334,9 @@
},
"help": {
"list": "Nápověda | Spoolman"
},
"locations": {
"list": "Umístění | Spoolman"
}
},
"help": {
@@ -277,11 +345,75 @@
"filament": "Značky vláken. Mají vlastnosti, jako je název, materiál, barva, průměr a další.",
"spool": "Jednotlivé fyzické cívky určitého vlákna."
},
"description": "<title>Nápověda</title><p>Zde je několik tipů, které vám pomohou začít.</p><p>Spoolman uchovává 3 různé typy dat:</p><itemsHelp/><p>Chcete-li do databáze přidat novou cívku, začněte vytvořením položky <filamentCreateLink>Filament</filamentCreateLink> objekt. Jakmile to uděláte, vytvořte položku<spoolCreateLink>Cívka</spoolCreateLink> objekt pro danou cívku. Pokud později získáte další cívky stejného filamenzu, jednoduše vygenerujte další objekty Cívky a znovu použijte stejný objekt Filament.</p><p>Volitelně můžete také vygenerovat <vendorCreateLink>Výrobce</vendorCreateLink> objekt společnosti vyrábějící vlákno, pokud si přejete tyto informace sledovat.</p><p>Máte možnost propojit se Spoolmanem další služby 3D tiskárny, například Moonraker, který může automaticky sledovat využití filamentu a aktualizovat objekty Cívek za vás. Pokyny k nastavení najdete v <readmeLink>Spoolman README</readmeLink>.</p>",
"description": "<title>Nápověda</title><p>Zde je několik tipů, které vám pomohou začít.</p><p>Spoolman ukládá 3 různé typy dat:</p><itemsHelp/><p>Chcete-li do databáze přidat novou cívku, začněte vytvořením položky <filamentCreateLink>Filament</filamentCreateLink> . Jakmile to uděláte, vytvořte položku <spoolCreateLink>Cívka</spoolCreateLink> s popisem pro danou cívku. Pokud později získáte další cívky stejného filamentu, jednoduše vygenerujte další Cívku a použijte opět stejný Filament.</p><p>Volitelně můžete také vygenerovat <vendorCreateLink>Výrobce</vendorCreateLink> název společnosti vyrábějící filamenty, pokud si přejete tyto informace sledovat.</p><p>Máte možnost propojit se Spoolmanem další služby 3D tiskárny, například Moonraker, který může automaticky sledovat využití filamentu a aktualizovat dané Cívky za vás. Pokyny k nastavení najdete v <readmeLink>Spoolman README</readmeLink>.</p>",
"help": "Nápověda"
},
"kofi": "Příspěvek na Ko-fi",
"kofi": "Příspěvek autorovi na Ko-fi",
"home": {
"home": "Domů"
"home": "Domů",
"description": "Vypadá to, že jste ještě nepřidali žádné cívky. Úvodní nápovědu najdete na stránce <helpPageLink>Nápověda</helpPageLink> .",
"welcome": "Vítejte ve vaší instanci Spoolman!"
},
"settings": {
"extra_fields": {
"tab": "Další pole",
"params": {
"key": "Klíč",
"name": "Název",
"field_type": "Typ",
"order": "Objednávka",
"default_value": "Výchozí hodnota",
"multi_choice": "Výběr z více možností",
"unit": "Jednotka",
"choices": "Volby"
},
"field_type": {
"text": "Text",
"integer": "Celé číslo",
"integer_range": "Rozsah celých čísel",
"float": "Plovoucí",
"datetime": "Datumčas",
"choice": "Volba",
"float_range": "Plovoucí rozsah",
"boolean": "Logická hodnota"
},
"boolean_true": "Ano",
"boolean_false": "Ne",
"choices_missing_error": "Volby nelze odstranit. Chybí následující volby: {{choices}}",
"non_unique_key_error": "Tento klíč musí být jedinečný.",
"key_not_changed": "Změňte prosím klíč na jiný.",
"delete_confirm": "Smazat pole {{name}}?",
"description": "<p>Zde můžete přidat další vlastní pole k subjektům.</p><p>Jakmile je pole přidáno, nelze změnit jeho klíč ani typ; u polí typu „výběr“ nelze odstranit možnosti ani změnit nastavení pro více výběrů. Pokud pole odstraníte, budou smazána související data u všech subjektů.</p><p>Klíč určuje, pod jakým názvem budou data čtena a zapisována jinými programy, takže pokud má být vaše vlastní pole integrováno s programem třetí strany ujistěte se, že je správně nastaveno. Výchozí hodnota se použije pouze u nových položek.</p><p>Vlastní pole lze zobrazit a seřadit v tabulkových zobrazeních. Pole s výběrem a logická pole zde také nabízejí možnosti filtrování a všechny typy vlastních polí podporují filtrování podle prázdných hodnot.</p>",
"delete_confirm_description": "Tím se odstraní pole a všechna související data pro všechny subjekty.",
"choices_reorder_hint": "Přetažením nebo pomocí šipek můžete změnit pořadí této volby."
},
"header": "Nastavení",
"general": {
"tab": "Obecné",
"currency": {
"label": "Měna"
},
"base_url": {
"label": "Základní URL",
"tooltip": "Základní adresa URL, která se má použít při generování funkcí, jako jsou QR kódy."
},
"round_prices": {
"label": "Zaokrouhlené ceny",
"tooltip": "Zaokrouhlete ceny na nejbližší celé číslo."
}
},
"settings": "Nastavení"
},
"locations": {
"locations": "Umístění",
"no_locations_help": "Na této stránce můžete uspořádat své cívky podle umístění, přidejte několik cívek a začněte!",
"new_location": "Nové umístění",
"no_location": "Žádné umístění"
},
"theme": {
"label": "Téma",
"system": "Systém",
"light": "Světlé",
"dark": "Tmavé"
}
}
+196 -71
View File
@@ -3,20 +3,37 @@
"qrcode": {
"extruderTemp": "ET: {{temp}}",
"showSpoolWeight": "Spole vægt",
"showContent": "Print label",
"showContent": "Print etiket",
"showVendorComment": "Sælger kommentarer",
"spoolWeight": "Spole vægt: {{weight}}",
"showTemperatures": "Temperaturer",
"showVendor": "Sælger",
"showSpoolComment": "Spole kommentarer",
"textSize": "Indholdstekst størrelse",
"title": "QR kode printer",
"textSize": "Etikette tekststørrelse",
"title": "Etiketprint",
"showSpoolmanIcon": "Vis Spoolman logo",
"showLotNr": "Gruppe nr",
"bedTemp": "BT: {{temp}}",
"lotNr": "Gruppe nr: {{lot}}",
"showFilamentComment": "Filament kommentarer",
"button": "Print QR koder"
"button": "Print etiketter",
"template": "Etiketskabelon",
"templateHelp": "Brug {} til at indsætte værdier for spolen som tekst. For eksempel vil {id} blive erstattet med spolens id, eller {filament.material} vil blive erstattet med materialet på spolen. Hvis en værdi mangler, vil den blive erstattet med \"?\". Et andet set af {} kan bruges for at fjerne dette. Derudover vil al tekst mellem sættene af {} blive fjernet, hvis værdien mangler. For eksemel, {Lot Nr: {lot_nr}} vil kun vise etiketten, hvis spolen har et partinummer (Lot Nr). Omgiv teksten med dobbelte asterisk ** for at gøre teksten fed. Klik på knappen for at vise en liste med mulige tags.",
"useHTTPUrl": {
"options": {
"default": "Standard",
"url": "Webadresse"
},
"preview": "Forhåndsvisning:",
"tooltip": "Vil bruge proprietært link, der kun vil virke, hvis skannet med Spoolmans skanningsfunktion (standard). Webadressen bruger enten den basis-webadresse, specificeret i indstillinger, eller den nuværende sides webadresse, hvis ikke indstillet.",
"label": "QR-kode link"
},
"showQRCodeMode": {
"withIcon": "Med ikon",
"no": "Nej",
"simple": "Simpel"
},
"showQRCode": "Print QR-kode"
},
"generic": {
"printerMarginBottom": "Safe-Zone bund",
@@ -34,39 +51,50 @@
"marginRight": "Højre margen",
"printerMarginRight": "Safe-Zone højre",
"helpPrinterMargin": "\"Safe-Zone\" bør indstilles til hvor tæt på kanten af papiret din printer kan printe. Ændringer her vil IKKE have indflydelse på hele layoutet.",
"previewScale": "Visnings størrelse",
"helpMargin": "Margener bør indstilles til at matche din printer og dit papir. Ændringer her vil have indflydelse på hele layoutet.",
"previewScale": "Visningsstørrelse",
"helpMargin": "Margener bør indstilles til at matche din printer og papir. Ændringer her vil have indflydelse på hele layoutet.",
"marginTop": "Top margen",
"skipItems": "Spring over",
"layoutSettings": "Layout indstillinger",
"paperSize": "Papir størrelse",
"layoutSettings": "Layoutindstillinger",
"paperSize": "Papirstørrelse",
"columns": "Kolonner",
"description": "Redigér nedenstående indstillinger for at opnå det ønskede layout. Husk på at printere og OS kan tilføje dets egne margener og skalering, så det kan kræve et par forsøg før alt er korrekt. Test på et stykke almindeligt papir før du udskriver på labels.",
"description": "Redigér nedenstående indstillinger for at opnå det ønskede layout. Husk på at printere og dit operativsystem kan tilføje deres egne margener og skalering, så det kan kræve et par forsøg, før alt er korrekt. Test på et stykke almindeligt papir, før du udskriver på etiketter.",
"marginBottom": "Bund margen",
"horizontalSpacing": "Horisontal afstand",
"marginLeft": "Venstre margen",
"showBorder": "Vis ramme",
"printerMarginLeft": "Safe-Zone venstre",
"dimensions": "Dimensioner",
"print": "Udskriv"
"print": "Udskriv",
"settings": "Forudindstillinger",
"defaultSettings": "Standard",
"addSettings": "Tilføj ny forudindstilling",
"newSetting": "Ny",
"deleteSettings": "Slet nuværende forudindstilling",
"deleteSettingsConfirm": "Er du sikker på, at du vil slette denne forudindstilling?",
"settingsName": "Forudindstillingsnavn",
"itemCopies": "Kopier",
"duplicateSettings": "Duplikér",
"saveSetting": "Gem forudindstillinger",
"saveAsImage": "Gem som Billede"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} spole valgt",
"selectedTotal_other": "{{count}} spoler valgt",
"description": "Vælg spoler der skal printes QR koder til.",
"description": "Vælg spoler, der skal printes etiketter til.",
"showArchived": "Vis arkiverede",
"title": "Vælg spoler",
"selectAll": "Vælg alle/ingen",
"noSpoolsSelected": "Du har ikke valgt nogle spoler."
"noSpoolsSelected": "Du har ikke valgt nogen spoler."
}
},
"documentTitle": {
"vendor": {
"create": "Opret sælger | Spoolman",
"clone": "#{{id}} Klon sælger | Spoolman",
"edit": "#{{id}} Redigér sælger | Spoolman",
"list": "Sælgere | Spoolman",
"show": "#{{id}} Vis sælger | Spoolman"
"create": "Opret producent | Spoolman",
"clone": "#{{id}} Klon producent | Spoolman",
"edit": "#{{id}} Redigér producent | Spoolman",
"list": "Producenter | Spoolman",
"show": "#{{id}} Vis producent | Spoolman"
},
"default": "Spoolman",
"spool": {
@@ -89,15 +117,18 @@
},
"help": {
"list": "Hjælp | Spoolman"
},
"locations": {
"list": "Lokationer | Spoolman"
}
},
"spool": {
"fields": {
"measured_weight": "Målt vægt",
"last_used": "Sidst brugt",
"last_used": "Sidst anvendt",
"material": "Materiale",
"weight_to_use": "Vægt",
"lot_nr": "Gruppe nr",
"lot_nr": "Partinummer",
"filament_name": "Filament",
"comment": "Kommentar",
"filament": "Filament",
@@ -106,11 +137,15 @@
"archived": "Arkiveret",
"remaining_length": "Resterende længde",
"registered": "Registreret",
"first_used": "Først brugt",
"first_used": "Først anvendt",
"used_weight": "Brugt vægt",
"id": "ID",
"remaining_weight": "Resterende vægt",
"price": ""
"price": "Pris",
"initial_weight": "Startvægt",
"spool_weight": "Tom vægt",
"filament_internal": "Intern",
"filament_external": "Ekstern"
},
"titles": {
"archive": "Arkivér spole",
@@ -119,31 +154,45 @@
"create": "Opret spole",
"clone": "Klon spole",
"show_title": "[Spole #{{id}}] {{name}}",
"list": "Spoler"
"list": "Spoler",
"adjust": "Tilpas spolefilament"
},
"form": {
"spool_updated": "Denne spole er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!",
"new_location_prompt": "Indtast ny placering"
"new_location_prompt": "Indtast ny placering",
"measurement_type_label": "Måletype",
"adjust_filament_value": "Fjern mængde",
"measurement_type": {
"weight": "Vægt",
"length": "Længde"
},
"adjust_filament_help": "Her kan du direkte tilføje eller fjerne filament fra spolen. En positiv værdi vil fjerne filament, en negativ værdi vil tilføje det."
},
"fields_help": {
"used_weight": "Hvor meget filament er der brugt fra spolen. En ny spole bør have 0g brugt.",
"weight_to_use": "Vælg hvilken vægt der skal angives. Målt vægt er kun tilgængelig hvis der er indstillet en spole vægt for det valgte filament.",
"measured_weight": "Hvor meget filament OG spole vejer.",
"lot_nr": "Producentens \"batchnummer\". Kan bruges til at sikre ensartede farver ved brug af flere ruller.",
"remaining_weight": "Hvor meget filament er der tilbage på spolen. For en ny spole bør dette matches spolens vægt.",
"location": "Placering af spolen. Hvis du har flere lokationer.",
"price": ""
"weight_to_use": "Vælg hvilken vægt der skal angives. Målt vægt er kun tilgængelig, hvis der er indstillet en spolevægt for det valgte filament.",
"measured_weight": "Hvor meget filament og spole vejer.",
"lot_nr": "Producentens \"batch/partinummer\". Kan bruges til at sikre ensartede farver ved brug af flere ruller.",
"remaining_weight": "Hvor meget filament er der tilbage på spolen. For en ny spole bør dette matche spolens vægt.",
"location": "Placering af spolen, hvis du opbevarer spoler flere steder.",
"price": "Pris for en fuld spole. Hvis ikke indstillet, vil prisen i stedet blive antaget.",
"external_filament": "Du har valgt et filament fra den eksterne database. Et filament (og muligvis en producent) vil blive oprettet automatisk, når du gemmer denne spole. Dette kan skabe duplikerede objekter, hvis du allerede har oprettet dette filament.",
"initial_weight": "Startvægten af filament på spolen (nettovægt). Bruger filamentets standardvægt, hvis ikke angivet.",
"spool_weight": "Vægten af spolen når den er tom. Et tomt felt vil bruge værdien fra filament eller producent."
},
"spool": "Spoler",
"messages": {
"archive": "Er du sikker på du vil arkivere denne spole?"
},
"formats": {
"last_used": "Sidst anvendt {{date}}"
}
},
"buttons": {
"create": "Opret",
"save": "Gem",
"show": "Vis",
"filter": "Filter",
"filter": "Filtrér",
"cancel": "Annuller",
"edit": "Redigér",
"showArchived": "Vis arkiverede",
@@ -158,51 +207,63 @@
"notAccessTitle": "Du har ikke tilladelse til at tilgå",
"hideArchived": "Skjul arkiverede",
"clearFilters": "Ryd filtre",
"unArchive": "Fjern fra arkiv",
"unArchive": "Gendan arkiv",
"clone": "Klon",
"clear": "Ryd",
"saveAndAdd": "Gem og Tilføj"
"saveAndAdd": "Gem og Tilføj",
"continue": "Fortsæt"
},
"filament": {
"fields_help": {
"name": "Filament navn, for at adskille dette filament fra andre fra samme producent. Kan for eksempel indeholde farven.",
"article_number": "f.eks. EAN, UPC osv.",
"name": "Filamentnavn, for at adskille dette filament fra andre fra samme producent. Kan for eksempel indeholde farven.",
"article_number": "F.eks. EAN, UPC, e.l.",
"spool_weight": "Vægten af en tom spole. Bruges til at angive den målte vægt af en spole.",
"price": "Pris for en ny spole i systemets valutaenhed.",
"weight": "Filamentets vægt på en ny spole (Netto vægt). Dette bør ikke inkludere vægten af selve spolen, kun filamentet. Det er som regel den angivne vægt på pakken.",
"material": "f.eks. PLA, ABS, PETG osv."
"price": "Pris for en ny spole.",
"weight": "Filamentets vægt på en ny spole (nettovægt). Dette bør ikke inkludere vægten af selve spolen, kun filamentet. Det er som regel den angivne vægt på pakken.",
"material": "F.eks. PLA, ABS, PETG, m.m.",
"multi_color_direction": "Filamenter kan have flere farver på to måder: Enten koaksialt, som 2- & 3-farvede filamenter med vedvarende flerfarvethed, eller gennem langsgående ændringer, som regnbuefilamenter, hvor farven skifter undervejs på spolen."
},
"fields": {
"material": "Materiale",
"vendor_name": "Sælger",
"settings_extruder_temp": "Extruder temperatur",
"vendor_name": "Producent",
"settings_extruder_temp": "Ekstrudertemperatur",
"comment": "Kommentar",
"diameter": "Diameter",
"weight": "Vægt",
"spool_weight": "Spole vægt",
"spool_weight": "Spolevægt",
"registered": "Registreret",
"name": "Navn",
"price": "Pris",
"settings_bed_temp": "Bed temperatur",
"vendor": "Sælger",
"settings_bed_temp": "Printpladetemperatur",
"vendor": "Producent",
"density": "Massefylde",
"id": "ID",
"color_hex": "Farve",
"article_number": "Produkt nummer",
"spools": "Vis spoler"
"article_number": "Produktnummer",
"spools": "Vis spoler",
"external_id": "Eksternt ID",
"longitudinal": "Langsgående",
"multi_color": "Flerfarvet",
"coaxial": "Koaksialt",
"single_color": "Enkeltfarvet"
},
"titles": {
"clone": "Klon filament",
"show_title": "[Filament # {{id}}] {{name}}",
"show_title": "[Filament #{{id}}] {{name}}",
"show": "Vis filament",
"create": "Opret filament",
"edit": "Redigér filament",
"list": "Filamenter"
},
"form": {
"filament_updated": "Dette filament er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
"filament_updated": "Dette filament er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!",
"import_external": "Importér fra ekstern kilde",
"import_external_description": "Vælg et filament på listen, for automatisk at udfylde detaljerne i formularen. Dette vil overskrive al data, du allerede har indtastet i formularen. <br><br> En producent vil blive oprettet automatisk, hvis nødvendigt, når du klikker OK."
},
"filament": "Filamenter"
"filament": "Filamenter",
"buttons": {
"add_spool": "Tilføj spole"
}
},
"actions": {
"create": "Opret",
@@ -213,47 +274,54 @@
},
"vendor": {
"titles": {
"show_title": "[Sælger #{{id}}] {{name}}",
"show": "Vis sælger",
"edit": "Redigér sælger",
"clone": "Klon sælger",
"create": "Opret sælger",
"list": "Sælgere"
"show_title": "[Producent #{{id}}] {{name}}",
"show": "Vis producent",
"edit": "Redigér producent",
"clone": "Klon producent",
"create": "Opret producent",
"list": "Producenter"
},
"form": {
"vendor_updated": "Denne sælger er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
"vendor_updated": "Denne producent er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
},
"fields": {
"registered": "Registreret",
"comment": "Kommentar",
"name": "Navn",
"id": "ID"
"id": "ID",
"external_id": "Eksternt ID",
"empty_spool_weight": "Tom Spolevægt"
},
"vendor": "Sælgere"
"vendor": "Producenter",
"fields_help": {
"empty_spool_weight": "Vægten af en tom spole fra denne producent."
}
},
"scanner": {
"title": "QR kode skanner",
"title": "QR-kode skanner",
"error": {
"insecureContext": "Denne side er ikke sikret med HTTPS.",
"notReadable": "Kameraet kan ikke indlæses.",
"notAllowed": "Du har ikke tilladt adgang til kameraet.",
"notAllowed": "Du har ikke givet adgang til kameraet.",
"unknown": "En ukendt fejl opstod. {{error}}",
"notFound": "Intet kamera fundet.",
"streamApiNotSupported": "Denne browser understøtter ikke MediaStream API'et."
},
"description": "Skan en Spoolman QR kode for at se detaljer om spolen."
"description": "Skan en Spoolman QR-kode for at se detaljer om spolen."
},
"notifications": {
"deleteSuccess": "Sletning af {{resource}} lykkedes",
"error": "Fejl (status kode: {{statusCode}})",
"editError": "Fejl ved redigering af {{resource}} (status kode:{{statusCode}}",
"deleteSuccess": "Sletning af {{resource}} var succesfuld(t)",
"error": "Fejl (statuskode: {{statusCode}})",
"editError": "Fejl ved redigering af {{resource}} (statuskode:{{statusCode}}",
"undoable": "Du har {{seconds}} sekunder til at fortryde.",
"importProgress": "Importerer: {{processed}}/{{total}}",
"createError": "Der var en fejl under oprettelse af {{resource}} (status kode: {{statusCode}})",
"createError": "Der var en fejl under oprettelsen af {{resource}} (statuskode: {{statusCode}})",
"success": "Succes",
"editSuccess": "Redigering af {{resource}} lykkedes",
"deleteError": "Fejl ved sletning af {{resource}} (status kode: {{statusCode}})",
"createSuccess": "Oprettelse af {{resource}} lykkedes"
"editSuccess": "Redigering af {{resource}} var succesfuld(t)",
"deleteError": "Fejl ved sletning af {{resource}} (statuskode: {{statusCode}})",
"createSuccess": "Oprettelse af {{resource}} var succesfuld(t)",
"saveSuccessful": "Gemt!",
"validationError": "Kunne ikke gennemføres: {{error}}"
},
"no": "Nej",
"unknown": "Ukendt",
@@ -262,7 +330,7 @@
"table": {
"actions": "Handlinger"
},
"warnWhenUnsavedChanges": "Er du sikker på du ønsker at forlade? Du har ændringer der ikke er gemt.",
"warnWhenUnsavedChanges": "Er du sikker på du ønsker at gå? Du har ændringer der ikke er gemt.",
"loading": "Indlæser",
"dashboard": {
"title": "Kontrolpanel"
@@ -272,15 +340,72 @@
},
"help": {
"resources": {
"vendor": "Firmaerne der laver filamentet.",
"filament": "Filament mærker. De har værdier såsom Navn, Materiale, Farve, Diameter m.fl.",
"vendor": "Firmaerne, der laver filamentet.",
"filament": "Filamentmærker. De har værdier, såsom Navn, Materiale, Farve, Diameter, m.m.",
"spool": "Individuelle fysiske spoler af et specifikt filament."
},
"description": "<title>Hjælp</title><p>Her er nogle tips til at komme i gang.</p><p>Spoolman indeholder 3 forskellige typer data:</p><itemsHelp/><p>For at tilføje en ny spole til databasen, skal du starte med at oprette et <filamentCreateLink>Filament</filamentCreateLink> objekt. Når dette er gjort, fortsættes der ved at oprette et <spoolCreateLink>Spole</spoolCreateLink> objekt for den specifikke spole. Hvis du har flere spoler af samme filament, opretter du blot et nyt spole objekt og genbruger samme filament. </p><p> Du har også mulighed for at oprette et <vendorCreateLink>Forhandler</vendorCreateLink> objekt for firmaet der producerer filamentet, hvis du ønsker.</p><p>Du har mulighed for at linke andre 3D printer services til Spoolman, Som f.eks. Moonraker. Dette giver mulighed for automatisk at opdatere og overvåge forbruget af filament for hvert print. Læs <readmeLink>Spoolman README</readmeLink> for information om opsætning af dette.</p>",
"description": "<title>Hjælp</title><p>Her er nogle tips til at komme i gang.</p><p>Spoolman indeholder 3 forskellige typer data:</p><itemsHelp/><p>For at tilføje en ny spole til databasen, skal du starte med at oprette et <filamentCreateLink>Filamentobjekt</filamentCreateLink>. Når dette er gjort, fortsættes der ved at oprette et <spoolCreateLink>Spoleobjekt</spoolCreateLink> for den specifikke spole. Hvis du har flere spoler af samme filament, opretter du blot et nyt spole objekt og genbruger samme filament. </p><p> Du har også mulighed for at oprette et <vendorCreateLink>Producentobjekt</vendorCreateLink> for firmaet, der producerer filamentet, hvis du ønsker at holde styr på den information.</p><p>Du har mulighed for at linke andre 3D-printertjenester til Spoolman, Som f.eks. Moonraker. Dette giver mulighed for automatisk at opdatere og overvåge forbruget af filament for hvert print. Læs <readmeLink>Spoolman README</readmeLink> for information om opsætning af dette.</p>",
"help": "Hjælp"
},
"kofi": "Donér på Ko-fi",
"home": {
"home": "Hjem"
"home": "Hjem",
"welcome": "Velkommen til din nye Spoolman instans!",
"description": "Det ser ud til, at du ikke har tilføjet nogen spoler endnu. Tjek <helpPageLink>her</helpPageLink> for hjælp med at komme i gang."
},
"settings": {
"header": "Indstillinger",
"general": {
"tab": "Generelt",
"currency": {
"label": "Valuta"
},
"base_url": {
"tooltip": "Basis-webadressen til at benytte ved generering af funktioner, såsom QR-koder.",
"label": "Basis-webadresse"
},
"round_prices": {
"label": "Afrund priser",
"tooltip": "Afrund priser til nærmeste heltal."
}
},
"extra_fields": {
"params": {
"key": "Nøgle",
"name": "Navn",
"field_type": "Type",
"unit": "Enhed",
"order": "Sortering",
"default_value": "Standardværdi",
"multi_choice": "Multi-valg",
"choices": "Valgmuligheder"
},
"field_type": {
"integer_range": "Heltalspænd",
"float": "Decimaltal",
"float_range": "Decimaltalspænd",
"datetime": "Dato-tid",
"boolean": "Boolean (Ja/Nej)",
"choice": "Valg",
"text": "Tekst",
"integer": "Heltal"
},
"choices_missing_error": "Du kan ikke fjerne valgmuligheder. De følgende valgmuligheder mangler: {{choices}}",
"tab": "Ekstra felter",
"description": "<p>Her kan du tilføje ekstra felter til dine poster.</p><p>Når et felt er tilføjet kan du ikke redigere dens Nøgle eller Type, og for visse felttyper kan du ikke fjerne valg, eller ændre multi-valgstatus. Hvis du fjerner et felt, vil associeret data i alle poster blive slettet.</p><p>Nøglen er hvad andre programmer læser/skriver data som, så hvis dit felt skal integreres med et tredjepartsprogram, skal du sørge for at indstille det korrekt. Standardværdi bliver kun tilføjet til nye felter.</p><p>Ekstra felter kan ikke sorteres eller filtreres i oversigterne.</p>",
"boolean_true": "Ja",
"boolean_false": "Nej",
"non_unique_key_error": "Nøglen skal være Unik.",
"key_not_changed": "Ændr venligst nøglen til noget andet.",
"delete_confirm": "Slet felt: {{name}}?",
"delete_confirm_description": "Dette vil slette feltet samt alle associerede data for alle poster."
},
"settings": "Indstillinger"
},
"locations": {
"new_location": "Ny lokation",
"no_location": "Ingen lokation",
"locations": "Lokationer",
"no_locations_help": "Denne side lader dig organisere dine spoler i lokationer, tilføj nogle spoler for at komme i gang!"
}
}
+173 -47
View File
@@ -13,37 +13,40 @@
"delete": "Löschen",
"edit": "Bearbeiten",
"cancel": "Abbrechen",
"confirm": "Sicher?",
"confirm": "bist du sicher?",
"filter": "Filter",
"clear": "Löschen",
"clear": "Bereinigen",
"refresh": "Aktualisieren",
"show": "Anzeigen",
"undo": "Rückgängig",
"import": "Importieren",
"clone": "Duplizieren",
"notAccessTitle": "Keine Berechtigung, um hierauf zuzugreifen",
"hideColumns": "Spalten anpassen",
"clearFilters": "Filter leeren",
"notAccessTitle": "Du hast keine Zugriffsberechtigung",
"hideColumns": "Spalten ausblenden",
"clearFilters": "Filter bereinigen",
"showArchived": "Archivierte anzeigen",
"archive": "Archivieren",
"archive": "Archiv",
"hideArchived": "Archivierte verstecken",
"unArchive": "Archivieren rückgängig",
"saveAndAdd": "Speichern und hinzufügen"
"unArchive": "Archivieren rückgängig machen",
"saveAndAdd": "Speichern und hinzufügen",
"continue": "Weiter"
},
"warnWhenUnsavedChanges": "Sicher? Nicht gespeicherte Änderungen gehen verloren.",
"warnWhenUnsavedChanges": "Sicher? Es sind nicht gespeicherte Änderungen vorhanden.",
"notifications": {
"success": "Erfolg",
"success": "Erfolgreich",
"error": "Fehler (status code: {{statusCode}})",
"undoable": "Noch {{seconds}} Sekunden um die Aktion rückgängig zu machen.",
"createSuccess": "{{resource}} erfolgreich erstellt",
"createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})",
"createError": "Fehler beim erstellen von {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} erfolgreich gelöscht",
"deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})",
"deleteError": "Fehler beim löschen von {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} erfolgreich bearbeitet",
"editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert"
"editError": "Fehler beim bearbeiten von {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert",
"saveSuccessful": "Erfolgreich gespeichert!",
"validationError": "Validierungsfehler: {{error}}"
},
"loading": "Laden",
"loading": "Läd",
"version": "Version",
"unknown": "Unbekannt",
"yes": "Ja",
@@ -56,19 +59,19 @@
},
"printing": {
"generic": {
"title": "Drucken",
"description": "Einstellungen unten anpassen, um das gewünschte Drucklayout zu erstellen. Der Drucker oder das Betriebssystem fügen möglichweise eigene Abstände und Skalierungen hinzu. Eventuell muss ein wenig experimentiert werden, bevor alles korrekt funktioniert. Auf normalem Papier testen, bevor auf Label-Papier gedruckt wird.",
"helpMargin": "Abstände sollten so konfiguriert werden, dass sie zum Papier und Drucker passen. Änderungen beeinflussen die Größe des gesamten Gitters.",
"helpPrinterMargin": "Der Sicherheitsabstand sollte so eingestellt werden, wie dicht der Drucker am Rand des Papiers drucken kann. Änderungen beeinflussen nicht die Größe des Gitters.",
"title": "Drucke",
"description": "Passe die Einstellungen unten an, um das gewünschte Drucklayout zu bekommen. Denk daran, dass Drucker und dein OS eigene Abstände und Skalierungen anwenden könnte, also musst du vielleicht ein bisschen ausprobieren, bis alles passt. Teste es auf einem normalen Blatt Papier, bevor du auf die richtigen Labels druckst.",
"helpMargin": "Abstände sollten so konfiguriert werden, dass sie zum Papier und Drucker passen. Änderungen beeinflussen die Abmessungen des gesamten Gitters.",
"helpPrinterMargin": "Der Sicherheitsabstand sollte so eingestellt werden, wie dicht der Drucker am Rand des Papiers drucken kann. Änderungen beeinflussen nicht die Abmessungen des Gitters.",
"print": "Drucken",
"columns": "Spalten",
"rows": "Reihen",
"rows": "Zeilen",
"paperSize": "Papiergröße",
"customSize": "Benutzerdefiniert",
"dimensions": "Dimensionen",
"showBorder": "Rahmen anzeigen",
"showBorder": "Ränder anzeigen",
"previewScale": "Vorschau-Skalierung",
"skipItems": "Elemente überspringen",
"skipItems": "Objekt überspringen",
"contentSettings": "Einstellungen: Inhalt",
"layoutSettings": "Einstellungen: Layout",
"horizontalSpacing": "Horizontaler Abstand",
@@ -83,17 +86,28 @@
"printerMarginBottom": "Sicherheitsabstand Unten",
"borders": {
"none": "Keine",
"border": "Rahmen",
"border": "Ränder",
"grid": "Gitter"
}
},
"saveAsImage": "Als Bild speichern",
"settings": "Vorlage",
"defaultSettings": "Standard",
"addSettings": "Neue Vorlage hinzufügen",
"newSetting": "Neu",
"deleteSettings": "Aktuelle Vorlage löschen",
"deleteSettingsConfirm": "Bist du dir sicher, dass du diese Vorlage löschen willst?",
"settingsName": "Vorlagenname",
"itemCopies": "Anzahl der Kopien",
"duplicateSettings": "Kopie erstellen",
"saveSetting": "Vorlage speichern"
},
"qrcode": {
"button": "QR-Codes drucken",
"title": "QR Codes Drucken",
"button": "Beschriftung drucken",
"title": "Beschriftung drucken",
"spoolWeight": "Gewicht Spule: {{weight}}",
"lotNr": "Ch. Nr: {{lot}}",
"lotNr": "Lot. Nr: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "DT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Textgröße Label",
"showSpoolmanIcon": "Spoolman Icon anzeigen",
"showVendor": "Hersteller",
@@ -103,13 +117,30 @@
"showTemperatures": "Temperaturen",
"showSpoolComment": "Kommentar: Spule",
"showFilamentComment": "Kommentar: Filament",
"showVendorComment": "Kommentar: Hersteller"
"showVendorComment": "Kommentar: Hersteller",
"showQRCodeMode": {
"no": "Nein",
"simple": "Einfach",
"withIcon": "Mit Icon"
},
"template": "Beschriftung Vorlagen",
"templateHelp": "Benutze {} um Texte der Spule dynamisch zu übernehmen. Zum Beispiel wird {id} durch die ID der Spule ersetzt, {filament.material} wird durch das Material der Spule ersetzt. Wenn ein Wert fehlt, wird er mit \"?\" ersetzt. Ein zweiter Satz von {} kann genutzt werden, um dieses zu umgehen. Zusätzlich wird jeglicher Text zwischen einem Satz von {} entfernt, wenn der Wert fehlt. Beispielsweise zeigt {Charge: {lot_nr}} nur das Label an, wenn eine Spule eine Charge definiert hat. Texte zwischen zwei Sternen ** werden fett dargestellt. Drücke auf den Knopf um alle verfügbaren Variablen zu sehen.",
"showQRCode": "Drucke QR Code",
"useHTTPUrl": {
"label": "QR Code Link",
"tooltip": "Die Standardeinstellung verwendet einen proprietären Link, der nur funktioniert, wenn er mit der Scanfunktion von Spoolman gescannt wird. URL verwendet entweder die in den Einstellungen angegebene Basis-URL oder wenn diese nicht festgelegt ist die aktuelle Seiten-URL.",
"options": {
"default": "Standard",
"url": "URL"
},
"preview": "Vorschau:"
}
},
"spoolSelect": {
"title": "Spulenauswahl",
"description": "Spulen auswählen, für die ein QR-Code gedruckt werden soll.",
"description": "Spulen auswählen, für die ein Label gedruckt werden soll.",
"showArchived": "Archivierte anzeigen",
"noSpoolsSelected": "Keine Spulen ausgewählt.",
"noSpoolsSelected": "Du hast keine Spulen ausgewählt.",
"selectAll": "Alle aus-/abwählen",
"selectedTotal_one": "{{count}} Spule ausgewählt",
"selectedTotal_other": "{{count}} Spulen ausgewählt"
@@ -117,7 +148,7 @@
},
"scanner": {
"title": "QR-Code Scanner",
"description": "Spoolman QR-Code scannen, um Details zur Spule anzuzeigen.",
"description": "Spoolman QR-Code scannen, um Details der Spule anzuzeigen.",
"error": {
"notAllowed": "Zugriff auf Kamera wurde nicht erlaubt.",
"insecureContext": "Die Seite wird nicht über HTTPS bereitgestellt.",
@@ -147,16 +178,23 @@
"registered": "Registriert",
"comment": "Kommentar",
"archived": "Archiviert",
"price": ""
"price": "Preis",
"initial_weight": "Anfangsgewicht",
"spool_weight": "Leergewicht",
"filament_internal": "Intern",
"filament_external": "Extern"
},
"fields_help": {
"weight_to_use": "Auswählen welches Gewicht verwendet werden soll. \"Gemessenes Gewicht\" ist nur verfügbar, wenn das Gewicht der Spule für das Filament hinterlegt ist.",
"weight_to_use": "Auswählen, welches Gewicht verwendet werden soll. Das gemessene Gewicht ist nur verfügbar, wenn das Gewicht der Spule für das Filament hinterlegt ist.",
"used_weight": "Wie viel Gewicht bereits verbraucht wurde z.B. 0g für eine neue Spule.",
"remaining_weight": "Wie viel Filament noch auf der Spule ist. Entspricht dem auf der Spule angegeben Nettogewicht, wenn die Spule neu ist.",
"measured_weight": "Gesamtgewicht von Spule und Filament.",
"location": "Ort, an dem die Spule gelagert wird.",
"lot_nr": "Chargennummer des Herstellers. Kann verwendet werden um gleichmäßige Farben zu garantieren, wenn mehrere Spulen verwendet werden.",
"price": ""
"price": "Preis einer vollen Spule. Falls nicht ausgewählt, wird der Preis des Filaments geschätzt.",
"initial_weight": "Das Anfangsgewicht des Filaments (Nettogewicht) auf der Spule. Wenn nicht gesetzt, dann wird der Wert aus dem Filament Objekt genutzt.",
"spool_weight": "Das Gewicht der leeren Spule. Leer lassen um das Gewicht vom Hersteller zu nutzen.",
"external_filament": "Sie haben ein Filament aus der externen Datenbank ausgewählt. Ein Filamentobjekt (und möglicherweise ein Herstellerobjekt) wird automatisch erstellt, wenn Sie diese Spule erstellen. Dadurch können doppelte Filamentobjekte entstehen, wenn Sie bereits ein Filamentobjekt für dieses Filament erstellt haben."
},
"titles": {
"create": "Spule erstellen",
@@ -165,14 +203,25 @@
"list": "Spulen",
"show": "Spule anzeigen",
"archive": "Spule archivieren",
"show_title": "[Spule #{{id}}] {{name}}"
"show_title": "[Spule #{{id}}] {{name}}",
"adjust": "Filamentspulen Anpassung"
},
"messages": {
"archive": "Sicher das die Spule archiviert werden soll?"
},
"form": {
"spool_updated": "Diese Spule wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!",
"new_location_prompt": "Neue Position eingeben"
"new_location_prompt": "Neue Position eingeben",
"adjust_filament_help": "Hier kannst du direkt Filament von der Spule hinzufügen oder entfernen. Eine positive Eingabe wird Filament als verbraucht markieren, eine negative wird es der Spule hinzufügen.",
"measurement_type": {
"length": "Länge",
"weight": "Gewicht"
},
"adjust_filament_value": "Verbrauchte Menge",
"measurement_type_label": "Messungstyp"
},
"formats": {
"last_used": "Zuletzt genutzt {{date}}"
}
},
"filament": {
@@ -194,7 +243,12 @@
"settings_extruder_temp": "Düsentemperatur",
"settings_bed_temp": "Betttemperatur",
"color_hex": "Farbe",
"spools": "Zeige Spulen"
"spools": "Zeige Spulen",
"external_id": "Externe ID",
"single_color": "Einfarbig",
"multi_color": "Mehrfarbig",
"coaxial": "Coextrudiert",
"longitudinal": "Längs"
},
"fields_help": {
"name": "Name des Filaments um es von anderen Typen des gleichen Herstellers zu unterscheiden. Sollte z.B. die Farbe enthalten.",
@@ -202,7 +256,8 @@
"price": "Preis einer vollen Spule in der im System konfigurierten Währung.",
"weight": "Das Gewicht einer vollen Spule (Nettogewicht). Sollte nicht das Gewicht der Spule, sondern nur dass des Filaments auf der Spule enthalten. Entspricht normalerweise dem Gewicht, das auf der Spule aufgedruckt ist.",
"spool_weight": "Das Gewicht der leeren Spule. Wird verwendet, um das gemessene Gewicht der Spule zu berechnen.",
"article_number": "z.B. EAN, UPC o.Ä."
"article_number": "z.B. EAN, UPC o.Ä.",
"multi_color_direction": "Filamente können auf zwei Arten mehrere Farben haben: entweder durch Koextrusion, wie bei zweifarbigen Filamenten mit konsistenten Mehrfarben, oder durch Farbveränderungen in Längsrichtung, wie bei Gradient-Filamenten, die die Farben entlang der Spule verschieben."
},
"titles": {
"create": "Filament erstellen",
@@ -213,7 +268,12 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Dieses Filament wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
"filament_updated": "Dieses Filament wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!",
"import_external": "Importieren von extern",
"import_external_description": "Wählen Sie ein Filament in der Liste aus, um dessen Details automatisch in das Formular zur Filamenterstellung einzutragen. Dadurch werden alle Daten, die Sie in das Formular eingegeben haben, überschrieben.<br><br>Wenn Sie auf Ok klicken, wird automatisch ein Herstellerobjekt erstellt, falls erforderlich."
},
"buttons": {
"add_spool": "Spule hinzufügen"
}
},
"vendor": {
@@ -222,18 +282,23 @@
"id": "ID",
"name": "Name",
"registered": "Registriert",
"comment": "Kommentar"
"comment": "Kommentar",
"empty_spool_weight": "Gewicht der leeren Spule",
"external_id": "Externe ID"
},
"titles": {
"create": "Hersteller erstellen",
"clone": "Hersteller duplizieren",
"edit": "Hersteller bearbeiten",
"list": "Hersteller",
"list": "Herstellerliste",
"show": "Hersteller anzeigen",
"show_title": "[Marke #{{id}}] {{name}}"
"show_title": "[Hersteller #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Diese Marke wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
"vendor_updated": "Dieser Hersteller wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
},
"fields_help": {
"empty_spool_weight": "Das Gewicht einer leeren Spule von diesem Hersteller."
}
},
"table": {
@@ -257,7 +322,7 @@
"clone": "#{{id}} Spule duplizieren | Spoolman"
},
"vendor": {
"list": "Hersteler | Spoolman",
"list": "Hersteller | Spoolman",
"show": "#{{id}} Hersteller anzeigen | Spoolman",
"edit": "#{{id}} Hersteller bearbeiten | Spoolman",
"create": "Herstellen erstellen | Spoolman",
@@ -268,6 +333,9 @@
},
"help": {
"list": "Hilfe | Spoolman"
},
"locations": {
"list": "Orte | Spoolman"
}
},
"help": {
@@ -281,6 +349,64 @@
},
"kofi": "Schick mir ein Trinkgeld über Ko-fi",
"home": {
"home": "Home"
}
"home": "Home",
"welcome": "Willkommen auf deiner Spoolman Instanz!",
"description": "Es sieht so aus, als hättest du noch keine Spulen hinzugefügt. Schau auf unsere <helpPageLink>Hilfeseite</helpPageLink>, falls du Hilfe beim Start benötigst."
},
"settings": {
"header": "Einstellungen",
"general": {
"tab": "Allgemein",
"currency": {
"label": "Währung"
},
"base_url": {
"label": "Basis URL",
"tooltip": "Die basis URL für die Generierung von Funktionen wie QR Codes."
},
"round_prices": {
"label": "Gerundeter Preis",
"tooltip": "Rundet die Preise auf die nächsthöhere ganze Zahl."
}
},
"extra_fields": {
"tab": "Extra Felder",
"params": {
"name": "Name",
"field_type": "Typ",
"unit": "Einheit",
"default_value": "Standardwert",
"order": "Reihenfolge",
"multi_choice": "Mehrfachauswahl",
"key": "Schlüssel",
"choices": "Auswahlmöglichkeiten"
},
"field_type": {
"text": "Text",
"float": "Dezimalzahl",
"integer": "Ganze Zahl",
"choice": "Auswahl",
"integer_range": "Ganzzahlbereich",
"boolean": "Wahr/Falsch",
"float_range": "Float-Bereich",
"datetime": "Datum und Uhrzeit"
},
"boolean_true": "Ja",
"boolean_false": "Nein",
"delete_confirm": "Feld {{name}} entfernen?",
"description": "<p>Hier können Sie zusätzliche benutzerdefinierte Felder zu Ihren Elementen hinzufügen.</p><p>Nach dem Hinzufügen eines Feldes können Sie dessen Schlüssel oder Typ nicht mehr ändern, und bei Auswahlfeldern können Sie keine Auswahlmöglichkeiten entfernen oder den Mehrfachauswahlstatus ändern. Wenn Sie ein Feld entfernen, werden die zugehörigen Daten für alle Elemente gelöscht.</p><p>Der Schlüssel ist derjenige, unter dem andere Programme die Daten lesen/schreiben, wenn Ihr benutzerdefiniertes Feld also mit einem Programm eines Drittanbieters integriert werden soll, stellen Sie sicher, dass Sie ihn richtig einstellen. Der Standardwert wird nur auf neue Elemente angewendet.</p><p>Zusätzliche Felder können in den Tabellenansichten nicht sortiert oder gefiltert werden.</p>",
"choices_missing_error": "Du kannst die Auswahl nicht entfernen. Folgende Auswahlmöglichkeiten fehlen: {{choices}}",
"key_not_changed": "Bitte ändere den Schlüssel auf einen anderen Wert.",
"delete_confirm_description": "Dieses Feld und alle damit verbundenen Einträge werden gelöscht.",
"non_unique_key_error": "Der Schlüssel muss einzigartig sein."
},
"settings": "Einstellungen"
},
"locations": {
"no_location": "Kein Ort",
"no_locations_help": "Diese Seite lässt Sie Ihre Spulen zu Orten zuweisen, fügen Sie ein paar Spulen hinzu um loszulegen!",
"locations": "Orte",
"new_location": "Neuer Ort"
},
"sponsor": "Unterstütze mich auf GitHub"
}
+141 -16
View File
@@ -16,7 +16,24 @@
"bedTemp": "Θερμοκρασία κρεβατιού: {{temp}}",
"lotNr": "Αριθμός παρτίδας: {{lot}}",
"showFilamentComment": "Σχόλια Νήματος",
"button": "Tύπωση Κωδικών QR"
"button": "Tύπωση Κωδικών QR",
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
},
"showQRCodeMode": {
"withIcon": "",
"no": "",
"simple": ""
},
"templateHelp": "",
"showQRCode": "",
"template": ""
},
"generic": {
"printerMarginBottom": "Ασφαλές Περιθώριο Κάτω",
@@ -48,7 +65,18 @@
"showBorder": "Εμφάνιση Περιθωρίου",
"printerMarginLeft": "Ασφαλές Περιθώριο Αριστερά",
"dimensions": "Διαστάσεις",
"print": "Τύπωσε"
"print": "Τύπωσε",
"deleteSettingsConfirm": "",
"deleteSettings": "",
"itemCopies": "",
"settingsName": "",
"newSetting": "",
"addSettings": "",
"duplicateSettings": "",
"settings": "",
"saveSetting": "",
"saveAsImage": "",
"defaultSettings": ""
},
"spoolSelect": {
"selectedTotal_one": "{{count}} καρούλι έχει επιλεγεί",
@@ -89,6 +117,9 @@
},
"help": {
"list": "Βοήθεια | Spoolman"
},
"locations": {
"list": ""
}
},
"spool": {
@@ -110,7 +141,11 @@
"used_weight": "Χρησιμοποιημένο Βάρος",
"id": "Α/Α",
"remaining_weight": "Υπολειπόμενο Βάρος",
"price": ""
"price": "Τιμή",
"spool_weight": "Βάρος Κενού Καρουλιού",
"initial_weight": "Αρχικό Βάρος",
"filament_internal": "",
"filament_external": ""
},
"titles": {
"archive": "Αρχειοθέτηση Καρουλιού",
@@ -119,11 +154,19 @@
"create": "Δημιουργία Καρουλιού",
"clone": "Αντιγραφή Καρουλιού",
"show_title": "[Καρούλι #{{id}}] {{name}}",
"list": "Καρούλια"
"list": "Καρούλια",
"adjust": ""
},
"form": {
"spool_updated": "Αυτό το καρούλι έχει ενημερωθεί από κάποιον/κάτι άλλο από τότε που ανοίξατε αυτήν τη σελίδα. Η αποθήκευση θα αντικαταστήσει αυτές τις αλλαγές!",
"new_location_prompt": "Προσθήκη νέας τοποθεσίας αποθήκευσης"
"new_location_prompt": "Προσθήκη νέας τοποθεσίας αποθήκευσης",
"measurement_type_label": "",
"adjust_filament_value": "",
"measurement_type": {
"weight": "",
"length": ""
},
"adjust_filament_help": ""
},
"fields_help": {
"used_weight": "Πόσο νήμα έχει χρησιμοποιηθεί από το καρούλι. Σε ένα καινούργιο καρούλι θα πρέπει να έχουν χρησιμοποιηθεί 0g.",
@@ -132,11 +175,17 @@
"lot_nr": "Αριθμός παρτίδας κατασκευαστή. Μπορεί να χρησιμοποιηθεί για να διασφαλιστεί ότι μια εκτύπωση έχει σταθερό χρώμα εάν χρησιμοποιούνται πολλαπλά καρούλια.",
"remaining_weight": "Πόσο νήμα έχει απομείνει στο καρούλι. Για ένα καινούργιο καρούλι αυτό πρέπει να συμπίπτει με το βάρος του καρουλιού.",
"location": "Που βρίσκεται το καρούλι, εάν υπάρχουν πολλαπλά σημεία αποθήκευσης για τα καρούλια.",
"price": ""
"price": "Τιμή πλήρους καρουλιού. Εάν δεν οριστεί, η τιμή του νήματος θα θεωρηθεί αντ' αυτού.",
"external_filament": "",
"spool_weight": "",
"initial_weight": ""
},
"spool": "Καρούλια",
"messages": {
"archive": "Είστε σίγουρος ότι θέλετε να αρχειοθετήσετε αυτό το καρούλι;"
},
"formats": {
"last_used": ""
}
},
"buttons": {
@@ -161,16 +210,18 @@
"unArchive": "Επαναφορά Αρχειοθετημένου",
"clone": "Αντιγραφή",
"clear": "Εκκαθάριση",
"saveAndAdd": "Αποθήκευση και Προσθήκη"
"saveAndAdd": "Αποθήκευση και Προσθήκη",
"continue": ""
},
"filament": {
"fields_help": {
"name": "Όνομα νήματος, για να διακρίνει αυτόν τον τύπο νήματος μεταξύ άλλων από τον ίδιο προμηθευτή. Θα πρέπει να περιέχει το χρώμα για παράδειγμα.",
"article_number": "Π.χ. EAN, UPC, κλπ.",
"spool_weight": "Το βάρος ενός άδειου καρουλιού. Χρησιμοποιείται για τον προσδιορισμό του μετρημένου βάρους ενός καρουλιού.",
"price": "Τιμή ενός πλήρους καρουλιού στο νόμισμα που έχει διαμορφωθεί από το σύστημα.",
"price": "Τιμή ενός πλήρους καρουλιού.",
"weight": "Το βάρος του νήματος ενός πλήρους καρουλιού (καθαρό βάρος). Αυτό δεν πρέπει να περιλαμβάνει το βάρος του ίδιου του καρουλιού, μόνο το βάρος του νήματος. Είναι αυτό που συνήθως αναγράφεται στη συσκευασία.",
"material": "Π.χ. PLA, ABS, PETG, κλπ."
"material": "Π.χ. PLA, ABS, PETG, κλπ.",
"multi_color_direction": ""
},
"fields": {
"material": "Υλικό",
@@ -189,7 +240,12 @@
"id": "Α/Α",
"color_hex": "Χρώμα",
"article_number": "Κωδικός Προϊόντος",
"spools": "Προβολή Καρουλιών"
"spools": "Προβολή Καρουλιών",
"multi_color": "",
"external_id": "",
"single_color": "",
"longitudinal": "",
"coaxial": ""
},
"titles": {
"clone": "Αντιγραφή Νήματος",
@@ -200,9 +256,14 @@
"list": "Νήματα"
},
"form": {
"filament_updated": "Αυτό το νήμα έχει ενημερωθεί από κάποιον/κάτι άλλο από τότε που ανοίξατε αυτήν τη σελίδα. Η αποθήκευση θα αντικαταστήσει αυτές τις αλλαγές!"
"filament_updated": "Αυτό το νήμα έχει ενημερωθεί από κάποιον/κάτι άλλο από τότε που ανοίξατε αυτήν τη σελίδα. Η αποθήκευση θα αντικαταστήσει αυτές τις αλλαγές!",
"import_external": "",
"import_external_description": ""
},
"filament": "Νήματα"
"filament": "Νήματα",
"buttons": {
"add_spool": "Προσθήκη Καρουλιού"
}
},
"actions": {
"create": "Δημιουργία",
@@ -227,9 +288,14 @@
"registered": "Ημερομηνία Εγγραφής",
"comment": "Σχόλια",
"name": "Όνομα",
"id": "Α/Α"
"id": "Α/Α",
"external_id": "",
"empty_spool_weight": ""
},
"vendor": "Προμηθευτές"
"vendor": "Προμηθευτές",
"fields_help": {
"empty_spool_weight": ""
}
},
"scanner": {
"title": "Αναγνώστης κωδικού QR",
@@ -253,7 +319,9 @@
"success": "Επιτυχής",
"editSuccess": "Επιτυχής τροποποίηση του {{resource}}",
"deleteError": "Σφάλμα κατά τη διαγραφή του {{resource}} (status code: {{statusCode}})",
"createSuccess": "Δημιουργήθηκε επιτυχώς {{resource}}"
"createSuccess": "Δημιουργήθηκε επιτυχώς {{resource}}",
"saveSuccessful": "Επιτυχής Αποθήκευση!",
"validationError": "Κείμενο σφάλματος που εμφανίζεται εάν η φόρμα είχε προβλήματα κατά την υποβολή {{error}}"
},
"no": "Όχι",
"unknown": "Άγνωστος",
@@ -281,6 +349,63 @@
},
"kofi": "Φιλοδώρημα στο Ko-fi",
"home": {
"home": "Αρχική"
"home": "Αρχική",
"welcome": "",
"description": ""
},
"settings": {
"extra_fields": {
"params": {
"default_value": "Προεπιλεγμένη τιμή",
"name": "Όνομα",
"field_type": "Τύπος",
"choices": "Επιλογές",
"multi_choice": "Πολλαπλή επιλογή",
"unit": "Μονάδα",
"order": "Σειρά",
"key": "Κλειδί"
},
"field_type": {
"float_range": "\"Float Range\" πληκτρολογήστε επιπλέον πεδίο, ένα εύρος δύο αριθμών με δεκαδικούς αριθμούς",
"choice": "\"Choice\" πληκτρολογήστε επιπλέον πεδίο, οι χρήστες μπορούν να επιλέξουν από μια λίστα τιμών",
"text": "Πληκτρολογήστε επιπλέον πεδίο \"Text\".",
"datetime": "Πληκτρολογήστε επιπλέον πεδίο \"Date+Time\"",
"boolean": "Πρόσθετο πεδίο τύπου \"Boolean\", Ναι/Όχι",
"integer": "\"Integer\" πληκτρολογήστε επιπλέον πεδίο, ακέραιο αριθμό χωρίς δεκαδικούς αριθμούς",
"float": "\"Float\" πληκτρολογήστε επιπλέον πεδίο, έναν αριθμό με δεκαδικούς αριθμούς",
"integer_range": "\"Integer Range\" πληκτρολογήστε επιπλέον πεδίο, ένα εύρος δύο ακέραιων αριθμών χωρίς δεκαδικoύς αριθμούς"
},
"non_unique_key_error": "Το κλειδί πρέπει να είναι μοναδικό.",
"key_not_changed": "Παρακαλώ αλλάξτε το κλειδί σε κάποιο άλλο.",
"delete_confirm": "Διαγραφή πεδίου {{name}};",
"tab": "Επιπλέον Πεδία",
"boolean_true": "Τιμή για ένα πρόσθετο πεδίο boolean Αληθές",
"boolean_false": "Τιμή για ένα πρόσθετο πεδίο boolean Λανθασμένο",
"choices_missing_error": "Δεν μπορείτε να αφαιρέσετε επιλογές. Λείπουν οι ακόλουθες επιλογές: {{choices}}",
"description": "<p>Εδώ μπορείτε να προσθέσετε επιπλέον προσαρμοσμένα πεδία στις οντότητες σας.</p><p>Μόλις προστεθεί ένα πεδίο, δεν μπορείτε να αλλάξετε το κλειδί ή τον τύπο του και για στα πεδία τύπου επιλογής δεν μπορείτε να αφαιρέσετε επιλογές ή να αλλάξετε την κατάσταση πολλαπλών επιλογών. Εάν καταργήσετε ένα πεδίο, τα συσχετισμένα δεδομένα για όλες τις οντότητες θα διαγραφούν.</p><p>Το κλειδί είναι πώς τα άλλα προγράμματα διαβάζουν/εγγράφουν τα δεδομένα, οπότε αν το προσαρμοσμένο πεδίο σας υποτίθεται ότι ενσωματώνεται με ένα πρόγραμμα τρίτου κατασκευαστή, φροντίστε να το ρυθμίσετε σωστά. Η προεπιλεγμένη τιμή εφαρμόζεται μόνο σε νέα στοιχεία.</p><p>Τα πρόσθετα πεδία δεν μπορούν να ταξινομηθούν ή να φιλτραριστούν στις προβολές πίνακα.</p>",
"delete_confirm_description": "Με αυτόν τον τρόπο θα διαγραφεί το πεδίο και όλα τα σχετικά δεδομένα για όλες τις οντότητες."
},
"general": {
"tab": "Γενικές",
"currency": {
"label": "Ετικέτα για τη ρύθμιση του νομίσματος"
},
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
}
},
"header": "Ρυθμίσεις",
"settings": "Ρυθμίσεις"
},
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
}
}
+121 -43
View File
@@ -15,6 +15,7 @@
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"continue": "Continue",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
@@ -45,12 +46,18 @@
"saveSuccessful": "Save successful!",
"validationError": "Validation error: {{error}}"
},
"kofi": "Tip me on Ko-fi",
"sponsor": "Sponsor me on GitHub",
"loading": "Loading",
"version": "Version",
"unknown": "Unknown",
"yes": "Yes",
"no": "No",
"theme": {
"label": "Theme",
"system": "System",
"light": "Light",
"dark": "Dark"
},
"tags": {
"clone": "Clone"
},
@@ -72,6 +79,7 @@
"showBorder": "Show Border",
"previewScale": "Preview Scale",
"skipItems": "Skip Items",
"itemCopies": "Item Copies",
"contentSettings": "Content Settings",
"layoutSettings": "Layout Settings",
"horizontalSpacing": "Horizontal Spacing",
@@ -88,29 +96,44 @@
"none": "None",
"border": "Border",
"grid": "Grid"
}
},
"settings": "Presets",
"defaultSettings": "Default",
"addSettings": "Add New Preset",
"newSetting": "New",
"duplicateSettings": "Duplicate",
"deleteSettings": "Delete Current Preset",
"deleteSettingsConfirm": "Are you sure you want to delete this preset?",
"settingsName": "Preset Name",
"saveSetting": "Save Presets",
"saveAsImage": "Save as Image"
},
"qrcode": {
"button": "Print QR Codes",
"title": "QR Code Printing",
"spoolWeight": "Spool Weight: {{weight}}",
"lotNr": "Lot Nr: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Content Text Size",
"showSpoolmanIcon": "Show Spoolman Icon",
"showVendor": "Vendor",
"button": "Print Labels",
"title": "Label Printing",
"template": "Label Template",
"templateHelp": "Use {} to insert values of the spool object as text. For example, {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. if a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Lot Nr: {lot_nr}} will only show the label if the spool has a lot number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
"textSize": "Label Text Size",
"showContent": "Print Label",
"showLotNr": "Lot Nr",
"showSpoolWeight": "Spool Weight",
"showTemperatures": "Temperatures",
"showSpoolComment": "Spool Comment",
"showFilamentComment": "Filament Comment",
"showVendorComment": "Vendor Comment"
"useHTTPUrl": {
"label": "QR code link",
"tooltip": "Will use proprietary link that will work only if scanned from Spoolman's scanning feature (default). URL uses either the base URL specified in settings, or the current page URL if not set.",
"options": {
"default": "Default",
"url": "URL"
},
"preview": "Preview:"
},
"showQRCode": "Print QR Code",
"showQRCodeMode": {
"no": "No",
"simple": "Simple",
"withIcon": "With Icon"
}
},
"spoolSelect": {
"title": "Select Spools",
"description": "Select spools to print QR codes for.",
"description": "Select spools to print labels for.",
"showArchived": "Show Archived",
"noSpoolsSelected": "You have not selected any spools.",
"selectAll": "Select/Unselect All",
@@ -136,6 +159,8 @@
"id": "ID",
"filament_name": "Filament",
"filament": "Filament",
"filament_internal": "Internal",
"filament_external": "External",
"price": "Price",
"material": "Material",
"weight_to_use": "Weight",
@@ -144,6 +169,8 @@
"measured_weight": "Measured Weight",
"used_length": "Used Length",
"remaining_length": "Remaining Length",
"initial_weight": "Initial Weight",
"spool_weight": "Empty Weight",
"location": "Location",
"lot_nr": "Lot Nr",
"first_used": "First Used",
@@ -153,13 +180,16 @@
"archived": "Archived"
},
"fields_help": {
"price": "Price of a full spool in the system configured currency. If not set, the price of the filament will be assumed instead.",
"price": "Price of a full spool. If not set, the price of the filament will be assumed instead.",
"weight_to_use": "Select what weight value to enter. Measured weight is only available if the spool weight is set for the selected filament.",
"used_weight": "How much filament has been used from the spool. A new spool should have 0g used.",
"remaining_weight": "How much filament is left on the spool. For a new spool this should match the spool weight.",
"measured_weight": "How much the filament and spool weigh.",
"initial_weight": "The initial weight of filament on the spool (net weight). Will use the weight from the filament object if not set.",
"spool_weight": "The weight of the spool when it is empty. Leave empty to use the value from the filament or the manufacturer.",
"location": "Where the spool is located if you have multiple locations where you store your spools.",
"lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used."
"lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used.",
"external_filament": "You have selected a filament from the external database. A filament object (and possibly a manufacturer object) will be created automatically when you create this spool. This can create duplicate filament objects if you have already created a filament object for this filament."
},
"titles": {
"create": "Create Spool",
@@ -168,22 +198,33 @@
"list": "Spools",
"show": "Show Spool",
"show_title": "[Spool #{{id}}] {{name}}",
"archive": "Archive Spool"
"archive": "Archive Spool",
"adjust": "Adjust Spool Filament"
},
"form": {
"measurement_type": {
"length": "Length",
"weight": "Weight"
},
"measurement_type_label": "Measurement Type",
"adjust_filament_help": "Here you can directly add or subtract filament from the spool. A positive value will consume filament, a negative value will add it.",
"adjust_filament_value": "Consume Amount",
"new_location_prompt": "Enter a new location",
"spool_updated": "This spool has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
},
"messages": {
"archive": "Are you sure you want to archive this spool?"
},
"formats": {
"last_used": "Last used {{date}}"
}
},
"filament": {
"filament": "Filaments",
"fields": {
"id": "ID",
"vendor_name": "Vendor",
"vendor": "Vendor",
"vendor_name": "Manufacturer",
"vendor": "Manufacturer",
"name": "Name",
"material": "Material",
"price": "Price",
@@ -197,15 +238,21 @@
"settings_extruder_temp": "Extruder Temp",
"settings_bed_temp": "Bed Temp",
"color_hex": "Color",
"single_color": "Single",
"multi_color": "Multi",
"coaxial": "Coextruded",
"longitudinal": "Longitudinal",
"external_id": "External ID",
"spools": "Show Spools"
},
"fields_help": {
"name": "Filament name, to distinguish this filament type among others from the same vendor. Should contain the color for example.",
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
"material": "E.g. PLA, ABS, PETG, etc.",
"price": "Price of a full spool in the system configured currency.",
"price": "Price of a full spool.",
"weight": "The filament weight of a full spool (net weight). This should not include the weight of the spool itself, only the filament. It is what is usually written on the packaging.",
"spool_weight": "The weight of an empty spool. Used to determine measured weight of a spool.",
"article_number": "E.g. EAN, UPC, etc."
"article_number": "E.g. EAN, UPC, etc.",
"multi_color_direction": "Filaments can have multiple colors in two ways: either through coextrusion, like dual-color filaments with consistent multi-colors, or through longitudinal color changes, like gradient filaments that shift colors along the spool."
},
"titles": {
"create": "Create Filament",
@@ -216,31 +263,43 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
"import_external": "Import from External",
"import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.<br><br>A Manufacturer object will be created automatically when you click Ok, if necessary."
},
"buttons": {
"add_spool": "Add Spool"
}
},
"vendor": {
"vendor": "Vendors",
"vendor": "Manufacturers",
"fields": {
"id": "ID",
"name": "Name",
"empty_spool_weight": "Empty Spool Weight",
"external_id": "External ID",
"registered": "Registered",
"comment": "Comment"
},
"fields_help": {
"empty_spool_weight": "The weight of an empty spool from this manufacturer."
},
"titles": {
"create": "Create Vendor",
"clone": "Clone Vendor",
"edit": "Edit Vendor",
"list": "Vendors",
"show": "Show Vendor",
"show_title": "[Vendor #{{id}}] {{name}}"
"create": "Create Manufacturer",
"clone": "Clone Manufacturer",
"edit": "Edit Manufacturer",
"list": "Manufacturers",
"show": "Show Manufacturer",
"show_title": "[Manufacturer #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "This vendor has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
}
},
"home": {
"home": "Home"
"home": "Home",
"welcome": "Welcome to your Spoolman instance!",
"description": "It looks like you haven't added any spools yet. See the <helpPageLink>Help page</helpPageLink> for help getting started."
},
"help": {
"help": "Help",
@@ -249,22 +308,31 @@
"spool": "Individual physical spools of a specific filament.",
"vendor": "The companies that make the filament."
},
"description": "<title>Help</title><p>Here are some tips to get you started.</p><p>Spoolman holds 3 different types of data:</p><itemsHelp/><p>To add a new spool to the database, start by creating a <filamentCreateLink>Filament</filamentCreateLink> object. Once that's done, proceed to create a <spoolCreateLink>Spool</spoolCreateLink> object for that specific spool. If you acquire additional spools of the same filament later on, simply generate additional Spool objects and reuse the same Filament object.</p><p>Optionally, you can also generate a <vendorCreateLink>Vendor</vendorCreateLink> object for the company manufacturing the filament if you wish to track that information.</p><p>You have the option to link other 3D printer services to Spoolman, like Moonraker, which can automatically monitor filament usage and update the Spool objects for you. Refer to the <readmeLink>Spoolman README</readmeLink> for guidance on how to set that up.</p>"
"description": "<title>Help</title><p>Here are some tips to get you started.</p><p>Spoolman holds 3 different types of data:</p><itemsHelp/><p>To add a new spool to the database, start by creating a <filamentCreateLink>Filament</filamentCreateLink> object. Once that's done, proceed to create a <spoolCreateLink>Spool</spoolCreateLink> object for that specific spool. If you acquire additional spools of the same filament later on, simply generate additional Spool objects and reuse the same Filament object.</p><p>Optionally, you can also generate a <vendorCreateLink>Manufacturer</vendorCreateLink> object for the company manufacturing the filament if you wish to track that information.</p><p>You have the option to link other 3D printer services to Spoolman, like Moonraker, which can automatically monitor filament usage and update the Spool objects for you. Refer to the <readmeLink>Spoolman README</readmeLink> for guidance on how to set that up.</p>"
},
"table": {
"actions": "Actions"
},
"settings": {
"settings": "Settings",
"header": "Settings",
"general": {
"tab": "General",
"currency": {
"label": "Currency"
},
"base_url": {
"label": "Base URL",
"tooltip": "The base URL to use when generating features such as QR codes."
},
"round_prices": {
"label": "Round prices",
"tooltip": "Round prices to the nearest whole number."
}
},
"extra_fields": {
"tab": "Extra Fields",
"description": "<p>Here you can add extra custom fields to your entities.</p><p>Once a field is added, you can not change its key or type, and for choice type fields you can not remove choices or change the multi choice state. If you remove a field, the associated data for all entities will be deleted.</p><p>The key is what other programs read/write the data as, so if your custom field is supposed to integrate with a third-party program, make sure to set it correctly. Default value is only applied to new items.</p><p>Extra fields can not be sorted or filtered in the table views.</p>",
"description": "<p>Here you can add extra custom fields to your entities.</p><p>Once a field is added, you can not change its key or type, and for choice type fields you can not remove choices or change the multi choice state. If you remove a field, the associated data for all entities will be deleted.</p><p>The key is what other programs read/write the data as, so if your custom field is supposed to integrate with a third-party program, make sure to set it correctly. Default value is only applied to new items.</p><p>Extra fields can be shown and sorted in the table views. Choice and boolean fields expose filter options there as well, and all extra field types support filtering for empty values.</p>",
"params": {
"key": "Key",
"name": "Name",
@@ -288,6 +356,7 @@
"boolean_true": "Yes",
"boolean_false": "No",
"choices_missing_error": "You cannot remove choices. The following choices are missing: {{choices}}",
"choices_reorder_hint": "Drag, or use the arrow keys, to reorder this choice.",
"non_unique_key_error": "The key must be unique.",
"key_not_changed": "Please change the key to something else.",
"delete_confirm": "Delete field {{name}}?",
@@ -303,6 +372,9 @@
"help": {
"list": "Help | Spoolman"
},
"locations": {
"list": "Locations | Spoolman"
},
"filament": {
"list": "Filaments | Spoolman",
"show": "#{{id}} Show Filament | Spoolman",
@@ -318,11 +390,17 @@
"clone": "#{{id}} Clone Spool | Spoolman"
},
"vendor": {
"list": "Vendors | Spoolman",
"show": "#{{id}} Show Vendor | Spoolman",
"edit": "#{{id}} Edit Vendor | Spoolman",
"create": "Create Vendor | Spoolman",
"clone": "#{{id}} Clone Vendor | Spoolman"
"list": "Manufacturers | Spoolman",
"show": "#{{id}} Show Manufacturer | Spoolman",
"edit": "#{{id}} Edit Manufacturer | Spoolman",
"create": "Create Manufacturer | Spoolman",
"clone": "#{{id}} Clone Manufacturer | Spoolman"
}
},
"locations": {
"locations": "Locations",
"new_location": "New Location",
"no_location": "No Location",
"no_locations_help": "This page lets you organize your spools in locations, add some spools to get started!"
}
}
+211 -86
View File
@@ -4,7 +4,7 @@
"create": "Crear",
"edit": "Editar",
"show": "Mostrar",
"clone": "Duplicar"
"clone": "Clonar"
},
"buttons": {
"create": "Crear",
@@ -13,14 +13,14 @@
"delete": "Borrar",
"edit": "Editar",
"cancel": "Cancelar",
"confirm": "Seguro?",
"confirm": "¿Estás seguro?",
"filter": "Filtrar",
"clear": "Limpiar",
"refresh": "Renovar",
"refresh": "Actualizar",
"show": "Mostrar",
"undo": "Deshacer",
"import": "Importar",
"clone": "Duplicar",
"clone": "Clonar",
"archive": "Archivar",
"unArchive": "Desarchivar",
"hideArchived": "Ocultar Archivados",
@@ -28,9 +28,10 @@
"notAccessTitle": "No tienes permiso de acceso",
"hideColumns": "Ocultar Columnas",
"clearFilters": "Borrar filtros",
"saveAndAdd": "Guardar y añadir"
"saveAndAdd": "Guardar y añadir",
"continue": "Continuar"
},
"warnWhenUnsavedChanges": "Seguro que quieres salir?. Hay cambios sin guardar.",
"warnWhenUnsavedChanges": "¿Estás seguro que quieres salir? Hay cambios sin guardar.",
"notifications": {
"success": "Éxito",
"error": "Error (status code: {{statusCode}})",
@@ -41,7 +42,9 @@
"deleteError": "Error al borrar {{resource}} (status code: {{statusCode}})",
"editSuccess": "Has editado {{resource}} con éxito",
"editError": "Error al editar {{resource}} (status code: {{statusCode}})",
"importProgress": "Importando: {{processed}}/{{total}}"
"importProgress": "Importando: {{processed}}/{{total}}",
"saveSuccessful": "¡Se guardo correctamente!",
"validationError": "Error de validación: {{error}}"
},
"loading": "Cargando",
"version": "Versión",
@@ -49,13 +52,13 @@
"yes": "Si",
"no": "No",
"tags": {
"clone": "Duplicar"
"clone": "Clonar"
},
"dashboard": {
"title": "Panel"
"title": "Dashboard"
},
"spool": {
"spool": "Carretes",
"spool": "Bobinas",
"fields": {
"id": "ID",
"filament_name": "Filamento",
@@ -63,9 +66,9 @@
"material": "Material",
"used_weight": "Peso Usado",
"remaining_weight": "Peso Restante",
"used_length": "Largo Usado",
"remaining_length": "Largo Restante",
"location": "Locación",
"used_length": "Longitud usada",
"remaining_length": "Longitud restante",
"location": "Ubicación",
"lot_nr": "No. Lote",
"first_used": "Fecha de Primer Uso",
"last_used": "Ultimo Uso",
@@ -74,62 +77,86 @@
"archived": "Archivado",
"measured_weight": "Peso Medido",
"weight_to_use": "Peso",
"price": ""
"price": "Precio",
"initial_weight": "Peso inicial",
"spool_weight": "Peso vacio",
"filament_internal": "Interno",
"filament_external": "Externo"
},
"fields_help": {
"used_weight": "Cuanto filamento ha sido usado en el carrete. Un carrete nuevo debería tener 0g usados.",
"location": "Donde esta el carrete si se guardan los carretes en lugares distintos.",
"lot_nr": "Numero de lote de manufactura. Puede ser usado si el modelo a imprimir necesita color homogéneo y varios carretes son necesarios.",
"weight_to_use": "Elegir el peso a usar. El peso medido solo esta disponible si existe el valor de peso del carrete seleccionado.",
"measured_weight": "Cuanto pesa el filamento y el carrete.",
"remaining_weight": "Cuanto filamento resta en el carrete. Para un carrete nuevo el peso debe ser igual al peso indicado de fabrica.",
"price": ""
"used_weight": "Cuanto filamento ha sido usado de la bobina. Una bobina nueva debería tener 0g usados.",
"location": "Dónde se encuentra el carrete si tiene varias ubicaciones donde almacena sus carretes.",
"lot_nr": "Número de lote del fabricante. Puede ser usado si el modelo a imprimir necesita color homogéneo y varias bobinas son necesarias.",
"weight_to_use": "Elegir el peso a usar. El peso medido solo está disponible si existe el valor de peso del carrete seleccionado.",
"measured_weight": "Cuanto pesa el filamento y la bobina.",
"remaining_weight": "Cuanto filamento resta en la bobina. Para una bobina nueva el peso debe ser igual al peso indicado de fábrica.",
"price": "Precio de una bobina completa. Si no se establece, se asumirá en su lugar el precio del filamento.",
"initial_weight": "El peso inicial del filamento en el carrete (peso neto). Utilizará el peso del objeto de filamento si no está configurado.",
"spool_weight": "El peso de la bobina cuando está vacía. Dejar vacío para usar el valor del filamento o del fabricante.",
"external_filament": "Has elegido un filamento desde la base de datos externa. Un objeto filamento (y posiblemente un objeto fabricante) será creado automáticamente cuando crees este carrete. Esto puede crear objetos filamento duplicados si ya has creado un objeto filamento para este filamento."
},
"titles": {
"create": "Crear Carrete",
"clone": "Duplicar Carrete",
"edit": "Editar Carrete",
"list": "Carretes",
"show": "Mostrar Carrete",
"archive": "Archivar Carrete",
"show_title": "[Carrete #{{id}}] {{name}}"
"create": "Crear Bobina",
"clone": "Duplicar Bobina",
"edit": "Editar Bobina",
"list": "Bobinas",
"show": "Mostrar Bobina",
"archive": "Archivar Bobina",
"show_title": "[Bobina #{{id}}] {{name}}",
"adjust": "Ajustar la bobina del filamento"
},
"messages": {
"archive": "Esta seguro que quiere archivar este carrete?"
"archive": "¿Esta seguro que quiere archivar esta bobina?"
},
"form": {
"new_location_prompt": "Introduce una nueva ubicación",
"spool_updated": "Este spool ha sido actualizado por alguien/algo más desde que abriste esta página. ¡Al guardar, se sobrescribirán los cambios!"
"spool_updated": "Esta bobina ha sido actualizada por alguien/algo desde que abriste esta página. ¡Al guardar, se sobrescribirán los cambios!",
"measurement_type": {
"length": "Longitud",
"weight": "Peso"
},
"measurement_type_label": "Tipo de medición",
"adjust_filament_value": "Cantidad consumida",
"adjust_filament_help": "Aquí puedes añadir o restar directamente filamento de la bobina. Un valor positivo consumirá filamento, un valor negativo lo añadirá."
},
"formats": {
"last_used": "Utilizado por última vez {{date}}"
}
},
"filament": {
"filament": "Filamentos",
"fields": {
"id": "ID",
"vendor_name": "Marca",
"vendor": "Marca",
"vendor_name": "Fabricante",
"vendor": "Fabricante",
"name": "Nombre",
"material": "Material",
"price": "Precio",
"density": "Densidad",
"diameter": "Diámetro",
"weight": "Peso",
"spool_weight": "Peso del Carrete",
"spool_weight": "Peso de la Bobina",
"article_number": "Numero de Articulo",
"registered": "Registrado",
"comment": "Comentarios",
"settings_extruder_temp": "Temp. de Extrusor",
"settings_extruder_temp": "Temp. del Extrusor",
"settings_bed_temp": "Temp. de Cama",
"color_hex": "Color",
"spools": "Mostrar los carretes"
"spools": "Mostrar las Bobinas",
"external_id": "ID Externo",
"coaxial": "coextruido",
"single_color": "Único",
"longitudinal": "Longitudinal",
"multi_color": "Multiple"
},
"fields_help": {
"name": "Nombre del filamento, para distinguirlo de otros de la misma marca. Por ejemplo, debería contener el nombre del color.",
"name": "Nombre del filamento, para distinguir este tipo de filamento entre otros del mismo fabricante. Debe contener por ejemplo: el color.",
"material": "ej. PLA, ABS, PETG, etc.",
"price": "Precio del carrete completo en la divisa configurada en el sistema.",
"weight": "El peso de un carrete entero (peso neto). Esta cifra no debe incluir el peso del carrete vacío, solo el filamento. Es, usualmente, el peso escrito en la etiqueta del carrete.",
"spool_weight": "El peso de un carrete vacío. Se utiliza para determinar el peso medido de un carrete.",
"article_number": "ej. EAN, UPC, etc."
"price": "Precio de una bobina completa.",
"weight": "El peso de una bobina entera (peso neto). Esta cifra no debe incluir el peso de la bobina vacía, solo el filamento. Es, normalmente, el peso escrito en la etiqueta de la bobina.",
"spool_weight": "El peso de una bobina vacía. Se utiliza para determinar el peso medido de una bobina.",
"article_number": "ej. EAN, UPC, etc.",
"multi_color_direction": "Los filamentos pueden tener varios colores de dos formas: mediante coextrusión, como los filamentos bicolor con multicolores consistentes, o mediante cambios de color longitudinales, como los filamentos degradados que cambian de color a lo largo de la bobina."
},
"titles": {
"create": "Crear Filamento",
@@ -140,27 +167,37 @@
"show_title": "[Filamento #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Este filamento ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
"filament_updated": "Este filamento ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!",
"import_external": "Importado desde un externo",
"import_external_description": "Seleccione un filamento en la lista para rellenar sus detalles de manera automática en el formulario de creación del filamento. Esto sobrescribirá cualquier información que hayas escrito en el formulario.<br><br>Un objeto fabricante será creado automáticamente cuando presiones Ok, si es necesario."
},
"buttons": {
"add_spool": "Añadir bobina"
}
},
"vendor": {
"vendor": "Marcas",
"vendor": "Fabricantes",
"fields": {
"id": "ID",
"name": "Nombre",
"registered": "Registrado",
"comment": "Comentarios"
"comment": "Comentarios",
"empty_spool_weight": "Peso de bobina vacía",
"external_id": "ID Externa"
},
"titles": {
"create": "Crear Marca",
"clone": "Duplicar Marca",
"edit": "Editar Marca",
"list": "Marcas",
"show": "Mostrar Marcas",
"show_title": "[Proveedor #{{id}}] {{name}}"
"create": "Crear un fabricante",
"clone": "Duplicar un fabricante",
"edit": "Editar un fabricante",
"list": "Fabricantes",
"show": "Mostrar Fabricante",
"show_title": "[Fabricante #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Este vendedor ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
"vendor_updated": "Este fabricante ha sido actualizado por alguien/algo desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
},
"fields_help": {
"empty_spool_weight": "El peso de una bobina vacía de este fabricante."
}
},
"table": {
@@ -177,44 +214,64 @@
"clone": "#{{id}} Duplicar Filamento | Spoolman"
},
"spool": {
"list": "Carretes | Spoolman",
"show": "#{{id}} Mostrar Carrete | Spoolman",
"edit": "#{{id}} Editar Carrete | Spoolman",
"create": "Crear Carrete | Spoolman",
"clone": "#{{id}} Duplicar Carrete | Spoolman"
"list": "Bobinas | Spoolman",
"show": "#{{id}} Mostrar Bobina | Spoolman",
"edit": "#{{id}} Editar Bobina | Spoolman",
"create": "Crear Bobina | Spoolman",
"clone": "#{{id}} Duplicar Bobina | Spoolman"
},
"vendor": {
"list": "Marcas | Spoolman",
"show": "#{{id}} Mostrar Marca | Spoolman",
"edit": "#{{id}} Editar Marca | Spoolman",
"create": "Crear Marca | Spoolman",
"clone": "#{{id}} Duplicar Marca | Spoolman"
"list": "Fabricantes | Spoolman",
"show": "#{{id}} Mostrar el fabricante | Spoolman",
"edit": "#{{id}} Editar el fabricante | Spoolman",
"create": "Crear un fabricante | Spoolman",
"clone": "#{{id}} Duplicar un fabricante | Spoolman"
},
"home": {
"list": "Inicio | Spoolman"
},
"help": {
"list": "Ayuda | Spoolman"
},
"locations": {
"list": "Ubicaciones | Spoolman"
}
},
"printing": {
"qrcode": {
"extruderTemp": "TE:{{temp}}",
"showSpoolWeight": "Peso Carrete",
"showSpoolWeight": "Peso de la Bobina",
"showContent": "Imprimir Etiqueta",
"showVendorComment": "Comentarios de Marca",
"spoolWeight": "Peso de Carrete: {{weight}}",
"showVendorComment": "Comentarios del Fabricante",
"spoolWeight": "Peso de la Bobina: {{weight}}",
"showTemperatures": "Temperaturas",
"showVendor": "Marca",
"showSpoolComment": "Comentario de Carrete",
"textSize": "Tamaño Letras de Contenido",
"title": "Impresión de Códigos QR",
"showVendor": "Fabricante",
"showSpoolComment": "Comentario de la Bobina",
"textSize": "Tamaño del texto para la etiqueta",
"title": "Imprimiendo las etiquetas",
"showSpoolmanIcon": "Mostrar Icono de Spoolman",
"showLotNr": "No. Lote",
"bedTemp": "TC:{{temp}}",
"lotNr": "No. Lote:{{lot}}",
"showFilamentComment": "Comentario de Filamento",
"button": "Imprimir Códigos QR"
"button": "Imprimir las etiquetas",
"template": "Plantilla para la etiqueta",
"templateHelp": "Utiliza {} para insertar valores del objeto spool como texto. Por ejemplo, {id} se sustituirá por el id del carrete, o {filament.material} se sustituirá por el material del carrete. si falta un valor, se sustituirá por \"?\". Se puede utilizar un segundo conjunto de {} para eliminarlo. Además, cualquier texto entre los {} se eliminará si falta el valor. Por ejemplo, {Lot Nr: {lot_nr}} solo mostrará la etiqueta si la bobina tiene un número de lote. Encierra el texto con doble asterisco ** para ponerlo en negrita. Haz clic en el botón para ver una lista de todas las etiquetas disponibles.",
"showQRCode": "Imprimir un código qr",
"showQRCodeMode": {
"no": "No",
"simple": "Sencilla",
"withIcon": "Con un icono"
},
"useHTTPUrl": {
"label": "Enlace al código QR",
"options": {
"default": "Por defecto",
"url": "URL"
},
"preview": "Vista previa:",
"tooltip": "Se utilizará un enlace propietario que funcionará solo si se escanea desde la función de escaneo de Spoolman (predeterminado). La URL utiliza la URL base especificada en la configuración o la URL de la página actual si no está configurada."
}
},
"generic": {
"printerMarginBottom": "Zona Segura Inferior",
@@ -226,50 +283,61 @@
"border": "Borde"
},
"printerMarginTop": "Zona Segura Superior",
"customSize": "Tamaño Personal",
"customSize": "Personalizar",
"title": "Imprimiendo",
"contentSettings": "Ajustes de Contenido",
"marginRight": "Margen Derecho",
"printerMarginRight": "Zona Segura Derecha",
"helpPrinterMargin": "La Zona de Seguridad debe ajustarse lo mas cercano posible al borde que tu impresora es capaz de imprimir. Cambiar estos valores no afectaran todas las etiquetas.",
"helpPrinterMargin": "La Zona de Seguridad debe ajustarse lo más cercano posible al borde que tu impresora es capaz de imprimir. Cambiar estos valores no afectaran todas las etiquetas.",
"previewScale": "Previsualizar Escala",
"helpMargin": "Márgenes deben ser configurados a ser los mismos que tu papel e impresora. Cambiar los mismos afectaran todas las etiquetas.",
"helpMargin": "Los márgenes deben ser configurados a ser los mismos que tu papel e impresora. Cambiar los mismos afectaran todas las etiquetas.",
"marginTop": "Margen Superior",
"skipItems": "Saltar Elementos",
"layoutSettings": "Ajustes de Diseño",
"paperSize": "Tamaño de Papel",
"columns": "Columnas",
"description": "Ajustar los parámetros debajo para adquirir el diseño deseado. Recuerda que impresoras y el sistema operativo aplican sus propios márgenes y escala. Es probable que tengas que probar con valores varias veces hasta que el diseño te parezca correcto. Prueba en papel normal antes de usar etiquetas.",
"description": "Ajuste los parámetros siguientes para obtener el diseño de impresión deseado. Recuerda que impresoras y el sistema operativo aplican sus propios márgenes y escala. Es probable que tengas que probar con valores varias veces hasta que el diseño te parezca correcto. Prueba en papel normal antes de usar etiquetas.",
"marginBottom": "Margen Inferior",
"horizontalSpacing": "Espacio Horizontal",
"marginLeft": "Margen Izquierdo",
"showBorder": "Mostrar Borde",
"printerMarginLeft": "Zona Segura Izquierda",
"dimensions": "Dimensiones",
"print": "Imprimir"
"print": "Imprimir",
"itemCopies": "Copias del artículo",
"settings": "Ajustes preestablecidos",
"defaultSettings": "Por defecto",
"addSettings": "Añadir un nuevo ajustes preestablecidos",
"newSetting": "Nuevo",
"deleteSettings": "Eliminar ajustes preestablecidos",
"deleteSettingsConfirm": "¿Estás seguro de que deseas eliminar este ajuste preestablecido?",
"settingsName": "Nombre preestablecido",
"duplicateSettings": "Duplicar",
"saveSetting": "Guardar ajustes preestablecidos",
"saveAsImage": "Guardar como una imagen"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} carrete seleccionado",
"selectedTotal_many": "{{count}} carretes seleccionados",
"selectedTotal_other": "{{count}} carretes seleccionados",
"description": "Seleccionar Carretes para imprimir códigos de QR.",
"showArchived": "Mostrar Archivados",
"title": "Selección de Carretes",
"selectedTotal_one": "{{count}} bobina seleccionada",
"selectedTotal_many": "{{count}} bobinas seleccionadas",
"selectedTotal_other": "{{count}} bobinas seleccionadas",
"description": "Selecciona los carretes para los que deseas imprimir las etiquetas.",
"showArchived": "Mostrar archivados",
"title": "Selección de Bobinas",
"selectAll": "Seleccionar/Deseleccionar Todos",
"noSpoolsSelected": "No has seleccionado ningún carrete."
"noSpoolsSelected": "No has seleccionado ninguna bobina."
}
},
"scanner": {
"title": "Escaner de Código QR",
"title": "Escáner de Código QR",
"error": {
"insecureContext": "La página no se sirve a través de HTTPS.",
"notReadable": "La camara no ha sido leída.",
"insecureContext": "La página no se muestra a través de HTTPS.",
"notReadable": "La cámara no es legible.",
"notAllowed": "No has dado permiso de acceso a la cámara.",
"unknown": "Un error desconocido ocurrió. ({{error}})",
"notFound": "La cámara no ha sido detectada.",
"streamApiNotSupported": "El navegador no es compatible con la API MediaStream."
},
"description": "Escanear códigos QR a Spoolman para ver detalles del carrete."
"description": "Escanea un código QR de Spoolman para ver detalles de la bobina."
},
"help": {
"resources": {
@@ -277,11 +345,68 @@
"filament": "Marcas de filamentos. Tienen propiedades como nombre, material, color, diámetro y más.",
"spool": "Bobinas físicas individuales de un filamento específico."
},
"description": "<title>Ayuda</title><p>Aquí tiene algunos consejos para empezar.</p><p>Spoolman contiene 3 tipos diferentes de datos:</p><itemsHelp/><p>Para añadir un nuevo spool a la base de datos, empiece creando un objeto <filamentCreateLink>Filament</filamentCreateLink>. Una vez hecho esto, proceda a crear un objeto <spoolCreateLink>Spool</spoolCreateLink> para ese spool específico. Si más adelante adquiere carretes adicionales del mismo filamento, simplemente genere objetos Spool adicionales y reutilice el mismo objeto Filament.</p><p>Opcionalmente, también puede generar un objeto <vendorCreateLink>Vendor</vendorCreateLink> para la empresa que fabrica el filamento si desea realizar un seguimiento de esa información.</p><p>Tiene la opción de vincular otros servicios de impresoras 3D a Spoolman, como Moonraker, que puede supervisar automáticamente el uso del filamento y actualizar los objetos Spool por usted. Consulte el <readmeLink>LÉEME de Spoolman</readmeLink> para obtener orientación sobre cómo configurarlo.</p> <p>Spoolman es un servicio de impresión 3D",
"description": "<title> Ayuda </title> <p> A continuación se ofrecen algunos consejos para empezar. </p> <p> Spoolman contiene 3 tipos diferentes de datos: </p> <itemsHelp/> <p> Para agregar una nueva bobina a la base de datos, comience creando un objeto <filamentCreateLink> Filamento </filamentCreateLink> . Una vez hecho esto, proceda a crear un objeto <spoolCreateLink> Bobina</spoolCreateLink> para esa bobina específica. Si adquiere bobinas adicionales del mismo filamento más adelante, simplemente genere objetos Bonina adicionales y reutilice el mismo objeto Filamento. </p> <p> Opcionalmente, también puede generar un objeto <vendorCreateLink> Fabricante</vendorCreateLink> para la empresa que fabrica el filamento si desea realizar un seguimiento de esa información. </p> <p> Tiene la opción de vincular otros servicios de impresora 3D a Spoolman, como Moonraker, que puede monitorear automáticamente el uso del filamento y actualizar los objetos de Bobina por usted. Consulte el <readmeLink> Spoolman README </readmeLink> para obtener orientación sobre cómo configurarlo. </p>",
"help": "Ayuda"
},
"kofi": "Donar en Ko-fi",
"home": {
"home": "Inicio"
"home": "Inicio",
"welcome": "¡Bienvenido a tu instancia de Spoolman!",
"description": "Parece que aún no has añadido ninguna bobina aún. Visita la <helpPageLink>página de ayuda</helpPageLink> para comenzar."
},
"settings": {
"header": "Ajustes",
"general": {
"tab": "General",
"currency": {
"label": "Moneda"
},
"base_url": {
"label": "URL base",
"tooltip": "La URL base que se utilizará al generar funciones como códigos QR."
},
"round_prices": {
"label": "Redondear precios",
"tooltip": "Redondear precios al número entero más cercano."
}
},
"extra_fields": {
"tab": "Campos adicionales",
"params": {
"key": "Clave",
"name": "Nombre",
"field_type": "Tipo",
"unit": "Unidad",
"order": "Orden",
"default_value": "Valor predeterminado",
"choices": "Opciones",
"multi_choice": "Múltiples opciones"
},
"field_type": {
"text": "Texto",
"integer": "Entero",
"integer_range": "Rango de enteros",
"float": "Decimal",
"float_range": "Rango Decimal",
"datetime": "Fecha y hora",
"boolean": "Booleano",
"choice": "Opción"
},
"boolean_true": "Sí",
"boolean_false": "No",
"choices_missing_error": "No se pueden eliminar opciones. Faltan las siguientes opciones: {{choices}}",
"non_unique_key_error": "La clave debe ser única.",
"key_not_changed": "Por favor, cambie la clave por otra.",
"delete_confirm": "¿Borrar campo {{name}}?",
"delete_confirm_description": "Esto eliminará el campo y todos los datos asociados para todas las entidades.",
"description": "<p> Aquí puede agregar más campos personalizados a los asuntos.</p><p> Una vez que se agrega un campo, no puede cambiar su clave o tipo, y no puede eliminar opciones ni cambiar el estado de múltiples opciones para campos seleccionados. Si elimina un campo, se eliminarán los datos asociados a todos los sujetos.</p><p> La forma en que otros programas leen/escriben los datos es clave, por lo que si su campo personalizado se va a integrar con un programa de terceros, asegúrese de que esté configurado correctamente. El valor predeterminado solo se utilizará para elementos nuevos.</p><p> Los campos de salida no se pueden ordenar ni filtrar en las vistas de tabla.</p>"
},
"settings": "Ajustes"
},
"locations": {
"locations": "Ubicaciones",
"new_location": "Nueva ubicación",
"no_location": "Sin ubicación",
"no_locations_help": "Esta página te permite organizar tus carretes en ubicaciones, ¡añade algunos carretes para empezar!"
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"actions": {
"list": "Kuva",
"create": "Loo",
"edit": "Muuda",
"show": "Näita",
"clone": "Klooni"
},
"buttons": {
"create": "Loo",
"save": "Salvesta",
"confirm": "Oled kindel?",
"clear": "Tühjenda",
"refresh": "Uuenda",
"show": "Näita",
"hideColumns": "Peida tulbad",
"clearFilters": "Tühjenda filtrid",
"edit": "Muuda",
"undo": "Võta tagasi",
"clone": "Klooni",
"archive": "Arhiveeri",
"notAccessTitle": "Sul pole õigusi sisenemiseks",
"saveAndAdd": "Salvesta ja Lisa",
"logout": "Logi välja",
"delete": "Kustuta",
"cancel": "Tühista",
"import": "Impordi",
"filter": "Filtreeri",
"unArchive": "Võta arhiivist tagasi",
"hideArchived": "Peida arhiveeritud",
"showArchived": "Näita arhiveeritud",
"continue": "Jätka"
},
"notifications": {
"error": "Viga (kood: {{statusCode}})",
"undoable": "Sul on {{seconds}} sekundit, et muudatus tagasi võtta.",
"success": "Õnnestunud",
"createSuccess": "Loodi {{resource}}",
"createError": "Tekkis viga {{resource}} loomisel. (staatuse kood: {{statusCode}})",
"deleteSuccess": "Edukalt kustutatud {{resource}}",
"deleteError": "Viga {{resource}} kustutamisel. (staatuse kood: {{statusCode}})",
"editSuccess": "Edukalt muudetud {{resource}}",
"editError": "Viga {{resource}} muutmisel (staatuse kood: {{statusCode}})",
"importProgress": "Importimine: {{processed}}/{{total}}",
"saveSuccessful": "Salvesta edukalt!",
"validationError": "Viga validerimisel: {{error}}"
},
"warnWhenUnsavedChanges": "Oled sa kindel, et soovid lahkuda? Sul on salvestamata muudatusi.",
"kofi": "Anna mulle tippi Ko-fi's",
"loading": "Laadimine",
"version": "Versioon",
"unknown": "Tundmatu",
"yes": "Jah",
"no": "Ei",
"tags": {
"clone": "Klooni"
},
"dashboard": {
"title": "Juhtpaneel"
},
"printing": {
"generic": {
"title": "Trükkib",
"printerMarginBottom": "",
"verticalSpacing": "",
"rows": "",
"borders": {
"none": "",
"grid": "",
"border": ""
},
"printerMarginTop": "",
"deleteSettingsConfirm": "",
"deleteSettings": "",
"customSize": "",
"contentSettings": "",
"marginRight": "",
"printerMarginRight": "",
"itemCopies": "",
"helpPrinterMargin": "",
"settingsName": "",
"previewScale": "",
"helpMargin": "",
"marginTop": "",
"skipItems": "",
"newSetting": "",
"addSettings": "",
"layoutSettings": "",
"paperSize": "",
"columns": "",
"description": "",
"marginBottom": "",
"horizontalSpacing": "",
"duplicateSettings": "",
"settings": "",
"saveSetting": "",
"marginLeft": "",
"showBorder": "",
"saveAsImage": "",
"printerMarginLeft": "",
"dimensions": "",
"defaultSettings": "",
"print": ""
},
"qrcode": {
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
},
"showContent": "",
"showQRCodeMode": {
"withIcon": "",
"no": "",
"simple": ""
},
"templateHelp": "",
"textSize": "",
"showQRCode": "",
"template": "",
"title": "",
"button": ""
},
"spoolSelect": {
"selectedTotal_one": "",
"selectedTotal_other": "",
"description": "",
"showArchived": "",
"title": "",
"selectAll": "",
"noSpoolsSelected": ""
}
},
"vendor": {
"fields": {
"external_id": "",
"registered": "",
"comment": "",
"name": "",
"empty_spool_weight": "",
"id": ""
},
"fields_help": {
"empty_spool_weight": ""
},
"titles": {
"show_title": "",
"show": "",
"edit": "",
"clone": "",
"create": "",
"list": ""
},
"form": {
"vendor_updated": ""
},
"vendor": ""
},
"documentTitle": {
"vendor": {
"create": "",
"clone": "",
"edit": "",
"list": "",
"show": ""
},
"default": "",
"home": {
"list": ""
},
"spool": {
"edit": "",
"list": "",
"create": "",
"show": "",
"clone": ""
},
"filament": {
"list": "",
"show": "",
"clone": "",
"create": "",
"edit": ""
},
"suffix": "",
"locations": {
"list": ""
},
"help": {
"list": ""
}
},
"spool": {
"fields": {
"measured_weight": "",
"last_used": "",
"filament_internal": "",
"material": "",
"spool_weight": "",
"weight_to_use": "",
"lot_nr": "",
"filament_name": "",
"price": "",
"comment": "",
"filament": "",
"location": "",
"used_length": "",
"archived": "",
"remaining_length": "",
"registered": "",
"first_used": "",
"used_weight": "",
"id": "",
"remaining_weight": "",
"initial_weight": "",
"filament_external": ""
},
"titles": {
"archive": "",
"adjust": "",
"show": "",
"edit": "",
"create": "",
"clone": "",
"show_title": "",
"list": ""
},
"fields_help": {
"external_filament": "",
"used_weight": "",
"weight_to_use": "",
"measured_weight": "",
"price": "",
"spool_weight": "",
"lot_nr": "",
"remaining_weight": "",
"location": "",
"initial_weight": ""
},
"form": {
"spool_updated": "",
"measurement_type_label": "",
"adjust_filament_value": "",
"new_location_prompt": "",
"measurement_type": {
"weight": "",
"length": ""
},
"adjust_filament_help": ""
},
"spool": "",
"formats": {
"last_used": ""
},
"messages": {
"archive": ""
}
},
"filament": {
"form": {
"import_external": "",
"filament_updated": "",
"import_external_description": ""
},
"fields_help": {
"name": "",
"article_number": "",
"spool_weight": "",
"price": "",
"weight": "",
"multi_color_direction": "",
"material": ""
},
"fields": {
"material": "",
"vendor_name": "",
"settings_extruder_temp": "",
"comment": "",
"multi_color": "",
"diameter": "",
"weight": "",
"spool_weight": "",
"external_id": "",
"single_color": "",
"registered": "",
"spools": "",
"name": "",
"price": "",
"settings_bed_temp": "",
"vendor": "",
"density": "",
"longitudinal": "",
"id": "",
"color_hex": "",
"article_number": "",
"coaxial": ""
},
"titles": {
"clone": "",
"show_title": "",
"show": "",
"create": "",
"edit": "",
"list": ""
},
"buttons": {
"add_spool": ""
},
"filament": ""
},
"settings": {
"extra_fields": {
"non_unique_key_error": "",
"params": {
"unit": "",
"choices": "",
"default_value": "",
"field_type": "",
"multi_choice": "",
"order": "",
"name": "",
"key": ""
},
"boolean_true": "",
"choices_missing_error": "",
"boolean_false": "",
"delete_confirm_description": "",
"field_type": {
"boolean": "",
"float": "",
"choice": "",
"float_range": "",
"datetime": "",
"integer": "",
"integer_range": "",
"text": ""
},
"tab": "",
"description": "",
"delete_confirm": "",
"key_not_changed": ""
},
"general": {
"tab": "",
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
},
"currency": {
"label": ""
}
},
"header": "",
"settings": ""
},
"home": {
"welcome": "",
"home": "",
"description": ""
},
"scanner": {
"title": "",
"error": {
"insecureContext": "",
"notReadable": "",
"notAllowed": "",
"unknown": "",
"notFound": "",
"streamApiNotSupported": ""
},
"description": ""
},
"help": {
"resources": {
"vendor": "",
"filament": "",
"spool": ""
},
"description": "",
"help": ""
},
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
},
"table": {
"actions": ""
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"actions": {
"show": "نمایش",
"clone": "تکثیر",
"create": "اضافه کردن",
"list": "لیست",
"edit": "ویرایش"
},
"buttons": {
"save": "ذخیره",
"saveAndAdd": "ذخیره و اضافه کردن",
"cancel": "انصراف",
"refresh": "تازه سازی",
"show": "نمایش",
"showArchived": "نمایش آرشیو",
"notAccessTitle": "شما مجوز های لازم برای انجام این کار را ندارید",
"hideColumns": "مخفی کردن ستون",
"create": "اضافه کردن",
"logout": "خروج",
"edit": "ویرایش",
"delete": "حذف",
"confirm": "آیا از انجام این عمل اطمینان دارید؟",
"filter": "فیلتر",
"continue": "ادامه",
"clear": "پاک کردن",
"undo": "بازگشت به حالت قبلی",
"clone": "تکثیر",
"import": "وارد کردن",
"archive": "آرشیو کردن",
"unArchive": "درآوردن از آرشیو",
"hideArchived": "آرشیو مخفی",
"clearFilters": "حذف فیلتر ها"
},
"notifications": {
"success": "ثبت موفق",
"error": "خطای {{statusCode}} رخ داد",
"createSuccess": "{{resource}} با موفقیت ایجاد شد",
"deleteSuccess": "{{resource}} حذف شد",
"editError": "در زمان ویرایش {{resource}} خطای {{statusCode}} رخ داد",
"validationError": "خطای تایید اطلاعات {{error}}",
"undoable": "برای برگشت به حالت قبل {{seconds}} ثانیه فرصت دارید...",
"createError": "در زمان ساخت {{resource}} مشکل {{statusCode}} رخ داد",
"deleteError": "حذف {{resource}} با خطای {{statusCode}} مواجه شد",
"editSuccess": "{{resource}} با موفقیت ویرایش شد",
"importProgress": "در حال وارد کردن {{processed}} از مجموع {{total}}",
"saveSuccessful": "با موفقیت ثبت شد!"
},
"warnWhenUnsavedChanges": "از خارج شدن خود مطمئن هستید؟ موارد ذخیره نشده نادیده گرفته خواهند شد.",
"kofi": "حمایت از سازنده این برنامه",
"loading": "در حال بارگذاری",
"version": "ورژن",
"unknown": "نامشخص",
"yes": "بله",
"no": "خیر",
"tags": {
"clone": "تکثیر"
},
"dashboard": {
"title": "داشبورد"
},
"printing": {
"generic": {
"title": "در حال پرینت",
"description": "تنظیمات زیر را جوری تنظیم کنید که چیدمان پرینت لیبل مد نظرتان بدست بیاید. توجه کنید پرینتر و سیستم عامل شما ممکن است تنظیمات خودشان را لحاظ کنند، لذا ممکن است کمی آزمون و خطا نیاز باشد تا نتیجه مطلوب بدست بیاید. قبل اینکه لیبل نهایی را پرینت کنید ابتدا روی یک تکه کاغذ معمولی پرینت بگیرید و تست کنید.",
"helpMargin": "حاشیه‌ها باید با کاغذ برچسب و چاپگر شما هماهنگ شوند، تغییر دادن این تنظیمات بر اندازه کل شبکه تأثیر می‌گذارد.",
"print": "پرینت",
"columns": "ستون",
"rows": "سطر",
"paperSize": "اندازه کاغذ",
"customSize": "کاستوم",
"dimensions": "ابعاد",
"showBorder": "نمایش کناره ها",
"previewScale": "پیش نمایش اندازه",
"skipItems": "نادیده گرفتن یک آیتم",
"itemCopies": "تعداد کپی هر آیتم",
"contentSettings": "تنظیمات محتوی",
"layoutSettings": "تنظیمات چیدمان",
"horizontalSpacing": "فاصله افقی",
"verticalSpacing": "فاصله عمودی",
"marginLeft": "فاصله از چپ",
"marginRight": "فاصله از راست",
"printerMarginLeft": "منطقه ایمن از چپ",
"printerMarginRight": "منطقه ایمن از راست",
"printerMarginTop": "منطقه ایمن از بالا",
"printerMarginBottom": "منطقه ایمن از پایین",
"borders": {
"none": "هیچ کدام",
"border": "حاشیه",
"grid": "شبکه"
},
"settings": "تنظیمات پیشفرض",
"defaultSettings": "پیشفرض",
"addSettings": "اضافه کردن پیشفرض جدید",
"newSetting": "جدید",
"duplicateSettings": "تکثیر کردن",
"deleteSettings": "حذف پیشفرض فعلی",
"deleteSettingsConfirm": "از حذف این پیشفرض مطمئن هستید؟",
"settingsName": "اسم پیشفرض",
"saveSetting": "ذخیره کردن پیش فرض",
"saveAsImage": "ذخیره به عنوان عکس",
"helpPrinterMargin": "ناحیه امن باید بر اساس نزدیک‌ترین فاصله‌ای که چاپگر شما می‌تواند تا لبه کاغذ چاپ کند تنظیم شود. تغییر این تنظیمات بر کل شبکه تأثیری نخواهد داشت.",
"marginTop": "فاصله از بالا",
"marginBottom": "فاصله از پایین"
},
"qrcode": {
"button": "پرینت لیبل",
"title": "در حال پرینت لیبل",
"template": "قالب لیبل",
"textSize": "اندازه متن لیبل",
"useHTTPUrl": {
"label": "لینک بارکد QR",
"options": {
"default": "پیشفرض",
"url": "لینک"
},
"preview": "پیش نمایش:",
"tooltip": "از لینک اختصاصی استفاده خواهد شد که فقط در صورتی کار می‌کند که از ویژگی اسکن Spoolman اسکن شود (پیش‌فرض). آدرس URL یا از آدرس پایه‌ای که در تنظیمات مشخص شده استفاده می‌کند، یا در صورت عدم تنظیم، از آدرس صفحه فعلی."
},
"showContent": "پرینت کردن لیبل",
"showQRCode": "پرینت بارکد QR",
"showQRCodeMode": {
"no": "خیر",
"simple": "ساده",
"withIcon": "با آیکون"
},
"templateHelp": "از {} برای وارد کردن مقادیر مربوط به شیء spool به‌عنوان متن استفاده کنید. به‌عنوان مثال، {id} با شناسه spool جایگزین می‌شود، یا {filament.material} با جنس فیلامنت spool جایگزین خواهد شد. اگر مقداری وجود نداشته باشد، با \"?\" جایگزین می‌شود. می‌توانید از یک مجموعه دوم {} استفاده کنید تا این مورد حذف شود. همچنین، هر متنی که بین این دو مجموعه {} قرار گرفته باشد، اگر مقدار وجود نداشته باشد، حذف خواهد شد. به‌عنوان مثال، {Lot Nr: {lot_nr}} فقط زمانی نمایش داده می‌شود که spool دارای شماره lot باشد. برای برجسته‌کردن متن از دو علامت ** استفاده کنید. برای مشاهده لیست کامل برچسب‌ها روی دکمه کلیک کنید."
},
"spoolSelect": {
"title": "انتخاب رول فیلامنت",
"description": "انتخاب رول فیلامنتی که لیبل پرینت میکنید.",
"showArchived": "نمایش آرشیو",
"noSpoolsSelected": "هیچ رول فیلامنتی را انتخاب نکردید.",
"selectAll": "انتخاب/عدم انتخاب همه",
"selectedTotal_one": "{{count}} رول فیلامنت انتخاب شد",
"selectedTotal_other": "تعدا د {{count}} رول فیلامنت انتخاب شد"
}
},
"scanner": {
"title": "اسکنر بارکد QR",
"description": "یک بارکد QR از Spoolman را اسکن کنید تا جزئیات مربوط به spool را مشاهده کنید.",
"error": {
"notAllowed": "شما اجازه دسترسی به دوربین را نداده‌اید.",
"insecureContext": "این صفحه از طریق HTTPS ارائه نمی‌شود.",
"streamApiNotSupported": "مرورگر از API رسانه‌ای (MediaStream API) پشتیبانی نمی‌کند.",
"notReadable": "دوربین قابل خواندن نیست.",
"notFound": "دوربینی پیدا نشد.",
"unknown": "خطای نامشخص {{error}} رخ داد"
}
},
"spool": {
"spool": "رول های فیلامنت",
"fields": {
"id": "شماره",
"filament_name": "فیلامنت",
"filament": "فیلامنت",
"filament_internal": "داخلی",
"filament_external": "خارجی",
"price": "قیمت",
"material": "متریال",
"weight_to_use": "وزن",
"used_weight": "مقدار استفاده شده",
"remaining_weight": "مقدار باقی مانده",
"measured_weight": "مقدار اندازه گیری شده",
"used_length": "طول مصرف شده",
"remaining_length": "طول باقی مانده",
"location": "محل نگهداری",
"lot_nr": "شماره تولید",
"initial_weight": "وزن خالص رول کامل",
"spool_weight": "وزن قرقره خالی",
"first_used": "زمان اولین استفاده",
"last_used": "زمان آخرین استفاده",
"registered": "ثبت شده",
"comment": "توضیحات",
"archived": "آرشیو شده"
},
"fields_help": {
"price": "قیمت یک قرقره کامل. در صورت عدم تنظیم، قیمت فیلامنت در نظر گرفته می‌شود.",
"used_weight": "چقدر فیلامنت از قرقره استفاده شده است. یک قرقره جدید باید 0 گرم استفاده شده داشته باشد.",
"remaining_weight": "چقدر فیلامنت در قرقره باقی مانده است. برای یک قرقره جدید، این باید با وزن قرقره مطابقت داشته باشد.",
"measured_weight": "وزن فیلامنت و قرقره چقدر است.",
"location": "مکانی که قرقره در آن قرار دارد اگر چندین مکان برای ذخیره قرقره‌ها دارید.",
"lot_nr": "شماره بچ تولیدکننده. می‌تواند برای اطمینان از اینکه چاپ رنگ ثابتی دارد در صورتی که از چندین قرقره استفاده شود، استفاده شود.",
"weight_to_use": "وزن مورد نظر برای ورود را انتخاب کنید. وزن اندازه‌گیری‌شده فقط در صورتی در دسترس است که وزن قرقره برای فیلامنت انتخاب شده تنظیم شده باشد.",
"initial_weight": "وزن اولیه فیلامنت روی قرقره (وزن خالص). اگر تنظیم نشده باشد، از وزن شیء فیلامنت استفاده خواهد شد.",
"external_filament": "شما یک فیلامنت از پایگاه داده خارجی انتخاب کرده‌اید. یک شیء فیلامنت (و احتمالاً یک شیء تولیدکننده) به‌طور خودکار زمانی که این قرقره را ایجاد می‌کنید، ایجاد خواهد شد. این ممکن است منجر به ایجاد اشیاء فیلامنت تکراری شود اگر قبلاً یک شیء فیلامنت برای این فیلامنت ایجاد کرده باشید.",
"spool_weight": "وزن قرقره زمانی که خالی است. برای استفاده از مقدار فیلامنت یا تولیدکننده، این قسمت را خالی بگذارید."
},
"titles": {
"create": "ساخت رول فیلامنت",
"clone": "تکثیر رول فیلامنت",
"edit": "ویرایش رول فیلامنت",
"list": "رول های فیلامنت",
"show": "نمایش رول فیلامنت",
"show_title": "[رول شماره{{id}}] {{name}}",
"archive": "آرشیو کردن رول فلامنت",
"adjust": "تنظیم کردن مقدار باقی مانده"
},
"form": {
"measurement_type": {
"length": "طول",
"weight": "وزن"
},
"measurement_type_label": "اصلاح برحسب",
"adjust_filament_help": "اینجا می‌توانید به‌طور مستقیم فیلامنت را از قرقره کم یا اضافه کنید. یک مقدار مثبت فیلامنت را مصرف می‌کند و یک مقدار منفی آن را اضافه می‌کند.",
"adjust_filament_value": "مقدار مصرف شده",
"new_location_prompt": "محل نگهداری جدید",
"spool_updated": "این قرقره توسط شخص یا چیزی دیگر از زمانی که این صفحه را باز کرده‌اید، به‌روز شده است. ذخیره‌سازی تغییرات شما، آن تغییرات را نادیده می‌گیرد!"
},
"messages": {
"archive": "آیا مطمئن هستید که می‌خواهید این قرقره را بایگانی کنید؟"
},
"formats": {
"last_used": ""
}
},
"filament": {
"filament": "فیلامنت ها",
"fields": {
"id": "شماره",
"vendor_name": "تولید کننده",
"vendor": "تولید کننده",
"name": "عنوان",
"material": "متریال",
"price": "قیمت",
"density": "چگالی (Density)",
"weight": "وزن",
"diameter": "ضخامت",
"article_number": "شماره انحصاری تولید",
"spool_weight": "وزن قرقره فیلامنت",
"registered": "ثبت شده",
"comment": "توضیحات",
"settings_extruder_temp": "دمای اکسترودر",
"settings_bed_temp": "دمای هیت بد",
"single_color": "تک رنگ",
"multi_color": "تغییر رنگ",
"coaxial": "کوپلیمری",
"longitudinal": "طولی",
"external_id": "آیدی خارجی",
"spools": "نمایش رول فیلامنت ها",
"color_hex": "رنگ"
},
"fields_help": {
"material": "برای مثال PLA، ABS، PETG یا ...",
"price": "قیمت یک رول کامل.",
"spool_weight": "وزن یک قرقره خالی. برای تعیین وزن اندازه‌گیری‌شده یک قرقره استفاده می‌شود.",
"article_number": "برای مثال EAN/UPC یا ...",
"name": "نام فیلامنت، برای تمایز این نوع فیلامنت از سایرین از همان تولیدکننده. باید شامل رنگ نیز باشد.",
"weight": "وزن فیلامنت یک قرقره کامل (وزن خالص). این باید شامل وزن خود قرقره نباشد و فقط فیلامنت را شامل شود. معمولاً روی بسته‌بندی نوشته شده است.",
"multi_color_direction": "فیلامنت‌ها می‌توانند به دو روش دارای چندین رنگ باشند: یا از طریق کوپلیمری، مانند فیلامنت‌های دو رنگ با رنگ‌های چندگانه ثابت، یا از طریق تغییر رنگ طولی، مانند فیلامنت‌های گرادیانت که رنگ‌ها را در طول قرقره تغییر می‌دهند."
},
"titles": {
"create": "ساخت فیلامنت جدید",
"clone": "تکثیر فیلامنت",
"edit": "ویرایش فیلامنت",
"list": "فیلامنت ها",
"show": "نمایش فیلامنت",
"show_title": "[فیلامنت شماره {{id}}] {{name}}"
},
"form": {
"filament_updated": "این فیلامنت توسط شخص یا چیزی دیگر از زمانی که این صفحه را باز کرده‌اید، به‌روز شده است. ذخیره‌سازی تغییرات شما، آن تغییرات را نادیده می‌گیرد!",
"import_external": "درون ریزی از لیست خارجی",
"import_external_description": "یک فیلامنت را در لیست انتخاب کنید تا جزئیات آن به‌طور خودکار در فرم ایجاد فیلامنت پر شود. این عمل هر داده‌ای که در فرم وارد کرده‌اید را نادیده می‌گیرد.<br><br>یک شیء تولیدکننده به‌طور خودکار زمانی که روی \"تأیید\" کلیک کنید، در صورت نیاز ایجاد خواهد شد."
},
"buttons": {
"add_spool": "اضافه کردن رول فیلامنت"
}
},
"vendor": {
"vendor": "تولید کننده ها",
"fields": {
"id": "شماره",
"name": "اسم",
"external_id": "شماره خارجی",
"registered": "ثبت شده",
"comment": "توضیحات",
"empty_spool_weight": "وزن قرقره فیلامنت خالی"
},
"fields_help": {
"empty_spool_weight": "وزن یک قرقره خالی فیلامنت ارائه شده از تولید کننده."
},
"titles": {
"create": "تعریف تولید کننده جدید",
"clone": "تکثیر تولید کننده",
"edit": "ویرایش تولید کننده",
"list": "تولید کننده ها",
"show": "نمایش تولید کننده",
"show_title": "[تولید کننده شماره {{id}}] {{name}}"
},
"form": {
"vendor_updated": "این تولیدکننده توسط شخص یا چیزی دیگر از زمانی که این صفحه را باز کرده‌اید، به‌روز شده است. ذخیره‌سازی تغییرات شما، آن تغییرات را نادیده می‌گیرد!"
}
},
"home": {
"home": "صفحه اصلی",
"welcome": "",
"description": ""
},
"help": {
"help": "راهنما",
"resources": {
"filament": "برندهای فیلامنت. آن‌ها دارای ویژگی‌هایی مانند نام، جنس، رنگ، قطر و موارد دیگر هستند.",
"spool": "قرقره‌های فیزیکی جداگانه از یک فیلامنت خاص.",
"vendor": "شرکت‌هایی که فیلامنت تولید می‌کنند."
},
"description": "<title>راهنما</title><p>در اینجا چند نکته برای شروع کارتان آورده شده است.</p><p>Spoolman سه نوع مختلف داده را نگه‌داری می‌کند:</p><itemsHelp/><p>برای اضافه کردن یک قرقره جدید به پایگاه داده، ابتدا یک <filamentCreateLink>شیء فیلامنت</filamentCreateLink> ایجاد کنید. پس از انجام این کار، به ایجاد یک <spoolCreateLink>شیء قرقره</spoolCreateLink> برای آن قرقره خاص ادامه دهید. اگر بعداً قرقره‌های اضافی از همان فیلامنت به دست آوردید، به سادگی اشیاء قرقره اضافی را ایجاد کرده و از همان شیء فیلامنت استفاده کنید.</p><p>به‌صورت اختیاری، می‌توانید یک <vendorCreateLink>شیء تولیدکننده</vendorCreateLink> برای شرکتی که فیلامنت را تولید می‌کند ایجاد کنید، اگر بخواهید این اطلاعات را دنبال کنید.</p><p>شما این امکان را دارید که سایر خدمات چاپ سه‌بعدی را به Spoolman پیوند دهید، مانند Moonraker، که می‌تواند به‌طور خودکار استفاده از فیلامنت را نظارت کرده و اشیاء قرقره را برای شما به‌روزرسانی کند. برای راهنمایی در مورد نحوه تنظیم این موارد به <readmeLink>راهنمای Spoolman</readmeLink> مراجعه کنید.</p>"
},
"table": {
"actions": "اعمال قابل انجام"
},
"settings": {
"settings": "تنظیمات",
"header": "تنظیمات",
"general": {
"tab": "تنظیمات کلی",
"currency": {
"label": "ارز مورد استفاده"
},
"base_url": {
"tooltip": "آدرس پایگاه داده Spoolman که باید هنگام ایجاد ویژگی‌هایی مانند کدهای QR استفاده شود.",
"label": "لینک Spoolman"
},
"round_prices": {
"label": "",
"tooltip": ""
}
},
"extra_fields": {
"tab": "فیلد های اضافه",
"params": {
"key": "شاخصه کلیدی",
"name": "عنوان",
"unit": "واحد",
"field_type": "نوع",
"order": "ترتیب",
"default_value": "مقدار پیشفرض",
"choices": "آپشن های قابل استفاده",
"multi_choice": "قابلیت انتخاب چند آپشن همزمان"
},
"field_type": {
"text": "متن",
"integer": "عدد بدون اعشار",
"integer_range": "رنج عددی بدون اعشار",
"float": "عدد اعشاری",
"float_range": "رنج عددی اعشاری",
"datetime": "زمان و تاریخ",
"boolean": "Boolean",
"choice": "انتخاب"
},
"boolean_true": "بله",
"boolean_false": "خیر",
"choices_missing_error": "شما نمی‌توانید گزینه‌ها را حذف کنید. گزینه‌های زیر موجود نیستند: {{choices}}",
"non_unique_key_error": "شاخصه کلیدی باید منحصر به فرد باشد.",
"key_not_changed": "لطفاً شاخصه کلیدی را به چیز دیگری تغییر دهید.",
"delete_confirm": "آیا میخواهید {{name}} را حذف کنید؟",
"delete_confirm_description": "این کار فیلد و تمام داده‌های مرتبط با آن را برای همه موجودی‌ها حذف خواهد کرد.",
"description": "<p>در اینجا می‌توانید فیلدهای سفارشی اضافی را به موجودی‌های خود اضافه کنید.</p><p>پس از افزودن یک فیلد، نمی‌توانید کلید یا نوع آن را تغییر دهید و برای فیلدهای نوع انتخاب، نمی‌توانید گزینه‌ها را حذف یا وضعیت چندانتخابی را تغییر دهید. اگر یک فیلد را حذف کنید، داده‌های مرتبط با تمام موجودی‌ها حذف خواهد شد.</p><p>کلید، اطلاعاتی است که برنامه‌های دیگر آن را می‌خوانند یا می‌نویسند، بنابراین اگر فیلد سفارشی شما قرار است با یک برنامه شخص ثالث ادغام شود، اطمینان حاصل کنید که به درستی تنظیم شده باشد. مقدار پیش‌فرض فقط بر روی اقلام جدید اعمال می‌شود.</p><p>فیلدهای اضافی نمی‌توانند در نماهای جدول مرتب یا فیلتر شوند.</p>"
}
},
"documentTitle": {
"default": "اسپول من",
"suffix": " | اسپول من",
"home": {
"list": "صفحه اصلی | اسپول من"
},
"help": {
"list": "راهنما | اسپول من"
},
"filament": {
"list": "فیلامنت ها | اسپول من",
"show": "نمایش فیلامنت شماره {{id}} | اسپول من",
"edit": "ویرایش فیلامنت شماره {{id}} | اسپول من",
"create": "ساخت فیلامنت جدید | اسپول من",
"clone": "تکثیر فیلامنت شماره {{id}} | اسپول من"
},
"spool": {
"list": "رول های فیلامنت | اسپول من",
"show": "نمایش رول فیلامنت شماره {{id}} | اسپول من",
"edit": "ویرایش رول فیلامنت شماره {{id}} | اسپول من",
"create": "ساخت رول فیلامنت جدید | اسپول من",
"clone": "تکثیر رول فیلامنت شماره {{id}} | اسپول من"
},
"vendor": {
"list": "تولید کنندگان فیلامنت | اسپول من",
"edit": "ویرایش تولید کننده فیلامنت شماره {{id}} | اسپول من",
"show": "نمایش تولید کننده فیلامنت شماره {{id}} | اسپول من",
"create": "تعریف کردن تولید کننده فیلامنت جدید | اسپول من",
"clone": "تکثیر تولید کننده فیلامنت شماره {{id}} | اسپول من"
},
"locations": {
"list": ""
}
},
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
}
}
+185 -52
View File
@@ -4,26 +4,43 @@
"extruderTemp": "ET : {{temp}}",
"showSpoolWeight": "Poids de la bobine",
"showContent": "Imprimer l'étiquette",
"showVendorComment": "Commentaire sur le vendeur",
"showVendorComment": "Commentaire sur le fabricant",
"spoolWeight": "Poids de la bobine : {{weight}}",
"showTemperatures": "Températures",
"showVendor": "Vendeur",
"showVendor": "Fabricant",
"showSpoolComment": "Commentaire sur la bobine",
"textSize": "Contenu Taille du texte",
"title": "Impression de codes QR",
"textSize": "Taille du texte de l'étiquette",
"title": "Impression d'étiquettes",
"showSpoolmanIcon": "Montrer l'icône Spoolman",
"showLotNr": "N° de Lot",
"bedTemp": "BT : {{temp}}",
"lotNr": "N° de Lot : {{lot}}",
"showFilamentComment": "Commentaire sur le filament",
"button": "Imprimer des codes QR"
"button": "Imprimer des étiquettes",
"showQRCode": "Imprimer le QR Code",
"templateHelp": "Utilisez {} pour insérer les valeurs de l'objet bobine sous forme de texte. Par exemple, {id} sera remplacé par l'identifiant de la bobine ou {filament.material} sera remplacé par le matériau de la bobine. Si une variable n'existe pas elle sera remplacée par \"?\". Un deuxième jeu d'accolade peut empêcher cet affichage. De plus si une accolade est utilisée dans une autre tout le texte présenta dans la première accolade sera retiré si la variable est manquante. Par exemple {N° lot : {lot_nr}} sera affiché uniquement si la bobine a un numéro de lot. Entourez le texte d'un double astérisque ** pour le mettre en gras. Cliquez sur le bouton pour afficher une liste de toutes les balises disponibles.",
"template": "Modèle d'étiquette",
"showQRCodeMode": {
"no": "Non",
"simple": "Simple",
"withIcon": "Avec icône"
},
"useHTTPUrl": {
"tooltip": "Utilise un lien privé qui ne marchera que si il est scanné depuis Spoolman (défaut). URL utilises soit l'URL spécifiée dans les options soit l'URL de la page actuelle si ce n'est pas défini.",
"options": {
"default": "Défaut",
"url": "URL"
},
"preview": "Prévisualisation :",
"label": "Lien du QR code"
}
},
"generic": {
"printerMarginBottom": "Zone de sécurité en bas",
"verticalSpacing": "Espacement vertical",
"rows": "Rangs",
"rows": "Lignes",
"borders": {
"none": "Aucun",
"none": "Aucune",
"grid": "Grille",
"border": "Bordure"
},
@@ -35,26 +52,37 @@
"printerMarginRight": "Zone de sécurité à droite",
"helpPrinterMargin": "La zone de sécurité doit être définie en fonction de la proximité du bord du papier que votre imprimante peut imprimer, la modification de ces paramètres n'affectera pas l'ensemble de la grille.",
"previewScale": "Échelle de prévisualisation",
"helpMargin": "Les marges doivent être configurées en fonction de votre papier à étiquettes et de votre imprimante, car leur modification aura une incidence sur la taille de l'ensemble de la grille.",
"helpMargin": "Les marges doivent être configurées en fonction de votre papier à étiquettes et de votre imprimante, leur modification aura une incidence sur la taille de l'ensemble de la grille.",
"marginTop": "Marge supérieure",
"skipItems": "Passer les articles",
"skipItems": "Articles ignorés",
"layoutSettings": "Paramètres de mise en page",
"paperSize": "Format du papier",
"columns": "Colonnes",
"description": "Ajustez les paramètres ci-dessous pour obtenir la mise en page d'impression souhaitée. Gardez à l'esprit que les imprimantes et votre système d'exploitation peuvent appliquer leurs propres marges et mises à l'échelle, il se peut donc que vous deviez faire quelques essais et erreurs avant que tout soit correct. Testez-le sur une feuille de papier ordinaire avant d'imprimer les étiquettes proprement dites.",
"description": "Ajustez les paramètres ci-dessous pour obtenir la mise en page d'impression souhaitée. Gardez à l'esprit que les imprimantes et votre système d'exploitation peuvent appliquer leurs propres marges et mises à l'échelle, il se peut donc que vous deviez faire quelques essais avant que tout soit correct. Testez-le sur une feuille de papier ordinaire avant d'imprimer les étiquettes en elles mêmes.",
"marginBottom": "Marge inférieure",
"horizontalSpacing": "Espacement horizontal",
"marginLeft": "Marge gauche",
"showBorder": "Afficher les bords",
"printerMarginLeft": "Zone de sécurité à gauche",
"dimensions": "Dimensions",
"print": "Imprimer"
"print": "Imprimer",
"settings": "Préréglages",
"defaultSettings": "Défaut",
"addSettings": "Ajouter un nouveau préréglage",
"newSetting": "Nouveau",
"deleteSettings": "Supprimer le préréglage actuel",
"deleteSettingsConfirm": "Êtes-vous sûr de vouloir supprimer ce préréglage ?",
"settingsName": "Nom du préréglage",
"saveAsImage": "Enregistrer comme image",
"itemCopies": "Nombre d'articles",
"duplicateSettings": "Dupliquer",
"saveSetting": "Enregistrer les préréglages"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} bobine sélectionnée",
"selectedTotal_many": "{{count}} bobines sélectionnées",
"selectedTotal_other": "{{count}} bobines sélectionnées",
"description": "Sélectionnez les bobines pour lesquelles vous souhaitez imprimer des codes QR.",
"description": "Sélectionnez les bobines pour lesquelles imprimer des étiquettes.",
"showArchived": "Afficher les archives",
"title": "Sélectionner les bobines",
"selectAll": "Sélectionner/Désélectionner tout",
@@ -63,11 +91,11 @@
},
"documentTitle": {
"vendor": {
"create": "Créer le Vendeur | Spoolman",
"clone": "#{{id}} Cloner le Vendeur | Spoolman",
"edit": "#{{id}} Editer le Vendeur | Spoolman",
"list": "Vendeurs | Spoolman",
"show": "#{{id}} Afficher le vendeur | Spoolman"
"create": "Créer le fabricant | Spoolman",
"clone": "#{{id}} Cloner le fabricant | Spoolman",
"edit": "#{{id}} Éditer le fabricant| Spoolman",
"list": "Fabricants | Spoolman",
"show": "#{{id}} Afficher le fabricant | Spoolman"
},
"default": "Spoolman",
"spool": {
@@ -90,6 +118,9 @@
},
"help": {
"list": "Aide | Spoolman"
},
"locations": {
"list": "Emplacements | Spoolman"
}
},
"spool": {
@@ -109,9 +140,13 @@
"registered": "Enregistré",
"first_used": "Première utilisation",
"used_weight": "Poids utilisé",
"id": "ID",
"id": "Identifiant",
"remaining_weight": "Poids restant",
"price": ""
"price": "Prix",
"initial_weight": "Poids initial",
"spool_weight": "Poids à vide",
"filament_internal": "Interne",
"filament_external": "Externe"
},
"titles": {
"archive": "Archiver la bobine",
@@ -120,7 +155,8 @@
"create": "Créer une bobine",
"clone": "Cloner une bobine",
"list": "Bobines",
"show_title": "[Bobine #{{id}}] {{name}}"
"show_title": "[Bobine #{{id}}] {{name}}",
"adjust": "Ajuster la bobine de filament"
},
"fields_help": {
"used_weight": "Combien de filament a été utilisé à partir de la bobine. Une bobine neuve devrait avoir 0g utilisé.",
@@ -129,7 +165,10 @@
"lot_nr": "Numéro de lot du fabricant. Peut être utilisé pour s'assurer que la couleur d'une impression est homogène si plusieurs bobines sont utilisées.",
"remaining_weight": "La quantité de filament restant sur la bobine. Pour une nouvelle bobine, cela devrait correspondre au poids de la bobine.",
"location": "L'emplacement de la bobine si vous stockez vos bobines à plusieurs endroits.",
"price": ""
"price": "Prix d'une bobine complète. S'il n'est pas défini, le prix du filament sera pris en compte.",
"initial_weight": "Le poids initial du filament sur la bobine (poids net). Utilisera le poids de l'objet filament s'il n'est pas défini.",
"spool_weight": "Le poids de la bobine lorsqu'elle est vide. Laissez vide pour utiliser la valeur du filament ou du fabricant.",
"external_filament": "Vous avez sélectionné un filament dans la base de données externe. Un objet filament (et éventuellement un objet fabricant) sera créé automatiquement lorsque vous créerez cette bobine. Cela peut créer des objets filament en double si vous avez déjà créé un objet filament pour ce filament."
},
"spool": "Bobines",
"messages": {
@@ -137,7 +176,17 @@
},
"form": {
"spool_updated": "Cette bobine a été mise à jour par quelqu'un/quelque chose depuis la dernière ouverture de la page. Enregistrer écrasera ces changements !",
"new_location_prompt": "Entrer une nouvelle localisation"
"new_location_prompt": "Entrer une nouvelle localisation",
"measurement_type": {
"length": "Longueur",
"weight": "Poids"
},
"measurement_type_label": "Type de mesure",
"adjust_filament_help": "Ici, vous pouvez directement ajouter ou soustraire du filament à la bobine. Une valeur positive consommera du filament, une valeur négative en ajoutera.",
"adjust_filament_value": "Quantité consommée"
},
"formats": {
"last_used": "Dernière utilisation {{date}}"
}
},
"buttons": {
@@ -147,35 +196,37 @@
"filter": "Filtre",
"cancel": "Abandonner",
"edit": "Editer",
"showArchived": "Voir les archives",
"showArchived": "Afficher les archives",
"archive": "Archiver",
"hideColumns": "Masquer les colonnes",
"confirm": "êtes-vous sur ?",
"confirm": "Êtes-vous sur ?",
"delete": "Supprimer",
"undo": "Annuler",
"import": "Importer",
"logout": "Déconnexion",
"refresh": "Rafraichir",
"notAccessTitle": "Vous n'avez pas l'autorisation d'accéder",
"hideArchived": "Cacher les archives",
"notAccessTitle": "Vous n'avez pas l'autorisation d'accès",
"hideArchived": "Masquer les archives",
"clearFilters": "Effacer les filtres",
"unArchive": "Désarchiver",
"clone": "Cloner",
"clear": "Effacer",
"saveAndAdd": "Sauvegarder et ajouter"
"saveAndAdd": "Sauvegarder et ajouter",
"continue": "Continuer"
},
"filament": {
"fields_help": {
"name": "Nom du filament, pour distinguer ce type de filament parmi d'autres du même fournisseur. Il doit contenir la couleur, par exemple.",
"name": "Nom du filament, pour distinguer ce type de filament parmi d'autres du même fabricant. Devrait contenir la couleur par exemple.",
"article_number": "Exemple EAN, UPC, etc.",
"spool_weight": "Le poids d'une bobine vide. Utilisé pour déterminer le poids d'une bobine.",
"price": "Prix d'une bobine complète dans la devise configurée par le système.",
"price": "Prix d'une bobine complète.",
"weight": "Le poids du filament d'une bobine complète (poids net). Ce poids ne doit pas inclure le poids de la bobine elle-même, mais uniquement celui du filament. C'est ce qui est généralement indiqué sur l'emballage.",
"material": "Exemple PLA, ABS, PETG, etc."
"material": "Exemple PLA, ABS, PETG, etc.",
"multi_color_direction": "Les filaments peuvent avoir plusieurs couleurs de deux manières : soit par coextrusion, comme les filaments bicolores avec plusieurs couleurs uniformes, soit par des changements de couleur longitudinaux, comme les filaments dégradés qui changent de couleurs le long de la bobine."
},
"fields": {
"material": "Matériau",
"vendor_name": "Vendeur",
"vendor_name": "Fabricant",
"settings_extruder_temp": "Température de l'extrudeuse",
"comment": "Commentaire",
"diameter": "Diamètre",
@@ -185,12 +236,17 @@
"name": "Nom",
"price": "Prix",
"settings_bed_temp": "Température du lit",
"vendor": "Vendeur",
"vendor": "Fabricant",
"density": "Densité",
"id": "ID",
"id": "Identifiant",
"color_hex": "Couleur",
"article_number": "Numéro d'article",
"spools": "Afficher les bobines"
"spools": "Afficher les bobines",
"external_id": "ID externe",
"single_color": "Unique",
"multi_color": "Multi",
"longitudinal": "Longitudinale",
"coaxial": "Coextrudée"
},
"titles": {
"clone": "Cloner un filament",
@@ -202,7 +258,12 @@
},
"filament": "Filaments",
"form": {
"filament_updated": "Ce filament a été mis à jour par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !"
"filament_updated": "Ce filament a été mis à jour par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !",
"import_external": "Importer depuis externe",
"import_external_description": "Sélectionnez un filament dans la liste pour renseigner automatiquement ses détails dans le formulaire de création de filament. Cela écrasera toutes les données que vous avez saisies dans le formulaire.<br><br>Un objet fabricant sera créé automatiquement lorsque vous cliquerez sur Ok, si nécessaire."
},
"buttons": {
"add_spool": "Ajouter une bobine"
}
},
"actions": {
@@ -230,32 +291,39 @@
"editError": "Erreur lors de l'édition {{resource}} (status code : {{statusCode}})",
"undoable": "Vous avez {{seconds}} secondes pour annuler.",
"importProgress": "Importation : {{processed}}/{{total}}",
"createError": "Une erreur s'est produite lors de la création de {{resource}}(status code : {{statusCode}})",
"createError": "Une erreur s'est produite lors de la création de {{resource}} (status code : {{statusCode}})",
"success": "Réussite",
"editSuccess": "Édité avec succès {{resource}}",
"deleteError": "Erreur lors de la suppression de {{resource}} (status code : {{statusCode}})",
"createSuccess": "Création réussie de {{resource}}"
"createSuccess": "Création réussie de {{resource}}",
"saveSuccessful": "Sauvegarde réussie !",
"validationError": "Erreur de validation : {{error}}"
},
"no": "Non",
"unknown": "Inconnu",
"vendor": {
"titles": {
"show": "Afficher le vendeur",
"edit": "Editer un Vendeur",
"clone": "Cloner un Vendeur",
"create": "Créer un Vendeur",
"list": "Vendeurs",
"show_title": "[Vendeur #{{id}}] {{name}}"
"show": "Afficher le fabricant",
"edit": "Éditer un fabricant",
"clone": "Cloner un fabricant",
"create": "Créer un fabricant",
"list": "Fabricants",
"show_title": "[#{{id}} fabricant] {{name}}"
},
"fields": {
"registered": "Enregistré",
"comment": "Commentaire",
"name": "Nom",
"id": "ID"
"id": "Identifiant",
"empty_spool_weight": "Poids de la bobine vide",
"external_id": "ID externe"
},
"vendor": "Vendeurs",
"vendor": "Fabricants",
"form": {
"vendor_updated": "Ce vendeur a été mis à jours par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !"
"vendor_updated": "Ce fabricant a été mis à jours par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !"
},
"fields_help": {
"empty_spool_weight": "Le poids d'une bobine vide de ce fabricant."
}
},
"yes": "Oui",
@@ -263,7 +331,7 @@
"table": {
"actions": "Actions"
},
"warnWhenUnsavedChanges": "Êtes-vous sûr de vouloir quitter ? Vous avez des changements non sauvés.",
"warnWhenUnsavedChanges": "Êtes-vous sûr de vouloir quitter ? Vous avez des changements non sauvegardés.",
"loading": "Chargement",
"dashboard": {
"title": "Tableau de bord"
@@ -273,15 +341,80 @@
},
"help": {
"resources": {
"vendor": "Les compagnies qui fabriquent le filament.",
"vendor": "Les sociétés qui fabriquent le filament.",
"filament": "Marques de filament. Contient les propriétés comme le nom, le matériau, la couleur, le diamètre, etc.",
"spool": "Les bobines physique pour un filament spécifique."
"spool": "Les bobines physiques pour un filament spécifique."
},
"description": "<title>Aide</title><p>Voici quelques informations pour commencer.</p><p>Spoolman garde 3 types d'informations différentes :</p><itemsHelp/><p>Pour ajouter une nouvelle bobine dans la base de donnée, commencer par créer un objet <filamentCreateLink>Filament</filamentCreateLink>. Une fois cela fait, créez un objet <spoolCreateLink>bobine</spoolCreateLink> pour cette bobine spécifique. Si vous achetez plus tard des bobines supplémentaire du même filament, créez simplement un objet bobine supplémentaire et réutilisez le même objet Filament.</p><p>En option, vous pouvez créer un <vendorCreateLink>Vendeur</vendorCreateLink> pour la compagnie qui fabrique le filament si vous voulez garder une trace de cette information.</p><p>Vous avez l'option de connecter d'autres service d'impression 3D à Spoolman, comme Moonraker, pour qu'ils puissent suivre automatiquement votre utilisation de filament et mettre automatiquement à jour l'objet Bobine pour vous. Voir le <readmeLink>README Spoolman</readmeLink> pour avoir la marche à suivre pour le mettre en place.</p>",
"description": "<title>Aide</title><p>Voici quelques informations pour commencer.</p><p>Spoolman garde 3 types d'informations différentes :</p><itemsHelp/><p>Pour ajouter une nouvelle bobine dans la base de données, commencez par créer un objet <filamentCreateLink>Filament</filamentCreateLink>. Une fois cela fait, créez un objet <spoolCreateLink>bobine</spoolCreateLink> pour cette bobine spécifique. Si vous achetez plus tard des bobines supplémentaires du même filament, créez simplement un objet bobine supplémentaire et réutilisez le même objet Filament.</p><p>En option, vous pouvez créer un <vendorCreateLink>Fabricant</vendorCreateLink> pour la société qui fabrique le filament si vous voulez garder une trace de cette information.</p><p>Vous avez l'option de connecter d'autres service d'impression 3D à Spoolman, comme Moonraker, pour qu'ils puissent suivre automatiquement votre utilisation de filament et mettre automatiquement à jour l'objet Bobine pour vous. Voir le <readmeLink>README Spoolman</readmeLink> pour avoir la marche à suivre pour le mettre en place.</p>",
"help": "Aide"
},
"kofi": "Me donner un pourboire sur Ko-fi",
"home": {
"home": "Accueil"
}
"home": "Accueil",
"welcome": "Bienvenue sur votre instance Spoolman !",
"description": "Il semble que vous n'ayez pas encore ajouté de bobines. Consultez la <helpPageLink>page d'aide</helpPageLink> pour bien démarrer."
},
"settings": {
"settings": "Paramètres",
"extra_fields": {
"params": {
"choices": "Choix",
"multi_choice": "Choix multiple",
"default_value": "Valeur par défaut",
"key": "Clé",
"name": "Nom",
"field_type": "Type",
"unit": "Unité",
"order": "Ordre"
},
"field_type": {
"text": "Texte",
"choice": "Choix",
"integer": "Entier",
"float_range": "Plage décimal",
"datetime": "Date et heure",
"boolean": "Booléen",
"integer_range": "Plage d'entier",
"float": "Décimal"
},
"tab": "Champs additionnel",
"boolean_true": "Oui",
"boolean_false": "Non",
"non_unique_key_error": "La clé doit être unique.",
"delete_confirm_description": "Ceci supprimera le champ ainsi que toutes les données associées pour toutes les entités.",
"choices_missing_error": "Vous ne pouvez pas retirer les choix. Les choix suivants sont manquants : {{choices}}",
"key_not_changed": "Changer la clé pour quelque chose d'autre.",
"delete_confirm": "Supprimer le champ {{name}} ?",
"description": "<p>Ici vous pouvez ajouter des champs personnalisés supplémentaires à vos entités.</p><p>Une fois qu'un champ est ajouté, vous ne pouvez plus changer sa clé ou son type, et pour les champs de types \"Choix\", vous ne pouvez plus retirer un choix ou changer l'état des choix multiples. Si vous supprimez un champ, les données associées aux entités seront supprimées.</p><p>La clé correspond à ce que les autres programmes lisent/écrivent, donc si votre champs doit s'intégrer avec un programme tiers, assurez-vous de le configurer correctement. La valeur par défaut n'est appliquées qu'aux nouveaux éléments.</p><p>Les champs supplémentaires peuvent être affichés, triés ou filtrés dans les vues de table. Les champs de type \"Choix\" et booléen offrent également des options de filtrage, et tous les types de champs supplémentaires prennent en charge le filtrage des valeurs vides. </p>",
"choices_reorder_hint": "Glissez, ou utilisez les touches fléchées, pour réorganiser ce choix."
},
"header": "Paramètres",
"general": {
"currency": {
"label": "Devise"
},
"tab": "Général",
"base_url": {
"label": "Base de l'URL",
"tooltip": "La base de l'URL à utiliser lors de la génération de QR codes."
},
"round_prices": {
"label": "Arrondir les prix",
"tooltip": "Arrondir les prix au nombre entier le plus proche."
}
}
},
"locations": {
"locations": "Emplacements",
"new_location": "Créer un emplacement",
"no_location": "Pas de localisation",
"no_locations_help": "Cette page vous permet d'organiser vos bobines par lieu, ajoutez une bobine pour commencer !"
},
"theme": {
"label": "Thème",
"system": "Système",
"light": "Clair",
"dark": "Sombre"
},
"sponsor": "Soutenez-moi sur GitHub"
}
+399
View File
@@ -0,0 +1,399 @@
{
"actions": {
"list": "Suchi",
"create": "Banaye",
"edit": "Sampadan Karen",
"show": "Dikhayen",
"clone": "Clone"
},
"buttons": {
"create": "Banaye",
"save": "Sahejen",
"edit": "Sampadan Karen",
"confirm": "Kya aapko pakka hai?",
"continue": "Aage badhen",
"show": "Dikhayen",
"filter": "",
"cancel": "",
"showArchived": "",
"archive": "",
"hideColumns": "",
"delete": "",
"undo": "",
"import": "",
"logout": "",
"refresh": "",
"notAccessTitle": "",
"saveAndAdd": "",
"hideArchived": "",
"clearFilters": "",
"unArchive": "",
"clone": "",
"clear": ""
},
"vendor": {
"fields": {
"external_id": "",
"registered": "",
"comment": "",
"name": "",
"empty_spool_weight": "",
"id": ""
},
"fields_help": {
"empty_spool_weight": ""
},
"titles": {
"show_title": "",
"show": "",
"edit": "",
"clone": "",
"create": "",
"list": ""
},
"form": {
"vendor_updated": ""
},
"vendor": ""
},
"documentTitle": {
"vendor": {
"create": "",
"clone": "",
"edit": "",
"list": "",
"show": ""
},
"default": "",
"home": {
"list": ""
},
"spool": {
"edit": "",
"list": "",
"create": "",
"show": "",
"clone": ""
},
"filament": {
"list": "",
"show": "",
"clone": "",
"create": "",
"edit": ""
},
"suffix": "",
"locations": {
"list": ""
},
"help": {
"list": ""
}
},
"spool": {
"fields": {
"measured_weight": "",
"last_used": "",
"filament_internal": "",
"material": "",
"spool_weight": "",
"weight_to_use": "",
"lot_nr": "",
"filament_name": "",
"price": "",
"comment": "",
"filament": "",
"location": "",
"used_length": "",
"archived": "",
"remaining_length": "",
"registered": "",
"first_used": "",
"used_weight": "",
"id": "",
"remaining_weight": "",
"initial_weight": "",
"filament_external": ""
},
"titles": {
"archive": "",
"adjust": "",
"show": "",
"edit": "",
"create": "",
"clone": "",
"show_title": "",
"list": ""
},
"fields_help": {
"external_filament": "",
"used_weight": "",
"weight_to_use": "",
"measured_weight": "",
"price": "",
"spool_weight": "",
"lot_nr": "",
"remaining_weight": "",
"location": "",
"initial_weight": ""
},
"form": {
"spool_updated": "",
"measurement_type_label": "",
"adjust_filament_value": "",
"new_location_prompt": "",
"measurement_type": {
"weight": "",
"length": ""
},
"adjust_filament_help": ""
},
"spool": "",
"formats": {
"last_used": ""
},
"messages": {
"archive": ""
}
},
"filament": {
"form": {
"import_external": "",
"filament_updated": "",
"import_external_description": ""
},
"fields_help": {
"name": "",
"article_number": "",
"spool_weight": "",
"price": "",
"weight": "",
"multi_color_direction": "",
"material": ""
},
"fields": {
"material": "",
"vendor_name": "",
"settings_extruder_temp": "",
"comment": "",
"multi_color": "",
"diameter": "",
"weight": "",
"spool_weight": "",
"external_id": "",
"single_color": "",
"registered": "",
"spools": "",
"name": "",
"price": "",
"settings_bed_temp": "",
"vendor": "",
"density": "",
"longitudinal": "",
"id": "",
"color_hex": "",
"article_number": "",
"coaxial": ""
},
"titles": {
"clone": "",
"show_title": "",
"show": "",
"create": "",
"edit": "",
"list": ""
},
"buttons": {
"add_spool": ""
},
"filament": ""
},
"printing": {
"generic": {
"printerMarginBottom": "",
"verticalSpacing": "",
"rows": "",
"borders": {
"none": "",
"grid": "",
"border": ""
},
"printerMarginTop": "",
"deleteSettingsConfirm": "",
"deleteSettings": "",
"customSize": "",
"title": "",
"contentSettings": "",
"marginRight": "",
"printerMarginRight": "",
"itemCopies": "",
"helpPrinterMargin": "",
"settingsName": "",
"previewScale": "",
"helpMargin": "",
"marginTop": "",
"skipItems": "",
"newSetting": "",
"addSettings": "",
"layoutSettings": "",
"paperSize": "",
"columns": "",
"description": "",
"marginBottom": "",
"horizontalSpacing": "",
"duplicateSettings": "",
"settings": "",
"saveSetting": "",
"marginLeft": "",
"showBorder": "",
"saveAsImage": "",
"printerMarginLeft": "",
"dimensions": "",
"defaultSettings": "",
"print": ""
},
"qrcode": {
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
},
"showContent": "",
"showQRCodeMode": {
"withIcon": "",
"no": "",
"simple": ""
},
"templateHelp": "",
"textSize": "",
"showQRCode": "",
"template": "",
"title": "",
"button": ""
},
"spoolSelect": {
"selectedTotal_one": "",
"selectedTotal_other": "",
"description": "",
"showArchived": "",
"title": "",
"selectAll": "",
"noSpoolsSelected": ""
}
},
"settings": {
"extra_fields": {
"non_unique_key_error": "",
"params": {
"unit": "",
"choices": "",
"default_value": "",
"field_type": "",
"multi_choice": "",
"order": "",
"name": "",
"key": ""
},
"boolean_true": "",
"choices_missing_error": "",
"boolean_false": "",
"delete_confirm_description": "",
"field_type": {
"boolean": "",
"float": "",
"choice": "",
"float_range": "",
"datetime": "",
"integer": "",
"integer_range": "",
"text": ""
},
"tab": "",
"description": "",
"delete_confirm": "",
"key_not_changed": ""
},
"general": {
"tab": "",
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
},
"currency": {
"label": ""
}
},
"header": "",
"settings": ""
},
"home": {
"welcome": "",
"home": "",
"description": ""
},
"scanner": {
"title": "",
"error": {
"insecureContext": "",
"notReadable": "",
"notAllowed": "",
"unknown": "",
"notFound": "",
"streamApiNotSupported": ""
},
"description": ""
},
"help": {
"resources": {
"vendor": "",
"filament": "",
"spool": ""
},
"description": "",
"help": ""
},
"notifications": {
"deleteSuccess": "",
"error": "",
"editError": "",
"saveSuccessful": "",
"undoable": "",
"importProgress": "",
"createError": "",
"validationError": "",
"success": "",
"editSuccess": "",
"deleteError": "",
"createSuccess": ""
},
"no": "",
"unknown": "",
"yes": "",
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
},
"version": "",
"kofi": "",
"table": {
"actions": ""
},
"warnWhenUnsavedChanges": "",
"loading": "",
"dashboard": {
"title": ""
},
"tags": {
"clone": ""
}
}
+158 -33
View File
@@ -9,14 +9,31 @@
"showTemperatures": "Hőmérséklet",
"showVendor": "Gyártó",
"showSpoolComment": "Orsó Megjegyzés",
"textSize": "Tartalom szöveg mérete",
"title": "QR kód nyomtatása",
"textSize": "Cimke Szövegméret",
"title": "Cimkenyomtatás",
"showSpoolmanIcon": "Spoolman ikon megjelenítése",
"showLotNr": "Tételszám",
"bedTemp": "BT: {{temp}}",
"lotNr": "Tételszám: {{lot}}",
"showFilamentComment": "Filament Komment",
"button": "QR kódok nyomtatása"
"button": "Cimkék Nyomtatása",
"useHTTPUrl": {
"options": {
"default": "Alapbeállítás",
"url": "URL"
},
"preview": "Előnézet:",
"tooltip": "",
"label": "QR kód link"
},
"showQRCodeMode": {
"withIcon": "Iconnal",
"no": "Nem",
"simple": "Egyszerű"
},
"templateHelp": "",
"showQRCode": "QR kód nyomtatása",
"template": "Cimke Sablon"
},
"generic": {
"printerMarginBottom": "Biztonságos zóna alja",
@@ -48,15 +65,26 @@
"showBorder": "Szélek megjelenítése",
"printerMarginLeft": "Biztonságos zóna balra",
"dimensions": "Dimenziók",
"print": "Nyomtat"
"print": "Nyomtat",
"deleteSettingsConfirm": "Biztos vagy benne, hogy törölni szeretnéd ezt a beállítást?",
"deleteSettings": "Jelenlegi Beállítás Törlése",
"itemCopies": "Termék másolatok",
"settingsName": "Preset neve",
"newSetting": "Új",
"addSettings": "Új beállítás hozzáadása",
"duplicateSettings": "Duplikáció",
"settings": "Beállítások",
"saveSetting": "Beállítás Mentése",
"saveAsImage": "Mentés Képként",
"defaultSettings": "Alapbeállítás"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} tekercs kiválasztva",
"selectedTotal_other": "{{count}} db tekercs kiválasztva",
"description": "Válassza ki a QR-kódok nyomtatásához szükséges tekercseket.",
"description": "Válassza ki a tekercseket a cimkék nyomtatásához.",
"showArchived": "Archivált megjelenítése",
"title": "Tekercs kiválasztása",
"selectAll": "Összes kijelölése/kijelölésének törlése",
"selectAll": "Összes kijelölése/kijelölés törlése",
"noSpoolsSelected": "Nem választotál ki egyetlen tekercset sem."
}
},
@@ -85,16 +113,19 @@
},
"suffix": " | Spoolman",
"home": {
"list": ""
"list": "Kezdőlap | Spoolman"
},
"help": {
"list": ""
"list": "Segítség | Spoolman"
},
"locations": {
"list": "Tároló helyek | Spoolman"
}
},
"spool": {
"fields": {
"measured_weight": "Mért súly",
"last_used": "Utolsó használat",
"last_used": "Utoljára használt",
"material": "Anyag",
"weight_to_use": "Súly",
"lot_nr": "Tételszám",
@@ -106,11 +137,15 @@
"archived": "Arhivált",
"remaining_length": "Fennmaradó hossz",
"registered": "Nyilvántartott",
"first_used": "Első használat",
"first_used": "Először használt",
"used_weight": "Felhasznált súly",
"id": "ID",
"remaining_weight": "Hátralévő súly",
"price": ""
"remaining_weight": "Maradék súly",
"price": "Ár",
"filament_internal": "Belső",
"spool_weight": "Üres súly",
"initial_weight": "Kezdeti súly",
"filament_external": "Külső"
},
"titles": {
"archive": "Arhivált tekercs",
@@ -119,7 +154,8 @@
"create": "Tekercs létrehozása",
"clone": "Tekercs másolása",
"show_title": "[Tekercs #{{id}}] {{name}}",
"list": "Tekercs"
"list": "Tekercs",
"adjust": "Tekercs Szál Módosítása"
},
"fields_help": {
"used_weight": "Mennyi filament használódott el a tekercsről. Egy új tekercsnek 0 gramm használt mennyiséggel kell rendelkeznie.",
@@ -128,15 +164,28 @@
"lot_nr": "Gyártói tételszám. A szín egységesség ellenőrzésére használható, ha több tekercset használsz egy nyomtatáshoz.",
"remaining_weight": "Mennyi filament van még a tekercsen. Egy új tekercsnél ez a tekercs súlyával kell megegyeznie.",
"location": "Hol található a tekercs, ha több helyen tárolod a tekercseidet.",
"price": ""
"price": "Egy teljes tekercs ára. Ha nincs beállítva, a rendszer az filament árát veszi fel helyette.",
"external_filament": "A kiválasztott tekercs egy külső adatbázisból származik. A szál elem(és valószínűleg a gyártó elem) automatikusan létrehozásra kerül amikor ezt a tekercset létrehozza. Ez okozhat szál duplikációt, ha ez a szál korábban már létre lett hozva.",
"spool_weight": "Az üres tekercs súlya. Hagyja üresen a szál vagy a gyártó értékének a használatához.",
"initial_weight": "A tekercsen lévő filament kezdeti súlya. Ha nincs beállítva, a filament rekord súlya lesz használva."
},
"form": {
"new_location_prompt": "Add meg az új helyet",
"spool_updated": "Ez a tekercs azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!"
"spool_updated": "Ezt a spoolot valaki / valami más frissítette, mivel megnyitotta ezt az oldalt. A megtakarítás felülírja ezeket a változásokat!",
"measurement_type_label": "Mérési típus",
"adjust_filament_value": "",
"measurement_type": {
"weight": "Súly",
"length": "Hossz"
},
"adjust_filament_help": ""
},
"spool": "Tekercs",
"spool": "Tekercsek",
"messages": {
"archive": "Biztosan archiválni szeretné ezt a tekercset?"
},
"formats": {
"last_used": "Utolsó használat {{date}}"
}
},
"buttons": {
@@ -161,16 +210,18 @@
"unArchive": "Arhiválás visszavonása",
"clone": "Másolás",
"clear": "Törlés",
"saveAndAdd": "Mentés és hozzáadás"
"saveAndAdd": "Mentés és hozzáadás",
"continue": "Folytatás"
},
"filament": {
"fields_help": {
"name": "Filament neve, hogy megkülönböztesd ezt a filament típust másoktól ugyanattól a gyártótól. Tartalmazhatja például a színét.",
"article_number": "Például. EAN, UPC stb.",
"spool_weight": "Az üres tekercs súlya. Ezt használják a tekercs mért súlyának meghatározásához.",
"price": "A teljes tekercs ára a konfigurált pénznemben a rendszerben.",
"price": "A teljes tekercs ára.",
"weight": "A teljes tekercs súlya (nettó súly), amely nem tartalmazza a tekercs saját súlyát, csak a filament súlyát. Általában ez szerepel a csomagoláson.",
"material": "Például. PLA, ABS, PETG stb."
"material": "Például. PLA, ABS, PETG stb.",
"multi_color_direction": ""
},
"fields": {
"material": "Anyag",
@@ -189,7 +240,12 @@
"id": "ID",
"color_hex": "Szín",
"article_number": "Cikkszám",
"spools": "Tekercsek megjelenítése"
"spools": "Tekercsek megjelenítése",
"multi_color": "",
"external_id": "Külső ID",
"single_color": "",
"longitudinal": "",
"coaxial": ""
},
"titles": {
"clone": "Filament másolása",
@@ -199,9 +255,14 @@
"edit": "Filament szerkesztése",
"list": "Filamentek"
},
"filament": "Filamentek",
"filament": "Szálak",
"form": {
"filament_updated": "Ez a szál anyag azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!"
"filament_updated": "Ez a szál anyag azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!",
"import_external": "Importálás külső forrásból",
"import_external_description": ""
},
"buttons": {
"add_spool": "Tekercs hozzáadása"
}
},
"actions": {
@@ -224,11 +285,16 @@
"registered": "Nyilvántartott",
"comment": "Megjegyzés",
"name": "Név",
"id": "ID"
"id": "ID",
"external_id": "Külső ID",
"empty_spool_weight": "Üres tekercs súlya"
},
"vendor": "Gyártók",
"form": {
"vendor_updated": "Ez a szállító azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!"
"vendor_updated": "Ez a gyártó azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!"
},
"fields_help": {
"empty_spool_weight": "Ettől a gyártótól származó üres tekercs súlya."
}
},
"scanner": {
@@ -236,7 +302,7 @@
"error": {
"insecureContext": "Az oldal kiszolgálása nem HTTPS-en keresztül történik.",
"notReadable": "A kamera nem használható.",
"notAllowed": "Nincs jováhagyva a számodra a kamera használata.",
"notAllowed": "Nem engedélyezte a kamerához való hozzáférést.",
"unknown": "Ismeretlen hiba történt. ({{error}})",
"notFound": "Nem található kamera.",
"streamApiNotSupported": "A böngésző nem támogatja a MediaStream API-t."
@@ -253,7 +319,9 @@
"success": "Sikeres",
"editSuccess": "Sikeresen módosítva {{resource}}",
"deleteError": "Hiba a(z) {{resource}} törlésekor (hibakód: {{statusCode}})",
"createSuccess": "Sikeresen létrehozva {{resource}}"
"createSuccess": "Sikeresen létrehozva {{resource}}",
"saveSuccessful": "Sikeres mentés!",
"validationError": "Validációs hiba:{{error}}"
},
"no": "Nem",
"unknown": "Ismeretlen",
@@ -270,17 +338,74 @@
"tags": {
"clone": "Másolás"
},
"kofi": "",
"kofi": "Tipp a Ko-fi-ra",
"home": {
"home": ""
"home": "Kezdő lap",
"welcome": "Üdv a Spoolmanodban!",
"description": "Úgy tűnik, még nem hoztál létre tekercset. Lásd a <helpPageLink>Help page </helpPageLink> a kezdeti lépésekhez."
},
"help": {
"resources": {
"spool": "",
"vendor": "",
"filament": ""
"spool": "Egyedi fizikai tekercs egy adott filamentből.",
"vendor": "Azok a cégek, amelyek gyártják a filamentet.",
"filament": "Filament márkája. Olyan tulajdonságokkal rendelkeznek, mint a név, az anyag, a szín, az átmérő stb."
},
"help": "",
"description": ""
"help": "Segítség",
"description": "<title>Segítség</title><p>Íme néhány tipp a kezdéshez.</p><p>A Spoolman 3 különböző adattípust tartalmaz:</p><itemsHelp/><p>Ha új tekercs szeretne hozzáadni az adatbázishoz, kezdje azzal, hogy létrehoz egy <filamentCreateLink>Filament</filamentCreateLink> objektumot. Amikor ezzel megvan, folytassa azzal, hogy létrehoz egy <spoolCreateLink>Tekercs</spoolCreateLink> objektumot az adott tekercs. Ha később további tekercsket szerez ugyanabból a filamentből, egyszerűen generáljon további Tekercs objektumokat, és újra használja ugyanazt a Filament objektumot.</p><p>Opcionálisan létrehozhat egy <vendorCreateLink>Gyártó</vendorCreateLink> objektumot is a filamentet gyártó vállalathoz, ha szeretné követni ezt az információt.</p><p>A Spoolmanhez más 3D-nyomtató szolgáltatásokat is csatlakoztathat, például a Moonrakert, amely automatikusan figyelemmel kíséri a filamenthasználatot, és frissíti az Tekercs objektumokat. Olvassa el a <readmeLink>Spoolman README</readmeLink> fájlt a beállításokkal kapcsolatos útmutatásért.</p>"
},
"settings": {
"settings": "Beállítások",
"extra_fields": {
"params": {
"choices": "Választás",
"multi_choice": "Több kiválasztása",
"key": "Kulcs",
"name": "Név",
"field_type": "Típus",
"unit": "Egység",
"order": "Rendezés",
"default_value": "Alapértelmezet érték"
},
"field_type": {
"text": "Szöveg",
"integer": "Egész szám",
"datetime": "Dátum és idő",
"float": "Lebegőpontos szám",
"integer_range": "Egész szám tartománya",
"float_range": "Lebegőpontos szám tartomány",
"choice": "Választás",
"boolean": "Logikai"
},
"key_not_changed": "Kérlek változtasd meg a kulcsot valami másra.",
"boolean_false": "Nem",
"tab": "Extra mezők",
"boolean_true": "Igen",
"choices_missing_error": "Nem tudod eltávolítani a választásokat. A következő választások hiányoznak: {{choices}}",
"non_unique_key_error": "A kulcsnak egyedinek kell lennie.",
"delete_confirm": "Töröli a következő mezőt: {{name}}?",
"delete_confirm_description": "Ez a mező törlése és az összes entitáshoz tartozó adat törlése.",
"description": "<p>Itt hozzáadhatsz további egyedi mezőket az entitásokhoz.</p><p>Ha egy mezőt hozzáadtál, nem lehet megváltoztatni annak kulcsát vagy típusát, és a választási típusú mezők esetében nem lehet eltávolítani a választásokat vagy megváltoztatni a többválasztásos állapotot. Ha eltávolítasz egy mezőt, az összes entitásra vonatkozó adat törlődik.</p><p>A kulcs az, amit más programok olvasnak/írnak az adatokként, tehát ha az egyedi meződ egy harmadik féllel integrálódik, győződj meg róla, hogy helyesen van beállítva. Az alapértelmezett érték csak az új elemekre vonatkozik.</p><p>Az extra mezőket nem lehet rendezni vagy szűrni a táblázatnézetekben.</p>"
},
"header": "Beállítások",
"general": {
"currency": {
"label": "Pénznem"
},
"tab": "Általános",
"base_url": {
"tooltip": "",
"label": "Alap URL"
},
"round_prices": {
"label": "Árak kerekítése",
"tooltip": "Az árak legközelebbi egész számra való kerekítése."
}
}
},
"locations": {
"new_location": "Új hely",
"no_location": "Nincs hely megadva",
"locations": "Helyek",
"no_locations_help": "Ez az oldal lehetővé teszi, hogy rendszerezd a tekercseket helyek szerint, adj hozzá néhány tekercset a kezdéshez!"
}
}
+155 -30
View File
@@ -9,14 +9,31 @@
"showTemperatures": "Temperature",
"showVendor": "Produttore",
"showSpoolComment": "Note aggiuntive Bobina",
"textSize": "Dimensioni testo del contenuto",
"title": "Stampando QR-Codes",
"textSize": "Dimensioni testo dell'etichetta",
"title": "Stampando etichetta",
"showSpoolmanIcon": "Mostra Icona Spoolman",
"showLotNr": "Lotto Nr",
"bedTemp": "BT: {{temp}}",
"lotNr": "Lotto Nr: {{lot}}",
"showFilamentComment": "Note Aggiuntive Filamento",
"button": "Stampa QR-Codes"
"button": "Stampa etichette",
"template": "Modello di etichetta",
"templateHelp": "Utilizza {} per inserire i valori della bobina nel testo. Ad esempio, {id} verrà sostituito con l'id della bobina, oppure {filament.material} verrà sostituito con il materiale della bobina. Racchiudi il testo con un doppio asterisco ** per renderlo in grassetto. Fai clic sul pulsante per visualizzare un elenco di tutte le etichette disponibili.",
"showQRCodeMode": {
"no": "No",
"simple": "Semplice",
"withIcon": "Con icona"
},
"showQRCode": "Stampa QR Code",
"useHTTPUrl": {
"label": "Link codice QR",
"tooltip": "Verrà utilizzato un link che funzionerà solo se scansionato dalla funzione di scansione di Spoolman (impostazione predefinita). L'URL utilizza l'URL di base specificato nelle impostazioni o l'URL della pagina corrente, se non impostato.",
"options": {
"default": "Preimpostato",
"url": "URL"
},
"preview": "Anteprima:"
}
},
"generic": {
"printerMarginBottom": "Safe-Zone Inferiore",
@@ -48,13 +65,24 @@
"showBorder": "Mostra Bordi",
"printerMarginLeft": "Safe-Zone Sinistra",
"dimensions": "Dimensioni",
"print": "Stampa"
"print": "Stampa",
"settings": "Impostazioni",
"defaultSettings": "Predefinito",
"addSettings": "Aggiungi nuova impostazione",
"newSetting": "Nuovo",
"deleteSettings": "Elimina impostazione corrente",
"deleteSettingsConfirm": "Sei sicuro di voler eliminare questa impostazione?",
"settingsName": "Nome impostazione",
"itemCopies": "Copie dell'elemento",
"saveAsImage": "Salva come immagine",
"duplicateSettings": "Duplica",
"saveSetting": "Salva impostazione"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} bobina selezionata",
"selectedTotal_many": "{{count}} bobine selezionate",
"selectedTotal_other": "{{count}} bobine selezionate",
"description": "Seleziona le bobine per le quali stampare i QR-Codes.",
"description": "Seleziona le bobine per le quali stampare le etichette.",
"showArchived": "Mostra Archiviati",
"title": "Seleziona Bobina",
"selectAll": "Seleziona/Deseleziona Tutti",
@@ -64,9 +92,9 @@
"documentTitle": {
"vendor": {
"create": "Crea Produttore | Spoolman",
"clone": "#{{id}} Clona Produttore | Spoolman",
"clone": "#{{id}} Duplica Produttore | Spoolman",
"edit": "#{{id}} Modifica Produttore | Spoolman",
"list": "Produttore | Spoolman",
"list": "Produttori | Spoolman",
"show": "#{{id}} Mostra Produttore | Spoolman"
},
"default": "Spoolman",
@@ -86,10 +114,13 @@
},
"suffix": " | Spoolman",
"home": {
"list": ""
"list": "Home | Spoolman"
},
"help": {
"list": ""
"list": "Aiuto | Spoolman"
},
"locations": {
"list": "Posizione | Spoolman"
}
},
"spool": {
@@ -111,7 +142,11 @@
"used_weight": "Peso Utilizzato",
"id": "ID",
"remaining_weight": "Peso Rimanente",
"price": ""
"price": "Prezzo",
"filament_external": "Esterno",
"filament_internal": "Interno",
"spool_weight": "Peso a vuoto/tara",
"initial_weight": "Peso iniziale"
},
"titles": {
"archive": "Archivia Bobina",
@@ -120,7 +155,8 @@
"create": "Crea Bobina",
"clone": "Clona Bobina",
"show_title": "[Bobina #{{id}}] {{name}}",
"list": "Bobine"
"list": "Bobine",
"adjust": "Regola il filamento della bobina"
},
"fields_help": {
"used_weight": "Quanto filamento è stato utilizzato dalla bobina. Una nuova bobina dovrebbe avere 0 gr utilizzati.",
@@ -129,15 +165,28 @@
"lot_nr": "Numero di lotto del produttore. Può essere utilizzato per garantire che la stampa abbia colori uniformi se vengono utilizzate più bobine.",
"remaining_weight": "Quanto filamento è rimasto sulla bobina. Per una nuova bobina questo dovrebbe corrispondere al peso della bobina.",
"location": "Dove si trova la bobina se disponi di più posizioni in cui conservare le bobine.",
"price": ""
"price": "Prezzo di una bobina completa. Se non viene impostato, verrà utilizzato il prezzo del filamento.",
"spool_weight": "Il peso della bobina vuota (tara). Lasciare vuoto per usare il valore dal profilo filamento o dal profilo produttore.",
"external_filament": "Hai selezionato un filamento da un database esterno. Un profilo filamento (e possibilmente anche un profilo produttore) verrà creato automaticamente quando crei questa bobina. Ciò potrebbe creare un profilo filamento duplicato se hai già creato un profilo per questo filamento.",
"initial_weight": "Peso iniziale del filamento sulla bobina (peso netto). Se non impostato, viene utilizzato il peso dal profilo del filamento."
},
"form": {
"new_location_prompt": "Inserisci nuova posizione",
"spool_updated": "Ci sono stati degli aggiornamenti per questa bobina da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!"
"spool_updated": "Ci sono stati degli aggiornamenti per questa bobina da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!",
"measurement_type": {
"length": "Lunghezza",
"weight": "Peso"
},
"adjust_filament_value": "Quantità di consumo",
"measurement_type_label": "Tipo di misurazione",
"adjust_filament_help": "Qui puoi aggiungere o sottrarre direttamente filamento dalla bobina. Un valore positivo consumerà filamento, un valore negativo lo aggiungerà."
},
"spool": "Bobine",
"messages": {
"archive": "Sei sicuro di voler archiviare questa bobina?"
},
"formats": {
"last_used": "Ultimo utilizzo {{date}}"
}
},
"buttons": {
@@ -162,16 +211,18 @@
"unArchive": "Ripristina",
"clone": "Clona",
"clear": "Pulisci",
"saveAndAdd": "Salva e aggiungi"
"saveAndAdd": "Salva e aggiungi",
"continue": "Continua"
},
"filament": {
"fields_help": {
"name": "Nome del filamento, per distinguere questo tipo di filamento dagli altri dello stesso produttore. Dovrebbe contenere il colore, ad esempio.",
"article_number": "Es.: EAN, UPC, etc.",
"spool_weight": "Il peso di una bobina vuota. Utilizzato per determinare il peso misurato di una bobina.",
"price": "Prezzo di una bobina intera nella valuta configurata dal sistema.",
"price": "Prezzo di una bobina piena.",
"weight": "Il peso del filamento di una bobina piena (peso netto). Questo non dovrebbe includere il peso della bobina stessa, ma solo del filamento. È quello che solitamente c'è scritto sulla confezione.",
"material": "Es.: PLA, ABS, PETG, etc."
"material": "Es.: PLA, ABS, PETG, etc.",
"multi_color_direction": "I filamenti possono avere più colori in due modi: attraverso la coestrusione, come i filamenti a due colori con più colori omogenei, o attraverso cambiamenti di colore longitudinali, come i filamenti a gradiente che cambiano colore lungo la bobina."
},
"fields": {
"material": "Materiale",
@@ -184,13 +235,18 @@
"registered": "Registrato",
"name": "Nome",
"price": "Prezzo",
"settings_bed_temp": "Temperatura Bed",
"settings_bed_temp": "Temperatura Piatto",
"vendor": "Produttore",
"density": "Densità",
"id": "ID",
"color_hex": "Colore",
"article_number": "Numero Articolo",
"spools": "Mostra bobine"
"spools": "Mostra bobine",
"external_id": "ID esterno",
"single_color": "Unico",
"multi_color": "Multiplo",
"coaxial": "Coestruso",
"longitudinal": "Longitudinale"
},
"titles": {
"clone": "Clona Filamento",
@@ -202,7 +258,12 @@
},
"filament": "Filamenti",
"form": {
"filament_updated": "Ci sono stati degli aggiornamenti per questo filamento da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!"
"filament_updated": "Ci sono stati degli aggiornamenti per questo filamento da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!",
"import_external": "Importa da Esterno",
"import_external_description": "Seleziona un filamento dalla lista per compilare automaticamente i dettagli del modulo creazione filamento. Ciò sovrascriverà qualsiasi informazione hai precedentemente inserito nel modulo. <rb><br> Se necessario, verrà creato automaticamente un profilo Produttore quando clicchi Ok."
},
"buttons": {
"add_spool": "Aggiungi Bobina"
}
},
"actions": {
@@ -217,7 +278,7 @@
"show_title": "[Produttore #{{id}}] {{name}}",
"show": "Mostra Produttori",
"edit": "Modifica Produttore",
"clone": "Clona Produttore",
"clone": "Duplica Produttore",
"create": "Crea Produttore",
"list": "Produttori"
},
@@ -225,11 +286,16 @@
"registered": "Registrato",
"comment": "Note",
"name": "Nome",
"id": "ID"
"id": "ID",
"external_id": "ID Esterno",
"empty_spool_weight": "Peso a vuoto della bobina/tara"
},
"vendor": "Produttori",
"form": {
"vendor_updated": "Ci sono stati degli aggiornamenti per questo produttore da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!"
"vendor_updated": "Ci sono stati degli aggiornamenti per questo produttore da quando hai aperto questa pagina. Il salvataggio sovrascriverà le modifiche!"
},
"fields_help": {
"empty_spool_weight": "Il peso di una bobina vuota di questo produttore."
}
},
"scanner": {
@@ -254,7 +320,9 @@
"success": "Riuscito",
"editSuccess": "{{resource}} modificata con successo",
"deleteError": "Error nella cancellazione di {{resource}} (status code: {{statusCode}})",
"createSuccess": "{{resource}} creata correttamente"
"createSuccess": "{{resource}} creata correttamente",
"saveSuccessful": "Salvataggio completato!",
"validationError": "Errore di convalida: {{error}}"
},
"no": "No",
"unknown": "Sconosciuto",
@@ -272,16 +340,73 @@
"clone": "Clona"
},
"help": {
"description": "",
"description": "<title>Aiuto</title><p>Ecco alcuni suggerimenti per iniziare.</p><p>Spoolman può contenere 3 diversi tipi di dati:</p><itemsHelp/><p>Per aggiungere una nuova bobina al database, inizia creando una sezione <filamentCreateLink>filamento</filamentCreateLink>. Fatto ciò, procedi con la creazione di una sezione <spoolCreateLink>bobina</spoolCreateLink> per la questa specifica bobina. Se in seguito si acquisiscono altre bobine dello stesso filamento, sarà sufficiente generare altre sezioni bobina e riutilizzare la stessa sezione filamento.</p><p>Opzionalmente, si può anche creare una sezione <vendorCreateLink>produttore</vendorCreateLink> per l'azienda produttrice del filamento, se si desidera tenere traccia di tali informazioni.</p><p>È possibile collegare a Spoolman altri servizi per stampanti 3D, come Moonraker, che può monitorare l'utilizzo del filamento e aggiornare le sezioni Spool automaticamente. Fare riferimento al <readmeLink>README di Spoolman</readmeLink> per maggiori informazioni sulla configurazione.</p>",
"resources": {
"spool": "",
"filament": "",
"vendor": ""
"spool": "Bobine fisiche individuali di uno specifico filamento.",
"filament": "Marche di filamento. Hanno proprietà come nome, materiale, colore, diametro e altro.",
"vendor": "Le aziende che producono il filamento."
},
"help": ""
"help": "Aiuto"
},
"kofi": "",
"kofi": "Donazione su Ko-fi",
"home": {
"home": ""
"home": "Home",
"welcome": "Benvenuto nella tua istanza Spoolman!",
"description": "Sembra che tu non abbia ancora aggiunto alcuna bobina. Vai su <helpPageLink>Help page</helpPageLink> per ricevere assistenza su come iniziare."
},
"settings": {
"settings": "Configurazione",
"extra_fields": {
"params": {
"order": "Ordine",
"default_value": "Valore di Default",
"key": "Chiave",
"name": "Nome",
"field_type": "Tipo",
"choices": "Scelte",
"multi_choice": "Scelta multipla",
"unit": "Unità"
},
"field_type": {
"text": "\"Testo\" per tipo campo extra",
"datetime": "Data e ora",
"choice": "Scelta",
"float": "Decimali",
"integer_range": "Intervallo di numeri interi",
"float_range": "Intervallo decimali",
"boolean": "Booleano",
"integer": "\"Intero\" per tipo campo extra, numero intero senza decimali"
},
"choices_missing_error": "Non è possibile rimuovere le scelte. Mancano i seguenti elementi: {{choices}}",
"non_unique_key_error": "La chiave deve essere univoca.",
"key_not_changed": "Si prega di utilizzare una chiave diversa.",
"boolean_true": "Si",
"boolean_false": "No",
"delete_confirm": "Cancellare il campo {{name}}?",
"delete_confirm_description": "Questo eliminerà il campo e tutti i dati associati di tutte le selezioni.",
"tab": "Campi extra",
"description": "<p>Qui è possibile aggiungere campi personalizzati.</p><p>Una volta aggiunto un campo, non è possibile modificarne la chiave o il tipo, e per i campi di tipo scelta non è possibile rimuovere le scelte o modificare lo stato di scelta multipla. Se si rimuove un campo, tutti i dati associati saranno cancellati.</p><p>La chiave è il modo in cui gli altri programmi leggono/scrivono i dati, quindi se il campo personalizzato deve integrarsi con un programma di terze parti, assicurarsi di impostarla correttamente. Il valore predefinito viene applicato solo ai nuovi elementi.</p><p>I campi extra non possono essere ordinati o filtrati nelle viste tabella.</p>"
},
"header": "Configurazione",
"general": {
"tab": "Generale",
"currency": {
"label": "Valuta"
},
"base_url": {
"label": "URL di base",
"tooltip": "L'URL di base da utilizzare quando si generano funzioni come i codici QR."
},
"round_prices": {
"label": "Prezzi arrotondati",
"tooltip": "Prezzi arrotondati al numero intero più vicino."
}
}
},
"locations": {
"locations": "Posizioni",
"new_location": "Nuova Posizione",
"no_location": "Nessuna Posizione",
"no_locations_help": "Questa pagina ti consente di organizzare le bobine in base alla posizione. Aggiungi alcune bobine per iniziare!"
}
}
+398
View File
@@ -0,0 +1,398 @@
{
"notifications": {
"undoable": "取り消しまで残り{{seconds}}秒",
"editSuccess": "{{resource}}の編集が完了",
"success": "完了",
"error": "エラー (status code: {{statusCode}})",
"createSuccess": "{{resource}}の作成が完了",
"deleteSuccess": "{{resource}}の削除が完了",
"editError": "{{resource}}の編集中にエラーが発生しました (status code: {{statusCode}})",
"importProgress": "インポート中: {{processed}}/{{total}}",
"saveSuccessful": "保存完了!",
"createError": "{{resource}}を作成中にエラーが発生しました。 (status code: {{statusCode}})",
"validationError": "検証エラー: {{error}}",
"deleteError": "{{resource}}を削除中にエラーが発生しました (status code: {{statusCode}})"
},
"actions": {
"create": "作成",
"edit": "編集",
"clone": "複製",
"list": "リスト",
"show": "表示"
},
"buttons": {
"archive": "アーカイブ",
"saveAndAdd": "保存して追加",
"create": "作成",
"edit": "編集",
"cancel": "キャンセル",
"confirm": "本当によろしいですか?",
"continue": "続ける",
"filter": "フィルター",
"clear": "クリア",
"refresh": "再読込",
"show": "表示",
"undo": "元に戻す",
"hideArchived": "アーカイブを隠す",
"showArchived": "アーカイブを表示",
"unArchive": "非アーカイブ",
"hideColumns": "コラムを隠す",
"notAccessTitle": "アクセス権限がありません",
"save": "保存",
"logout": "ログアウト",
"delete": "削除",
"clone": "複製",
"clearFilters": "フィルタをクリア",
"import": "インポート"
},
"printing": {
"generic": {
"verticalSpacing": "垂直方向の余白",
"printerMarginTop": "上側のセーフゾーン",
"deleteSettings": "現在のプリセットを削除",
"itemCopies": "複製",
"print": "印刷",
"paperSize": "用紙サイズ",
"customSize": "カスタム",
"showBorder": "線を表示",
"previewScale": "スケールを表示",
"skipItems": "スキップ",
"horizontalSpacing": "水平方向の余白",
"marginLeft": "左側の余白",
"marginBottom": "下側の余白",
"printerMarginBottom": "下側のセーフゾーン",
"borders": {
"none": "なし",
"grid": "グリッド",
"border": "線"
},
"settingsName": "プリセット名",
"saveSetting": "プリセットを保存",
"saveAsImage": "画像として保存",
"columns": "列",
"marginRight": "右側の余白",
"defaultSettings": "デフォルト",
"title": "プリント中",
"description": "以下の設定を調整して、希望する印刷レイアウトを得てください。プリンターやOSが独自のマージンや拡大縮小を適用する場合があるため、すべてが正確になるまでには試行錯誤が必要かもしれません。実際のラベルに印刷する前に、普通の紙でテストしてください。",
"helpMargin": "マージンはラベル用紙とプリンターに合わせて設定する必要があります。これらを変更すると、グリッド全体のサイズに影響します。",
"settings": "プリセット",
"helpPrinterMargin": "セーフゾーンは、プリンターが用紙の端にどれだけ近づけて印刷できるかの設定です。これらを変更しても、グリッド全体には影響しません。",
"dimensions": "寸法",
"duplicateSettings": "複製",
"contentSettings": "コンテンツ設定",
"marginTop": "上側の余白",
"printerMarginRight": "右側のセーフゾーン",
"layoutSettings": "レイアウト設定",
"addSettings": "新しいプリセットを追加",
"deleteSettingsConfirm": "本当にこのプリセットを削除しますか?",
"newSetting": "新規",
"rows": "行",
"printerMarginLeft": "左側のセーフゾーン"
},
"qrcode": {
"button": "ラベルを印刷",
"title": "ラベル印刷",
"template": "ラベルテンプレート",
"textSize": "ラベルのテキストサイズ",
"useHTTPUrl": {
"options": {
"default": "デフォルト",
"url": "URL"
},
"preview": "プレビュー:",
"label": "QRコードリンク",
"tooltip": "Spoolmanのスキャン機能からスキャンした場合にのみ機能する独自リンクを使用します(デフォルト)。URLは設定で指定されたベースURLを使用するか、設定されていない場合は現在のページURLを使用します。"
},
"showQRCode": "QRコード印刷",
"showContent": "ラベル印刷",
"templateHelp": "値をテキストとして挿入するには{}を使用します。例えば、{id}はスプールIDに置き換えられ、{filament.material}はスプールの材質に置き換えられます。値が欠けている場合は「?」に置き換えられます。二重の{}を使用して、この置換を削除することができます。{}のセット間のテキストは、値が欠けている場合に削除されます。例えば、{Lot Nr: {lot_nr}}は、スプールにロット番号がある場合にのみラベルを表示します。テキストを太字にするには、二重のアスタリスク**で囲みます。利用可能なすべてのタグのリストを表示するには、ボタンをクリックしてください。",
"showQRCodeMode": {
"no": "いいえ",
"withIcon": "アイコン付き",
"simple": "シンプル"
}
},
"spoolSelect": {
"description": "スプールのラベル印刷対象を選択してください。",
"showArchived": "アーカイブを表示",
"selectAll": "すべてを選択 / 選択解除",
"title": "スプールを選択",
"selectedTotal_other": "{{count}}本のスプールを選択",
"noSpoolsSelected": "スプールが選択されていません。"
}
},
"warnWhenUnsavedChanges": "本当に離れますか? 編集が保存されていません。",
"kofi": "Ko-fiでチップをください",
"unknown": "未知",
"version": "バージョン",
"loading": "読み込み中",
"no": "いいえ",
"dashboard": {
"title": "ダッシュボード"
},
"yes": "はい",
"tags": {
"clone": "複製"
},
"scanner": {
"description": "スプールの詳細を表示するには、SpoolmanのQRコードをスキャンしてください。",
"error": {
"notReadable": "カメラを読み込むことができません。",
"notFound": "カメラが見つかりません。",
"unknown": "未知のエラーが発生しました。({{error}})",
"insecureContext": "このページはHTTPSで接続されていません。",
"streamApiNotSupported": "ブラウザがMediaStream APIに対応していません。",
"notAllowed": "カメラへのアクセスが許可されていません。"
},
"title": "QRコードスキャナ"
},
"spool": {
"fields": {
"filament_internal": "内部",
"filament_external": "外部",
"price": "価格",
"remaining_weight": "残りの重さ",
"measured_weight": "計測した重さ",
"lot_nr": "ロットナンバー",
"first_used": "最初の使用",
"last_used": "最後の使用",
"comment": "コメント",
"material": "マテリアル",
"weight_to_use": "重さ",
"registered": "登録",
"archived": "アーカイブ",
"used_length": "使用した長さ",
"initial_weight": "最初の重さ",
"spool_weight": "空の重さ",
"location": "場所",
"filament": "フィラメント",
"used_weight": "使用した重さ",
"remaining_length": "残りの長さ",
"id": "ID",
"filament_name": "フィラメント"
},
"fields_help": {
"price": "スプール全体の価格。設定されていない場合は、フィラメントの価格が代わりに使用されます。",
"measured_weight": "フィラメントとスプールの重量。",
"location": "スプールを複数の場所に保管している場合、そのスプールの保管場所。",
"used_weight": "スプールからどれだけのフィラメントが使用されたか。新しいスプールは0gとなるはずです。",
"weight_to_use": "入力する重さを選択してください。測定重量は、選択したフィラメントにスプール重量が設定されている場合のみ利用可能です。",
"remaining_weight": "スプールに残っているフィラメントの量。新しいスプールの場合、これはスプールの重量と一致するはずです。",
"initial_weight": "スプール上のフィラメントの初期重量(正味重量)。設定されていない場合は、フィラメントオブジェクトからの重量が使用されます。",
"lot_nr": "メーカーのロット番号。複数のスプールを使用する場合、印刷の色が一定であることを確認するために使用できます。",
"external_filament": "外部データベースからフィラメントを選択しました。このスプールを作成する際に、フィラメントオブジェクト(場合によってはメーカーオブジェクトも)が自動的に作成されます。すでにこのフィラメントのフィラメントオブジェクトを作成している場合、重複したフィラメントオブジェクトが作成される可能性があります。",
"spool_weight": "空の時のスプールの重量。フィラメントまたはメーカーの値を使用する場合は空のままにしてください。"
},
"titles": {
"create": "スプールを作成",
"clone": "スプールを複製",
"show_title": "[スプール #{{id}}] {{name}}",
"adjust": "スプールフィラメントの調整",
"edit": "スプールを編集",
"archive": "アーカイブしたスプール",
"list": "スプール",
"show": "スプールを表示"
},
"form": {
"measurement_type_label": "計測タイプ",
"new_location_prompt": "新しい場所を入力",
"measurement_type": {
"length": "長さ",
"weight": "重さ"
},
"adjust_filament_help": "ここではスプールに直接フィラメントを追加したり差し引いたりすることができます。正の値はフィラメントを消費し、負の値はフィラメントを追加します。",
"spool_updated": "このスプールは、このページを開いてから他の誰か/何かによって更新されています。保存すると、それらの変更が上書きされます!",
"adjust_filament_value": "消費量"
},
"messages": {
"archive": "本当にこのスプールをアーカイブしますか?"
},
"formats": {
"last_used": "最終使用日: {{date}}"
},
"spool": "スプール"
},
"filament": {
"fields": {
"id": "ID",
"name": "名称",
"material": "マテリアル",
"price": "価格",
"density": "密度",
"spool_weight": "スプールの重さ",
"registered": "登録",
"comment": "コメント",
"color_hex": "色",
"single_color": "単色",
"multi_color": "複数色",
"longitudinal": "縦方向",
"article_number": "製品番号",
"coaxial": "断面",
"external_id": "外部ID",
"vendor": "製造者",
"diameter": "直径",
"settings_extruder_temp": "エクストルーダー温度",
"settings_bed_temp": "ベッド温度",
"weight": "重さ",
"vendor_name": "製造者",
"spools": "スプールを表示"
},
"filament": "フィラメント",
"fields_help": {
"name": "フィラメント名: 同じメーカーの他のフィラメントと区別するための名称です。例えば色を含めると良いでしょう。",
"article_number": "例: EAN, UPC, etc.",
"spool_weight": "空のスプールの重量。スプールの測定重量を決定するために使用されます。",
"material": "例: PLA, ABS, PETG, etc.",
"multi_color_direction": "フィラメントは2つの方法で複数の色を持つことができます。断面: 押出し時に円の断面に複数色を持つ二色フィラメントのような形態です。縦方向: 長手方向の色変化によるもので、スプールに沿って色が変化するグラデーションフィラメントのような形態です。",
"price": "スプール全体の価格。",
"weight": "フィラメントの正味重量(フルスプール時)。スプール自体の重量は含まず、フィラメントのみの重量です。通常、パッケージに記載されている重量です。"
},
"titles": {
"create": "フィラメントを作成",
"clone": "フィラメントを複製",
"list": "フィラメント",
"edit": "フィラメントを編集",
"show_title": "[フィラメント #{{id}}] {{name}}",
"show": "フィラメントを表示"
},
"form": {
"filament_updated": "このフィラメントは、このページを開いてから他の誰か/何かによって更新されています。保存すると、それらの変更が上書きされます!",
"import_external": "外部からインポート",
"import_external_description": "リストからフィラメントを選択すると、フィラメント作成フォームに詳細情報が自動的に入力されます。これにより、フォームに入力したデータは上書きされます。<br><br>「OK」をクリックすると、メーカーオブジェクトが自動的に作成されます。"
},
"buttons": {
"add_spool": "スプールを追加"
}
},
"vendor": {
"fields": {
"id": "ID",
"registered": "登録",
"external_id": "外部ID",
"name": "名称",
"empty_spool_weight": "空スプールの重さ",
"comment": "コメント"
},
"fields_help": {
"empty_spool_weight": "この製造者の空スプールの重量。"
},
"titles": {
"create": "製造者を作成",
"list": "製造者",
"show": "製造者を表示",
"clone": "製造者を複製",
"edit": "製造者を編集",
"show_title": "[製造者 #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "この製造者は、このページを開いてから他の誰か/何かによって更新されています。保存すると、それらの変更が上書きされます!"
},
"vendor": "製造者"
},
"help": {
"help": "ヘルプ",
"resources": {
"filament": "フィラメントのブランド。名前、素材、色、直径などの特性を持っています。",
"spool": "特定のフィラメントの個々の物理的なスプール。",
"vendor": "フィラメントを製造している会社。"
},
"description": "<title>ヘルプ</title><p>使い始めるためのヒントをいくつか紹介します。</p><p>Spoolmanは3種類のデータを管理しています:</p><itemsHelp/><p>データベースに新しいスプールを追加するには、まず<filamentCreateLink>フィラメント</filamentCreateLink>を作成することから始めます。それが完了したら、<spoolCreateLink>スプール</spoolCreateLink>を作成します。後日、同じフィラメントの追加スプールを入手した場合は、追加のスプールを生成し、同じフィラメントを再利用するだけです。</p><p>必要に応じて、フィラメントを製造している<vendorCreateLink>製造者</vendorCreateLink>を作成して、その情報を追跡することもできます。</p><p>Moonrakerなどの他の3DプリンターサービスをSpoolmanにリンクすることもでき、フィラメントの使用状況を自動的に監視し、スプールを更新することができます。設定方法については<readmeLink>Spoolman README</readmeLink>を参照してください。</p>"
},
"table": {
"actions": "アクション"
},
"settings": {
"settings": "設定",
"header": "設定",
"general": {
"tab": "一般",
"currency": {
"label": "通過"
},
"base_url": {
"label": "ベースURL",
"tooltip": "QRコードなどの機能を生成する際に使用する基本URL。"
},
"round_prices": {
"label": "価格の端数処理",
"tooltip": "価格の小数点以下を四捨五入する。"
}
},
"extra_fields": {
"tab": "追加項目",
"params": {
"unit": "単位",
"order": "数量",
"default_value": "デフォルト値",
"choices": "選択肢",
"multi_choice": "複数選択",
"name": "名称",
"key": "キー",
"field_type": "タイプ"
},
"field_type": {
"float": "小数点型",
"float_range": "小数点型の範囲",
"datetime": "日時",
"boolean": "真偽値",
"choice": "選択肢",
"integer_range": "数値範囲",
"text": "テキスト",
"integer": "整数"
},
"boolean_true": "はい",
"key_not_changed": "別のキーに変更してください。",
"boolean_false": "いいえ",
"delete_confirm": "{{name}}のフィールドを削除しますか?",
"delete_confirm_description": "フィールドとすべてのエンティティの関連データがすべて削除されます。",
"description": "<p>ここで追加のカスタムフィールドを追加できます。</p><p>フィールドを追加すると、そのキーやタイプを変更することはできません。また、選択肢タイプのフィールドでは、選択肢の削除や複数選択の状態を変更することはできません。フィールドを削除すると、すべてのエンティティの関連データが削除されます。</p><p>キーは他のプログラムがデータを読み書きする際に使用するものなので、カスタムフィールドがサードパーティのプログラムと連携する予定がある場合は、正しく設定してください。デフォルト値は新しいアイテムにのみ適用されます。</p><p>追加フィールドはテーブルビューで並べ替えやフィルタリングすることはできません。</p>",
"choices_missing_error": "選択肢を削除できません。下記の選択肢がありません: {{choices}}",
"non_unique_key_error": "キーは一意の値である必要があります。"
}
},
"documentTitle": {
"home": {
"list": "ホーム | Spoolman"
},
"help": {
"list": "ヘルプ | Spoolman"
},
"default": "Spoolman",
"suffix": " | Spoolman",
"filament": {
"edit": "フィラメントの編集 #{{id}} | Spoolman",
"clone": "フィラメントの複製 #{{id}} | Spoolman",
"show": "フィラメント #{{id}} | Spoolman",
"create": "フィラメントの作成 | Spoolman",
"list": "フィラメント | Spoolman"
},
"spool": {
"list": "スプール | Spoolman",
"show": "スプール #{{id}} | Spoolman",
"create": "スプールの作成 | Spoolman",
"edit": "スプールの編集 #{{id}} | Spoolman",
"clone": "スプールの複製 #{{id}} | Spoolman"
},
"vendor": {
"edit": "製造者の編集 #{{id}} | Spoolman",
"clone": "製造者の複製 #{{id}} | Spoolman",
"show": "製造者 #{{id}} | Spoolman",
"create": "製造者の作成 | Spoolman",
"list": "製造者 | Spoolman"
},
"locations": {
"list": "場所 | Spoolman"
}
},
"locations": {
"locations": "場所",
"new_location": "新しい場所",
"no_location": "場所なし",
"no_locations_help": "このページでは、スプールを場所ごとに整理することができます!"
},
"home": {
"home": "ホーム",
"welcome": "",
"description": ""
}
}
+405
View File
@@ -0,0 +1,405 @@
{
"actions": {
"list": "목록",
"create": "생성",
"edit": "수정",
"show": "보기",
"clone": "복제"
},
"buttons": {
"create": "생성",
"save": "저장",
"saveAndAdd": "저장 및 추가",
"logout": "로그아웃",
"delete": "삭제",
"edit": "수정",
"cancel": "취소",
"confirm": "정말 하시겠습니까?",
"continue": "계속",
"filter": "필터",
"refresh": "새로고침",
"show": "보기",
"undo": "되돌리기",
"import": "가져오기",
"clone": "복제",
"archive": "보관",
"unArchive": "보관 해제",
"hideArchived": "보관 숨기기",
"showArchived": "보관된 항목 표시",
"notAccessTitle": "접근 권한이 없습니다",
"hideColumns": "열 숨김",
"clearFilters": "필터 초기화",
"clear": "지우기"
},
"warnWhenUnsavedChanges": "정말 나가시겠습니까? 저장하지 않은 변경 사항이 있습니다.",
"notifications": {
"success": "성공",
"error": "오류 (상태 코드: {{statusCode}})",
"saveSuccessful": "저장 완료!",
"validationError": "검증 오류: {{error}}",
"undoable": "{{seconds}}초 이내에 되돌릴 수 있습니다.",
"createSuccess": "{{resource}} 생성 완료",
"createError": "{{resource}} 생성 중 오류가 발생했습니다 (상태 코드: {{statusCode}})",
"deleteSuccess": "{{resource}} 삭제 완료",
"deleteError": "{{resource}} 삭제 중 오류 발생 (상태 코드: {{statusCode}})",
"editSuccess": "수정 완료 {{resource}}",
"editError": "{{resource}} 편집 중 오류 발생 (상태 코드: {{statusCode}})",
"importProgress": "가져오기: {{processed}}/{{total}}"
},
"kofi": "Ko-fi로 개발 지원",
"loading": "로딩",
"version": "버전",
"unknown": "알 수 없음",
"yes": "네",
"no": "아니오",
"tags": {
"clone": "복제"
},
"dashboard": {
"title": "대시보드"
},
"printing": {
"generic": {
"title": "인쇄",
"print": "인쇄",
"columns": "열",
"rows": "행",
"paperSize": "종이 크기",
"customSize": "커스텀",
"dimensions": "치수",
"showBorder": "테두리 보이기",
"contentSettings": "내용 설정",
"layoutSettings": "레이아웃 설정",
"horizontalSpacing": "수평 간격",
"verticalSpacing": "수직 간격",
"marginLeft": "왼쪽 여백",
"marginRight": "오른쪽 여백",
"marginTop": "위쪽 여백",
"marginBottom": "아래쪽 여백",
"borders": {
"border": "테두리",
"grid": "격자",
"none": "없음"
},
"settings": "프리셋",
"defaultSettings": "기본값",
"addSettings": "프리셋 추가",
"newSetting": "신규",
"duplicateSettings": "복제",
"deleteSettings": "프리셋 삭제",
"deleteSettingsConfirm": "이 프리셋을 삭제하시겠습니까?",
"settingsName": "프리셋 이름",
"saveSetting": "프리셋 저장",
"saveAsImage": "그림으로 저장",
"skipItems": "항목 건너뛰기",
"itemCopies": "항목 복사본 수",
"description": "아래 설정을 조정하여 원하는 인쇄 레이아웃을 얻으십시오. 프린터와 운영체제(OS)가 자체 여백 및 크기 조정을 적용할 수 있으므로, 완벽하게 맞출 때까지 몇 번의 시행착오가 필요할 수 있습니다. 실제 라벨에 인쇄하기 전에 일반 용지에 테스트 인쇄를 해보십시오.",
"helpMargin": "여백은 라벨 용지와 프린터에 맞게 설정해야 하며, 이를 변경하면 전체 그리드 크기에 영향을 미칩니다.",
"previewScale": "미리보기 배율",
"helpPrinterMargin": "안전구역은 프린터가 용지 가장자리에서 얼마나 가까운 위치까지 인쇄할 수 있는지를 설정하는 것이며, 이 값을 변경해도 전체 격자에는 영향을 미치지 않습니다.",
"printerMarginLeft": "안전구역 왼쪽",
"printerMarginRight": "안전구역 오른쪽",
"printerMarginTop": "안전구역 위쪽",
"printerMarginBottom": "안전구역 아래쪽"
},
"qrcode": {
"button": "라벨 인쇄",
"title": "라벨 인쇄",
"template": "라벨 템플릿",
"templateHelp": "{}를 사용하여 스풀 객체의 값을 텍스트로 삽입합니다. 예를 들어, {id}는 스풀 id로 대체되고, {filament.material}은 스풀의 재료로 대체됩니다. 값이 누락된 경우 \"?\"로 대체됩니다. 두 번째 {} 세트를 사용하여 이를 제거할 수 있습니다. 또한, {} 쌍 사이에 있는 텍스트는 값이 누락된 경우 제거됩니다. 예를 들어, {Lot Nr: {lot_nr}}은 스풀에 로트 번호가 있을 때만 라벨을 표시합니다. 텍스트를 두 개의 별표 **로 감싸면 굵게 표시됩니다. 사용 가능한 모든 태그 목록을 보려면 버튼을 클릭하세요.",
"textSize": "라벨 글씨 크기",
"showQRCode": "QR 코드 인쇄",
"showContent": "라벨 인쇄",
"useHTTPUrl": {
"label": "QR 코드 링크",
"tooltip": "Spoolman의 스캔 기능으로 스캔한 경우에만 작동하는 전용 링크를 사용합니다(기본값). URL은 설정에 지정된 기본 URL을 사용하며, 설정되지 않은 경우 현재 페이지 URL을 사용합니다.",
"options": {
"default": "기본값",
"url": "URL"
},
"preview": "미리보기:"
},
"showQRCodeMode": {
"no": "아니오",
"simple": "단순",
"withIcon": "아이콘 표시"
}
},
"spoolSelect": {
"title": "스풀 선택",
"description": "라벨을 인쇄할 스풀을 선택합니다.",
"showArchived": "보관 보이기",
"noSpoolsSelected": "스풀을 선택하지 않았습니다.",
"selectAll": "모두 선택/해제",
"selectedTotal_other": "스풀 {{count}}개 선택됨"
}
},
"scanner": {
"title": "QR 코드 스캐너",
"description": "Spoolman QR 코드를 스캔하여 스풀에 대한 세부 정보를 확인하세요.",
"error": {
"notAllowed": "카메라 접근 권한을 허용하지 않았습니다.",
"insecureContext": "해당 페이지는 HTTPS를 통해 제공되지 않습니다.",
"streamApiNotSupported": "브라우저가 MediaStream API를 지원하지 않습니다.",
"notReadable": "카메라를 읽을 수 없습니다.",
"notFound": "카메라를 찾을 수 없습니다.",
"unknown": "알 수 없는 오류가 발생했습니다. ({{error}})"
}
},
"spool": {
"spool": "스풀",
"fields": {
"id": "ID",
"filament_name": "필라멘트",
"filament": "필라멘트",
"filament_internal": "내부",
"filament_external": "외부",
"price": "가격",
"material": "재료",
"weight_to_use": "중량",
"used_weight": "사용 중량",
"remaining_weight": "남은 무게",
"measured_weight": "측정 중량",
"used_length": "사용 길이",
"remaining_length": "잔여 길이",
"initial_weight": "초기 중량",
"spool_weight": "빈 스풀 중량",
"location": "위치",
"lot_nr": "로트 번호",
"first_used": "첫 사용",
"last_used": "최종 사용",
"registered": "등록됨",
"archived": "보관됨",
"comment": "주석"
},
"fields_help": {
"price": "전체 스풀 가격. 설정되지 않은 경우 필라멘트 가격이 대신 적용됩니다.",
"weight_to_use": "입력할 중량 값을 선택합니다. 측정 중량은 선택한 필라멘트에 스풀 중량이 설정된 경우에만 사용할 수 있습니다.",
"used_weight": "스풀에서 사용된 필라멘트 양입니다. 새 스풀은 사용량이 0g이어야 합니다.",
"remaining_weight": "스풀에 남은 필라멘트 양입니다. 새 스풀이라면 스풀 무게와 일치해야 합니다.",
"measured_weight": "필라멘트와 스풀을 합한 무게입니다.",
"initial_weight": "스풀에 감긴 필라멘트의 초기 중량(순중량). 설정되지 않은 경우 필라멘트 객체의 중량을 사용합니다.",
"spool_weight": "스풀이 비어 있을 때의 무게. 필라멘트 또는 제조사의 값을 사용하려면 비워 두십시오.",
"location": "여러 위치에 스풀을 보관하는 경우 스풀이 위치한 곳.",
"lot_nr": "제조사의 로트 번호입니다. 여러 개의 스풀을 사용할 경우 인쇄물의 색상을 일관되게 유지하는 데 사용할 수 있습니다.",
"external_filament": "외부 데이터베이스에서 필라멘트를 선택하였습니다. 이 스풀을 생성할 때 필라멘트 객체(및 경우에 따라 제조사 객체)가 자동으로 생성됩니다. 해당 필라멘트에 대해 이미 필라멘트 객체가 생성된 경우 중복 객체가 생성될 수 있습니다."
},
"titles": {
"create": "스풀 생성",
"clone": "스풀 복제",
"edit": "스풀 수정",
"list": "스풀",
"show": "스풀 보기",
"show_title": "[스풀 #{{id}}] {{name}}",
"archive": "스풀 보관",
"adjust": "스풀 필라멘트 조정"
},
"form": {
"measurement_type": {
"length": "길이",
"weight": "중량"
},
"measurement_type_label": "측정 유형",
"adjust_filament_help": "여기서 스풀의 필라멘트를 직접 더하거나 뺄 수 있습니다. 양수 값은 필라멘트를 소모하고, 음수 값은 필라멘트를 추가합니다.",
"adjust_filament_value": "소모량",
"new_location_prompt": "새 위치 입력",
"spool_updated": "이 페이지를 연 이후 다른 사용자나 프로그램이 이 스풀을 업데이트했습니다. 저장하면 해당 변경 사항을 덮어쓰게 됩니다!"
},
"messages": {
"archive": "이 스풀을 보관하시겠습니까?"
},
"formats": {
"last_used": "최종 사용 {{date}}"
}
},
"filament": {
"filament": "필라멘트",
"fields": {
"id": "ID",
"vendor_name": "제조사",
"vendor": "제조사",
"name": "이름",
"material": "재료",
"price": "가격",
"density": "밀도",
"diameter": "직경",
"weight": "중량",
"spool_weight": "스풀 중량",
"article_number": "제품 번호",
"registered": "등록됨",
"comment": "주석",
"settings_extruder_temp": "압출기 온도",
"settings_bed_temp": "베드 온도",
"color_hex": "색상",
"single_color": "단일",
"multi_color": "다중",
"coaxial": "공압출",
"external_id": "외부 ID",
"spools": "스풀 보기",
"longitudinal": "종방향"
},
"fields_help": {
"name": "필라멘트 이름, 동일한 제조사의 다른 필라멘트 유형과 구별하기 위한 명칭. 예를 들어 색상을 포함해야 합니다.",
"material": "예: PLA, ABS, PETG 등.",
"price": "전체 스풀 가격.",
"weight": "전체 스풀의 필라멘트 중량(순중량). 여기에는 스풀 자체의 무게는 포함되지 않으며 필라멘트만 포함됩니다. 일반적으로 포장지에 표기되는 내용입니다.",
"spool_weight": "빈 스풀의 무게. 스풀의 측정 무게를 결정하는 데 사용됩니다.",
"article_number": "예: EAN, UPC 등",
"multi_color_direction": "필라멘트가 여러 색상을 갖는 방식은 두 가지입니다. 하나는 색상이 일정하게 유지되는 이중 색상 필라멘트처럼 공압출을 이용하는 방식이고, 다른 하나는 스풀을 따라 색상이 바뀌는 그라데이션 필라멘트처럼 종방향으로 색상이 변하는 방식입니다."
},
"titles": {
"create": "필라멘트 생성",
"clone": "필라멘트 복제",
"edit": "필라멘트 편집",
"list": "필라멘트",
"show": "필라멘트 보기",
"show_title": "[필라멘트 #{{id}}] {{name}}"
},
"form": {
"filament_updated": "이 필라멘트는 이 페이지를 연 이후 다른 사람 또는 다른 프로그램에 의해 업데이트되었습니다. 저장하면 해당 변경 사항을 덮어쓰게 됩니다!",
"import_external": "외부에서 가져오기",
"import_external_description": "목록에서 필라멘트를 선택하면 필라멘트 생성 양식에 해당 세부 정보가 자동으로 채워집니다. 이 경우 양식에 입력한 기존 데이터는 모두 덮어쓰게 됩니다.<br><br>필요한 경우 확인을 클릭하면 제조사 객체가 자동으로 생성됩니다."
},
"buttons": {
"add_spool": "스풀 추가"
}
},
"vendor": {
"vendor": "제조사",
"fields": {
"id": "ID",
"name": "이름",
"empty_spool_weight": "빈 스풀 중량",
"external_id": "외부 ID",
"registered": "등록됨",
"comment": "주석"
},
"fields_help": {
"empty_spool_weight": "이 제조사의 빈 스풀 중량."
},
"titles": {
"create": "제조사 생성",
"clone": "제조사 복제",
"edit": "제조사 편집",
"list": "제조사",
"show": "제조사 보기",
"show_title": "[제조사 #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "이 제조사 정보는 이 페이지를 연 이후 다른 사람 또는 다른 프로그램에 의해 업데이트되었습니다. 저장하면 해당 변경 사항을 덮어쓰게 됩니다!"
}
},
"home": {
"home": "홈",
"welcome": "Spoolman 인스턴스에 오신 것을 환영합니다!",
"description": "아직 스풀을 추가하지 않은 것 같습니다. 시작하는 방법에 대한 도움말은 <helpPageLink>도움말 페이지</helpPageLink>를 참조하세요."
},
"help": {
"help": "도움말",
"resources": {
"filament": "필라멘트 브랜드. 이름, 재료, 색상, 직경 등의 속성을 가집니다.",
"spool": "특정 필라멘트의 개별 물리적 스풀.",
"vendor": "필라멘트를 제조하는 기업들."
},
"description": "<title>도움말</title><p>시작하는 데 도움이 될 몇 가지 팁을 소개합니다.</p><p>Spoolman은 3가지 유형의 데이터를 보유합니다:</p><itemsHelp/><p>데이터베이스에 새 스풀을 추가하려면 먼저 <filamentCreateLink>필라멘트</filamentCreateLink> 객체를 생성하세요. 그런 다음 해당 스풀에 대한 <spoolCreateLink>스풀</spoolCreateLink> 객체를 생성합니다. 나중에 동일한 필라멘트의 추가 스풀을 구입한 경우, 동일한 필라멘트 객체를 재사용하고 추가 스풀 객체만 생성하면 됩니다.</p><p>필요에 따라 필라멘트 제조사를 추적하려면 해당 회사에 대한 <vendorCreateLink>제조사</vendorCreateLink> 객체를 생성할 수도 있습니다. </p><p>필라멘트 사용량을 자동으로 모니터링하고 스풀 객체를 업데이트해주는 Moonraker 같은 다른 3D 프린터 서비스를 Spoolman에 연결할 수도 있습니다. 설정 방법은 <readmeLink>Spoolman README</readmeLink>를 참고하세요.</p>"
},
"table": {
"actions": "작업"
},
"settings": {
"settings": "설정",
"header": "설정",
"general": {
"tab": "일반",
"currency": {
"label": "통화"
},
"base_url": {
"label": "기본 URL",
"tooltip": "QR 코드와 같은 기능을 생성할 때 사용할 기본 URL입니다."
},
"round_prices": {
"label": "가격 반올림",
"tooltip": "가격을 가장 가까운 정수로 반올림합니다."
}
},
"extra_fields": {
"tab": "추가 필드",
"description": "<p>여기에서 엔티티에 추가 사용자 정의 필드를 추가할 수 있습니다.</p><p>필드를 추가한 후에는 키나 유형을 변경할 수 없으며, 선택형 필드의 경우 선택 항목을 제거하거나 다중 선택 상태를 변경할 수 없습니다. 필드를 삭제하면 모든 엔티티의 관련 데이터가 삭제됩니다.</p><p>키 값은 다른 프로그램이 데이터를 읽고 쓰는 방식이므로, 사용자 정의 필드가 타사 프로그램과 연동될 예정이라면 반드시 올바르게 설정해야 합니다. 기본값은 새 항목에만 적용됩니다.</p><p>추가 필드는 테이블 보기에 표시하고 정렬할 수 있습니다. 선택형과 불리언 필드는 필터 옵션도 함께 제공하며, 모든 추가 필드 유형은 빈 값 필터링을 지원합니다.</p>",
"params": {
"key": "키",
"name": "이름",
"field_type": "유형",
"unit": "단위",
"order": "순번",
"default_value": "기본값",
"choices": "선택 항목",
"multi_choice": "다중 선택"
},
"field_type": {
"text": "텍스트",
"integer": "정수",
"integer_range": "정수 범위",
"float": "실수",
"float_range": "실수 범위",
"datetime": "날짜 및 시간",
"boolean": "논리형",
"choice": "선택"
},
"boolean_true": "네",
"boolean_false": "아니오",
"choices_missing_error": "선택 항목을 제거할 수 없습니다. 다음 선택 항목이 누락되었습니다: {{choices}}",
"non_unique_key_error": "키는 고유해야 합니다.",
"key_not_changed": "키 값을 다른 것으로 바꿔 주세요.",
"delete_confirm": "{{name}} 필드 삭제?",
"delete_confirm_description": "이 작업은 모든 엔티티에 대해 해당 필드와 관련된 모든 데이터를 삭제합니다.",
"choices_reorder_hint": "드래그하거나 방향키를 사용해 이 선택 항목의 순서를 변경하세요."
}
},
"documentTitle": {
"default": "Spoolman",
"suffix": " | Spoolman",
"home": {
"list": "홈 | Spoolman"
},
"help": {
"list": "도움말 | Spoolman"
},
"locations": {
"list": "위치 | Spoolman"
},
"filament": {
"list": "필라멘트 | Spoolman",
"show": "#{{id}} 필라멘트 보기 | Spoolman",
"edit": "#{{id}} 필라멘트 편집 | Spoolman",
"create": "필라멘트 생성 | Spoolman",
"clone": "#{{id}} 필라멘트 복제 | Spoolman"
},
"spool": {
"list": "스풀 | Spoolman",
"show": "#{{id}} 스풀 보기 | Spoolman",
"edit": "#{{id}} 스풀 편집 | Spoolman",
"create": "스풀 생성 | Spoolman",
"clone": "#{{id}} 스풀 복제 | Spoolman"
},
"vendor": {
"list": "제조사 | Spoolman",
"show": "#{{id}} 제조사 보기 | Spoolman",
"edit": "#{{id}} 제조사 편집 | Spoolman",
"create": "제조사 생성 | Spoolman",
"clone": "#{{id}} 제조사 복제 | Spoolman"
}
},
"locations": {
"locations": "위치",
"new_location": "새로운 위치",
"no_location": "위치 없음",
"no_locations_help": "이 페이지에서는 스풀을 위치별로 정리할 수 있습니다. 시작하려면 스풀을 추가하세요!"
},
"theme": {
"label": "테마",
"system": "시스템",
"light": "라이트",
"dark": "다크"
}
}
+406
View File
@@ -0,0 +1,406 @@
{
"actions": {
"list": "Sąrašas",
"create": "Sukurti",
"edit": "Redaguoti",
"show": "Rodyti",
"clone": "Klonuoti"
},
"buttons": {
"create": "Sukurti",
"save": "Išsaugoti",
"saveAndAdd": "Išsaugoti ir pridėti",
"logout": "Atsijungti",
"delete": "Šalinti",
"edit": "Redaguoti",
"cancel": "Atšaukti",
"confirm": "Ar tikrai?",
"continue": "Tęsti",
"filter": "Filtras",
"clear": "Išvalyti",
"refresh": "Atnaujinti",
"show": "Rodyti",
"undo": "Gražinti",
"import": "Importuoti",
"clone": "Klonuoti",
"archive": "Archyvas",
"unArchive": "Išarchyvuoti",
"hideArchived": "Paslėpti archyvuotus",
"showArchived": "Rodyti archyvuotus",
"notAccessTitle": "Nepakanka teisių",
"hideColumns": "Slėpti stulpelius",
"clearFilters": "Valyti filtrus"
},
"warnWhenUnsavedChanges": "Ar tikrai norite išeiti? Yra neišsaugotų pakeitimų.",
"notifications": {
"success": "Sėkmingai",
"error": "Klaida (kodas: {{statusCode}})",
"undoable": "Jūs turite {{seconds}} sekundes, kad atšauktumėte.",
"createSuccess": "Sėkmingai sukurta {{resource}}",
"createError": "Kūrimo klaida {{resource}} (kodas: {{statusCode}})",
"deleteSuccess": "Sėkmingai ištrinta {{resource}}",
"deleteError": "Tkaida trinant {{resource}} (kodas: {{statusCode}})",
"editSuccess": "Sėkmingai redaguota {{resource}}",
"editError": "Redagavimo klaida {{resource}} (kodas: {{statusCode}})",
"importProgress": "Importuojama: {{processed}}/{{total}}",
"saveSuccessful": "Įrašytaa sėkmingai!",
"validationError": "Tikrinimo klaida: {{error}}"
},
"kofi": "Paremk mane Ko-fi",
"loading": "Kraunasi",
"version": "Versija",
"unknown": "Nežinoma",
"yes": "Taip",
"no": "Ne",
"tags": {
"clone": "Klonuoti"
},
"dashboard": {
"title": "Darbastalis"
},
"printing": {
"generic": {
"title": "Spausdinama",
"description": "Norėdami gauti norimą spausdinimo maketą, pakoreguokite toliau pateiktus nustatymus. Atminkite, kad spausdintuvai ir jūsų operacinė sistema gali taikyti savo paraštes ir mastelį, todėl gali tekti atlikti bandymus ir klaidas, kol viskas bus teisinga. Prieš spausdindami ant etikečių, išbandykite ant įprasto popieriaus lapo.",
"helpMargin": "Paraštės turėtų būti sukonfigūruotos taip, kad atitiktų jūsų etikečių popierių ir spausdintuvą, nes jų keitimas turės įtakos viso tinklelio dydžiui.",
"helpPrinterMargin": "Saugios zonos nustatymas turėtų atitikti spausdinimo atstumą nuo popieriaus krašto. Šios zonos keitimas neturės įtakos visam tinkleliui.",
"print": "Spausdinti",
"columns": "Stulpeliai",
"rows": "Eilutės",
"paperSize": "Popieriaus dydis",
"customSize": "Tinkintas",
"dimensions": "Matmenys",
"showBorder": "Rodyti paraštes",
"previewScale": "Peržiūros mastelis",
"skipItems": "Praleisti elementus",
"itemCopies": "Elemento kopijos",
"contentSettings": "Turinio nustatymai",
"layoutSettings": "Išdėstymo nustatymai",
"horizontalSpacing": "Horizontalūs tarpai",
"verticalSpacing": "Vertikalūs tarpai",
"marginLeft": "Kairioji paraštė",
"marginRight": "Dešinioji paraštė",
"marginTop": "Viršutinė paraštė",
"marginBottom": "Apatinė paraštė",
"printerMarginLeft": "Saugi zona kairėje",
"printerMarginRight": "Saugi zona dešinėje",
"printerMarginTop": "Saugi zona viršuje",
"printerMarginBottom": "Saugi zona apačioje",
"borders": {
"none": "Nėra",
"border": "Paraštė",
"grid": "Tinklelis"
},
"settings": "Išankstiniai nustatymai",
"defaultSettings": "Numatytasis",
"addSettings": "Pridėti naują išankstinį nustatymą",
"newSetting": "Naujas",
"duplicateSettings": "Kopija",
"deleteSettings": "Ištrinti dabartinį išankstinį nustatymą",
"deleteSettingsConfirm": "Ar tikrai norite ištrinti šį išankstinį nustatymą?",
"settingsName": "Iš anksto nustatytas pavadinimas",
"saveSetting": "Išsaugoti išankstinius nustatymus",
"saveAsImage": "Įrašyti kaip paveikslėlį"
},
"qrcode": {
"button": "Spausdinti etiketes",
"title": "Etikečių spausdinimas",
"template": "Etiketės šablonas",
"templateHelp": "Norėdami įterpti ritės objekto reikšmes kaip tekstą, naudokite {}. Pavyzdžiui, {id} bus pakeistas ritės ID arba {filament.material} bus pakeistas ritės medžiaga. Jei reikšmės trūksta, ji bus pakeista simboliu „?“. Norėdami tai pašalinti, galima naudoti antrą {} rinkinį. Be to, bet koks tekstas tarp {} rinkinių bus pašalintas, jei reikšmės trūksta. Pavyzdžiui, {Lot Nr: {lot_nr}} rodys žymą tik tuo atveju, jei ritė turi partijos numerį. Apsukite tekstą dviguba žvaigždute **, kad jis būtų paryškintas. Spustelėkite mygtuką, kad peržiūrėtumėte visų galimų žymų sąrašą.",
"textSize": "Etiketės teksto dydis",
"showContent": "Spausdinti etiketę",
"useHTTPUrl": {
"label": "QR kodo nuoroda",
"tooltip": "Bus naudojama patentuota nuoroda, kuri veiks tik nuskaityta naudojant „Spoolman“ nuskaitymo funkciją (numatytasis nustatymas). URL naudoja arba nustatymuose nurodytą bazinį URL, arba dabartinio puslapio URL, jei nenustatytas.",
"options": {
"default": "Numatytasis",
"url": "URL"
},
"preview": "Peržiūra:"
},
"showQRCode": "Spausdinti QR kodą",
"showQRCodeMode": {
"no": "Ne",
"simple": "Paprasta",
"withIcon": "Su piktograma"
}
},
"spoolSelect": {
"title": "Pasirinkite ritę",
"description": "Pasirinkite rites, kurioms norite spausdinti etiketes.",
"showArchived": "Rodyti archyvuotus",
"noSpoolsSelected": "Nepasirinkote jokių ričių.",
"selectAll": "Pasirinkti/Atšaukti visų pasirinkimą",
"selectedTotal_one": "{{count}} ritė pasirinkta",
"selectedTotal_few": "{{count}} ritės pasirinktos",
"selectedTotal_other": "{{count}} ričių pasirinkta"
}
},
"scanner": {
"title": "QR kodo skaitytuvas",
"description": "Nuskaitykite Spoolman QR kodą, kad peržiūrėtumėte išsamią informaciją apie ritę.",
"error": {
"notAllowed": "Jūs nesuteikėte leidimo pasiekti kameros.",
"insecureContext": "Puslapis neperduodamas per HTTPS.",
"streamApiNotSupported": "Naršyklė nepalaiko „MediaStream“ API.",
"notReadable": "Kameros rodmenys neįskaitomi.",
"notFound": "Nerasta jokios kameros.",
"unknown": "Įvyko nežinoma klaida. ({{error}})"
}
},
"spool": {
"spool": "Ritės",
"fields": {
"id": "ID",
"filament_name": "Gija",
"filament": "Gija",
"filament_internal": "Vidinis",
"filament_external": "Išorinis",
"price": "Kaina",
"material": "Medžiaga",
"weight_to_use": "Svoris",
"used_weight": "Naudotas svoris",
"remaining_weight": "Likęs svoris",
"measured_weight": "Išmatuotas svoris",
"used_length": "Panaudotas ilgis",
"remaining_length": "Likęs ilgis",
"initial_weight": "Pradinis svoris",
"spool_weight": "Tuščas svoris",
"location": "Vieta",
"lot_nr": "Lot Nr",
"first_used": "Pirmą kartą panaudota",
"last_used": "Paskutinį kartą panaudota",
"registered": "Registruota",
"comment": "Komentaras",
"archived": "Archyvuota"
},
"fields_help": {
"price": "Pilnos ritės kaina. Jei nenustatyta, bus laikoma, kad kaina yra pačios gijos kaina.",
"weight_to_use": "Pasirinkite, kokią svorio vertę įvesti. Išmatuotas svoris galimas tik tuo atveju, jei pasirinktai kaitinimo medžiagai nustatytas ritės svoris.",
"used_weight": "Kiek plastiko buvo sunaudota iš ritės. Naujoje ritėje turėtų būti sunaudota 0 g.",
"remaining_weight": "Kiek gijos liko ant ritės. Naujos ritės svoris turėtų atitikti ritės svorį.",
"measured_weight": "Kiek sveria gija ir ritė.",
"initial_weight": "Pradinis gijos svoris ant ritės (grynasis svoris). Jei nenustatytas, bus naudojamas gijos objekto svoris.",
"spool_weight": "Tuščios ritės svoris. Palikite tuščią, jei norite naudoti vertę, nurodytą ant gijos arba gamintojo.",
"location": "Kur yra ritė, jei rites laikote keliose vietose.",
"lot_nr": "Gamintojo partijos numeris. Gali būti naudojamas siekiant užtikrinti, kad spaudinio spalva būtų vienoda, jei naudojamos kelios ritės.",
"external_filament": "Pasirinkote gijąiš išorinės duomenų bazės. Kuriant šią ritę, bus automatiškai sukurtas gijos objektas (ir galbūt gamintojo objektas). Tai gali sukurti pasikartojančius gijų objektus, jei jau sukūrėte šios gijos objektą."
},
"titles": {
"create": "Sukurti ritę",
"clone": "Klonuoti ritę",
"edit": "Redaguoti ritę",
"list": "Ritės",
"show": "Rodyti ritę",
"show_title": "[Ritė #{{id}}] {{name}}",
"archive": "Archyvuoti ritę",
"adjust": "Reguliuoti ritės giją"
},
"form": {
"measurement_type": {
"length": "Ilgis",
"weight": "Svoris"
},
"measurement_type_label": "Matavimo tipas",
"adjust_filament_help": "Čia galite tiesiogiai pridėti arba atimti giją iš ritės. Teigiama vertė sunaudos giją, neigiama – jį pridės.",
"adjust_filament_value": "Suvartoti kiekį",
"new_location_prompt": "Įveskite naują vietą",
"spool_updated": "Šią ritę kažkas kitas atnaujino nuo tada, kai atidarėte šį puslapį. Išsaugojus šie pakeitimai bus perrašyti!"
},
"messages": {
"archive": "Ar tikrai norite archyvuoti šią ritę?"
},
"formats": {
"last_used": "Paskutinį kartą naudota {{date}}"
}
},
"filament": {
"filament": "Gijos",
"fields": {
"id": "ID",
"vendor_name": "Gamintojas",
"vendor": "Gamintojas",
"name": "Pavadinimas",
"material": "Medžiaga",
"price": "Kaina",
"density": "Tankis",
"diameter": "Skersmuo",
"weight": "Svoris",
"spool_weight": "Ritės svoris",
"article_number": "Gaminio numeris",
"registered": "Registruota",
"comment": "Komentaras",
"settings_extruder_temp": "Ekstruderio temperatūra",
"settings_bed_temp": "Pado temperatūra",
"color_hex": "Spalva",
"single_color": "Viena",
"multi_color": "Daugiaspalvis",
"coaxial": "Koekstruduotas",
"longitudinal": "Išilginis",
"external_id": "Išorinis ID",
"spools": "Rodyti rites"
},
"fields_help": {
"name": "Gijos pavadinimas, kad būtų galima atskirti šį gijos tipą nuo kitų to paties gamintojo gijų. Pavyzdžiui, turėtų būti nurodyta spalva.",
"material": "Pvz. PLA, ABS, PETG, ir tt.",
"price": "Pilnos ritės kaina.",
"weight": "Pilnos ritės gijos svoris (grynasis svoris). Į šį skaičių neturėtų būti įtrauktas pačios ritės svoris, tik gijos svoris. Tai paprastai nurodoma ant pakuotės.",
"spool_weight": "Tuščios ritės svoris. Naudojamas išmatuotam ritės svoriui nustatyti.",
"article_number": "Pvz. EAN, UPC, ir tt.",
"multi_color_direction": "Gijos turėti kelias spalvas dviem būdais: arba dėl koekstruzijos, kaip dviejų spalvų gijos su nuosekliomis daugiaspalvėmis spalvomis, arba dėl išilginių spalvų pokyčių, kaip gradientinės gijos, kurios keičia spalvas išilgai ritės."
},
"titles": {
"create": "Kirti giją",
"clone": "Klonuoti giją",
"edit": "Redaguoti giją",
"list": "Gijos",
"show": "Rodyti gijas",
"show_title": "[Gija #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Šią giją kažkas kitas atnaujino nuo tada, kai atidarėte šį puslapį. Išsaugojus šie pakeitimai bus perrašyti!",
"import_external": "Importuoti iš išorės",
"import_external_description": "Pasirinkite giją sąraše, kad jos informacija būtų automatiškai užpildyta gijos kūrimo formoje. Tai perrašys visus formoje įvestus duomenis.<br><br>Jei reikia, gamintojo objektas bus sukurtas automatiškai, kai spustelėsite „Gerai“."
},
"buttons": {
"add_spool": "Pridėti ritę"
}
},
"vendor": {
"vendor": "Gamintojai",
"fields": {
"id": "ID",
"name": "Pavadinimas",
"empty_spool_weight": "Tuščios ritės svoris",
"external_id": "Išorinis ID",
"registered": "Registruota",
"comment": "Komentaras"
},
"fields_help": {
"empty_spool_weight": "Tuščios šio gamintojo ritės svoris."
},
"titles": {
"create": "Sukurti gamintoją",
"clone": "Klonuoti gamintoją",
"edit": "Redaguoti gamintoją",
"list": "Gamintojai",
"show": "Rodyti gamintojus",
"show_title": "[Gamintojas #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Šio gamintojo duomenis atnaujino kažkas kitas nuo tada, kai atidarėte šį puslapį. Išsaugojus šiuos pakeitimus bus perrašyti!"
}
},
"home": {
"home": "Pradžia",
"welcome": "Sveiki atvykę į savo Spoolman!",
"description": "Panašu, kad dar nepridėjote jokių ričių. Norėdami gauti pagalbos, kaip pradėti, žr. <helpPageLink>pagalbos puslapį</helpPageLink>."
},
"help": {
"help": "Pagalba",
"resources": {
"filament": "Gijų prekės ženklai. Jie turi tokias savybes kaip pavadinimas, medžiaga, spalva, skersmuo ir kita.",
"spool": "Atskiros fizinės konkrečios gijos ritės.",
"vendor": "Įmonės, gaminančios giją."
},
"description": "<title>Pagalba</title><p>Štai keli patarimai, padėsiantys jums pradėti.</p><p>Spoolman saugo 3 skirtingų tipų duomenis:</p><itemsHelp/><p>Norėdami į duomenų bazę įtraukti naują ritę, pirmiausia sukurkite objektą <filamentCreateLink>Filament</filamentCreateLink>. Tai padarę, sukurkite objektą <spoolCreateLink>Ritė</spoolCreateLink> tai konkrečiai ritei. Jei vėliau įsigysite papildomų to paties siūlo ričių, tiesiog sugeneruokite papildomus „Ritės“ objektus ir pakartotinai naudokite tą patį „Gija“ objektą.</p><p>Pasirinktinai taip pat galite sugeneruoti objektą <vendorCreateLink>Gamintojo</vendorCreateLink> įmonei, gaminančiai giją, jei norite sekti šią informaciją.</p><p>Galite susieti su Spoolman kitas 3D spausdinimo paslaugas, pvz., „Moonraker“, kuri gali automatiškai stebėti siūlo naudojimą ir atnaujinti „Ritė“ objektus. Kaip tai nustatyti, žr. <readmeLink>„Spoolman README“</readmeLink>.</p>"
},
"table": {
"actions": "Veiksmai"
},
"settings": {
"settings": "Nustatymai",
"header": "Nustatymai",
"general": {
"tab": "Pagrindiniai",
"currency": {
"label": "Valiuta"
},
"base_url": {
"label": "Bazinis URL",
"tooltip": "Bazinis URL, naudojamas generuojant tokias funkcijas kaip QR kodai."
},
"round_prices": {
"label": "Apvalinti kainas",
"tooltip": "Apvalinkite kainas iki artimiausio sveikojo skaičiaus."
}
},
"extra_fields": {
"tab": "Papildomi laukai",
"description": "<p>Čia galite pridėti papildomų pasirinktinių laukų prie savo objektų.</p><p>Pridėjus lauką, negalite keisti jo rakto ar tipo, o pasirinkimų tipo laukų negalite pašalinti pasirinkimų ar pakeisti kelių pasirinkimų būsenos. Jei pašalinsite lauką, bus ištrinti su visais objektais susiję duomenys.</p><p>Raktas yra tai, kaip kitos programos skaito / rašo duomenis, todėl jei jūsų pasirinktinis laukas turėtų būti integruotas su trečiosios šalies programa, įsitikinkite, kad jis nustatytas teisingai. Numatytoji reikšmė taikoma tik naujiems elementams.</p><p>Papildomų laukų negalima rūšiuoti ar filtruoti lentelės rodiniuose.</p>",
"params": {
"key": "Raktas",
"name": "Pavadinimas",
"field_type": "Tipas",
"unit": "Vienetas",
"order": "Tvarka",
"default_value": "Numatytoji reikšmė",
"choices": "Pasirinkimai",
"multi_choice": "Keli pasirinkimai"
},
"field_type": {
"text": "Tekstas",
"integer": "Skaičius",
"integer_range": "Sveiki skaičiai",
"float": "Realus skaičius",
"float_range": "Realūs skaičiai",
"datetime": "Data Laikas",
"boolean": "Loginis",
"choice": "Pasirinkimas"
},
"boolean_true": "Taip",
"boolean_false": "Ne",
"choices_missing_error": "Negalite pašalinti pasirinkimų. Trūksta šių pasirinkimų: {{choices}}",
"non_unique_key_error": "Raktas turi būti unukalus.",
"key_not_changed": "Prašome pakeisti raktą į ką nors kitą.",
"delete_confirm": "Šalinti lauką {{name}}?",
"delete_confirm_description": "Tai ištrins lauką ir visus susijusius duomenis visiems objektams.",
"choices_reorder_hint": "Temkite arba naudokite rodyklių klavišus, kad pakeistumėte eilės tvarką."
}
},
"documentTitle": {
"default": "Spoolman",
"suffix": " | Spoolman",
"home": {
"list": "Pradžia | Spoolman"
},
"help": {
"list": "Pagalba | Spoolman"
},
"locations": {
"list": "Vieta | Spoolman"
},
"filament": {
"list": "Gijos | Spoolman",
"show": "#{{id}} Rodyti gijas | Spoolman",
"edit": "#{{id}} Redaguoti gijas | Spoolman",
"create": "Kurti giją | Spoolman",
"clone": "#{{id}} Klonuoti giją | Spoolman"
},
"spool": {
"list": "Ritės | Spoolman",
"show": "#{{id}} Rodyti rites| Spoolman",
"edit": "#{{id}} Redaguoti rites | Spoolman",
"create": "Kurti ritę | Spoolman",
"clone": "#{{id}} Klonuoti ritę | Spoolman"
},
"vendor": {
"list": "Gamintojai | Spoolman",
"show": "#{{id}} Rodyti gamintojus | Spoolman",
"edit": "#{{id}} Redaguoti gamintojus | Spoolman",
"create": "Kurti gamintoją | Spoolman",
"clone": "#{{id}} Klonuoti gamintoją | Spoolman"
}
},
"locations": {
"locations": "Vietos",
"new_location": "Nauja vieta",
"no_location": "Be vietos",
"no_locations_help": "Šiame puslapyje galite tvarkyti rites pagal vietas, pridėkite keletą ričių, kad pradėtumėte!"
},
"theme": {
"label": "Tema",
"system": "Sistema",
"dark": "Tamsus"
}
}
+400
View File
@@ -0,0 +1,400 @@
{
"actions": {
"create": "Izveidot",
"edit": "Rediģēt",
"clone": "Klonēt",
"list": "",
"show": ""
},
"buttons": {
"create": "Izveidot",
"save": "Saglabāt",
"saveAndAdd": "Saglabāt un pievienot",
"logout": "Izrakstīties",
"delete": "Dzēst",
"edit": "Rediģēt",
"confirm": "Vai esat pārliecināts?",
"continue": "Turpināt",
"filter": "Filtrēt",
"clear": "Notīrīt",
"show": "",
"cancel": "",
"showArchived": "",
"archive": "",
"hideColumns": "",
"undo": "",
"import": "",
"refresh": "",
"notAccessTitle": "",
"hideArchived": "",
"clearFilters": "",
"unArchive": "",
"clone": ""
},
"vendor": {
"fields": {
"external_id": "",
"registered": "",
"comment": "",
"name": "",
"empty_spool_weight": "",
"id": ""
},
"fields_help": {
"empty_spool_weight": ""
},
"titles": {
"show_title": "",
"show": "",
"edit": "",
"clone": "",
"create": "",
"list": ""
},
"form": {
"vendor_updated": ""
},
"vendor": ""
},
"documentTitle": {
"vendor": {
"create": "",
"clone": "",
"edit": "",
"list": "",
"show": ""
},
"default": "",
"home": {
"list": ""
},
"spool": {
"edit": "",
"list": "",
"create": "",
"show": "",
"clone": ""
},
"filament": {
"list": "",
"show": "",
"clone": "",
"create": "",
"edit": ""
},
"suffix": "",
"locations": {
"list": ""
},
"help": {
"list": ""
}
},
"spool": {
"fields": {
"measured_weight": "",
"last_used": "",
"filament_internal": "",
"material": "",
"spool_weight": "",
"weight_to_use": "",
"lot_nr": "",
"filament_name": "",
"price": "",
"comment": "",
"filament": "",
"location": "",
"used_length": "",
"archived": "",
"remaining_length": "",
"registered": "",
"first_used": "",
"used_weight": "",
"id": "",
"remaining_weight": "",
"initial_weight": "",
"filament_external": ""
},
"titles": {
"archive": "",
"adjust": "",
"show": "",
"edit": "",
"create": "",
"clone": "",
"show_title": "",
"list": ""
},
"fields_help": {
"external_filament": "",
"used_weight": "",
"weight_to_use": "",
"measured_weight": "",
"price": "",
"spool_weight": "",
"lot_nr": "",
"remaining_weight": "",
"location": "",
"initial_weight": ""
},
"form": {
"spool_updated": "",
"measurement_type_label": "",
"adjust_filament_value": "",
"new_location_prompt": "",
"measurement_type": {
"weight": "",
"length": ""
},
"adjust_filament_help": ""
},
"spool": "",
"formats": {
"last_used": ""
},
"messages": {
"archive": ""
}
},
"filament": {
"form": {
"import_external": "",
"filament_updated": "",
"import_external_description": ""
},
"fields_help": {
"name": "",
"article_number": "",
"spool_weight": "",
"price": "",
"weight": "",
"multi_color_direction": "",
"material": ""
},
"fields": {
"material": "",
"vendor_name": "",
"settings_extruder_temp": "",
"comment": "",
"multi_color": "",
"diameter": "",
"weight": "",
"spool_weight": "",
"external_id": "",
"single_color": "",
"registered": "",
"spools": "",
"name": "",
"price": "",
"settings_bed_temp": "",
"vendor": "",
"density": "",
"longitudinal": "",
"id": "",
"color_hex": "",
"article_number": "",
"coaxial": ""
},
"titles": {
"clone": "",
"show_title": "",
"show": "",
"create": "",
"edit": "",
"list": ""
},
"buttons": {
"add_spool": ""
},
"filament": ""
},
"printing": {
"generic": {
"printerMarginBottom": "",
"verticalSpacing": "",
"rows": "",
"borders": {
"none": "",
"grid": "",
"border": ""
},
"printerMarginTop": "",
"deleteSettingsConfirm": "",
"deleteSettings": "",
"customSize": "",
"title": "",
"contentSettings": "",
"marginRight": "",
"printerMarginRight": "",
"itemCopies": "",
"helpPrinterMargin": "",
"settingsName": "",
"previewScale": "",
"helpMargin": "",
"marginTop": "",
"skipItems": "",
"newSetting": "",
"addSettings": "",
"layoutSettings": "",
"paperSize": "",
"columns": "",
"description": "",
"marginBottom": "",
"horizontalSpacing": "",
"duplicateSettings": "",
"settings": "",
"saveSetting": "",
"marginLeft": "",
"showBorder": "",
"saveAsImage": "",
"printerMarginLeft": "",
"dimensions": "",
"defaultSettings": "",
"print": ""
},
"qrcode": {
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
},
"showContent": "",
"showQRCodeMode": {
"withIcon": "",
"no": "",
"simple": ""
},
"templateHelp": "",
"textSize": "",
"showQRCode": "",
"template": "",
"title": "",
"button": ""
},
"spoolSelect": {
"selectedTotal_zero": "",
"selectedTotal_one": "",
"selectedTotal_other": "",
"description": "",
"showArchived": "",
"title": "",
"selectAll": "",
"noSpoolsSelected": ""
}
},
"settings": {
"extra_fields": {
"non_unique_key_error": "",
"params": {
"unit": "",
"choices": "",
"default_value": "",
"field_type": "",
"multi_choice": "",
"order": "",
"name": "",
"key": ""
},
"boolean_true": "",
"choices_missing_error": "",
"boolean_false": "",
"delete_confirm_description": "",
"field_type": {
"boolean": "",
"float": "",
"choice": "",
"float_range": "",
"datetime": "",
"integer": "",
"integer_range": "",
"text": ""
},
"tab": "",
"description": "",
"delete_confirm": "",
"key_not_changed": ""
},
"general": {
"tab": "",
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
},
"currency": {
"label": ""
}
},
"header": "",
"settings": ""
},
"home": {
"welcome": "",
"home": "",
"description": ""
},
"scanner": {
"title": "",
"error": {
"insecureContext": "",
"notReadable": "",
"notAllowed": "",
"unknown": "",
"notFound": "",
"streamApiNotSupported": ""
},
"description": ""
},
"help": {
"resources": {
"vendor": "",
"filament": "",
"spool": ""
},
"description": "",
"help": ""
},
"notifications": {
"deleteSuccess": "",
"error": "",
"editError": "",
"saveSuccessful": "",
"undoable": "",
"importProgress": "",
"createError": "",
"validationError": "",
"success": "",
"editSuccess": "",
"deleteError": "",
"createSuccess": ""
},
"no": "",
"unknown": "",
"yes": "",
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
},
"version": "",
"kofi": "",
"table": {
"actions": ""
},
"warnWhenUnsavedChanges": "",
"loading": "",
"dashboard": {
"title": ""
},
"tags": {
"clone": ""
}
}
+151 -26
View File
@@ -21,7 +21,8 @@
"filter": "Filtrer",
"refresh": "Gjenoppfrisk",
"notAccessTitle": "Du har ikke rettighet til å få tilgang",
"saveAndAdd": "Lagre og legg til"
"saveAndAdd": "Lagre og legg til",
"continue": "Fortsett"
},
"filament": {
"fields": {
@@ -41,7 +42,12 @@
"vendor": "Leverandør",
"settings_bed_temp": "Printplate temp",
"density": "Tetthet",
"spools": "Vis ruller"
"spools": "Vis ruller",
"single_color": "Ensfarget",
"multi_color": "Flerfarget",
"coaxial": "Koaksial",
"longitudinal": "Langsgående",
"external_id": "Ekstern ID"
},
"fields_help": {
"name": "Filamentnavn, for å skille denne filamenttypen blant andre fra samme leverandør. Bør inneholde fargen f.eks.",
@@ -49,7 +55,8 @@
"spool_weight": "Vekten av en tom spole. Brukes til å bestemme målt vekt på en spole.",
"price": "Pris på en full spole i den system- valutaen.",
"weight": "Filamentvekten til en full spole (nettovekt). Bør ikke inkludere vekten av selve spolen, kun filamentet. Det er det som vanligvis står på emballasjen.",
"material": "F.eks PLA,ABS,PETG, osv."
"material": "F.eks PLA,ABS,PETG, osv.",
"multi_color_direction": "Retning av fargeovergang for flerfarget filament."
},
"titles": {
"clone": "Kopier Filament",
@@ -61,7 +68,12 @@
},
"filament": "Filamenter",
"form": {
"filament_updated": "Dette filamentet har blitt oppdatert av noen/noe siden du åpnet denne siden. Lagring nå vil overskrive disse endringene!"
"filament_updated": "Dette filamentet har blitt oppdatert av noen/noe siden du åpnet denne siden. Lagring nå vil overskrive disse endringene!",
"import_external": "Importer fra ekstern database",
"import_external_description": "Hent filamentdata automatisk fra en ekstern kilde."
},
"buttons": {
"add_spool": "Legg til spole"
}
},
"printing": {
@@ -95,25 +107,53 @@
"description": "Still inn innstillingene nedenfor for å få ønsket utskriftsoppsett. Husk at skrivere og operativsystemet ditt kan bruke sine egne marger og skalering, så det kan hende du må prøve og feile før alt er riktig. Test det på et stykke vanlig papir før du skriver ut på selve etikettene.",
"horizontalSpacing": "Horisontal avstand",
"marginLeft": "Venstre marg",
"showBorder": "Vis side"
"showBorder": "Vis side",
"defaultSettings": "Standard",
"newSetting": "Ny",
"addSettings": "Legg til ny mal",
"duplicateSettings": "Dupliser",
"deleteSettings": "Slett gjeldende mal",
"deleteSettingsConfirm": "Er du sikker på at du vil slette denne malen?",
"settingsName": "Navn på mal",
"saveSetting": "Lagre mal",
"settings": "Maler",
"saveAsImage": "Lagre som bilde",
"itemCopies": "Antall kopier"
},
"qrcode": {
"showTemperatures": "Temperaturer",
"button": "Skriv ut QR-koder",
"button": "Skriv ut etiketter",
"showVendorComment": "Leverandørkommentar",
"spoolWeight": "Spolevekt: {{weight}}",
"showVendor": "Leverandør",
"showSpoolComment": "Spolekommentar",
"title": "QR-kodeutskrift",
"title": "Etikettprinting",
"extruderTemp": "ET: {{temp}}",
"showSpoolWeight": "Spolevekt",
"showContent": "Skriv ut etikett",
"textSize": "Tekststørrelse på innhold",
"textSize": "Tekststørrelse på etikett",
"showSpoolmanIcon": "Vis Spoolman ikon",
"showLotNr": "Batch Nr",
"bedTemp": "BT: {{temp}}",
"lotNr": "Batch Nr: {{lot}}",
"showFilamentComment": "Filament kommentar"
"showFilamentComment": "Filament kommentar",
"template": "Etikett mal",
"showQRCodeMode": {
"simple": "Simpel",
"no": "Nei",
"withIcon": "Med ikon"
},
"showQRCode": "Print QR-kode",
"useHTTPUrl": {
"label": "QR-kodelenke",
"options": {
"default": "Standard",
"url": "URL"
},
"preview": "Forhåndsvisning:",
"tooltip": "Velg om QR-koden skal bruke HTTP- eller intern URL."
},
"templateHelp": "Velg hvilken etikettemplate du ønsker å bruke."
},
"spoolSelect": {
"showArchived": "Vis arkiverte",
@@ -121,7 +161,7 @@
"noSpoolsSelected": "Du har ikke valgt noen spoler.",
"selectedTotal_one": "{{count}} spole valgt",
"selectedTotal_other": "{{count}} spoler valgt",
"description": "Velg spoler å printe ut QR-koder for.",
"description": "Velg spoler å printe ut etiketter for.",
"selectAll": "Velg/fjern merking av alle"
}
},
@@ -146,7 +186,11 @@
"filament_name": "Filament",
"filament": "Filament",
"location": "Lokasjon",
"price": ""
"price": "Pris",
"filament_internal": "Internt",
"filament_external": "Eksternt",
"initial_weight": "Startvekt",
"spool_weight": "Tom vekt"
},
"titles": {
"archive": "Arkiver spole",
@@ -155,7 +199,8 @@
"create": "Opprett spole",
"clone": "Klon spole",
"show_title": "[Spole #{{id}}] {{name}}",
"list": "Spoler"
"list": "Spoler",
"adjust": "Juster spolefilament"
},
"spool": "Spoler",
"messages": {
@@ -168,11 +213,24 @@
"lot_nr": "Produsentens batch nummer. Kan bli benyttet or å verifiserer at print har en konsistent farge dersom flere spoler benyttes.",
"remaining_weight": "Hvor mye filament gjenstår på spolen. Ved en ny spole burde dette stemme med spolevekt.",
"location": "Hvor spolen er plassert hvis du har flere lokasjoner hvor du oppbevarer spoler.",
"price": ""
"price": "Pris på en full spole. Hvis ikke satt, vil prisen på filamentet bli antatt i stedet.",
"spool_weight": "Vekten av spolen når den er tom. La stå tomt for å bruke verdien fra filamentet eller produsenten.",
"initial_weight": "Startvekten av filament på spolen (nettovekt). Vil bruke vekten fra filament hvis ikke satt.",
"external_filament": "Du har valgt et filament fra den eksterne databasen. Et filamentobjekt (og muligens et produsentobjekt) opprettes automatisk når du oppretter denne spolen. Dette kan lage dupliserte filamentobjekter hvis du allerede har opprettet et filamentobjekt for denne filamentet."
},
"form": {
"new_location_prompt": "Skriv inn en ny lokasjon",
"spool_updated": "Denne rullen har blitt oppdatert av noen/noe siden du åpnet denne siden. Lagre nå vil overskrive disse endringene!"
"spool_updated": "Denne rullen har blitt oppdatert av noen/noe siden du åpnet denne siden. Lagre nå vil overskrive disse endringene!",
"measurement_type": {
"length": "Lengde",
"weight": "Vekt"
},
"measurement_type_label": "Måleenhet",
"adjust_filament_help": "Justér verdien hvis målt filamentmengde avviker fra forventet.",
"adjust_filament_value": "Juster mengde"
},
"formats": {
"last_used": "Sist brukt {{date}}"
}
},
"yes": "Ja",
@@ -182,7 +240,9 @@
"registered": "Registrert",
"name": "Navn",
"id": "ID",
"comment": "Kommentar"
"comment": "Kommentar",
"empty_spool_weight": "Tom spolevekt",
"external_id": "Ekstern ID"
},
"titles": {
"show_title": "[Leverandør #{{id}}] {{name}}",
@@ -195,6 +255,9 @@
"vendor": "Leverandører",
"form": {
"vendor_updated": "Denne leverandøren har blitt oppdatert av noen/noe siden du åpnet denne siden. Lagring nå vil overskrive disse endringene!"
},
"fields_help": {
"empty_spool_weight": "Vekten av en tom spole for denne leverandøren."
}
},
"actions": {
@@ -229,7 +292,9 @@
"createError": "Kunne ikke opprette {{resource}} (statuskode: {{statusCode}})",
"editSuccess": "{{resource}} redigert",
"deleteError": "Kunne ikke slette {{resource}} (statuskode: {{statusCode}})",
"createSuccess": "Opprettet {{resource}}"
"createSuccess": "Opprettet {{resource}}",
"saveSuccessful": "Lagring vellykket!",
"validationError": "Valideringsfeil: {{error}}"
},
"loading": "Laster",
"documentTitle": {
@@ -257,30 +322,90 @@
"edit": "#{{id}} Endre Filament | Spoolman"
},
"home": {
"list": ""
"list": "Hjem | Spoolman"
},
"help": {
"list": ""
"list": "Hjelp | Spoolman"
},
"locations": {
"list": "Lokasjoner | Spoolman"
}
},
"table": {
"actions": "Handliger"
"actions": "Handlinger"
},
"warnWhenUnsavedChanges": "Er du sikker på at du vil forlate? Du har ulagrede endringer.",
"dashboard": {
"title": "Oversikt"
},
"kofi": "",
"kofi": "Gi meg tips på Ko-fi",
"home": {
"home": ""
"home": "Hjem",
"welcome": "Velkommen til Spoolman!",
"description": "Det ser ut som du ikke har lagt til noen spoler ennå. Se <helpPageLink>hjelpesiden</helpPageLink> for hjelp til å komme i gang."
},
"help": {
"help": "",
"help": "Hjelp",
"resources": {
"filament": "",
"spool": "",
"vendor": ""
"filament": "Filament leverandører. De har egenskaper som navn, materiale, farge, diameter og mer.",
"spool": "Individuelle fysiske ruller med spesifikt filament.",
"vendor": "Produsent av filament."
},
"description": ""
"description": "<title>Hjelp</title><p>Her er noen tips for å komme i gang.</p><p>Spoolman inneholder tre forskjellige typer data:</p><itemsHelp/><p>For å legge til en ny spole i databasen, start med å opprette et <filamentCreateLink>Filament</filamentCreateLink>-objekt. Når det er gjort, fortsett med å opprette et <spoolCreateLink>Spool</spoolCreateLink>-objekt for den spesifikke spolen. Hvis du anskaffer flere spoler av samme filament senere, genererer du ganske enkelt flere spoleobjekter og bruker det samme filamentobjektet på nytt.</p><p>Alternativt kan du også generere et <vendorCreateLink>Manufacturer</vendorCreateLink>-objekt for selskapet som produserer filamentet hvis du ønsker å spore denne informasjonen.</p><p>Du har muligheten til å koble andre 3D-printertjenester til Spoolman, som Moonraker, som automatisk kan overvåke filamentbruk og oppdatere spoleobjektene for deg. Se <readmeLink>Spoolman README</readmeLink> for veiledning om hvordan du konfigurerer dette.</p>"
},
"locations": {
"locations": "Lokasjoner",
"new_location": "Ny lokasjon",
"no_location": "Ingen lokasjon",
"no_locations_help": "Denne siden mar deg organisere rullene med filament i lokasjoner. Legg til noen ruller for å starte!"
},
"settings": {
"settings": "Innstillinger",
"header": "Innstillinger",
"general": {
"tab": "Generelt",
"currency": {
"label": "Valuta"
},
"base_url": {
"label": "Base-URL",
"tooltip": "URL som brukes ved generering av QR-koder og eksterne lenker."
},
"round_prices": {
"label": "Avrund priser",
"tooltip": "Runder priser til nærmeste hele verdi."
}
},
"extra_fields": {
"tab": "Ekstra felt",
"description": "<p>Her kan du legge til ekstra tilpassede felt til enhetene dine.</p><p>Når et felt er lagt til, kan du ikke endre nøkkelen eller typen, og for valgtypefelt kan du ikke fjerne valg eller endre flervalgsstatusen. Hvis du fjerner et felt, slettes de tilknyttede dataene for alle enheter.</p><p>Nøkkelen er det andre programmer leser/skriver dataene som, så hvis det tilpassede feltet ditt skal integreres med et tredjepartsprogram, må du sørge for å angi det riktig. Standardverdien brukes bare på nye elementer.</p><p>Ekstra felt kan ikke sorteres eller filtreres i tabellvisningene.</p>",
"params": {
"key": "Nøkkel",
"name": "Navn",
"field_type": "Felttype",
"unit": "Enhet",
"order": "Rekkefølge",
"default_value": "Standardverdi",
"choices": "Valg",
"multi_choice": "Flervalg"
},
"field_type": {
"text": "Tekst",
"integer": "Heltall",
"integer_range": "Heltallsområde",
"float": "Desimaltall",
"float_range": "Desimalområde",
"datetime": "Dato/Tid",
"boolean": "Ja/Nei",
"choice": "Valg"
},
"boolean_true": "Ja",
"boolean_false": "Nei",
"choices_missing_error": "Du kan ikke fjerne valg. Følgende valg mangler: {{choices}}",
"non_unique_key_error": "Nøkkelen må være unik.",
"key_not_changed": "Nøkkelen kan ikke endres.",
"delete_confirm": "Slette felt {{name}}?",
"delete_confirm_description": "Dette vil fjerne feltet permanent fra systemet."
}
}
}
+176 -43
View File
@@ -9,19 +9,20 @@
"showArchived": "Gearchiveerd tonen",
"archive": "Archiveren",
"hideColumns": "Kolommen verbergen",
"confirm": "Weet je het zeker?",
"confirm": "Weet u het zeker?",
"delete": "Verwijderen",
"undo": "Ongedaan maken",
"import": "Importeren",
"logout": "Uitloggen",
"refresh": "Vernieuwen",
"notAccessTitle": "Je hebt geen toegang tot",
"notAccessTitle": "U heeft geen toegang tot",
"hideArchived": "Gearchiveerd verbergen",
"clearFilters": "Filters leegmaken",
"unArchive": "Dearchiveren",
"unArchive": "Terugzetten",
"clone": "Klonen",
"clear": "Wissen",
"saveAndAdd": "Opslaan en Toevoegen"
"saveAndAdd": "Opslaan en Toevoegen",
"continue": "Doorgaan"
},
"actions": {
"create": "Aanmaken",
@@ -39,10 +40,10 @@
"helpPrinterMargin": "Safe-Zone moet worden ingesteld op hoe dicht bij de papierrand uw printer kan afdrukken. Als u deze wijzigt, heeft dit geen invloed op het hele raster.",
"previewScale": "Voorbeeldschaal",
"helpMargin": "Marges moeten worden geconfigureerd zodat ze overeenkomen met je labelpapier en printer. Als u deze wijzigt, heeft dit invloed op de grootte van het hele raster.",
"skipItems": "Items overlaan",
"skipItems": "Items overslaan",
"paperSize": "Papierformaat",
"columns": "Kolommen",
"description": "Stem de onderstaande instellingen af om de gewenste printlay-out te krijgen. Houd er rekening mee dat printers en je besturingssysteem hun eigen marges en schaling kunnen toepassen, dus het kan zijn dat je met wat instellingen moet spelen voordat alles correct is. Test het op een stuk gewoon papier voordat u op de daadwerkelijke etiketten afdrukt.",
"description": "Stem de onderstaande instellingen af om de gewenste print lay-out te krijgen. Houd er rekening mee dat printers en je besturingssysteem hun eigen marges en schaling kunnen toepassen, dus het kan zijn dat je met wat instellingen moet spelen voordat alles correct is. Test het op een stuk gewoon papier voordat u op de daadwerkelijke etiketten afdrukt.",
"showBorder": "Rand tonen",
"dimensions": "Afmetingen",
"print": "Printen",
@@ -61,7 +62,18 @@
"marginBottom": "Onderste marge",
"horizontalSpacing": "Horizontale afstand",
"marginLeft": "Linker marge",
"printerMarginLeft": "Linker Safe-Zone"
"printerMarginLeft": "Linker Safe-Zone",
"settings": "Voorinstellingen",
"defaultSettings": "Standaard",
"addSettings": "Nieuwe voorinstelling toevoegen",
"newSetting": "Nieuw",
"deleteSettings": "Huidige voorinstelling wissen",
"deleteSettingsConfirm": "Weet je zeker dat je deze voorinstelling wilt verwijderen?",
"settingsName": "Voorinstellingsnaam",
"itemCopies": "Itemkopieën",
"duplicateSettings": "Dupliceren",
"saveSetting": "Voorinstelling opslaan",
"saveAsImage": "Opslaan als afbeelding"
},
"qrcode": {
"extruderTemp": "ET: {{temp}}",
@@ -72,42 +84,61 @@
"showTemperatures": "Temperaturen",
"showVendor": "Fabrikant",
"showSpoolComment": "Opmerking spoel",
"textSize": "Inhoud tekstgrootte",
"title": "QR-Code afdrukken",
"textSize": "Label tekstgrootte",
"title": "Label afdrukken",
"showSpoolmanIcon": "Spoolman icoon tonen",
"showLotNr": "Lotnr",
"showLotNr": "Lotnummer",
"bedTemp": "BT: {{temp}}",
"lotNr": "Lotnr: {{lot}}",
"lotNr": "Lotnummer: {{lot}}",
"showFilamentComment": "Opmerking filament",
"button": "QR-Codes afdrukken"
"button": "Labels afdrukken",
"template": "Label Sjabloon",
"templateHelp": "Gebruik {} om waarden van het spool-object als tekst in te voegen. Bijvoorbeeld, {id} wordt vervangen door de spool-id, of {filament.material} wordt vervangen door het materiaal van de spool. Als een waarde ontbreekt, wordt deze vervangen door \"?\". Een tweede set {} kan worden gebruikt om dit te verwijderen. Bovendien wordt alle tekst tussen de sets {} verwijderd als de waarde ontbreekt. Bijvoorbeeld, {Lot Nr: {lot_nr}} toont alleen het label als de spool een lotnummer heeft. Sluit tekst in met dubbele asterisk ** om deze vet te maken. Klik op de knop om een lijst met alle beschikbare tags te bekijken.",
"showQRCodeMode": {
"no": "Nee",
"simple": "Simpel",
"withIcon": "Met Icoon"
},
"showQRCode": "Print QR-Code",
"useHTTPUrl": {
"options": {
"url": "URL",
"default": "Standaard"
},
"label": "QR-codelink",
"tooltip": "Er wordt een speciale link gebruikt die alleen werkt als deze wordt gescand via de scanfunctie van Spoolman (standaard). De URL gebruikt de basis-URL die is opgegeven in de instellingen of de URL van de huidige pagina als deze niet is ingesteld.",
"preview": "Voorbeeld:"
}
},
"spoolSelect": {
"selectedTotal_one": "{{count}} spoel geselecteerd",
"selectedTotal_other": "{{count}} spoelen geselecteerd",
"description": "Selecteer spoelen waarvoor je QR-codes wilt afdrukken.",
"description": "Selecteer spoelen waarvoor je labels wilt afdrukken.",
"showArchived": "Gearchiveerde tonen",
"title": "Spoelen selecteren",
"selectAll": "Alles selecteren/deselecteren",
"noSpoolsSelected": "Je hebt geen spoelen geselecteerd."
"selectAll": "Alles (de)selecteren",
"noSpoolsSelected": "U heeft geen spoelen geselecteerd."
}
},
"notifications": {
"deleteSuccess": "{{resource}} succesvol verwijderd",
"error": "Fout (fout code {{statusCode}})",
"editError": "Fout opgetreden bij het bewerken van {{resource}} (status code {{statusCode}})",
"undoable": "Je hebt {{seconds}} seconden om ongedaan te maken.",
"undoable": "U heeft {{seconds}} seconden om ongedaan te maken.",
"importProgress": "Importeren: {{processed}}/{{total}}",
"createError": "Er is een fout opgetreden bij het aanmaken van {{resource}} (status code {{statusCode}})",
"success": "Succesvol",
"editSuccess": "{{resource}} succesvol bewerkt",
"deleteError": "Fout opgetreden bij het verwijderen van {{resource}} (status code {{statusCode}})",
"createSuccess": "{{resource}} succesvol aangemaakt"
"createSuccess": "{{resource}} succesvol aangemaakt",
"saveSuccessful": "Succesvol opgeslagen!",
"validationError": "Validatie error: {{error}}"
},
"no": "Nee",
"unknown": "Onbekend",
"yes": "Ja",
"version": "Versie",
"warnWhenUnsavedChanges": "Weet je zeker dat je wil vertrekken? Er zijn niet-opgeslagen wijzigingen.",
"warnWhenUnsavedChanges": "Weet u zeker dat u deze pagina wilt verlaten? Er zijn niet-opgeslagen wijzigingen.",
"loading": "Laden",
"dashboard": {
"title": "Dashboard"
@@ -118,7 +149,7 @@
"documentTitle": {
"vendor": {
"create": "Fabrikant maken | Spoolman",
"clone": "#{{id}} Leverancier klonen | Spoolman",
"clone": "#{{id}} Fabrikant klonen | Spoolman",
"edit": "#{{id}} Fabrikant bewerken | Spoolman",
"list": "Fabrikanten | Spoolman",
"show": "#{{id}} Fabrikant tonen | Spoolman"
@@ -144,6 +175,9 @@
},
"help": {
"list": "Help | Spoolman"
},
"locations": {
"list": "Locaties | Spoolman"
}
},
"spool": {
@@ -152,7 +186,7 @@
"last_used": "Laatst gebruikt",
"material": "Materiaal",
"weight_to_use": "Gewicht",
"lot_nr": "Lotnr",
"lot_nr": "Lotnummer",
"filament_name": "Filament",
"comment": "Opmerking",
"filament": "Filament",
@@ -165,7 +199,11 @@
"used_weight": "Gebruikt gewicht",
"id": "ID",
"remaining_weight": "Overgebleven gewicht",
"price": ""
"price": "Prijs",
"initial_weight": "Initieel gewicht",
"spool_weight": "Leeg gewicht",
"filament_internal": "Intern",
"filament_external": "Extern"
},
"titles": {
"archive": "Spoel archiveren",
@@ -174,34 +212,49 @@
"create": "Spoel maken",
"clone": "Spoel klonen",
"list": "Spoelen",
"show_title": "[Spoel #{{id}}] {{name}}"
"show_title": "[Spoel #{{id}}] {{name}}",
"adjust": "Spoel Filament aanpassen"
},
"fields_help": {
"used_weight": "Hoeveel filament er van de spoel is gebruikt. Op een nieuwe spoel zal 0g gebruikt zijn.",
"weight_to_use": "Selecteer welke gewichtswaarde je wilt invoeren. Het gemeten gewicht is alleen beschikbaar als het spoelgewicht is ingesteld voor het geselecteerde filament.",
"weight_to_use": "Selecteer welke gewichtswaarde u wilt invoeren. Het gemeten gewicht is alleen beschikbaar als het spoelgewicht is ingesteld voor het geselecteerde filament.",
"measured_weight": "Hoeveel het filament en de spoel wegen.",
"lot_nr": "Lotnummer van de fabrikant. Kan worden gebruikt om ervoor te zorgen dat een afdruk een consistente kleur heeft als er meerdere spoelen worden gebruikt.",
"remaining_weight": "Hoeveel filament zit er nog op de spoel. Bij een nieuwe spoel zal dit overeenkomen met het spoelgewicht.",
"location": "Waar de spoel zich bevindt als je meerdere locaties hebt waar je jouw spoelen opslaat.",
"price": ""
"location": "Waar de spoel zich bevindt, wanneer er meerdere opslag locaties zijn.",
"price": "Prijs van een volledige spoel. Als dit niet wordt ingevuld, wordt in plaats daarvan uitgegaan van de prijs van het filament.",
"initial_weight": "Het initiële gewicht van het filament op de spoel (nettogewicht). Als dit niet is opgegeven, wordt het gewicht van het filament-object gebruikt.",
"spool_weight": "Het gewicht van de spoel als deze leeg is. Laat dit leeg om de waarde van het filament of de fabrikant te gebruiken.",
"external_filament": "U heeft een filament uit de externe database geselecteerd. Er wordt automatisch een filament-object (en mogelijk een fabrikant-object) gemaakt wanneer u deze spoel maakt. Hierdoor kunnen dubbele filament-objecten ontstaan als u al een filament-object voor dit filament hebt gemaakt."
},
"spool": "Spoelen",
"messages": {
"archive": "Weet je zeker dat je deze spoel wil archiveren?"
"archive": "Weet u zeker dat u deze spoel wil archiveren?"
},
"form": {
"new_location_prompt": "Voer een nieuwe locatie in",
"spool_updated": "Deze spoel is bijgewerkt door iemand/iets anders sinds je deze pagina hebt geopend. Opslaan zal die wijzigingen overschrijven!"
"spool_updated": "Deze spoel is bijgewerkt door iemand/iets anders sinds je deze pagina hebt geopend. Opslaan zal die wijzigingen overschrijven!",
"adjust_filament_help": "Hier kunt u direct filament toevoegen of verwijderen van de spoel. Een positieve waarde zal filament verbruiken, een negatieve waarde zal het toevoegen.",
"measurement_type": {
"length": "Lengte",
"weight": "Gewicht"
},
"measurement_type_label": "Meettype",
"adjust_filament_value": "Verbruikte hoeveelheid"
},
"formats": {
"last_used": "Laatst gebruikt {{date}}"
}
},
"filament": {
"fields_help": {
"name": "Filamentnaam, om dit filamenttype onder andere van dezelfde fabrikant te onderscheiden. Kan bijvoorbeeld de kleur bevatten.",
"name": "Filamentnaam, om dit filamenttype onder andere van dezelfde fabrikant te onderscheiden. Moet bijvoorbeeld de kleur bevatten.",
"article_number": "Bv. EAN. UPC, enz.",
"spool_weight": "Het gewicht van een lege spoel. Wordt gebruikt om het gemeten gewicht van een spoel te bepalen.",
"price": "Prijs van een volledige spoel in de door het systeem geconfigureerde valuta.",
"price": "Prijs van een volledige spoel.",
"weight": "Het filamentgewicht van een volle spoel (nettogewicht). Dit mag niet het gewicht van de spoel zelf omvatten, alleen het filament. Dit staat meestal op de verpakking.",
"material": "Bv. PLA, ABS, PETG, enz."
"material": "Bv. PLA, ABS, PETG, enz.",
"multi_color_direction": "Filamenten kunnen op twee manieren meerdere kleuren hebben: door coëxtrusie, zoals tweekleurige filamenten met consistente meerkleuren, of door kleurveranderingen in de lengterichting, zoals verloopfilamenten die van kleur veranderen langs de spoel."
},
"fields": {
"material": "Materiaal",
@@ -219,20 +272,30 @@
"density": "Dichtheid",
"id": "ID",
"color_hex": "Kleur",
"article_number": "Artiekelnummer",
"spools": "Spoelen tonen"
"article_number": "Artikel nummer",
"spools": "Spoelen tonen",
"external_id": "Extern ID",
"single_color": "Enkel",
"multi_color": "Meervoudig",
"coaxial": "Gecoëxtrudeerd",
"longitudinal": "Longitudinaal"
},
"titles": {
"clone": "Flimanent klonen",
"clone": "Filament klonen",
"show": "Filament tonen",
"create": "Flimament maken",
"create": "Filament maken",
"edit": "Filament bewerken",
"list": "Filamenten",
"show_title": "[Filament #{{id}}] {{name}}"
},
"filament": "Filamenten",
"form": {
"filament_updated": "Dit filament is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. Deze veranderingen worden overschreven als je op slaat!"
"filament_updated": "Dit filament is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. Deze veranderingen worden overschreven als je op slaat!",
"import_external_description": "Selecteer een filament uit de lijst om de details automatisch aan het formulier voor het maken van filamenten toe te voegen. Hierdoor worden alle gegevens die je in het formulier ingevoerd hebt, overschreven.<br><br>Er wordt indien nodig automatisch een fabrikant-object aangemaakt als je op OK klikt.",
"import_external": "Importeren van extern"
},
"buttons": {
"add_spool": "Spoel Toevoegen"
}
},
"scanner": {
@@ -251,8 +314,8 @@
"titles": {
"show": "Fabrikant tonen",
"edit": "Fabrikant bewerken",
"clone": "Fabrikant klonen",
"create": "Fabrikant maken",
"clone": "Fabrikant Klonen",
"create": "Fabrikant Aanmaken",
"list": "Fabrikanten",
"show_title": "[Fabrikant #{{id}}] {{name}}"
},
@@ -260,15 +323,20 @@
"registered": "Geregistreerd",
"comment": "Opmerking",
"name": "Naam",
"id": "ID"
"id": "ID",
"empty_spool_weight": "Gewicht Lege Spoel",
"external_id": "Externe ID"
},
"vendor": "Fabrikanten",
"form": {
"vendor_updated": "Deze leverancier is bijgewerkt dor iemand/iets anders sinds je deze pagina geopend hebt. De veranderingen worden overschreven als je opslaat!"
"vendor_updated": "Deze fabrikant is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. De veranderingen worden overschreven als je opslaat!"
},
"fields_help": {
"empty_spool_weight": "Het gewicht van een lege spoel van deze fabrikant."
}
},
"table": {
"actions": "Akties"
"actions": "Acties"
},
"help": {
"resources": {
@@ -276,11 +344,76 @@
"filament": "Merken filament. Ze hebben eigenschappen zoals naam, materiaal, kleur, diameter en meer.",
"spool": "Individuele fysieke spoelen van een specifiek filament."
},
"description": "<title>Help</title><p>Hier zijn enkele tips om u op weg te helpen.</p><p>Spoolman bewaart 3 verschillende soorten gegevens:</p><itemsHelp/><p>Om een nieuwe toe spoel toe te voegen in de database, begin dan met het maken van een <filamentCreateLink>Filament</filamentCreateLink> object. Zodra dat is gebeurd, gaat u verder met het maken van een <spoolCreateLink>Spoel</spoolCreateLink>-object voor die specifieke spoel. Als u later extra spoelen van hetzelfde filament aanschaft, genereert u eenvoudigweg extra Spoel-objecten en hergebruikt u hetzelfde Filament-object.</p><p>Optioneel kunt u ook een <vendorCreateLink>Vendor</vendorCreateLink>-object voor het bedrijf genereren als u die informatie wilt bijhouden.</p><p>U heeft de mogelijkheid om andere 3D-printerservices aan Spoolman te koppelen, zoals Moonraker, die automatisch het filament gebruik kan monitoren en de Spoel-objecten voor u kan bijwerken. Raadpleeg de <readmeLink>Spoolman README</readmeLink> voor richtlijnen over hoe u dat kunt instellen.</p>",
"description": "<title>Help</title><p>Hier zijn enkele tips om u op weg te helpen.</p><p>Spoolman bewaart 3 verschillende soorten gegevens:</p><itemsHelp/><p>Om een nieuwe toe spoel toe te voegen in de database, begin dan met het maken van een <filamentCreateLink>Filament</filamentCreateLink> object. Zodra dat is gebeurd, gaat u verder met het maken van een <spoolCreateLink>Spoel</spoolCreateLink>-object voor die specifieke spoel. Als u later extra spoelen van hetzelfde filament aanschaft, genereert u eenvoudigweg extra Spoel-objecten en hergebruikt u hetzelfde Filament-object.</p><p>Optioneel kunt u ook een <vendorCreateLink>Fabrikant</vendorCreateLink>-object voor het bedrijf genereren als u die informatie wilt bijhouden.</p><p>U heeft de mogelijkheid om andere 3D-printerservices aan Spoolman te koppelen, zoals Moonraker, die automatisch het filament gebruik kan monitoren en de Spoel-objecten voor u kan bijwerken. Raadpleeg de <readmeLink>Spoolman README</readmeLink> voor richtlijnen over hoe u dat kunt instellen.</p>",
"help": "Help"
},
"kofi": "Tip mij op Ko-fi",
"home": {
"home": "Thuis"
}
"home": "Thuis",
"welcome": "Welkom bij je eigen Spoolman-account!",
"description": "Het lijkt erop dat je nog geen spoelen hebt toegevoegd. Zie de <helpPageLink>Help-pagina</helpPageLink> voor hulp bij het opstarten."
},
"settings": {
"header": "Instellingen",
"general": {
"tab": "Algemeen",
"currency": {
"label": "Valuta"
},
"base_url": {
"label": "Basis-URL",
"tooltip": "De basis-URL die moet worden gebruikt bij het genereren van functies zoals QR-codes."
},
"round_prices": {
"label": "Afgeronde prijzen",
"tooltip": "Rond prijzen af op het dichtstbijzijnde gehele getal."
}
},
"extra_fields": {
"tab": "Extra velden",
"params": {
"name": "Naam",
"key": "Sleutel",
"field_type": "Type",
"unit": "Eenheid",
"order": "Volgorde",
"default_value": "Standaardwaarde",
"choices": "Keuzes",
"multi_choice": "Meerkeuze"
},
"field_type": {
"text": "Tekst",
"integer": "Gehele getallen",
"integer_range": "Bereik van gehele getallen",
"float": "Kommagetal",
"float_range": "Bereik van kommagetallen",
"datetime": "Datum/tijd",
"boolean": "Booleaanse waarde",
"choice": "Keuze"
},
"boolean_true": "Ja",
"boolean_false": "Nee",
"choices_missing_error": "U kunt geen keuzes verwijderen. De volgende keuzes ontbreken: {{choices}}",
"non_unique_key_error": "De key moet uniek zijn.",
"key_not_changed": "Key niet gewijzigd.",
"delete_confirm": "Veld {{name}} verwijderen?",
"delete_confirm_description": "Hierdoor worden het veld en alle bijbehorende gegevens voor alle entiteiten verwijderd.",
"description": "<p>Hier kunt u extra aangepaste velden toevoegen aan uw entiteiten.</p><p>Zodra een veld is toegevoegd, kunt u de key of het type ervan niet wijzigen, en voor velden met keuzetypes kunt u geen keuzes verwijderen of de meervoudige velden wijzigen. Als u een veld verwijdert, worden de bijbehorende gegevens voor alle entiteiten verwijderd.</p><p>De key is hoe andere programma's de gegevens lezen/schrijven, dus of uw aangepaste veld moet worden geïntegreerd met een derde partij programma, zorg ervoor dat u het correct instelt. De standaardwaarde wordt alleen toegepast op nieuwe items.</p><p>Extra velden kunnen worden weergegeven en gesorteerd in de tabelweergaven. Keuze- en booleaanse-velden bieden daar ook filteropties, en alle typen extra velden ondersteunen het filteren op lege waarden.</p>",
"choices_reorder_hint": "Sleep of gebruik de pijltjestoetsen om deze keuze opnieuw te ordenen."
},
"settings": "Instellingen"
},
"locations": {
"no_location": "Geen locatie",
"no_locations_help": "Op deze pagina kunt u uw spoelen op verschillende locaties ordenen. Voeg spoelen toe om aan de slag te gaan!",
"locations": "Locaties",
"new_location": "Nieuwe locatie"
},
"theme": {
"light": "Licht",
"dark": "Donker",
"system": "Systeem",
"label": "Thema"
},
"sponsor": "Steun me op GitHub"
}
+185 -51
View File
@@ -8,7 +8,7 @@
},
"buttons": {
"edit": "Edytuj",
"unArchive": "Odarchiwizuj",
"unArchive": "Przywróć",
"cancel": "Anuluj",
"create": "Utwórz",
"save": "Zapisz",
@@ -28,7 +28,8 @@
"notAccessTitle": "Nie masz zezwolenia na dostęp",
"hideColumns": "Edytuj kolumny",
"clearFilters": "Wyczyść filtry",
"saveAndAdd": "Zapisz i dodaj"
"saveAndAdd": "Zapisz i dodaj",
"continue": "Kontynuuj"
},
"spool": {
"titles": {
@@ -38,7 +39,8 @@
"create": "Tworzenie szpuli",
"clone": "Klonowanie szpuli",
"archive": "Zarchiwizuj szpulę",
"show_title": "[Szpula #{{id}}] {{name}}"
"show_title": "[Szpula #{{id}}] {{name}}",
"adjust": "Dostosuj szpulę filamentu"
},
"spool": "Szpule",
"fields": {
@@ -47,19 +49,23 @@
"filament": "Filament",
"material": "Materiał",
"remaining_weight": "Pozostała waga",
"used_length": "Użyta długość",
"used_length": "Zużyta długość",
"remaining_length": "Pozostała długość",
"location": "Lokalizacja",
"lot_nr": "Numer partii (LOT)",
"first_used": "Pierwsze użycie",
"last_used": "Ostatnie użycie",
"comment": "Komentarz",
"archived": "Zarchiwizowano",
"used_weight": "Użyta waga",
"archived": "Zarchiwizowana",
"used_weight": "Zużyta waga",
"registered": "Zarejestrowano",
"measured_weight": "Zmierzona waga",
"weight_to_use": "Waga",
"price": ""
"price": "Cena",
"initial_weight": "Waga początkowa",
"spool_weight": "Waga pusta",
"filament_internal": "Wewnętrzne",
"filament_external": "Zewnętrzne"
},
"fields_help": {
"location": "Wpisz gdzie znajduje się szpula, jeśli masz wiele lokalizacji, w których przechowujesz szpule.",
@@ -68,14 +74,27 @@
"remaining_weight": "Ile filamentu pozostało na szpuli. W przypadku nowej szpuli powinno to odpowiadać wadze szpuli.",
"measured_weight": "Waga filamentu i szpuli.",
"weight_to_use": "Wybierz jaką wagę wprowadzić. Zmierzona waga jest dostępna tylko wtedy, gdy waga szpuli jest ustawiona dla wybranego filamentu.",
"price": ""
"price": "Cena pełnej szpuli. Jeśli nie ustawiona, cena filamentu będzie domyślna.",
"initial_weight": "Masa początkowa filamentu(netto). Może być podana przez producenta, ale możesz ustawić dla każdej szpuli, aby uwzględnić ewentualne niedokładności.",
"spool_weight": "Waga pustej szpuli. Pozostaw puste, aby użyć wartości z filamentu lub od producenta.",
"external_filament": "Wybrałeś filament z zewnętrznej bazy. Element filamentu (możliwe też że element producenta) zostanie stworzony automatycznie gdy stworzysz szpulę. Może to spowodować utworzenie się duplikatów elementów filamentu jeżeli już stworzyłeś element filamentu dla tego filamentu."
},
"messages": {
"archive": "Czy na pewno chcesz zarchiwizować tę szpulę?"
},
"form": {
"new_location_prompt": "Wprowadź nową lokalizację",
"spool_updated": "Szpula została zaktualizowana od czasu otwarcia tej strony. Zapisanie nadpisze te zmiany!"
"spool_updated": "Szpula została zaktualizowana od czasu otwarcia tej strony. Zapisanie nadpisze te zmiany!",
"measurement_type": {
"length": "Długość",
"weight": "Waga"
},
"measurement_type_label": "Typ pomiaru",
"adjust_filament_help": "Tutaj możesz bezpośrednio dodać lub usunąć filament z szpuli. Dodatnia wartość spowoduje odjęcie filamentu, ujemna wartość spowoduje jego dodanie.",
"adjust_filament_value": "Ilość zużycia"
},
"formats": {
"last_used": "Ostatnio użyty {{date}}"
}
},
"dashboard": {
@@ -91,7 +110,9 @@
"editError": "Błąd podczas edycji {{resource}} (kod statusu: {{statusCode}})",
"importProgress": "Importowanie: {{processed}}/{{total}}",
"error": "Błąd (kod statusu: {{statusCode}})",
"createError": "Wystąpił błąd podczas tworzenia {{resource}} (kod statusu: {{statusCode}})"
"createError": "Wystąpił błąd podczas tworzenia {{resource}} (kod statusu: {{statusCode}})",
"saveSuccessful": "Zapisano pomyślnie!",
"validationError": "Błąd walidacji: {{error}}"
},
"loading": "Wczytywanie",
"version": "Wersja",
@@ -104,42 +125,53 @@
"printing": {
"generic": {
"title": "Drukowanie",
"helpMargin": "Marginesy powinny być skonfigurowane tak, aby pasowały do papieru etykietowego i drukarki, ich zmiana wpłynie na rozmiar całej siatki.",
"helpPrinterMargin": "Bezpieczna strefa powinna być ustawiona tak, aby znajdowała się jak najbliżej krawędzi papieru, na którym drukarka może drukować, zmiana tych ustawień nie wpłynie na całą siatkę.",
"helpMargin": "Marginesy powinny odpowiadać papierowi etykiet i drukarce — ich zmiana wpływa na rozmiar całej siatki.",
"helpPrinterMargin": "Strefę bezpieczną ustaw na to, jak blisko krawędzi papieru drukarka jeszcze drukuje. Zmiana tych wartości nie wpływa na całą siatkę.",
"print": "Drukuj",
"columns": "Kolumny",
"rows": "Wiersze",
"paperSize": "Rozmiar papieru",
"customSize": "Niestandardowy",
"customSize": "Własny",
"dimensions": "Wymiary",
"showBorder": "Pokaż obramowanie",
"previewScale": "Skala podglądu",
"skipItems": "Pomiń elementy",
"skipItems": "Pomiń pozycje",
"contentSettings": "Ustawienia zawartości",
"layoutSettings": "Ustawienia układu",
"verticalSpacing": "Pionowy odstęp",
"marginLeft": "Lewy margines",
"marginRight": "Prawy margines",
"marginTop": "Górny margines",
"marginBottom": "Dolny margines",
"printerMarginLeft": "Bezpieczna strefa po lewej",
"printerMarginRight": "Bezpieczna strefa po prawej",
"printerMarginTop": "Bezpieczna strefa u góry",
"verticalSpacing": "Odstęp pionowy",
"marginLeft": "Margines lewy",
"marginRight": "Margines prawy",
"marginTop": "Margines górny",
"marginBottom": "Margines dolny",
"printerMarginLeft": "Strefa bezpieczna lewo",
"printerMarginRight": "Strefa bezpieczna prawo",
"printerMarginTop": "Strefa bezpieczna góra",
"borders": {
"none": "Brak",
"border": "Obwódka",
"grid": "Siatka"
},
"description": "Dostosuj poniższe ustawienia, aby uzyskać pożądany układ wydruku. Należy pamiętać, że drukarki i system operacyjny mogą stosować własne marginesy i skalowanie, więc może być konieczne wykonanie kilku prób i błędów, zanim wszystko będzie prawidłowe. Przed wydrukowaniem etykiet przetestuj je na zwykłym papierze.",
"horizontalSpacing": "Poziomy odstęp",
"printerMarginBottom": "Bezpieczna strefa u dołu"
"description": "Dopasuj ustawienia poniżej, aby uzyskać właściwy układ wydruku. Pamiętaj, że drukarka i system mogą dodać własne marginesy i skalowanie, więc zwykle trzeba trochę poeksperymentować. Przetestuj na zwykłej kartce, zanim zujesz właściwe etykiety.",
"horizontalSpacing": "Odstęp poziomy",
"printerMarginBottom": "Strefa bezpieczna dół",
"newSetting": "Nowe",
"defaultSettings": "Domyślne",
"itemCopies": "Kopie pozycji",
"settings": "Presety",
"saveAsImage": "Zapisz jako obraz",
"duplicateSettings": "Zduplikuj",
"deleteSettingsConfirm": "Czy jesteś pewny, że chcesz usunąć to ustawienie?",
"addSettings": "Dodaj nowe ustawienia",
"deleteSettings": "Usuń aktualne ustawienie",
"settingsName": "Nazwa ustawienia",
"saveSetting": "Zapisz ustawienie"
},
"qrcode": {
"button": "Drukuj kody QR",
"title": "Drukowanie kodów QR",
"button": "Drukuj etykiety",
"title": "Drukowanie etykiety",
"bedTemp": "TS: {{temp}}",
"extruderTemp": "TE: {{temp}}",
"textSize": "Rozmiar tekstu",
"textSize": "Rozmiar tekstu etykiety",
"showSpoolmanIcon": "Pokaż ikonę Spoolman",
"showVendor": "Producent",
"showContent": "Etykieta",
@@ -150,11 +182,28 @@
"showVendorComment": "Komentarz producenta",
"spoolWeight": "Waga szpuli: {{weight}}",
"lotNr": "LOT: {{lot}}",
"showSpoolComment": "Komentarz szpuli"
"showSpoolComment": "Komentarz szpuli",
"template": "Wzór etykiety",
"useHTTPUrl": {
"preview": "Podgląd:",
"label": "Link w kodzie QR",
"options": {
"default": "Domyślny",
"url": "URL"
},
"tooltip": "Użyje zastrzeżonego linku, który zadziała tylko po zeskanowaniu za pomocą funkcji skanowania w Spoolman (domyślnie). Opcja URL używa podstawowego adresu URL określonego w ustawieniach lub, jeśli nie jest ustawiony, adresu URL bieżącej strony."
},
"showQRCode": "Drukuj kod QR",
"showQRCodeMode": {
"no": "Nie",
"withIcon": "Z ikoną",
"simple": "Proste"
},
"templateHelp": "Użyj {} do wprowadzenia wartości elementu szpuli jako tekst. Na przykład, {id} zostanie zastąpione przez id szpuli lub {filament.material} zostanie zastąpione przez materiał szpuli. Jeżeli brakuje wartości, to zostanie ona zastąpiona znakiem \"?\". Drugi zestaw {} można użyć, aby usunąć brakujące wartości. Dodatkowo każdy tekst między znakami {} będzie usunięty jeżeli brakuje wartości. Na przykład {Numer partii: {lot_nr}} pokaże etykietę wtedy gdy szpula będzie miała numer partii. Otocz tekst gwiazdkami by go pogrubić. Kliknij na przycisk by zobaczyć listę dostępnych znaczników."
},
"spoolSelect": {
"title": "Wybierz szpule",
"description": "Wybierz szpule dla których mają być drukowane kody QR.",
"description": "Wybierz szpule dla których mają być drukowane etykiety.",
"showArchived": "Pokaż zarchiwizowane",
"selectAll": "Wybierz/Odznacz wszystko",
"selectedTotal_one": "Wybrano {{count}} szpulę",
@@ -167,13 +216,13 @@
"title": "Skaner kodów QR",
"error": {
"notAllowed": "Nie zezwolono na dostęp do kamery.",
"insecureContext": "Strona nie jest dostarczana przez HTTPS.",
"streamApiNotSupported": "Przeglądarka nie obsługuje API MediaStream.",
"insecureContext": "Strona nie jest serwowana przez HTTPS.",
"streamApiNotSupported": "Przeglądarka nie obsługuje MediaStream API.",
"notReadable": "Kamera jest niedostępna do odczytu.",
"notFound": "Nie znaleziono kamery.",
"unknown": "Wystąpił nieznany błąd. ({{error}})"
},
"description": "Zeskanuj kod QR Spoolman, aby wyświetlić szczegółowe informacje o szpuli."
"description": "Zeskanuj kod QR Spoolmana, aby zobaczyć szczegóły szpuli."
},
"filament": {
"filament": "Filamenty",
@@ -194,15 +243,21 @@
"settings_bed_temp": "Temperatura stołu",
"color_hex": "Kolor",
"article_number": "Numer artykułu",
"spools": "Zobacz szpule"
"spools": "Zobacz szpule",
"external_id": "Zewnętrzne ID",
"single_color": "Pojedynczy",
"multi_color": "Wielo",
"longitudinal": "Wzdłużny",
"coaxial": "Współwytłaczany"
},
"fields_help": {
"material": "Np. PLA, ABS, PETG itp.",
"price": "Cena pełnej szpuli w walucie skonfigurowanej przez system.",
"price": "Cena pełnej szpuli.",
"spool_weight": "Waga pustej szpuli. Używana do określenia ciężaru szpuli.",
"article_number": "Np. EAN, UPC itp.",
"name": "Nazwa filamentu, aby odróżnić ten typ filamentu od innych pochodzących od tego samego producenta. Powinna zawierać np. kolor.",
"weight": "Waga filamentu na pełnej szpuli (waga netto). Nie powinno to obejmować wagi samej szpuli, a jedynie filamentu. Jest to zwykle napisane na opakowaniu."
"weight": "Waga filamentu na pełnej szpuli (waga netto). Nie powinno to obejmować wagi samej szpuli, a jedynie filamentu. Jest to zwykle napisane na opakowaniu.",
"multi_color_direction": "Filamenty mogą mieć wiele kolorów na dwa sposoby: Kolory mogą się ciągnąć równolegle na całej długości filamentu lub kolory filamentu mogą się zmieniać na długości filamentu."
},
"titles": {
"create": "Tworzenie filamentu",
@@ -213,7 +268,12 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Ten filament został zaktualizowany przez kogoś/coś innego od czasu otwarcia tej strony. Zapisanie nadpisze te zmiany!"
"filament_updated": "Ten filament został zaktualizowany przez kogoś/coś innego od czasu otwarcia tej strony. Zapisanie nadpisze te zmiany!",
"import_external": "Importuj z zewnątrz",
"import_external_description": "Wybierz filament z listy by automatycznie zpopulować jego informacje w formularzu tworzenia filamentu. Spowoduje to nadpisanie danych które wpisałeś już w formularzu. Producent zostanie automatycznie stworzony jeżeli klikniesz Ok, jeżeli będzie to wymagane."
},
"buttons": {
"add_spool": "Dodaj Szpulę"
}
},
"vendor": {
@@ -222,18 +282,23 @@
"id": "ID",
"name": "Nazwa",
"registered": "Zarejestrowano",
"comment": "Komentarz"
"comment": "Komentarz",
"empty_spool_weight": "Waga pustej szpuli",
"external_id": "Zewnętrzne ID"
},
"titles": {
"create": "Tworzenie producenta",
"clone": "Klonowanie producenta",
"edit": "Edycja producenta",
"create": "Stwórz producenta",
"clone": "Sklonuj producenta",
"edit": "Edytuj producenta",
"list": "Producenci",
"show": "Pokaż producenta",
"show_title": "[Producent #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Ten producent został zaktualizowany przez kogoś/coś innego od czasu otwarcia tej strony. Zapisanie spowoduje nadpisanie tych zmian!"
},
"fields_help": {
"empty_spool_weight": "Waga pustej szpuli od tego producenta."
}
},
"table": {
@@ -244,7 +309,7 @@
"suffix": " | Spoolman",
"filament": {
"list": "Filamenty | Spoolman",
"edit": "#{{id}} Edycja filamentu | Spoolman",
"edit": "#{{id}} Edytuj filament | Spoolman",
"create": "Tworzenie filamentu | Spoolman",
"clone": "#{{id}} Klonowanie filamentu | Spoolman",
"show": "#{{id}} Filament | Spoolman"
@@ -252,22 +317,25 @@
"spool": {
"list": "Szpule | Spoolman",
"show": "#{{id}} Szpula | Spoolman",
"edit": "#{{id}} Edycja szpuli | Spoolman",
"edit": "#{{id}} Edytuj szpulę | Spoolman",
"create": "Tworzenie szpuli | Spoolman",
"clone": "#{{id}} Klonowanie szpuli | Spoolman"
},
"vendor": {
"show": "#{{id}} Producent | Spoolman",
"show": "#{{id}} Pokaż producenta | Spoolman",
"list": "Producenci | Spoolman",
"edit": "#{{id}} Edycja producenta | Spoolman",
"create": "Tworzenie producenta | Spoolman",
"clone": "#{{id}} Klonowanie producenta | Spoolman"
"edit": "#{{id}} Edytuj producenta | Spoolman",
"create": "Stwórz producenta | Spoolman",
"clone": "#{{id}} Sklonuj producenta | Spoolman"
},
"home": {
"list": "Strona główna | Spoolman"
},
"help": {
"list": "Pomoc | Spoolman"
},
"locations": {
"list": "Lokalizacje | Spoolman"
}
},
"warnWhenUnsavedChanges": "Czy na pewno chcesz wyjść? Masz niezapisane zmiany.",
@@ -277,10 +345,76 @@
"filament": "Producenci filamentów. Mają takie właściwości, jak nazwa, materiał, kolor, średnica i inne.",
"spool": "Indywidualne fizyczne szpule określonego filamentu."
},
"help": "Pomoc"
"help": "Pomoc",
"description": "<title>Pomoc</title><p>Oto kilka wskazówek na początek.</p><p>Spoolman przechowuje 3 różne typy danych:</p><itemsHelp/><p>Aby dodać nową szpulę do bazy danych, zacznij od utworzenia obiektu <filamentCreateLink>Filament</filamentCreateLink>. Gdy już to zrobisz, przejdź do utworzenia obiektu <spoolCreateLink>Szpula</spoolCreateLink> dla tej konkretnej szpuli. Jeśli później zdobędziesz dodatkowe szpule tego samego filamentu, po prostu wygeneruj dodatkowe obiekty Szpula i ponownie użyj tego samego obiektu Filament.</p><p>Opcjonalnie możesz także wygenerować obiekt <vendorCreateLink>Producent</vendorCreateLink> dla firmy produkującej filament, jeśli chcesz śledzić te informacje.</p><p>Masz możliwość połączenia innych usług drukarek 3D ze Spoolmanem, takich jak Moonraker, który może automatycznie monitorować zużycie filamentu i aktualizować obiekty Szpula. Wskazówki dotyczące konfiguracji można znaleźć w <readmeLink>Spoolman README</readmeLink>.</p>"
},
"kofi": "Daj napiwek na Ko-fi",
"kofi": "Wesprzyj mnie na Ko-fi",
"home": {
"home": "Strona główna"
}
"home": "Strona główna",
"welcome": "Witaj w instancji Spoolman!",
"description": "Wygląda na to, że nie dodano jeszcze żadnych szpul. Zobacz <helpPageLink>stronę pomocy</helpPageLink>, aby dowiedzieć się jak rozpocząć."
},
"settings": {
"settings": "Ustawienia",
"general": {
"currency": {
"label": "Waluta"
},
"tab": "Ogólne",
"base_url": {
"tooltip": "Bazowy URL zostanie użyty podczas generowania QR code oraz w innych funkcjonalnościach.",
"label": "Bazowy URL"
},
"round_prices": {
"tooltip": "Zaokrąglij ceny do najbliższej liczby całkowitej.",
"label": "Zaokrąglone ceny"
}
},
"extra_fields": {
"tab": "Dodatkowe parametry",
"boolean_true": "Tak",
"boolean_false": "Nie",
"params": {
"name": "Nazwa",
"field_type": "Typ",
"unit": "Jednostka",
"order": "Kolejność",
"default_value": "Domyślna wartość",
"key": "Klucz",
"choices": "Wybory",
"multi_choice": "Wielokrotny wybór"
},
"field_type": {
"integer": "Liczba całkowita",
"integer_range": "Zakres liczb",
"float": "Liczba zmiennoprzecinkowa",
"choice": "Wybór",
"text": "Tekst",
"float_range": "Zakres",
"datetime": "Data i Czas",
"boolean": "Wartość logiczna"
},
"non_unique_key_error": "Klucz musi być unikalny.",
"description": "<p>Tutaj możesz dodać dodatkowe pola niestandardowe do swoich encji.</p><p>Po dodaniu pola nie można zmienić jego klucza ani typu, a w przypadku pól typu „wybór” nie można usuwać opcji ani zmieniać ustawienia wielu wyborów. Usunięcie pola spowoduje usunięcie powiązanych danych dla wszystkich encji.</p><p>Klucz jest wartością, którą inne programy odczytują i zapisują jako dane, więc jeśli Twoje pole niestandardowe ma integrować się z programem zewnętrznym, upewnij się, że jest ustawione poprawnie. Wartość domyślna jest stosowana tylko do nowych elementów.</p><p>Pola dodatkowe można wyświetlać i sortować w widokach tabeli. Pola typu „wybór” i logiczne (boolean) udostępniają tam również opcje filtrowania, a wszystkie typy pól dodatkowych obsługują filtrowanie wartości pustych.</p>",
"choices_missing_error": "Nie możesz usunąc możliwośći wyboru. Ten wybór nie istnieje: {{choices}}",
"key_not_changed": "Zmień klucz na inny.",
"delete_confirm": "Usunąć pole {{name}}?",
"delete_confirm_description": "Ta akcja usunie wszystkie pola i powiązane z nimi dane dla wszystkich obiektów.",
"choices_reorder_hint": "Przeciągnij lub użyj klawiszy strzałek, aby zmienić kolejność tej opcji."
},
"header": "Ustawienia"
},
"locations": {
"locations": "Lokalizacje",
"new_location": "Nowa lokalizacja",
"no_location": "Brak lokalizacji",
"no_locations_help": "Ta strona pozwala uporządkować szpule w lokalizacjach, dodaj kilka szpul aby rozpocząć!"
},
"theme": {
"label": "Motyw",
"system": "System",
"light": "Jasny",
"dark": "Ciemny"
},
"sponsor": "Wesprzyj mnie na GitHubie"
}
+407
View File
@@ -0,0 +1,407 @@
{
"actions": {
"list": "Listar",
"create": "Criar",
"edit": "Editar",
"show": "Mostrar",
"clone": "Clonar"
},
"buttons": {
"create": "Criar",
"save": "Salvar",
"saveAndAdd": "Salvar e Adicionar",
"logout": "Sair",
"delete": "Excluir",
"edit": "Editar",
"cancel": "Cancelar",
"confirm": "Você tem certeza?",
"continue": "Continuar",
"filter": "Filtrar",
"clear": "Limpar",
"refresh": "Atualizar",
"show": "Mostrar",
"undo": "Desfazer",
"import": "Importar",
"clone": "Clonar",
"archive": "Arquivar",
"unArchive": "Desarquivar",
"hideArchived": "Ocultar Arquivados",
"showArchived": "Mostrar Arquivados",
"notAccessTitle": "Você não tem permissão para acessar",
"hideColumns": "Ocultar Colunas",
"clearFilters": "Limpar Filtros"
},
"warnWhenUnsavedChanges": "Você tem certeza que deseja sair? Você tem alterações não salvas.",
"notifications": {
"success": "Sucesso",
"error": "Erro (código de status: {{statusCode}})",
"undoable": "Você tem {{seconds}} segundos para desfazer.",
"createSuccess": "{{resource}} criado com sucesso",
"createError": "Ocorreu um erro ao criar {{resource}} (código de status: {{statusCode}})",
"deleteSuccess": "{{resource}} excluído com sucesso",
"deleteError": "Erro ao excluir {{resource}} (código de status: {{statusCode}})",
"editSuccess": "{{resource}} editado com sucesso",
"editError": "Erro ao editar {{resource}} (código de status: {{statusCode}})",
"importProgress": "Importando: {{processed}}/{{total}}",
"saveSuccessful": "Salvo com sucesso!",
"validationError": "Erro de validação: {{error}}"
},
"kofi": "Doe pelo Ko-fi",
"loading": "Carregando",
"version": "Versão",
"unknown": "Desconhecido",
"yes": "Sim",
"no": "Não",
"tags": {
"clone": "Clonar"
},
"dashboard": {
"title": "Painel"
},
"printing": {
"generic": {
"title": "Imprimindo",
"description": "Ajuste as configurações abaixo para obter o layout de impressão desejado. Tenha em mente que as impressoras e o seu sistema operacional podem aplicar suas próprias margens e dimensionamento, portanto, pode ser necessário realizar algumas tentativas e erros antes que tudo esteja correto. Teste em um papel comum antes de imprimir nas etiquetas reais.",
"helpMargin": "As margens devem ser configuradas para corresponder às suas etiquetas e impressora. Alterá-las afetará o tamanho de toda a grade.",
"helpPrinterMargin": "A Zona de Segurança deve ser definida para o quão próximo da borda do papel sua impressora pode imprimir. Alterá-la não afetará toda a grade.",
"print": "Imprimir",
"columns": "Colunas",
"rows": "Linhas",
"paperSize": "Tamanho do Papel",
"customSize": "Personalizado",
"dimensions": "Dimensões",
"showBorder": "Mostrar Borda",
"previewScale": "Escala da Pré-visualização",
"skipItems": "Pular Itens",
"itemCopies": "Cópias do Item",
"contentSettings": "Configurações de Conteúdo",
"layoutSettings": "Configurações de Layout",
"horizontalSpacing": "Espaçamento Horizontal",
"verticalSpacing": "Espaçamento Vertical",
"marginLeft": "Margem Esquerda",
"marginRight": "Margem Direita",
"marginTop": "Margem Superior",
"marginBottom": "Margem Inferior",
"printerMarginLeft": "Zona de Segurança Esquerda",
"printerMarginRight": "Zona de Segurança Direita",
"printerMarginTop": "Zona de Segurança Superior",
"printerMarginBottom": "Zona de Segurança Inferior",
"borders": {
"none": "Nenhuma",
"border": "Borda",
"grid": "Grade"
},
"settings": "Predefinições",
"defaultSettings": "Padrão",
"addSettings": "Adicionar Nova Predefinição",
"newSetting": "Novo",
"duplicateSettings": "Duplicar",
"deleteSettings": "Excluir Predefinição Atual",
"deleteSettingsConfirm": "Tem certeza de que deseja excluir esta predefinição?",
"settingsName": "Nome da Predefinição",
"saveSetting": "Salvar Predefinições",
"saveAsImage": "Salvar como Imagem"
},
"qrcode": {
"button": "Imprimir Etiquetas",
"title": "Impressão de Etiquetas",
"template": "Modelo de Etiqueta",
"templateHelp": "Use \"{tag}\" para inserir valores do objeto carretel como texto. Por exemplo, {id} será substituído pelo valor do \"id\" do carretel ou {filament.material} será substituído pelo material do carretel. Se um valor estiver faltando, ele será substituído por \"?\". Um segundo conjunto de chaves, exemplo \"{{tag}}\", pode ser usado para remover isso. Além disso, qualquer texto entre os conjuntos de chaves será removido se o valor não existir. Por exemplo, {Lote Nº: {lot_nr}} só mostrará o rótulo se o carretel tiver um número de lote. Coloque o texto com asterisco duplo ** para colocar em negrito. Clique no botão ao lado para ver uma lista de todas as tags disponíveis.",
"textSize": "Tamanho do Texto da Etiqueta",
"showContent": "Imprimir Etiqueta",
"useHTTPUrl": {
"label": "Endereço do código QR",
"tooltip": "Usará um endereço proprietário que funcionará somente se digitalizado a partir do recurso de digitalização do Spoolman (padrão). O URL usa o URL Base, especificada nas Configurações, ou o URL da página atual se nenhuma estiver definida.",
"options": {
"default": "Padrão",
"url": "URL"
},
"preview": "Pré-visualização:"
},
"showQRCode": "Imprimir Código QR",
"showQRCodeMode": {
"no": "Não",
"simple": "Simples",
"withIcon": "Com Ícone"
}
},
"spoolSelect": {
"title": "Selecionar Carretéis",
"description": "Selecione os carretéis para imprimir suas etiquetas.",
"showArchived": "Mostrar Arquivado",
"noSpoolsSelected": "Você não selecionou algum dos carretéis.",
"selectAll": "Selecionar/Desselecionar Tudo",
"selectedTotal_one": "{{count}} carretel selecionado",
"selectedTotal_many": "{{count}} carretéis selecionados",
"selectedTotal_other": "{{count}} carretéis selecionados"
}
},
"scanner": {
"title": "Leitor de Código QR",
"description": "Escaneie um código QR do Spoolman para ver detalhes sobre o carretel.",
"error": {
"notAllowed": "Você não deu permissão de acesso à câmera.",
"insecureContext": "A página não é servida por HTTPS.",
"streamApiNotSupported": "O navegador não oferece suporte à API MediaStream.",
"notReadable": "A câmera não está legível.",
"notFound": "Nenhuma câmera foi encontrada.",
"unknown": "Ocorreu um erro desconhecido. ({{error}})"
}
},
"spool": {
"spool": "Carretéis",
"fields": {
"id": "ID",
"filament_name": "Filamento",
"filament": "Filamento",
"filament_internal": "Interno",
"filament_external": "Externo",
"price": "Preço",
"material": "Material",
"weight_to_use": "Peso",
"used_weight": "Peso Utilizado",
"remaining_weight": "Peso Restante",
"measured_weight": "Peso Medido",
"used_length": "Comprimento Usado",
"remaining_length": "Comprimento Restante",
"initial_weight": "Peso Inicial",
"spool_weight": "Peso sem Filamento",
"location": "Localização",
"lot_nr": "Número do Lote",
"first_used": "Primeiro Uso",
"last_used": "Último Uso",
"registered": "Registrado",
"comment": "Comentário",
"archived": "Arquivado"
},
"fields_help": {
"price": "Preço de um carretel cheio. Se não for definido, o preço do filamento será assumido.",
"weight_to_use": "Selecione que tipo de valor de peso inserir. O \"Peso Medido\" só estará disponível se o peso do carretel estiver definido para o filamento selecionado.",
"used_weight": "Quanto filamento foi usado do carretel. Um novo carretel deve ter 0g usado.",
"remaining_weight": "Quanto filamento resta no carretel. Para um carretel novo, isso deve corresponder ao peso do carretel.",
"measured_weight": "Quanto pesa o filamento e o carretel.",
"initial_weight": "O peso inicial do filamento no carretel (peso líquido). Caso não seja especificado, será usado o peso da definição do filamento.",
"spool_weight": "O peso do carretel vazio. Deixe em branco para utilizar o valor do filamento ou do fabricante.",
"location": "Onde o carretel está localizado se você tiver vários locais onde armazena seus carretéis.",
"lot_nr": "Número de lote do fabricante. Pode ser usado para garantir que uma impressão tenha cores consistentes se vários carretéis forem usados.",
"external_filament": "Você selecionou um filamento do banco de dados externo. Um objeto de filamento (e possivelmente um objeto de fabricante) será criado automaticamente quando você criar este carretel. Isso pode criar objetos de filamento duplicados se você já tiver criado um objeto de filamento para esse filamento."
},
"titles": {
"create": "Criar Carretel",
"clone": "Clonar Carretel",
"edit": "Editar Carretel",
"list": "Carretéis",
"show": "Mostrar Carretel",
"show_title": "[Carretel #{{id}}] {{name}}",
"archive": "Arquivar Carretel",
"adjust": "Ajustar Filamento no Carretel"
},
"form": {
"measurement_type": {
"length": "Comprimento",
"weight": "Peso"
},
"measurement_type_label": "Tipo de Medida",
"adjust_filament_help": "Aqui você pode adicionar ou subtrair filamento diretamente do carretel. Um valor positivo consumirá filamento, um valor negativo adicionará filamento.",
"adjust_filament_value": "Quantidade Consumida",
"new_location_prompt": "Insira um novo local",
"spool_updated": "Este carretel foi atualizado por alguém/outra coisa desde que você abriu esta página. Salvar irá sobrescrever essas alterações!"
},
"messages": {
"archive": "Tem certeza de que deseja arquivar este carretel?"
},
"formats": {
"last_used": "Última utilização em {{date}}"
}
},
"filament": {
"filament": "Filamentos",
"fields": {
"id": "ID",
"vendor_name": "Fabricante",
"vendor": "Fabricante",
"name": "Nome",
"material": "Material",
"price": "Preço",
"density": "Densidade",
"diameter": "Diâmetro",
"weight": "Peso",
"spool_weight": "Peso do Carretel",
"article_number": "Número do Artigo",
"registered": "Registrado",
"comment": "Comentário",
"settings_extruder_temp": "Temperatura da Extrusora",
"settings_bed_temp": "Temperatura da Mesa",
"color_hex": "Cor",
"single_color": "Única",
"multi_color": "Múltiplas",
"coaxial": "Coextrusada",
"longitudinal": "Longitudinal",
"external_id": "ID Externo",
"spools": "Mostrar Carretéis"
},
"fields_help": {
"name": "Nome do filamento, para distinguir este tipo de filamento entre outros do mesmo fabricante. Deve conter a cor, por exemplo.",
"material": "Por exemplo: PLA, ABS, PETG, etc.",
"price": "Preço de um carretel cheio.",
"weight": "O peso do filamento de um carretel cheio (peso líquido). Isso não deve incluir o peso do carretel em si, apenas o filamento. É o que normalmente está escrito na embalagem.",
"spool_weight": "O peso de um carretel vazio. Usado para determinar o peso medido de um carretel.",
"article_number": "Por exemplo: EAN, UPC, etc.",
"multi_color_direction": "Os filamentos podem ter múltiplas cores de duas maneiras: por meio de coextrusão, como os filamentos bicolores com as cores consistentes ao longo do carretel, ou por meio de mudanças longitudinais de cor, como os filamentos gradientes que mudam as cores ao longo do carretel."
},
"titles": {
"create": "Criar Filamento",
"clone": "Clonar Filamento",
"edit": "Editar Filamento",
"list": "Filamentos",
"show": "Mostrar Filamento",
"show_title": "[Filamento #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Este filamento foi atualizado por alguém/outra coisa desde que você abriu esta página. Salvar irá sobrescrever essas alterações!",
"import_external": "Importar do Exterior",
"import_external_description": "Selecione um filamento na lista para preencher automaticamente seus detalhes no formulário de criação de filamento. Isso substituirá quaisquer dados que você inseriu no formulário.<br><br>Um objeto Fabricante será criado automaticamente quando você clicar em OK, se necessário."
},
"buttons": {
"add_spool": "Adicionar Carretel"
}
},
"vendor": {
"vendor": "Fabricantes",
"fields": {
"id": "ID",
"name": "Nome",
"empty_spool_weight": "Peso do Carretel Vazio",
"external_id": "ID Externo",
"registered": "Registrado",
"comment": "Comentário"
},
"fields_help": {
"empty_spool_weight": "O peso de um carretel vazio deste fabricante."
},
"titles": {
"create": "Criar Fabricante",
"clone": "Clonar Fabricante",
"edit": "Editar Fabricante",
"list": "Fabricantes",
"show": "Mostrar Fabricante",
"show_title": "[Fabricante #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Este fabricante foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar sobrescreverá essas alterações!"
}
},
"home": {
"home": "Início",
"welcome": "Bem-vindo à sua instância do Spoolman!",
"description": "Parece que você ainda não adicionou nenhum carretel. Veja a <helpPageLink>Página de Ajuda</helpPageLink> para obter ajuda sobre como começar."
},
"help": {
"help": "Ajuda",
"resources": {
"filament": "Marcas de filamento. Elas têm propriedades como nome, material, cor, diâmetro e mais.",
"spool": "Carretéis físicos individuais de um filamento específico.",
"vendor": "As empresas que fabricam o filamento."
},
"description": "<title>Ajuda</title><p>Aqui estão algumas dicas para você começar.</p><p>O Spoolman contém três tipos diferentes de dados:</p><itemsHelp/><p>Para adicionar um novo carretel para o banco de dados, comece criando um objeto <filamentCreateLink>Filamento</filamentCreateLink>. Feito isso, prossiga para criar um objeto <spoolCreateLink>Carretel</spoolCreateLink> para esse carretel específico. Se você adquirir carretéis adicionais do mesmo filamento posteriormente, simplesmente crie carretéis adicionais e reutilize o mesmo objeto Filamento.</p><p>Opcionalmente, você também pode gerar um objeto <vendorCreateLink>Fabricante</vendorCreateLink> para a empresa que fabrica o filamento, se você desejar rastrear essas informações.</p><p>Você tem a opção de vincular outros serviços de impressora 3D ao Spoolman, como o Moonraker, o qual pode automaticamente monitorar o uso do filamento e atualizar os carretéis para você. Consulte o <readmeLink>LEIA-ME do Spoolman</readmeLink> para obter orientação sobre como configurar isso.</p>"
},
"table": {
"actions": "Ações"
},
"settings": {
"settings": "Configurações",
"header": "Configurações",
"general": {
"tab": "Geral",
"currency": {
"label": "Moeda"
},
"base_url": {
"label": "URL Base",
"tooltip": "A URL base a ser usada ao gerar recursos como códigos QR."
},
"round_prices": {
"label": "Arredondar preços",
"tooltip": "Arredonda os preços para o número inteiro mais próximo."
}
},
"extra_fields": {
"tab": "Campos Extras",
"description": "<p>Aqui você pode adicionar campos personalizados extras às suas entidades.</p><p>Depois que um campo é adicionado, você não pode alterar sua chave ou tipo, e para campos do tipo \"escolha\" você não pode remover as opções ou alterar o estado de \"múltipla de escolha\". Se você remover um campo, os dados associados a ele em todas as entidades serão excluídos.</p><p>A chave é como outros programas leem/gravam os dados, portanto, se o seu campo personalizado for para uma integração com um programa de terceiros, certifique-se de configurá-lo corretamente. O valor padrão é aplicado somente a novos itens.</p><p>Campos extras não podem ser ordenados ou filtrados nas visualizações de tabela.</p>",
"params": {
"key": "Chave",
"name": "Nome",
"field_type": "Tipo",
"unit": "Unidade",
"order": "Ordem",
"default_value": "Valor Padrão",
"choices": "Opções",
"multi_choice": "Múltipla Escolha"
},
"field_type": {
"text": "Texto",
"integer": "Inteiro",
"integer_range": "Intervalo de Inteiros",
"float": "Decimal",
"float_range": "Intervalo Decimal",
"datetime": "Data e Hora",
"boolean": "Booleano",
"choice": "Opção"
},
"boolean_true": "Sim",
"boolean_false": "Não",
"choices_missing_error": "Você não pode remover as opções. As seguintes opções estão faltando: {{choices}}",
"non_unique_key_error": "A chave deve ser única.",
"key_not_changed": "Por favor, escolha outra chave.",
"delete_confirm": "Apagar o campo {{name}}?",
"delete_confirm_description": "Isso excluirá o campo e todos os dados associados em todas as entidades.",
"choices_reorder_hint": "Arraste, ou use as setas direcionais, para reordenar essa opção."
}
},
"documentTitle": {
"default": "Spoolman",
"suffix": " | Spoolman",
"home": {
"list": "Início | Spoolman"
},
"help": {
"list": "Ajuda | Spoolman"
},
"locations": {
"list": "Locais | Spoolman"
},
"filament": {
"list": "Filamentos | Spoolman",
"show": "Mostrar Filamento #{{id}} | Spoolman",
"edit": "Editar Filamento #{{id}} | Spoolman",
"create": "Criar Filamento | Spoolman",
"clone": "Clonar Filamento #{{id}} | Spoolman"
},
"spool": {
"list": "Carretéis | Spoolman",
"show": "Mostrar Carretel #{{id}} | Spoolman",
"edit": "Editar Carretel #{{id}} | Spoolman",
"create": "Criar Carretel | Spoolman",
"clone": "Clonar Carretel #{{id}} | Spoolman"
},
"vendor": {
"list": "Fabricantes | Spoolman",
"show": "Mostrar Fabricante #{{id}} | Spoolman",
"edit": "Editar Fabricante #{{id}} | Spoolman",
"create": "Criar Fabricante | Spoolman",
"clone": "Clonar Fabricante #{{id}} | Spoolman"
}
},
"locations": {
"locations": "Locais",
"new_location": "Novo Local",
"no_location": "Sem Local",
"no_locations_help": "Esta página permite que você organize seus carretéis em locais, adicione alguns carretéis para começar!"
},
"theme": {
"label": "Tema",
"system": "Sistema",
"light": "Claro",
"dark": "Escuro"
}
}
+257 -125
View File
@@ -7,12 +7,12 @@
"edit": "Editar"
},
"buttons": {
"save": "Salvar",
"save": "Guardar",
"logout": "Sair",
"delete": "Excluir",
"edit": "Editar",
"cancel": "Cancelar",
"confirm": "Tem certeza?",
"confirm": "Tem a certeza?",
"filter": "Filtrar",
"clear": "Limpar",
"refresh": "Atualizar",
@@ -22,26 +22,29 @@
"clone": "Clonar",
"archive": "Arquivar",
"unArchive": "Desarquivar",
"hideArchived": "Ocultar arquivados",
"showArchived": "Mostrar arquivado",
"notAccessTitle": "Você não tem permissão para acessar",
"hideColumns": "Ocultar colunas",
"clearFilters": "Limpar filtros",
"hideArchived": "Ocultar Arquivados",
"showArchived": "Mostrar Arquivados",
"notAccessTitle": "Não tem permissão para aceder",
"hideColumns": "Ocultar Colunas",
"clearFilters": "Limpar Filtros",
"create": "Criar",
"saveAndAdd": "Salvar e adicionar"
"saveAndAdd": "Guardar e Adicionar",
"continue": "Continuar"
},
"warnWhenUnsavedChanges": "Você tem certeza de que quer sair? Você tem alterações não salvas.",
"warnWhenUnsavedChanges": "Tem certeza de que quer sair? Tem alterações não guardadas.",
"notifications": {
"success": "Sucesso",
"error": "Erro (código de status: {{statusCode}})",
"undoable": "Você tem {{seconds}} segundos para desfazer.",
"undoable": "Tem {{seconds}} segundos para desfazer.",
"createSuccess": "{{resource}} criado com sucesso",
"createError": "Ocorreu um erro ao criar {{resource}} (código de status: {{statusCode}})",
"deleteSuccess": "{{resource}} excluído com sucesso",
"deleteError": "Erro ao excluir {{resource}} (código de status: {{statusCode}})",
"editSuccess": "{{resource}} editado com sucesso",
"editError": "Erro ao editar {{resource}} (código de status: {{statusCode}})",
"importProgress": "Importando: {{processed}}/{{total}}"
"importProgress": "Importando: {{processed}}/{{total}}",
"saveSuccessful": "Guardado com sucesso!",
"validationError": "Erro de validação: {{error}}"
},
"kofi": "Doe no Ko-fi",
"loading": "Carregando",
@@ -52,195 +55,259 @@
"clone": "Clonar"
},
"dashboard": {
"title": "Dashboard"
"title": "Painel"
},
"printing": {
"generic": {
"title": "Imprimindo",
"helpMargin": "As margens devem ser configuradas para corresponder ao papel de etiqueta e à impressora. Alterá-las afetará o tamanho de toda a grade.",
"helpMargin": "As margens devem ser configuradas para corresponder à sua etiqueta de papel e à impressora. Alterá-las afetará o tamanho de toda a grade.",
"helpPrinterMargin": "A zona segura deve ser definida para o quão próximo da borda do papel sua impressora pode imprimir, alterá-la não afetará toda a grade.",
"previewScale": "Visualizar escala",
"skipItems": "Pular itens",
"horizontalSpacing": "Espaçamento horizontal",
"marginRight": "Margem direita",
"marginTop": "Margem superior",
"description": "Ajuste as configurações abaixo para o layout de impressão desejado. Tenha em mente que as impressoras e o seu SO podem aplicar suas próprias margens e dimensionamento, portanto, pode ser necessário realizar algumas tentativas e erros antes que tudo esteja correto. Teste em um pedaço de papel comum antes de imprimir nas etiquetas reais.",
"previewScale": "Visualizar Escala",
"skipItems": "Pular Itens",
"horizontalSpacing": "Espaçamento Horizontal",
"marginRight": "Margem Direita",
"marginTop": "Margem Superior",
"description": "Ajuste as configurações abaixo para obter o layout de impressão desejado. Tenha em mente que impressoras e sistemas operacionais podem aplicar suas próprias margens e escalonamento, portanto, pode ser necessário fazer algumas tentativas antes que tudo estaja correto. Teste em uma folha de papel comum antes de imprimir nas etiquetas reais.",
"print": "Imprimir",
"columns": "Colunas",
"dimensions": "Dimensões",
"contentSettings": "Configurações de conteúdo",
"layoutSettings": "Configurações de layout",
"contentSettings": "Configurações de Conteúdo",
"layoutSettings": "Configurações de Layout",
"rows": "Linhas",
"paperSize": "Tamanho do papel",
"paperSize": "Tamanho do Papel",
"customSize": "Personalizado",
"showBorder": "Mostrar borda",
"verticalSpacing": "Espaçamento vertical",
"marginLeft": "Margem esquerda",
"marginBottom": "Margem inferior",
"printerMarginBottom": "Zona segura inferior",
"printerMarginLeft": "Zona segura esquerda",
"printerMarginRight": "Zona segura direita",
"showBorder": "Mostrar Borda",
"verticalSpacing": "Espaçamento Vertical",
"marginLeft": "Margem Esquerda",
"marginBottom": "Margem Inferior",
"printerMarginBottom": "Área Segura Inferior",
"printerMarginLeft": "Área Segura Esquerda",
"printerMarginRight": "Área Segura Direita",
"borders": {
"none": "Nenhuma",
"border": "Borda",
"grid": "Grade"
},
"printerMarginTop": "Zona segura superior"
"printerMarginTop": "Área Segura Superior",
"settings": "Pré-definições",
"defaultSettings": "Por omissão",
"addSettings": "Adicionar Pré-Definição",
"newSetting": "Novo",
"deleteSettings": "Remover Pré-definição Actual",
"deleteSettingsConfirm": "Tem a certeza que pretender remover esta pré-definição?",
"settingsName": "Nome da Pré-definição",
"itemCopies": "Cópias do Artigo",
"duplicateSettings": "Duplicar",
"saveSetting": "Guardar Pré-definições",
"saveAsImage": "Guardar como Imagem"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} carretel selecionado",
"selectedTotal_many": "{{count}} carretéis selecionados",
"selectedTotal_other": "{{count}} carretéis selecionados",
"noSpoolsSelected": "Você não selecionou nenhum carretel.",
"title": "Selecionar carretéis",
"description": "Selecione carretéis para imprimir códigos QR.",
"showArchived": "Mostrar arquivado",
"selectAll": "Selecionar/desmarcar tudo"
"selectedTotal_one": "{{count}} bobine selecionado",
"selectedTotal_many": "{{count}} bobines selecionados",
"selectedTotal_other": "{{count}} bobines selecionados",
"noSpoolsSelected": "Não selecionou nenhuma bobine.",
"title": "Selecionar Bobines",
"description": "Selecione as bobines para imprimir as etiquetas.",
"showArchived": "Mostrar Arquivado",
"selectAll": "Selecionar/Desmarcar Tudo"
},
"qrcode": {
"title": "Imprimindo códigos QR",
"button": "Imprimir códigos QR",
"spoolWeight": "Peso do carretel: {{weight}}",
"lotNr": "Número do lote: {{lot}}",
"bedTemp": "Temperatura da mesa: {{temp}}",
"extruderTemp": "Temperatura do bico: {{temp}}",
"textSize": "Tamanho do texto do conteúdo",
"title": "Impressão de Etiqueta",
"button": "Imprimir Etiquetas",
"spoolWeight": "Peso do Carretel: {{weight}}",
"lotNr": "Número do Lote: {{lot}}",
"bedTemp": "Temperatura da Mesa: {{temp}}",
"extruderTemp": "Temperatura do Bico: {{temp}}",
"textSize": "Tamanho do Texto da Etiqueta",
"showSpoolmanIcon": "Mostrar ícone do Spoolman",
"showVendor": "Fornecedor",
"showContent": "Etiqueta de impressão",
"showLotNr": "Número do lote",
"showSpoolWeight": "Peso do carretel",
"showVendor": "Fabricante",
"showContent": "Imprimir Etiqueta",
"showLotNr": "Número do Lote",
"showSpoolWeight": "Peso do Carretel",
"showTemperatures": "Temperaturas",
"showSpoolComment": "Comentário do carretel",
"showFilamentComment": "Comentário de filamento",
"showVendorComment": "Comentário do fornecedor"
"showSpoolComment": "Comentário sobre o Carretel",
"showFilamentComment": "Comentário sobre o Filamento",
"showVendorComment": "Comentário sobre o Fabricante",
"template": "Modelo de Etiqueta",
"templateHelp": "Use {} para incluir valores da bobine como texto. Por exemplo {id} será substituído pelo ID da bobine, ou {filament.material} será substituído pelo material da bobine. Coloque texto entre duplos asteriscos ** para colocar a negrito. Pressione o botão para ver a lista de etiquetas disponíveis.",
"showQRCode": "Imprimir Código QR",
"showQRCodeMode": {
"no": "Não",
"simple": "Simples",
"withIcon": "Com Ícone"
},
"useHTTPUrl": {
"label": "Endereço do Código QR",
"options": {
"url": "Endereço",
"default": "Por omissão"
},
"preview": "Pré-visualização:",
"tooltip": "Irá usar um endereço proprietário que funcionará apenas se for lido pela funcionalidade do Spoolman. O endereço usará o endereço base definido nas configurações ou, caso não esteja definido, o endereço da página actual."
}
}
},
"spool": {
"fields": {
"material": "Material",
"weight_to_use": "Peso",
"used_weight": "Peso usado",
"remaining_weight": "Peso restante",
"measured_weight": "Peso medido",
"used_length": "Comprimento usado",
"remaining_length": "Comprimento restante",
"used_weight": "Peso Utilizado",
"remaining_weight": "Peso Restante",
"measured_weight": "Peso Medido",
"used_length": "Comprimento Usado",
"remaining_length": "Comprimento Restante",
"location": "Localização",
"lot_nr": "Número do lote",
"first_used": "Usado pela primeira vez",
"last_used": "Usado por último",
"lot_nr": "Número do Lote",
"first_used": "Primeiro Uso",
"last_used": "Último Uso",
"registered": "Registrado",
"comment": "Comentário",
"archived": "Arquivado",
"id": "ID",
"filament_name": "Filamento",
"filament": "Filamento",
"price": ""
"price": "Preço",
"initial_weight": "Peso Inicial",
"spool_weight": "Peso sem Filamento",
"filament_internal": "Interno",
"filament_external": "Externo"
},
"fields_help": {
"weight_to_use": "Selecione qual valor de peso inserir. O peso medido só estará disponível se o peso do carretel estiver definido para o filamento selecionado.",
"lot_nr": "Número de lote do fabricante. Pode ser usado para garantir que uma impressão tenha cores consistentes se vários carretéis forem usados.",
"used_weight": "Quanto filamento foi usado no carretel. Um novo carretel deve ter 0g usado.",
"remaining_weight": "Quanto filamento resta no carretel. Para um carretel novo, isso deve corresponder ao peso do carretel.",
"measured_weight": "Quanto pesa o filamento e o carretel.",
"location": "Onde o carretel está localizado se você tiver vários locais onde armazena seus carretéis.",
"price": ""
"weight_to_use": "Selecione qual o valor de peso a inserir. O peso medido só estará disponível se o peso da bobine estiver definido para o filamento selecionado.",
"lot_nr": "Número de lote do fabricante. Pode ser usado para garantir que uma impressão tenha cores consistentes se várias bobines forem usadas.",
"used_weight": "Quanto filamento foi usado da bobine. Uma nova bobine deve ter usado 0g.",
"remaining_weight": "Quanto filamento resta na bobine. Para uma bobine nova, deve corresponder ao peso da bobine.",
"measured_weight": "Quanto pesa o filamento e a bobine.",
"location": "Onde a bobine está localizado se tiver vários locais onde armazena as suas bobines.",
"price": "Preço de uma bobine completa na moeda definida no sistema. Se não for definido, o preço do filamento será presumido.",
"initial_weight": "O peso inicial do filamento sem o peso da bobine (peso liquido). Se nada for indicado, será usado o peso da definição do filamento.",
"spool_weight": "O peso da bobine vazia. Deixe em branco para usar o valor do filamento ou do fabricante.",
"external_filament": "Escolheu um filamento de uma base de dados externa. O filamento (e possivelmente o fabricante) será criado automaticamente quando for criada a bobine. Isto poderá criar filamentos duplicados caso já tenha criado um filamento para este filamento específico."
},
"titles": {
"create": "Criar carretel",
"clone": "Clonar carretel",
"edit": "Editar carretel",
"list": "Carretéis",
"show": "Mostrar carretel",
"show_title": "[Carretel #{{id}}] {{name}}",
"archive": "Arquivar carretel"
"create": "Criar Bobine",
"clone": "Clonar Bobine",
"edit": "Editar Bobine",
"list": "Bobines",
"show": "Mostrar Bobine",
"show_title": "[Bobine #{{id}}] {{name}}",
"archive": "Arquivar Bobine",
"adjust": "Ajustar Filamento"
},
"form": {
"new_location_prompt": "Insira um novo local",
"spool_updated": "Este spool foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"spool_updated": "Esta bobine foi atualizada por alguém desde que abriu esta página. Guardar substituirá essas alterações!",
"measurement_type": {
"length": "Comprimento",
"weight": "Peso"
},
"measurement_type_label": "Tipo de Medida",
"adjust_filament_help": "Aqui pode adicionar ou subtrair filamento da bobine. Um valor positivo consome filamento, um valor negativo adiciona filamento.",
"adjust_filament_value": "Consumir Montante"
},
"messages": {
"archive": "Tem certeza de que deseja arquivar este carretel?"
"archive": "Tem certeza de que deseja arquivar esta bobine?"
},
"spool": "Carretéis"
"spool": "Bobines",
"formats": {
"last_used": "Última utilização {{date}}"
}
},
"filament": {
"filament": "Filamentos",
"fields": {
"id": "ID",
"vendor_name": "Fornecedor",
"vendor_name": "Fabricante",
"name": "Nome",
"price": "Preço",
"diameter": "Diâmetro",
"weight": "Peso",
"spool_weight": "Peso do carretel",
"article_number": "Número do artigo",
"spool_weight": "Peso da Bobine",
"article_number": "Número do Artigo",
"registered": "Registrado",
"vendor": "Fornecedor",
"vendor": "Fabricante",
"material": "Material",
"density": "Densidade",
"comment": "Comentário",
"settings_extruder_temp": "Temperatura do bico",
"settings_bed_temp": "Temperatura da mesa",
"settings_extruder_temp": "Temperatura do Bico",
"settings_bed_temp": "Temperatura da Mesa",
"color_hex": "Cor",
"spools": "Mostrar carretéis"
"spools": "Mostrar Bobines",
"external_id": "ID Externo",
"single_color": "Único",
"multi_color": "Múltiplo",
"coaxial": "Coextrusado",
"longitudinal": "Longitudinal"
},
"fields_help": {
"article_number": "Por exemplo EAN, UPC, etc.",
"price": "Preço de um spool completo na moeda configurada pelo sistema.",
"name": "Nome do filamento, para distinguir este tipo de filamento entre outros do mesmo fornecedor. Deve conter a cor, por exemplo.",
"price": "Preço de uma bobine completa.",
"name": "Nome do filamento, para distinguir este tipo de filamento entre outros do mesmo fabricante. Deve conter a cor, por exemplo.",
"material": "Por exemplo PLA, ABS, PETG, etc.",
"weight": "O peso do filamento de uma carretel cheio (peso líquido). Isso não deve incluir o peso do carretel em si, apenas o filamento. É o que normalmente está escrito na embalagem.",
"spool_weight": "O peso de um carretel vazio. Usado para determinar o peso medido de um carretel."
"weight": "O peso do filamento de uma bobine cheia (peso líquido). Isso não deve incluir o peso da bobine em si, apenas o filamento. É o que normalmente está escrito na embalagem.",
"spool_weight": "O peso de uma bobine vazia. Usado para determinar o peso medido de uma bobine.",
"multi_color_direction": "Os filamentos podem ter múltiplas cores de duas formas: por coextrusão, como os filamentos de duas cores com estas consistentes ao longo do filamento, ou através de mudança de cor longitudinal, como os filamentos que contêm gradientes de cores que vão mudando ao longo da bobine."
},
"titles": {
"create": "Criar filamento",
"clone": "Clonar filamento",
"edit": "Editar filamento",
"create": "Criar Filamento",
"clone": "Clonar Filamento",
"edit": "Editar Filamento",
"list": "Filamentos",
"show": "Mostrar filamento",
"show": "Mostrar Filamento",
"show_title": "[Filamento #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Este filamento foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"filament_updated": "Este filamento foi atualizado por alguém desde que abriu esta página. Guardar substituirá essas alterações!",
"import_external": "Importar do Exterior",
"import_external_description": "Escolha um filamento da lista para preencher automaticamente os detalhes do filamento no formulário de criação. Isto irá substituir quaisquer dados que já tenha introduzido no formulário.<br><br>Será criado o fabricante automaticamente caso seja necessário."
},
"buttons": {
"add_spool": "Adicionar Bobine"
}
},
"vendor": {
"vendor": "Fornecedores",
"vendor": "Fabricantes",
"fields": {
"id": "ID",
"name": "Nome",
"registered": "Registrado",
"comment": "Comentário"
"comment": "Comentário",
"empty_spool_weight": "Peso da Bobine Vazia",
"external_id": "ID Externo"
},
"titles": {
"create": "Criar fornecedor",
"clone": "Clonar fornecedor",
"edit": "Editar fornecedor",
"list": "Fornecedores",
"show": "Mostrar fornecedor",
"show_title": "[Fornecedor nº{{id}}] {{name}}"
"create": "Criar Fabricante",
"clone": "Clonar Fabricante",
"edit": "Editar Fabricante",
"list": "Fabricantes",
"show": "Mostrar Fabricante",
"show_title": "[Fabricante #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Este fornecedor foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"vendor_updated": "Este fabricante foi atualizado por alguém desde que abriu esta página. Guardar substituirá essas alterações!"
},
"fields_help": {
"empty_spool_weight": "O peso da bobine vazia do fabricante."
}
},
"home": {
"home": "Home"
"home": "Início",
"welcome": "Bem vindo à sua instância do Spoolman!",
"description": "Parece que ainda não adicionou nenhuma bobine. Veja a <helpPageLink>Página de Ajuda</helpPageLink> para obter ajuda a começar."
},
"help": {
"help": "Ajuda",
"resources": {
"filament": "Marcas de filamento. Eles têm propriedades como nome, material, cor, diâmetro e muito mais.",
"spool": "Carretéis físicos individuais de um filamento específico.",
"spool": "Bobines físicas individuais de um filamento específico.",
"vendor": "As empresas que fabricam o filamento."
},
"description": "<title>Ajuda</title><p>Aqui estão algumas dicas para você começar.</p><p>O Spoolman contém três tipos diferentes de dados:</p><itemsHelp/><p>Para adicionar um novo carretel para o banco de dados, comece criando um objeto <filamentCreateLink>Filamento</filamentCreateLink>. Feito isso, prossiga para criar um objeto <spoolCreateLink>Carretel </spoolCreateLink> para esse carretel específico. Se você adquirir carretéis adicionais do mesmo filamento posteriormente, simplesmente gere objetos Spool adicionais e reutilize o mesmo objeto filamento.</p><p>Opcionalmente, você também pode gerar um objeto <vendorCreateLink>Fornecedor</vendorCreateLink> para a empresa fabricar o filamento se desejar rastrear essas informações.</p><p>Você tem a opção de vincular outros serviços de impressora 3D ao Spoolman, como o Moonraker, que pode monitorar automaticamente o uso do filamento e atualizar os objetos carretel para você. Consulte o <readmeLink>README do Spoolman</readmeLink> para obter orientação sobre como configurar isso.</p>"
"description": "<title>Ajuda</title><p>Aqui estão algumas dicas para começar.</p><p>O Spoolman contém três tipos diferentes de dados:</p><itemsHelp/><p>Para adicionar uma nova bobine para a base de dados, comece por criar um <filamentCreateLink>Filamento</filamentCreateLink>. Feito isso, prossiga para criar uma <spoolCreateLink>Bobine</spoolCreateLink> para esse filamento específico. Se posteriormente adquirir bobines adicionais do mesmo filamento, simplesmente gere Bobines adicionais e reutilize o mesmo objeto Filamento.</p><p>Opcionalmente, também pode criar um <vendorCreateLink>Fabricante</vendorCreateLink> para a empresa que fabrica o filamento, se desejar rastrear essas informações.</p><p>Tem a opção de vincular outros serviços de impressora 3D ao Spoolman, como o Moonraker, que pode monitorizar automaticamente o uso do filamento e atualizar as bobines. Consulte o <readmeLink>README do Spoolman</readmeLink> para obter orientação sobre como configurar.</p>"
},
"documentTitle": {
"suffix": " | Spoolman",
"home": {
"list": "Home | Spoolman"
"list": "Início | Spoolman"
},
"help": {
"list": "Ajuda | Spoolman"
@@ -248,40 +315,105 @@
"filament": {
"list": "Filamentos | Spoolman",
"show": "#{{id}} Mostrar filamento | Spoolman",
"edit": "#{{id}} Editar filamento | Spoolman",
"edit": "#{{id}} Editar Filamento | Spoolman",
"create": "Criar filamento | Spoolman",
"clone": "#{{id}} Clonar filamento | Spoolman"
},
"spool": {
"list": "Carretéis | Spoolman",
"show": "#{{id}} Mostrar carretel | Spoolman",
"edit": "#{{id}} Editar carretel | Spoolman",
"create": "Criar carretel | Spoolman",
"clone": "#{{id}} Clonar carretel | Spoolman"
"list": "Bobines | Spoolman",
"show": "#{{id}} Mostrar Bobine | Spoolman",
"edit": "#{{id}} Editar Bobine | Spoolman",
"create": "Criar Bobine | Spoolman",
"clone": "#{{id}} Clonar Bobine | Spoolman"
},
"vendor": {
"show": "#{{id}} Mostrar fornecedor | Spoolman",
"list": "Fornecedores | Spoolman",
"create": "Criar fornecedor | Spoolman",
"edit": "#{{id}} Editar fornecedor | Spoolman",
"clone": "#{{id}} Clonar fornecedor | Spoolman"
"show": "#{{id}} Mostrar Fabricante | Spoolman",
"list": "Fabricantes | Spoolman",
"create": "Criar Fabricante | Spoolman",
"edit": "#{{id}} Editar Fabricante | Spoolman",
"clone": "#{{id}} Clonar Fabricante | Spoolman"
},
"default": "Spoolman"
"default": "Spoolman",
"locations": {
"list": "Locais | Spoolman"
}
},
"no": "Não",
"scanner": {
"title": "Leitor de código QR",
"description": "Digitalize um código QR do Spoolman para ver detalhes sobre o carretel.",
"title": "Leitor de Código QR",
"description": "Digitalize um código QR do Spoolman para ver detalhes sobre a bobine.",
"error": {
"notAllowed": "Você não permitiu acesso à câmera.",
"insecureContext": "A página não é veiculada por HTTPS.",
"notAllowed": "Não deu permissão de acesso à câmara.",
"insecureContext": "A página não é protegida por HTTPS.",
"streamApiNotSupported": "O navegador não oferece suporte à API MediaStream.",
"unknown": "Ocorreu um erro desconhecido. ({{error}})",
"notReadable": "A câmera não é legível.",
"notFound": "Nenhuma câmera foi encontrada."
"notReadable": "A câmara não está acessível.",
"notFound": "Nenhuma câmara foi encontrada."
}
},
"table": {
"actions": "Ações"
},
"settings": {
"extra_fields": {
"params": {
"multi_choice": "Múltiplas Opções",
"key": "Chave",
"name": "Nome",
"field_type": "Tipo",
"unit": "Unidade",
"order": "Ordem",
"default_value": "Valor Padrão",
"choices": "Opções"
},
"field_type": {
"datetime": "Date e Hora",
"text": "Texto",
"integer": "Inteiro",
"float": "Decimal",
"float_range": "Intervalo Decimal",
"integer_range": "Intervalo Inteiro",
"boolean": "Booleano",
"choice": "Opção"
},
"key_not_changed": "Por favor, altere a chave para algo diferente.",
"delete_confirm": "Apagar o campo {{name}}?",
"delete_confirm_description": "Isto irá apagar o campo e todos os dados associados a todas as entidades.",
"description": "<p>Aqui pode adicionar campos personalizados extras às suas entidades.</p><p>Uma vez que um campo é adicionado, não se pode alterar sua chave ou tipo, e para campos do tipo \"opção\" não se pode remover as opções ou alterar o estado da múltipla opção. Se remover um campo, os dados associados para todas as entidades serão excluídos.</p><p>A chave é como outros programas leem/gravam os dados, portanto, se o seu campo personalizado se destina a integrar com um programa de terceiros, certifique-se de defini-lo corretamente. O valor padrão só é aplicado a novos itens.</p><p>Campos extras não podem ser ordenados ou filtrados nas visualizações de tabela.</p>",
"choices_missing_error": "Não pode remover as opções. As seguintes opções serão perdidas: {{choices}}",
"tab": "Campos Extras",
"boolean_true": "Sim",
"boolean_false": "Não",
"non_unique_key_error": "A chave deve ser única.",
"choices_reorder_hint": "Arraste ou use as setas para reordenar esta escolha."
},
"settings": "Configurações",
"header": "Configurações",
"general": {
"tab": "Geral",
"currency": {
"label": "Moeda"
},
"base_url": {
"label": "Endereço Base",
"tooltip": "O endereço base a utilizar em funcionalidades tais como os códigos QR."
},
"round_prices": {
"tooltip": "Arredonda os preços para o número inteiro mais próximo.",
"label": "Arredondar preços"
}
}
},
"locations": {
"locations": "Locais",
"new_location": "Novo Local",
"no_location": "Sem Local",
"no_locations_help": "Esta página permite organizar as suas bobines em locais, adicione algumas bobines para começar!"
},
"theme": {
"label": "Tema",
"system": "Sistema",
"light": "Claro",
"dark": "Escuro"
}
}
+308 -183
View File
@@ -1,47 +1,50 @@
{
"buttons": {
"create": "creeaza",
"create": "creaza",
"save": "salveaza",
"show": "arata",
"filter": "filtreaza",
"cancel": "anuleaza",
"edit": "editeaza",
"confirm": "esti sigur?",
"delete": "sterge",
"confirm": "Ești sigur?",
"delete": "Șterge",
"undo": "inapoi",
"import": "importa",
"logout": "logout",
"logout": "Iesire",
"saveAndAdd": "salveazaSiAdauga",
"clear": "sterge",
"clear": "Șterge",
"refresh": "actualizeaza",
"clone": "cloneaza",
"archive": "arhiveaza",
"unArchive": "dezarhiveaza",
"hideArchived": "ascunde arhivate",
"showArchived": "arata arhivate",
"hideArchived": "ascundeArhivate",
"showArchived": "Arata arhivate",
"notAccessTitle": "nu ai permisiune de acces",
"hideColumns": "ascunde coloane",
"clearFilters": "reseteaza filtre"
"clearFilters": "reseteaza filtre",
"continue": "Continua"
},
"actions": {
"create": "creeaza",
"create": "creaza",
"clone": "cloneaza",
"list": "listeaza",
"list": "Listeaza",
"show": "arata",
"edit": "editeaza"
},
"warnWhenUnsavedChanges": "Esti sigur? Modificarile ne-salvate vor fi pierdurte",
"warnWhenUnsavedChanges": "Esti sigur? Modificarile ne-salvate vor fi pierdute.",
"notifications": {
"success": "succes",
"error": "eroare",
"undoable": "Ai inca {{seconds}} secunde sa corectezi",
"error": "Eroare (Cod: {{statusCode}})",
"undoable": "Ai inca {{seconds}} secunde sa corectezi.",
"createSuccess": "{{resource}} creata cu succes",
"createError": "Eroare la crearea {{resource}} (status code: {{statusCode}})",
"createError": "Eroare la crearea {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} stearsa cu succes",
"deleteError": "Eroare la stergerea {{resource}} (status code: {{statusCode}})",
"deleteError": "Eroare la stergerea {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} editata cu succes",
"editError": "eroare la editarea {{resource}} (status code: {{statusCode}})",
"importProgress": "Importare: {{processed}}/{{total}}"
"editError": "eroare la editarea {{resource}} (status code: {{statusCode}})",
"importProgress": "Importare: {{processed}}/{{total}}",
"saveSuccessful": "Salvare reusita!",
"validationError": "Eroare de validare: {{error}}"
},
"kofi": "Doneaza pe Ko-fi",
"loading": "Incarca",
@@ -54,234 +57,356 @@
},
"printing": {
"generic": {
"verticalSpacing": "",
"verticalSpacing": "Spațiere verticală",
"borders": {
"grid": "",
"none": "",
"border": ""
"grid": "Rețea",
"none": "Nici unul",
"border": "Frontieră"
},
"previewScale": "",
"contentSettings": "",
"helpMargin": "",
"description": "",
"marginRight": "",
"rows": "",
"marginBottom": "",
"marginTop": "",
"print": "",
"printerMarginRight": "",
"columns": "",
"helpPrinterMargin": "",
"layoutSettings": "",
"dimensions": "",
"skipItems": "",
"paperSize": "",
"printerMarginTop": "",
"horizontalSpacing": "",
"customSize": "",
"marginLeft": "",
"printerMarginLeft": "",
"showBorder": "",
"printerMarginBottom": "",
"title": ""
"previewScale": "Scala de previzualizare",
"contentSettings": "Setări de conținut",
"helpMargin": "Marginile trebuie configurate pentru a se potrivi cu hârtia de etichetă și imprimanta dvs., modificarea acestora va afecta dimensiunea întregii grile.",
"description": "Modificati setările de mai jos pentru a obține aspectul dorit al etichetelor. Rețineți că imprimantele și sistemul dvs. de operare pot aplica propriile setări, așa că poate fi necesar să incercati mai multe opțiuni înainte ca totul să fie corect. Testați-l pe o bucată de hârtie obișnuită înainte de a imprima pe etichetele reale.",
"marginRight": "Marja dreaptă",
"rows": "Rânduri",
"marginBottom": "Marja de jos",
"marginTop": "Marja superioară",
"print": "Imprimare",
"printerMarginRight": "Safe-Zone dreapta",
"columns": "Coloane",
"helpPrinterMargin": "Safe-Zone ar trebui să fie setată la cât de aproape de marginea hârtiei poate imprima imprimanta dvs., modificarea acestora nu va afecta întreaga grilă.",
"layoutSettings": "Setări Layout",
"dimensions": "Dimensiuni",
"skipItems": "Treci peste articole",
"paperSize": "Dimensiunea hârtiei",
"printerMarginTop": "Safe-Zone sus",
"horizontalSpacing": "Spațiere orizontală",
"customSize": "Personalizat",
"marginLeft": "Margine stângă",
"printerMarginLeft": "Safe-Zone stanga",
"showBorder": "Arată marginea",
"printerMarginBottom": "Safe-Zone jos",
"title": "Imprimare",
"itemCopies": "Copii ale obiectelor",
"settings": "Prestabilite",
"defaultSettings": "Implicit",
"addSettings": "Adăugarea de noi prestabilite",
"newSetting": "Nou",
"duplicateSettings": "Duplicat",
"deleteSettings": "Ștergerea presetării curente",
"deleteSettingsConfirm": "Ești sigur că vrei să ștergi această prestabilită?",
"settingsName": "Numele valorii prestabilite",
"saveSetting": "Salvare prestabilite",
"saveAsImage": "Salvare ca imagine"
},
"spoolSelect": {
"showArchived": "",
"title": "",
"description": "",
"selectedTotal_one": "",
"selectedTotal_few": "",
"selectedTotal_other": "",
"selectAll": "",
"noSpoolsSelected": ""
"showArchived": "Arata arhivate",
"title": "Selectați bobine",
"description": "Selectați rolele pentru care trebuie printate etichete.",
"selectedTotal_one": "{{count}} bobina selectata",
"selectedTotal_few": "{{count}} bobine selectate",
"selectedTotal_other": "{{count}} bobine selectate",
"selectAll": "Selecteaza/deselecteaza tot",
"noSpoolsSelected": "Nu ati selectat nicio bobina."
},
"qrcode": {
"bedTemp": "",
"title": "",
"button": "",
"showTemperatures": "",
"spoolWeight": "",
"showFilamentComment": "",
"showLotNr": "",
"showContent": "",
"showSpoolWeight": "",
"textSize": "",
"showVendorComment": "",
"showSpoolmanIcon": "",
"showSpoolComment": "",
"extruderTemp": "",
"showVendor": "",
"lotNr": ""
"bedTemp": "BT: {{temp}}",
"title": "Printarea codurilor QR",
"button": "Imprimați codurile QR",
"showTemperatures": "Temperaturi",
"spoolWeight": "Greutatea rolei de filament: {{weight}}",
"showFilamentComment": "Filament Comentariu",
"showLotNr": "Lot Nr",
"showContent": "Imprimă eticheta",
"showSpoolWeight": "Greutate bobină",
"textSize": "Mărimea textului",
"showVendorComment": "Comentariul furnizorului",
"showSpoolmanIcon": "Arata iconul Spoolman",
"showSpoolComment": "Bobină Comentariu",
"extruderTemp": "ET: {{temp}}",
"showVendor": "Furnizor",
"lotNr": "Lot Nr: {{lot}}",
"template": "Șablon etichetă",
"useHTTPUrl": {
"label": "Link către codul QR",
"tooltip": "Se va folosi un link proprietar care va funcționa doar dacă este scanat din funcția de scanare a Spoolman (implicit). URL-ul folosește fie URL-ul de bază specificat în setări, fie URL-ul paginii curente dacă nu este setat.",
"options": {
"default": "Implicit",
"url": "URL"
},
"preview": "Previzualizare:"
},
"showQRCode": "Printarea codului QR",
"showQRCodeMode": {
"no": "Nu",
"simple": "Simplu",
"withIcon": "Cu imagine"
},
"templateHelp": "Utilizați {} pentru a insera valorile obiectului filament ca text. De exemplu, {id} va fi înlocuit cu id-ul filamentului, sau {filament.material} va fi înlocuit cu materialul filamentului. Dacă o valoare lipsește, aceasta va fi înlocuită cu \"?\". Un al doilea set de {} poate fi folosit pentru a elimina acesta. În plus, orice text între seturile de {} va fi eliminat dacă valoarea lipsește. De exemplu, {Lot Nr: {lot_nr}} va afișa doar eticheta dacă filamentul are un număr de lot. Încadrați textul cu asteriscuri duble ** pentru a-l face îngroșat. Faceți clic pe buton pentru a vizualiza o listă cu toate etichetele disponibile."
}
},
"documentTitle": {
"vendor": {
"show": "",
"list": "",
"edit": "",
"create": "",
"clone": ""
"show": "#{{id}} Afișare producători | Spoolman",
"list": "Producători | Spoolman",
"edit": "#{{id}} Editare producător | Spoolman",
"create": "Adăugare producător | Spoolman",
"clone": "#{{id}} Duplicare producător | Spoolman"
},
"spool": {
"create": "",
"clone": "",
"list": "",
"edit": "",
"show": ""
"create": "Creare rolă| Spoolman",
"clone": "#{{id}} Clonare rolă | Spoolman",
"list": "Role | Spoolman",
"edit": "#{{id}} Editare rolă | Spoolman",
"show": "#{{id}} Afișare role | Spoolman"
},
"filament": {
"show": "",
"list": "",
"clone": "",
"edit": "",
"create": ""
"show": "#{{id}} Afișare filamente | Spoolman",
"list": "Filamente | Spoolman",
"clone": "#{{id}} Clonare filament | Spoolman",
"edit": "#{{id}} Editare filamente | Spoolman",
"create": "Creare filament| Spoolman"
},
"home": {
"list": ""
"list": "Acasă | Spoolman"
},
"suffix": "",
"default": "",
"suffix": " | Spoolman",
"default": "Spoolman",
"help": {
"list": ""
"list": "Ajutor | Spoolman"
},
"locations": {
"list": "Zone | Spoolman"
}
},
"help": {
"description": "",
"description": "<title>Ajutor</title><p>Iată câteva sfaturi pentru a te ajuta să începi.</p><p>Spoolman are 3 tipuri diferite de date:</p><itemsHelp/><p>Pentru a adăuga o nouă rolă în baza de date, începe prin a crea un obiect <filamentCreateLink>Filament</filamentCreateLink>. Odată ce ai terminat, continuă prin a crea un obiect <spoolCreateLink>Spool</spoolCreateLink> pentru aca rolă. Dacă mai achiziționezi role suplimentare din același filament mai târziu, pur și simplu generează obiecte Rolă suplimentare și reutilizează același obiect Filament.</p><p>Opțional, poți genera și un obiect <vendorCreateLink>Producător</vendorCreateLink> pentru compania care produce filamentul, dacă dorești să urmărești această informație.</p><p>Ai opțiunea de a lega alte servicii pentru imprimante 3D de Spoolman, cum ar fi Moonraker, care poate monitoriza automat utilizarea filamentului și actualiza obiectele Rolă pentru tine. Consultă <readmeLink>README-ul Spoolman</readmeLink> pentru orientări despre cum să configurezi acest lucru.</p>",
"resources": {
"spool": "",
"filament": "",
"vendor": ""
"spool": "Role de filament individuale ale unui filament specific.",
"filament": "Brand-uri de filament. Au proprietăți precum nume, material, culoare, diametru și altele.",
"vendor": "Companiile care produc filamentul."
},
"help": ""
"help": "Ajutor"
},
"spool": {
"fields": {
"weight_to_use": "",
"registered": "",
"used_weight": "",
"first_used": "",
"location": "",
"material": "",
"last_used": "",
"archived": "",
"price": "",
"used_length": "",
"measured_weight": "",
"lot_nr": "",
"comment": "",
"id": "",
"filament": "",
"filament_name": "",
"remaining_length": "",
"remaining_weight": ""
"weight_to_use": "Greutate",
"registered": "Înregistrat",
"used_weight": "Greutate folosita",
"first_used": "Prima utilizare",
"location": "Locație",
"material": "Material",
"last_used": "Ultima utilizare",
"archived": "Arhivat",
"price": "Preț",
"used_length": "Lungime folosita",
"measured_weight": "Greutate masurata",
"lot_nr": "Lot Nr",
"comment": "Comentariu",
"id": "ID",
"filament": "Filament",
"filament_name": "Filament",
"remaining_length": "Lungime ramasa",
"remaining_weight": "Greutate ramasa",
"filament_internal": "Intern",
"filament_external": "Extern",
"initial_weight": "Greutatea inițială",
"spool_weight": "Greutatea rolei"
},
"fields_help": {
"location": "",
"weight_to_use": "",
"used_weight": "",
"price": "",
"lot_nr": "",
"remaining_weight": "",
"measured_weight": ""
"location": "Locatia in care bobina este stocata.",
"weight_to_use": "Selectati greutatea care trebuie folosita. Greutatea masurata e disponibila numai daca greutatea bobinei goale este setata pentru filamentul respectiv.",
"used_weight": "Cat filament a fost folosit de pe bobina respectiva. O bobina noua ar trebui sa aiba 0 grame folosite.",
"price": "Pretul unei bobine. Daca nu e definit, pretul filamentului va fi folosit.",
"lot_nr": "Numarul de productie al lotului. Poate fi folosit pentru a garanta consistenta culorii, in caz ca mai multe bobine sunt folosite la acelasi print.",
"remaining_weight": "Cat filament a ramas pe bobina. Aceasta valoare reprezinta greutatea filamentului de pe bobina respectiva.",
"measured_weight": "Greutatea totala a bobinei si filamentului.",
"initial_weight": "Greutatea inițială a filamentului de pe rolă (greutate netă). Se va folosi greutatea din obiectul filament dacă nu este setată.",
"spool_weight": "Greutatea rolei atunci când este goală. Lăsați necompletat pentru a folosi valoarea de la filament sau de la producător.",
"external_filament": "Ai selectat un filament din baza de date externă. Un obiect filament (și posibil un obiect de la fabricant) va fi creat automat atunci când creați această rolă. Aceasta poate crea obiecte filament duplicate dacă ați creat deja un obiect filament pentru acest filament."
},
"titles": {
"list": "",
"archive": "",
"show_title": "",
"create": "",
"show": "",
"edit": "",
"clone": ""
"list": "Bobine",
"archive": "Archiveaza bobina",
"show_title": "[Bobina #{{id}}] {{name}}",
"create": "Creați bobină",
"show": "Arata bobina",
"edit": "Editeaza bobina",
"clone": "Cloneaza bobina",
"adjust": "Ajustarea rolei de filament"
},
"messages": {
"archive": ""
"archive": "Esti sigur ca vrei sa arhivezi aceasta bobina?"
},
"form": {
"spool_updated": "",
"new_location_prompt": ""
"spool_updated": "Aceasta bobina a fost modificata de altcineva in timpul scurs de la deschiderea acestei pagini, Salvarea va suprascrie aceste schimbari!",
"new_location_prompt": "Introduceti o locatie noua",
"measurement_type": {
"length": "Lungime",
"weight": "Greutate"
},
"measurement_type_label": "Tipul măsurării",
"adjust_filament_help": "Aici poți adăuga sau elimina direct filament din rolă. O valoare pozitivă va consuma filament, iar o valoare negativă îl va adăuga.",
"adjust_filament_value": "Cantitatea consumată"
},
"spool": ""
"spool": "Bobine",
"formats": {
"last_used": "Ultima utilizare {{date}}"
}
},
"filament": {
"fields": {
"registered": "",
"article_number": "",
"id": "",
"vendor": "",
"settings_bed_temp": "",
"material": "",
"price": "",
"name": "",
"color_hex": "",
"settings_extruder_temp": "",
"vendor_name": "",
"diameter": "",
"density": "",
"weight": "",
"spools": "",
"comment": "",
"spool_weight": ""
"registered": "Înregistrat",
"article_number": "Numarul articolului",
"id": "ID",
"vendor": "Producător",
"settings_bed_temp": "Temperatura patului",
"material": "Material",
"price": "Preț",
"name": "Nume",
"color_hex": "Culoare",
"settings_extruder_temp": "Temperatura extruderului",
"vendor_name": "Producător",
"diameter": "Diametru",
"density": "Densitate",
"weight": "Greutate",
"spools": "Arata bobinele",
"comment": "Comentariu",
"spool_weight": "Greutatea bobinei",
"single_color": "Unică",
"multi_color": "Multi",
"coaxial": "Co-extrudat",
"longitudinal": "Longitudinal",
"external_id": "ID extern"
},
"fields_help": {
"price": "",
"material": "",
"name": "",
"spool_weight": "",
"article_number": "",
"weight": ""
"price": "Pretului unei bobine noi.",
"material": "Ex. PLA, ABS, PETG, etc.",
"name": "Numele filamentului, pentru a-l putea distinge de altele ale aceluiași producător. Trebuie să conțină culoarea de exemplu.",
"spool_weight": "Greutatea unei bobine goale (fara filament). Este folosita pentru a calcula greutatea unei bobine cu filament.",
"article_number": "Ex. EAN, UPC etc.",
"weight": "Greutatea neta a filamentului de pe o bobina. Nu trebuie sa includa greutatea bobinei, ci doar cea a filamentului. De obicei greutatea filementului este cea inscrisa pe ambalaj.",
"multi_color_direction": "Filamentele pot avea multiple culori în două moduri: fie prin co-extrudare, cum ar fi filamentele de două culori cu culori multiple consistente, fie prin schimbări de culoare longitudinale, cum ar fi filamentele gradient care își schimbă culorile pe parcursul rolei."
},
"titles": {
"edit": "",
"show_title": "",
"create": "",
"show": "",
"list": "",
"clone": ""
"edit": "Modifica del filamento",
"show_title": "[Filament #{{id}}] {{name}}",
"create": "Creați filament",
"show": "Arată filamentul",
"list": "Filamente",
"clone": "Clonarea filamentului"
},
"form": {
"filament_updated": ""
"filament_updated": "Acest filament a fost actualizat de cineva/altceva de când ai deschis această pagină. Salvarea va suprascrie aceste modificări!",
"import_external": "Importare din exterior",
"import_external_description": "Selectați un filament din listă pentru a completa automat detaliile sale în formularul de creare a filamentului. Acest lucru va suprascrie orice date pe care le-ați introdus în formular.<br><br>Un obiect Producător va fi creat automat când faceți clic pe Ok, dacă este necesar."
},
"filament": ""
"filament": "Filamente",
"buttons": {
"add_spool": "Adăugare rolă"
}
},
"vendor": {
"vendor": "",
"vendor": "Producători",
"fields": {
"registered": "",
"comment": "",
"name": "",
"id": ""
"registered": "Înregistrat",
"comment": "Comentariu",
"name": "Nume",
"id": "ID",
"empty_spool_weight": "Greutatea rolei goale",
"external_id": "ID extern"
},
"titles": {
"clone": "",
"show": "",
"list": "",
"create": "",
"show_title": "",
"edit": ""
"clone": "Duplicarea producătorului",
"show": "Afișarea producătorilor",
"list": "Producători",
"create": "Adăugarea unui producător",
"show_title": "[Producător #{{id}}] {{name}}",
"edit": "Editarea producătorului"
},
"form": {
"vendor_updated": ""
"vendor_updated": "Acest producător a fost actualizat de cineva/altceva de când ai deschis această pagină. Salvarea va suprascrie acele modificări!"
},
"fields_help": {
"empty_spool_weight": "Greutatea unei role goale de la acest producător."
}
},
"dashboard": {
"title": ""
"title": "Tablou de bord"
},
"home": {
"home": ""
"home": "Acasă",
"welcome": "Bine ai venit în instanța ta Spoolman!",
"description": "Se pare că nu ați adăugat încă nici o rolă. Consultați pagina <helpPageLink>Ajutor</helpPageLink> pentru a începe."
},
"scanner": {
"error": {
"notFound": "",
"unknown": "",
"notAllowed": "",
"streamApiNotSupported": "",
"insecureContext": "",
"notReadable": ""
"notFound": "Nicio camera nu a fost gasita.",
"unknown": "Eroare necunoscuta. ({{error}})",
"notAllowed": "Accesul la camera nu a fost permis.",
"streamApiNotSupported": "Browserul nu suporta MediaStream API.",
"insecureContext": "Aceasta pagina nu este servita peste HTTPS.",
"notReadable": "Accesul la camera nu este posibil."
},
"title": "",
"description": ""
"title": "Scanner QR code",
"description": "Scanati QR code-ul Spoolman pentru detalii."
},
"table": {
"actions": ""
"actions": "Acțiuni"
},
"settings": {
"settings": "Setări",
"header": "Setări",
"general": {
"tab": "General",
"currency": {
"label": "Valută"
},
"base_url": {
"label": "URL de bază",
"tooltip": "URL-ul de bază care trebuie utilizat atunci când generați caracteristici, cum ar fi codurile QR."
},
"round_prices": {
"label": "Prețurile estimative",
"tooltip": "Prețurile estimative la cel mai apropiat întreg."
}
},
"extra_fields": {
"tab": "Câmpuri extra",
"description": "<p>Aici poți adăuga câmpuri suplimentare personalizate entităților tale.</p><p>Odată ce un câmp este adăugat, nu poți schimba cheia sau tipul acestuia, iar pentru câmpurile de tip alegere nu poți elimina opțiunile sau schimba starea de alegere multiplă. Dacă elimini un câmp, datele asociate pentru toate entitățile vor fi șterse.</p><p>Cheia este ceea ce alte programe folosesc pentru a citi/scrie date, așa că, dacă câmpul tău personalizat ar trebui să se integreze cu un program de terță parte, asigură-te că îl setezi corect. Valoarea implicită se aplică doar articolelor noi.</p><p>Câmpurile suplimentare nu pot fi sortate sau filtrate în tabele.</p>",
"params": {
"key": "Cheie",
"name": "Nume",
"field_type": "Tip",
"unit": "Unitate",
"order": "Stoc",
"default_value": "Valoarea implicită",
"choices": "Selecții",
"multi_choice": "Selecție multiplă"
},
"field_type": {
"text": "Text",
"integer": "Întreg",
"integer_range": "Interval întreg",
"float": "Număr zecimal",
"float_range": "Interval de numere zecimale",
"datetime": "Dată/timp",
"boolean": "Boolean",
"choice": "Selecție"
},
"boolean_true": "Da",
"boolean_false": "Nu",
"choices_missing_error": "Nu poți înlătura selecții. Următoarele selecții lipsesc: {{choices}}",
"non_unique_key_error": "Cheia trebuie să fie unică.",
"key_not_changed": "Te rog să schimbi cheia.",
"delete_confirm": "Ștergi câmpul{{name}}?",
"delete_confirm_description": "Ștergerea câmpului și a tuturor datelor asociate pentru toate entitățile."
}
},
"locations": {
"locations": "Zone",
"new_location": "Zonă nouă",
"no_location": "Fără zonă",
"no_locations_help": "Această pagină îți permite să îți organizezi rolele în zone, adaugă câteva role pentru a începe!"
}
}
+157 -24
View File
@@ -28,7 +28,8 @@
"notAccessTitle": "У вас нет разрешения на доступ",
"hideColumns": "Скрыть столбцы",
"clearFilters": "Очистить фильтр",
"saveAndAdd": "Сохранить и добавить"
"saveAndAdd": "Сохранить и добавить",
"continue": "Продолжить"
},
"warnWhenUnsavedChanges": "Вы уверены, что хотите выйти? У вас есть несохраненные изменения.",
"notifications": {
@@ -41,7 +42,9 @@
"deleteError": "Ошибка при удалении {{resource}} (status code: {{statusCode}})",
"editSuccess": "Успешно отредактировано {{resource}}",
"editError": "Ошибка при редактировании {{resource}} (status code: {{statusCode}})",
"importProgress": "Импорт: {{processed}}/{{total}}"
"importProgress": "Импорт: {{processed}}/{{total}}",
"saveSuccessful": "Успешное сохранение!",
"validationError": "Ошибка проверки:{{error}}"
},
"loading": "Загрузка",
"version": "Версия",
@@ -85,16 +88,27 @@
"none": "Без рамки",
"border": "Общая",
"grid": "Сетка"
}
},
"addSettings": "Добавить новый пресет",
"newSetting": "Новый",
"deleteSettings": "Удалить текущий пресет",
"deleteSettingsConfirm": "Вы уверены, что хотите удалить этот пресет?",
"settings": "Пресеты",
"defaultSettings": "По умолчанию",
"settingsName": "Имя пресета",
"itemCopies": "Копии элементов",
"saveAsImage": "Сохранить как изображение",
"duplicateSettings": "Дублировать",
"saveSetting": "Сохранить Пресеты"
},
"qrcode": {
"button": "Распечатать QR-коды",
"title": "Печать QR-кода",
"button": "Печать этикеток",
"title": "Печать этикетки",
"spoolWeight": "Вес катушки: {{weight}}",
"lotNr": "№ партии: {{lot}}",
"bedTemp": "ТС: {{temp}}",
"extruderTemp": "ТЕ: {{temp}}",
"textSize": "Размер текста",
"textSize": "Размер Текста Надписи",
"showSpoolmanIcon": "Показать иконку Spoolman",
"showVendor": "Производитель",
"showContent": "Печать информации",
@@ -103,11 +117,28 @@
"showTemperatures": "Температура",
"showSpoolComment": "Комментарий к катушке",
"showFilamentComment": "Комментарий к филаменту",
"showVendorComment": "Комментарий к продавцу"
"showVendorComment": "Комментарий к продавцу",
"template": "Шаблон этикетки",
"templateHelp": "Используйте {} для вставки значений объекта катушки в виде текста. Например, {id} будет заменен на идентификатор катушки, или {filament.material} будет заменен на материал катушки. если значение отсутствует, оно будет заменено на \"?\". Для удаления этого можно использовать второй набор {}. Кроме того, любой текст между наборами {} будет удален, если значение отсутствует. Например, {Номер партии: {lot_nr}} будет отображаться только в том случае, если на катушке указан номер партии. Для выделения текста жирным шрифтом выделите его двойной звездочкой **. Нажмите кнопку, чтобы просмотреть список всех доступных тегов.",
"useHTTPUrl": {
"tooltip": "Будет использоваться проприетарная ссылка, которая будет работать только при сканировании через функцию сканирования Spoolman (по умолчанию). URL использует либо базовый URL, указанный в настройках, либо URL текущей страницы, если он не задан.",
"label": "Ссылка на QR-код",
"options": {
"default": "По умолчанию",
"url": "URL"
},
"preview": "Предварительный просмотр:"
},
"showQRCodeMode": {
"no": "Нет",
"simple": "Простой",
"withIcon": "Со значком"
},
"showQRCode": "Печать QR-кода"
},
"spoolSelect": {
"title": "Выберите катушки",
"description": "Выберите катушки для печати QR-кодов.",
"description": "Выберите катушки для печати этикеток.",
"showArchived": "Показать архивные",
"noSpoolsSelected": "Вы не выбрали ни одной катушки.",
"selectAll": "Выбрать/отменить выбор всего",
@@ -134,7 +165,7 @@
"id": "ID",
"filament_name": "Филамент",
"filament": "Филамент",
"material": "Тип",
"material": "Материал",
"used_weight": "Использованный вес",
"remaining_weight": "Оставшийся вес",
"used_length": "Использованная длина",
@@ -148,16 +179,23 @@
"archived": "В архиве",
"measured_weight": "Измеренный вес",
"weight_to_use": "Масса",
"price": ""
"price": "Цена",
"initial_weight": "Начальный вес",
"spool_weight": "Пустой вес",
"filament_internal": "Внутренний",
"filament_external": "Внешний"
},
"fields_help": {
"used_weight": "Сколько филамента было использовано с катушки. На новой катушке должно быть использовано 0 г.",
"location": "Где находится катушка, если у вас есть несколько мест хранения катушек.",
"lot_nr": "Номер партии производителя. Может использоваться для обеспечения однородного цвета отпечатка при использовании нескольких катушек.",
"lot_nr": "Номер партии производителя. Может использоваться для обеспечения однородного цвета печати при использовании нескольких катушек.",
"weight_to_use": "Выберите, какое значение веса ввести. Измеренный вес доступен только в том случае, если вес катушки установлен для выбранной филамента.",
"measured_weight": "Сколько весит филамент и катушка.",
"remaining_weight": "Сколько филамента осталось на катушке. Для новой катушки этот вес должен соответствовать весу катушки.",
"price": ""
"price": "Цена полной катушки. Если не указано, будет использоваться цена филамента.",
"initial_weight": "Исходный вес филамента на катушке (вес нетто). Если не задан, будет использоваться вес объекта, на котором находится филамент.",
"spool_weight": "Вес пустой катушки. Оставьте значение \"пусто\", чтобы использовать значение, указанное в описании филамента или у производителя.",
"external_filament": "Вы выбрали филамент из внешней базы данных. Объект филамент (и, возможно, объект производителя) будет создан автоматически при создании этой катушки. При этом могут возникнуть повторяющиеся объекты филамента, если вы уже создали объект филамент для этой нити накала."
},
"titles": {
"create": "Создать Катушку",
@@ -166,14 +204,25 @@
"list": "Катушки",
"show": "Показать Катушку",
"archive": "Архивировать Катушку",
"show_title": "[Катушка #{{id}}] {{name}}"
"show_title": "[Катушка #{{id}}] {{name}}",
"adjust": "Отрегулируйте филамент катушки"
},
"messages": {
"archive": "Вы уверены, что хотите добавить в архив эту катушку?"
},
"form": {
"spool_updated": "Эта катушка была обновлена кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!",
"new_location_prompt": "Введите новое местоположение"
"new_location_prompt": "Введите новое местоположение",
"measurement_type": {
"weight": "Вес",
"length": "Длина"
},
"adjust_filament_help": "Здесь вы можете напрямую добавлять или вычитать филамент из катушки. При положительном значении филамент будет израсходован, при отрицательном - добавлен.",
"measurement_type_label": "Тип измерения",
"adjust_filament_value": "Потребляемое количество"
},
"formats": {
"last_used": "Последнее использование {{date}}"
}
},
"filament": {
@@ -183,7 +232,7 @@
"vendor_name": "Производитель",
"vendor": "Производитель",
"name": "Имя",
"material": "Тип",
"material": "Материал",
"price": "Цена",
"density": "Плотность",
"diameter": "Диаметр",
@@ -195,15 +244,21 @@
"settings_extruder_temp": "Температура экструдера",
"settings_bed_temp": "Температура стола",
"color_hex": "Цвет",
"spools": "Показать катушки"
"spools": "Показать катушки",
"coaxial": "Коэкструдированный",
"longitudinal": "Продольный",
"single_color": "Одноцветный",
"multi_color": "Многоцветный",
"external_id": "Внешний ID"
},
"fields_help": {
"name": "Название филамента, чтобы отличить этот тип филамента от друго филамента того же производителя. Например, должен содержать цвет.",
"name": "Название филамента, чтобы отличить этот тип филамента от других того же производителя. Например, должно содержать цвет.",
"material": "Например: PLA, ABS, PETG и т.д.",
"price": "Цена полной катушки в валюте, настроенной системой.",
"price": "Цена полной катушки.",
"weight": "Вес филамента на полной катушке (вес нетто). Сюда не следует включать вес самой катушки, а только филамента. Это то, что обычно пишут на упаковке.",
"spool_weight": "Вес пустой катушки. Используется для определения измеренного веса катушки.",
"article_number": "Например: EAN, UPC, и т.д."
"article_number": "Например: EAN, UPC, и т.д.",
"multi_color_direction": "Филаменты могут иметь несколько цветов двумя способами: либо через коэкструзию, как двойные цветные филаменты с постоянными разноцветными участками, либо через продольные изменения цвета, как градиентные филаменты, которые меняют цвет по всей длине катушки."
},
"titles": {
"create": "Создать филамент",
@@ -214,7 +269,12 @@
"show_title": "[Филамент #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Этот филамент был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!"
"filament_updated": "Этот филамент был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!",
"import_external": "Импорт из внешнего источника",
"import_external_description": "Выберите филамент из списка, чтобы автоматически заполнить его данные в форме создания филамента. Это перезапишет все данные, которые вы ввели в форму.<br><br>Объект \"Производитель\" будет создан автоматически, если это необходимо, когда вы нажмете \"Ок\"."
},
"buttons": {
"add_spool": "Добавить катушку"
}
},
"vendor": {
@@ -223,7 +283,9 @@
"id": "ID",
"name": "Имя",
"registered": "Зарегистрирован",
"comment": "Комментарий"
"comment": "Комментарий",
"empty_spool_weight": "Вес пустой катушки",
"external_id": "Внешний ID"
},
"titles": {
"create": "Создать производителя",
@@ -235,6 +297,9 @@
},
"form": {
"vendor_updated": "Этот производитель был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!"
},
"fields_help": {
"empty_spool_weight": "Вес пустой катушки от этого производителя."
}
},
"table": {
@@ -269,6 +334,9 @@
},
"home": {
"list": "Главная | Spoolman"
},
"locations": {
"list": "Местоположения | Spoolman"
}
},
"help": {
@@ -277,11 +345,76 @@
"filament": "Марка филамента. У них есть такие свойства, как имя, материал, цвет, диаметр и многое другое.",
"vendor": "Компании, производящие филаменты."
},
"description": "<title>Помощь</title><p>Вот несколько советов, которые помогут вам начать</p><p>Spoolman хранит 3 различных типа данных:</p><itemsHelp/><p>Чтобы добавить новую катушку в базу данных, начните с создания объекта <filamentCreateLink>Филамент</filamentCreateLink>. Как только это будет сделано, приступайте к созданию объекта <spoolCreateLink>Катушка</spoolCreateLink> для этой конкретной катушки. Если позже вы приобретете дополнительные катушки того же филамента, просто создайте дополнительные объекты катушек и повторно используйте тот же объект Филамент.</p><p>При желании вы также можете создать объект <vendorCreateLink>Поставщика</vendorCreateLink> для компании, производящий филамент, если хотите отслеживать эту информацию.</p><p>У вас есть возможность связать Spoolman с другими службами 3D-принтеров, например Moonraker, который может автоматически отслеживать использование филамента и обновлять объекты Катушек для вас. Обратитесь к <readmeLink>Spoolman README</readmeLink> для получения инструкций по настройке.</p>",
"description": "<title>Помощь</title><p>Вот несколько советов, которые помогут вам начать.</p><p>Spoolman хранит 3 различных типа данных:</p><itemsHelp/><p>Чтобы добавить новую катушку в базу данных, начните с создания объекта <filamentCreateLink>филамент</filamentCreateLink>. Как только это будет сделано, приступайте к созданию объекта <spoolCreateLink>катушка</spoolCreateLink> для этой конкретной катушки. Если позже вы приобретете дополнительные катушки того же филамента, просто создайте дополнительные объекты катушек и повторно используйте тот же объект филамент.</p><p>При желании вы также можете создать объект <vendorCreateLink>производитель</vendorCreateLink> для компании, производящей филамент, если хотите отслеживать эту информацию.</p><p>У вас есть возможность связать Spoolman с другими службами 3D-принтеров, например Moonraker, который может автоматически отслеживать использование филамента и обновлять объекты катушек для вас. Обратитесь к <readmeLink>Spoolman README</readmeLink> для получения инструкций по настройке.</p>",
"help": "Помощь"
},
"kofi": "Поддержите меня на Ko-fi",
"home": {
"home": "Главная"
}
"home": "Главная",
"welcome": "Добро пожаловать в ваш личный Spoolman!",
"description": "Похоже, что вы ещё не добавили ни одной катушки. Посетите <helpPageLink>Страницу помощи</helpPageLink>, для комфортного начала."
},
"settings": {
"extra_fields": {
"description": "<p>Здесь вы можете добавить дополнительные пользовательские поля к сущностям.</p><p>После добавления поля нельзя изменить его ключ или тип, а для полей выбора нельзя удалять варианты или изменять состояние множественного выбора. Если вы удалите поле, связанные данные для всех сущностей будут удалены.</p><p>Ключ используется другими программами для чтения и записи данных, поэтому если ваше пользовательское поле должно интегрироваться со сторонней программой, убедитесь, что задали его правильно. Значение по умолчанию применяется только к новым элементам.</p><p>Дополнительные поля можно отображать и сортировать в табличных представлениях. Поля выбора и логические поля также предоставляют там параметры фильтрации, а все типы дополнительных полей поддерживают фильтрацию по пустым значениям.</p>",
"tab": "Дополнительные поля",
"params": {
"key": "Ключ",
"name": "Имя",
"field_type": "Тип",
"unit": "Единица измерения",
"order": "Заказ",
"choices": "Выбор",
"default_value": "Значение по умолчанию",
"multi_choice": "Множественный выбор"
},
"field_type": {
"float": "Дробное",
"float_range": "Дробный диапазон",
"datetime": "Дата и время",
"boolean": "Логический",
"choice": "Выбор",
"text": "Текст",
"integer": "Целое число",
"integer_range": "Целочисленный диапазон"
},
"non_unique_key_error": "Ключ должен быть уникальным.",
"key_not_changed": "Пожалуйста, замените ключ на что-нибудь другое.",
"delete_confirm": "Удалить поле {{name}}?",
"delete_confirm_description": "Это приведет к удалению поля и всех связанных с ним данных для всех объектов.",
"boolean_true": "Да",
"boolean_false": "Нет",
"choices_missing_error": "Вы не можете удалить варианты. Отсутствуют следующие варианты: {{choices}}",
"choices_reorder_hint": "Перетащите или используйте стрелки."
},
"header": "Настройки",
"general": {
"tab": "Общие",
"currency": {
"label": "Валюта"
},
"base_url": {
"label": "Базовый URL-адрес",
"tooltip": "Базовый URL-адрес, используемый при создании таких функций, как QR-коды."
},
"round_prices": {
"tooltip": "Округлять цены до ближайшего целого числа.",
"label": "Округлять цены"
}
},
"settings": "Настройки"
},
"locations": {
"no_locations_help": "Эта страница позволяет организовать катушки по местоположениям. Добавьте несколько катушек, чтобы начать!",
"locations": "Местоположения",
"new_location": "Новое местоположение",
"no_location": "Нет местоположения"
},
"theme": {
"label": "Тема",
"system": "Как в системе",
"light": "Светлая",
"dark": "Тёмная"
},
"sponsor": "Поддержать меня на GitHub"
}
+400
View File
@@ -0,0 +1,400 @@
{
"buttons": {
"clone": "Klonovať",
"archive": "Archivovať",
"create": "Vytvoriť",
"save": "Uložiť",
"saveAndAdd": "Uložiť Ako",
"logout": "Odhlásiť",
"delete": "Zmazať",
"edit": "Editovať",
"cancel": "Zrušiť",
"confirm": "Ste si istý?",
"continue": "Pokračovať",
"filter": "Filter",
"clear": "Vyčistiť",
"refresh": "Obnoviť",
"show": "Zobraziť",
"import": "Importovať",
"undo": "Späť",
"hideArchived": "Skryť Archivované",
"showArchived": "Zobraziť Archivované",
"notAccessTitle": "Nedostatočné oprávnenia na prístup",
"hideColumns": "",
"clearFilters": "",
"unArchive": ""
},
"actions": {
"create": "Vytvoriť",
"edit": "Editovať",
"show": "Zobraziť",
"clone": "Klonovať",
"list": "Zoznam"
},
"notifications": {
"success": "Hotovo",
"error": "Chyba (status code: {{statusCode}})",
"undoable": "Máte {{seconds}} sekúnd do undo.",
"editSuccess": "Úspešne upravené {{resource}}",
"editError": "Chyba pri editácii {{resource}} (status kód: {{statusCode}})",
"importProgress": "Import: {{processed}}/{{total}}",
"deleteSuccess": "",
"saveSuccessful": "",
"createError": "",
"validationError": "",
"deleteError": "",
"createSuccess": ""
},
"vendor": {
"fields": {
"external_id": "",
"registered": "",
"comment": "",
"name": "",
"empty_spool_weight": "",
"id": ""
},
"fields_help": {
"empty_spool_weight": ""
},
"titles": {
"show_title": "",
"show": "",
"edit": "",
"clone": "",
"create": "",
"list": ""
},
"form": {
"vendor_updated": ""
},
"vendor": ""
},
"documentTitle": {
"vendor": {
"create": "",
"clone": "",
"edit": "",
"list": "",
"show": ""
},
"default": "",
"home": {
"list": ""
},
"spool": {
"edit": "",
"list": "",
"create": "",
"show": "",
"clone": ""
},
"filament": {
"list": "",
"show": "",
"clone": "",
"create": "",
"edit": ""
},
"suffix": "",
"locations": {
"list": ""
},
"help": {
"list": ""
}
},
"spool": {
"fields": {
"measured_weight": "",
"last_used": "",
"filament_internal": "",
"material": "",
"spool_weight": "",
"weight_to_use": "",
"lot_nr": "",
"filament_name": "",
"price": "",
"comment": "",
"filament": "",
"location": "",
"used_length": "",
"archived": "",
"remaining_length": "",
"registered": "",
"first_used": "",
"used_weight": "",
"id": "",
"remaining_weight": "",
"initial_weight": "",
"filament_external": ""
},
"titles": {
"archive": "",
"adjust": "",
"show": "",
"edit": "",
"create": "",
"clone": "",
"show_title": "",
"list": ""
},
"fields_help": {
"external_filament": "",
"used_weight": "",
"weight_to_use": "",
"measured_weight": "",
"price": "",
"spool_weight": "",
"lot_nr": "",
"remaining_weight": "",
"location": "",
"initial_weight": ""
},
"form": {
"spool_updated": "",
"measurement_type_label": "",
"adjust_filament_value": "",
"new_location_prompt": "",
"measurement_type": {
"weight": "",
"length": ""
},
"adjust_filament_help": ""
},
"spool": "",
"formats": {
"last_used": ""
},
"messages": {
"archive": ""
}
},
"filament": {
"form": {
"import_external": "",
"filament_updated": "",
"import_external_description": ""
},
"fields_help": {
"name": "",
"article_number": "",
"spool_weight": "",
"price": "",
"weight": "",
"multi_color_direction": "",
"material": ""
},
"fields": {
"material": "",
"vendor_name": "",
"settings_extruder_temp": "",
"comment": "",
"multi_color": "",
"diameter": "",
"weight": "",
"spool_weight": "",
"external_id": "",
"single_color": "",
"registered": "",
"spools": "",
"name": "",
"price": "",
"settings_bed_temp": "",
"vendor": "",
"density": "",
"longitudinal": "",
"id": "",
"color_hex": "",
"article_number": "",
"coaxial": ""
},
"titles": {
"clone": "",
"show_title": "",
"show": "",
"create": "",
"edit": "",
"list": ""
},
"buttons": {
"add_spool": ""
},
"filament": ""
},
"printing": {
"generic": {
"printerMarginBottom": "",
"verticalSpacing": "",
"rows": "",
"borders": {
"none": "",
"grid": "",
"border": ""
},
"printerMarginTop": "",
"deleteSettingsConfirm": "",
"deleteSettings": "",
"customSize": "",
"title": "",
"contentSettings": "",
"marginRight": "",
"printerMarginRight": "",
"itemCopies": "",
"helpPrinterMargin": "",
"settingsName": "",
"previewScale": "",
"helpMargin": "",
"marginTop": "",
"skipItems": "",
"newSetting": "",
"addSettings": "",
"layoutSettings": "",
"paperSize": "",
"columns": "",
"description": "",
"marginBottom": "",
"horizontalSpacing": "",
"duplicateSettings": "",
"settings": "",
"saveSetting": "",
"marginLeft": "",
"showBorder": "",
"saveAsImage": "",
"printerMarginLeft": "",
"dimensions": "",
"defaultSettings": "",
"print": ""
},
"qrcode": {
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
},
"showContent": "",
"showQRCodeMode": {
"withIcon": "",
"no": "",
"simple": ""
},
"templateHelp": "",
"textSize": "",
"showQRCode": "",
"template": "",
"title": "",
"button": ""
},
"spoolSelect": {
"selectedTotal_one": "",
"selectedTotal_few": "",
"selectedTotal_other": "",
"description": "",
"showArchived": "",
"title": "",
"selectAll": "",
"noSpoolsSelected": ""
}
},
"settings": {
"extra_fields": {
"non_unique_key_error": "",
"params": {
"unit": "",
"choices": "",
"default_value": "",
"field_type": "",
"multi_choice": "",
"order": "",
"name": "",
"key": ""
},
"boolean_true": "",
"choices_missing_error": "",
"boolean_false": "",
"delete_confirm_description": "",
"field_type": {
"boolean": "",
"float": "",
"choice": "",
"float_range": "",
"datetime": "",
"integer": "",
"integer_range": "",
"text": ""
},
"tab": "",
"description": "",
"delete_confirm": "",
"key_not_changed": ""
},
"general": {
"tab": "",
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
},
"currency": {
"label": ""
}
},
"header": "",
"settings": ""
},
"home": {
"welcome": "",
"home": "",
"description": ""
},
"scanner": {
"title": "",
"error": {
"insecureContext": "",
"notReadable": "",
"notAllowed": "",
"unknown": "",
"notFound": "",
"streamApiNotSupported": ""
},
"description": ""
},
"help": {
"resources": {
"vendor": "",
"filament": "",
"spool": ""
},
"description": "",
"help": ""
},
"no": "",
"unknown": "",
"yes": "",
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
},
"version": "",
"kofi": "",
"table": {
"actions": ""
},
"warnWhenUnsavedChanges": "",
"loading": "",
"dashboard": {
"title": ""
},
"tags": {
"clone": ""
}
}
+409
View File
@@ -0,0 +1,409 @@
{
"actions": {
"list": "Seznam",
"create": "Ustvari",
"edit": "Uredi",
"show": "Prikaži",
"clone": "Podvoji"
},
"buttons": {
"create": "Ustvari",
"save": "Shrani",
"saveAndAdd": "Shrani in dodaj",
"logout": "Odjava",
"delete": "Izbriši",
"edit": "Uredi",
"cancel": "Prekliči",
"confirm": "Ste prepričani?",
"continue": "Nadaljuj",
"filter": "Filter",
"clear": "Počisti",
"refresh": "Osveži",
"show": "Prikaži",
"undo": "Razveljavi",
"import": "Uvozi",
"clone": "Podvoji",
"archive": "Arhiviraj",
"unArchive": "Odstrani iz arhiva",
"hideArchived": "Skrij arhiv",
"showArchived": "Prikaži arhiv",
"notAccessTitle": "Nimate dovoljenja za dostop",
"hideColumns": "Skrij stolpce",
"clearFilters": "Počisti filtre"
},
"notifications": {
"saveSuccessful": "Uspešno shranjeno!",
"success": "Uspešno",
"error": "Napaka (status code: {{statusCode}})",
"undoable": "Imate {{seconds}} s časa za preklic.",
"createSuccess": "Uspešno ustvarjeno {{resource}}",
"createError": "Pri ustvarjanju {{resource}} je prišlo do napake (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} uspešno izbrisano",
"deleteError": "Napaka pri brisanju {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} uspešno urejeno",
"editError": "Napaka pri urejanju {{resource}} (status code:{{statusCode}})",
"importProgress": "Uvažanje: {{processed}}/{{total}}",
"validationError": "Napaka overjanja: {{error}}"
},
"loading": "Nalaganje",
"version": "Verzija",
"unknown": "Neznano",
"yes": "Da",
"no": "Ne",
"tags": {
"clone": "Podvoji"
},
"dashboard": {
"title": "Nadzorna plošča"
},
"printing": {
"generic": {
"title": "Tiskanje",
"print": "Natisni",
"columns": "Stolpci",
"rows": "Vrste",
"paperSize": "Velikost papirja",
"customSize": "Po meri",
"dimensions": "Dimenzije",
"showBorder": "Prikaži obrobo",
"layoutSettings": "Nastavitve postavitve",
"contentSettings": "Nastavitve vsebine",
"description": "Prilagodite spodnje nastavitve, da dobite želeno postavitev tiskanja. Upoštevajte, da lahko tiskalniki in vaš operacijski sistem uporabijo svoje robove in merilo, zato boste morda morali nekajkrat poskusiti, preden bo vse pravilno. Preizkusite to na navadnem listu papirja, preden natisnete na dejanske nalepke.",
"helpMargin": "Robove je treba nastaviti tako, da ustrezajo papirju za nalepke in tiskalniku; njihova sprememba bo vplivala na velikost celotne mreže.",
"helpPrinterMargin": "Varnostno območje je treba nastaviti glede na to, kako blizu roba papirja lahko tiskalnik tiska; sprememba teh nastavitev ne bo vplivala na celotno mrežo.",
"previewScale": "Merilo predogleda",
"skipItems": "Preskoči elemente",
"itemCopies": "Kopije elementov",
"horizontalSpacing": "Vodoraven razmik",
"verticalSpacing": "Navpičen razmik",
"marginLeft": "Lev rob",
"marginRight": "Desen rob",
"marginTop": "Zgornji rob",
"marginBottom": "Spodnji rob",
"printerMarginLeft": "Varno območje levo",
"printerMarginRight": "Varno območje desno",
"printerMarginTop": "Varno območje zgoraj",
"printerMarginBottom": "Varno območje spodaj",
"borders": {
"none": "Brez",
"border": "Obroba",
"grid": "Mreža"
},
"settings": "Prednastavitve",
"defaultSettings": "Privzeto",
"addSettings": "Dodaj novo prednastavitev",
"newSetting": "Nov",
"duplicateSettings": "Podvoji",
"deleteSettings": "Izbriši trenutno prednastavitev",
"deleteSettingsConfirm": "Ali ste prepričani, da želite izbrisati to prednastavitev?",
"settingsName": "Ime prednastavitve",
"saveSetting": "Shrani prednastavitve",
"saveAsImage": "Shrani kot sliko"
},
"qrcode": {
"button": "Natisni nalepke",
"title": "Tiskanje nalepk",
"template": "Predloga nalepke",
"templateHelp": "Uporabite {}, da vstavite vrednosti objekta kolut kot besedilo. Na primer, {id} bo nadomeščen z identifikacijsko številko koluta, {filament.material} pa z materialom koluta. Če vrednost manjka, bo nadomeščena z znakom »?«. To lahko odstranite z dodatnim nizom {}. Poleg tega bo vsak tekst med sklopi {} odstranjen, če vrednost manjka. Na primer, {Lot Nr: {lot_nr}} bo prikazal etiketo le, če ima kolut številko serije. Tekst obdajte z dvojno zvezdico **, da bo poudarjen. Kliknite gumb, da si ogledate seznam vseh razpoložljivih oznak.",
"showContent": "Natisni nalepko",
"textSize": "Velikost besedila na nalepki",
"useHTTPUrl": {
"label": "Povezava QR kode",
"options": {
"default": "Privzeto",
"url": "URL"
},
"preview": "Predogled:",
"tooltip": "Uporabljena bo lastniška povezava, ki bo delovala le, če jo skenirate s Kolutarjevo funkcijo skeniranja (privzeto). URL uporablja osnovni URL, določen v nastavitvah, ali pa URL trenutne strani, če ta ni nastavljen."
},
"showQRCode": "Natisni QR kodo",
"showQRCodeMode": {
"no": "Ne",
"simple": "Preprosto",
"withIcon": "Z ikono"
}
},
"spoolSelect": {
"description": "Izberite kolute, za katere želite natisniti nalepke.",
"title": "Izberite kolute",
"showArchived": "Prikaži arhiv",
"noSpoolsSelected": "Niste izbrali kolutov.",
"selectedTotal_one": "Izbran {{count}} kolut",
"selectedTotal_two": "Izbrana {{count}} koluta",
"selectedTotal_few": "Izbrani {{count}} koluti",
"selectedTotal_other": "Izbranih {{count}} kolutov",
"selectAll": "Izberi/Odstrani izbiro vseh"
}
},
"warnWhenUnsavedChanges": "Ali ste prepričani, da želite zapustiti stran? Imate ne shranjene spremembe.",
"kofi": "Prispevajte na Ko-fi",
"spool": {
"form": {
"measurement_type_label": "Vrsta meritve",
"measurement_type": {
"length": "Dolžina",
"weight": "Masa"
},
"adjust_filament_value": "Porabljena količina",
"new_location_prompt": "Vnesite novo lokacijo",
"adjust_filament_help": "Tukaj lahko neposredno odvzemate ali dodajate filament na kolut. Pozitivna vrednost pomeni porabo filamenta, negativna vrednost pa njegovo dodajanje.",
"spool_updated": "Ta kolut je spremenil nekdo drug, odkar ste odprli stran. Shranjevanje bo te spremembe prepisalo!"
},
"spool": "Koluti",
"fields": {
"id": "ID",
"filament_name": "Filament",
"filament": "Filament",
"filament_internal": "Notranje",
"filament_external": "Zunanje",
"price": "Cena",
"material": "Material",
"weight_to_use": "Masa",
"used_weight": "Porabljena masa",
"remaining_weight": "Preostala masa",
"measured_weight": "Izmerjena masa",
"used_length": "Porabljena dolžina",
"remaining_length": "Preostala dolžina",
"initial_weight": "Začetna masa",
"spool_weight": "Prazna masa",
"location": "Lokacija",
"lot_nr": "Št. serije",
"first_used": "Prvič uporabljeno",
"last_used": "Zadnjič uporabljeno",
"registered": "Registrirano",
"comment": "Komentar",
"archived": "Arhivirano"
},
"fields_help": {
"price": "Cena polnega koluta. Če ta vrednost ni nastavljena, se namesto nje upošteva cena filamenta.",
"weight_to_use": "Izberite, katero vrednost mase želite vnesti. Izmerjena masa je na voljo le, če je za izbran filament nastavljena masa koluta.",
"used_weight": "Koliko filamenta je bilo porabljenega s koluta. Pri novem kolutu je vrednost 0 g.",
"remaining_weight": "Koliko filamenta je še na kolutu. Pri novem kolutu se ta količina ujema z maso koluta.",
"measured_weight": "Koliko tehtata filament in kolut.",
"initial_weight": "Začetna masa filamenta na kolutu (neto masa). Če ta vrednost ni nastavljena, se uporabi masa iz objekta filamenta.",
"spool_weight": "Masa praznega koluta. Pustite prazno, če želite uporabiti vrednost, navedeno pri filamentu ali pri proizvajalcu.",
"location": "Kje se nahaja kolut, če imate več mest za shranjevanje.",
"lot_nr": "Številka serije proizvajalca. Uporablja se za zagotavljanje barvne doslednosti tiska, če se uporablja več kolutov.",
"external_filament": "Izbrali ste filament iz zunanje zbirke podatkov. Ob ustvarjanju koluta se bo samodejno ustvaril objekt filamenta (in morebiti tudi objekt proizvajalca). Če ste za ta filament že ustvarili objekt filamenta, lahko to povzroči nastanek podvojenih objektov filamenta."
},
"titles": {
"create": "Ustvari kolut",
"clone": "Podvoji kolut",
"edit": "Uredi kolut",
"list": "Koluti",
"show": "Prikaži kolut",
"show_title": "[Kolut #{{id}}] {{name}}",
"archive": "Arhiviraj kolut",
"adjust": "Prilagodi filament koluta"
},
"messages": {
"archive": "Ali res želite arhivirati ta kolut?"
},
"formats": {
"last_used": "Nazadnje uporabljeno: {{date}}"
}
},
"settings": {
"general": {
"currency": {
"label": "Valuta"
},
"base_url": {
"label": "Osnovni URL",
"tooltip": "Osnoven URL, ki se uporablja pri ustvarjanju elementov, kot npr. QR-kode."
},
"round_prices": {
"label": "Zaokrožene cene",
"tooltip": "Zaokroži cene na najbližje celo število."
},
"tab": "Splošno"
},
"settings": "Nastavitve",
"header": "Nastavitve",
"extra_fields": {
"tab": "Dodatna polja",
"params": {
"key": "Ključ",
"name": "Ime",
"field_type": "Tip",
"unit": "Enota",
"order": "Zaporedje",
"default_value": "Privzeta vrednost",
"choices": "Izbire",
"multi_choice": "Več izbir"
},
"field_type": {
"text": "Besedilo",
"integer": "Celo število",
"integer_range": "Območje celih števil",
"float": "Decimalno število",
"float_range": "Območje decimalnih števil",
"datetime": "Datum in čas",
"boolean": "Logična vrednost",
"choice": "Izbira"
},
"boolean_true": "Da",
"boolean_false": "Ne",
"non_unique_key_error": "Ključ mora biti edinstven.",
"key_not_changed": "Spremenite geslo v nekaj drugega.",
"delete_confirm": "Zbrišem polje {{name}}?",
"delete_confirm_description": "S tem boste izbrisali polje in vse povezane podatke za vse vnose.",
"description": "<p>Tukaj lahko svojim entitetam dodate dodatna polja po meri.</p><p>Ko je polje dodano, njegovega ključa ali tipa ni mogoče spremeniti, pri poljih tipa »izbira« pa ni mogoče odstraniti možnosti ali spremeniti nastavitve za večkratno izbiro. Če polje odstranite, bodo povezani podatki za vse entitete izbrisani.</p><p>Ključ je tisto, kar drugi programi uporabljajo za branje/pisanje podatkov, zato če naj bi se vaše polje po meri integriralo s programom tretje osebe, poskrbite, da ga nastavite pravilno. Privzeta vrednost se uporablja le za nove elemente.</p><p>V preglednicah je mogoče prikazati in razvrstiti dodatna polja. Polja tipa »izbira« in »booleanska« tam omogočajo tudi možnosti filtriranja, vsi tipi dodatnih polj pa podpirajo filtriranje po praznih vrednostih.</p>",
"choices_missing_error": "Možnosti ni mogoče odstraniti. Manjkajo naslednje možnosti: {{choices}}",
"choices_reorder_hint": "Za spremembo vrstnega reda izbire, povlecite ali uporabite smerne tipke."
}
},
"scanner": {
"title": "Čitalnik QR kode",
"description": "Preberite Kolutarjevo QR kodo za ogled podrobnosti o kolutu.",
"error": {
"notAllowed": "Niste omogočili dostopa do kamere.",
"insecureContext": "Stran ni dostopna prek protokola HTTPS.",
"streamApiNotSupported": "Brskalnik ne podpira MediaStream API.",
"notReadable": "Kamere ni mogoče prebrati.",
"notFound": "Kamere ni bilo mogoče najti.",
"unknown": "Prišlo je do neznane napake. ({{error}})"
}
},
"filament": {
"filament": "Filamenti",
"fields": {
"id": "ID",
"vendor_name": "Proizvajalec",
"vendor": "Proizvajalec",
"name": "Ime",
"material": "Material",
"price": "Cena",
"density": "Gostota",
"diameter": "Premer",
"weight": "Masa",
"spool_weight": "Masa koluta",
"article_number": "Številka izdelka",
"registered": "Registrirano",
"comment": "Komentar",
"settings_extruder_temp": "Temperatura ekstruderja",
"settings_bed_temp": "Temperatura mize",
"color_hex": "Barva",
"single_color": "Enobarvno",
"multi_color": "Večbarvno",
"external_id": "Zunanji ID",
"spools": "Prikaži kolute",
"longitudinal": "Vzdolžno",
"coaxial": "Soosno"
},
"fields_help": {
"material": "Npr. PLA, ABS, PETG itd.",
"price": "Cena polnega koluta.",
"article_number": "Npr. EAN, UPC itd.",
"name": "Ime filamenta, ki omogoča razlikovanje tega tipa filamenta od drugih izdelkov istega proizvajalca. Vsebovati mora, na primer, barvo.",
"weight": "Masa filamenta na polnem kolutu (neto masa). V to se ne sme vključiti mase samega koluta, temveč le masa filamenta. To je običajno navedeno na embalaži.",
"spool_weight": "Masa praznega koluta. Uporablja se za določitev izmerjene mase koluta.",
"multi_color_direction": "Filamenti imajo lahko več barv na dva načina: bodisi s sočasnim iztiskom, kot na primer dvobarvni filamenti z enakomerno porazdeljenimi večbarvnimi odtenki, bodisi z vzdolžnimi barvnimi prehodi, kot na primer filamenti z barvnim prehodom, pri katerih se barva spreminja po dolžini koluta."
},
"titles": {
"create": "Ustvari filament",
"clone": "Podvoji filament",
"edit": "Uredi filament",
"list": "Filamenti",
"show": "Prikaži filament",
"show_title": "[Filament #{{id}}]{{name}}"
},
"form": {
"import_external": "Uvozi iz zunanjih",
"filament_updated": "Ta filament je spremenil nekdo drug, odkar ste odprli stran. Shranjevanje bo te spremembe prepisalo!",
"import_external_description": "Izberite filament s seznama, da se njegovi podatki samodejno vpišejo v obrazec za ustvarjanje filamenta. S tem se bodo nadomestili vsi podatki, ki ste jih vnesli v obrazec.<br><br>Ob kliku na »V redu« se bo po potrebi samodejno ustvaril objekt »Proizvajalec«."
},
"buttons": {
"add_spool": "Dodaj kolut"
}
},
"vendor": {
"vendor": "Proizvajalci",
"fields": {
"id": "ID",
"name": "Ime",
"empty_spool_weight": "Masa praznega koluta",
"external_id": "Zunanji ID",
"registered": "Registrirano",
"comment": "Komentar"
},
"titles": {
"create": "Ustvari proizvajalca",
"clone": "Podvoji proizvajalca",
"edit": "Uredi proizvajalca",
"list": "Proizvajalci",
"show": "Prikaži proizvajalca",
"show_title": "[Proizvajalec #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Ta proizvajalec je spremenil nekdo drug, odkar ste odprli stran. Shranjevanje bo te spremembe prepisalo!"
},
"fields_help": {
"empty_spool_weight": "Masa praznega koluta tega proizvajalca."
}
},
"home": {
"home": "Dom",
"welcome": "Dobrodošli v Kolutarju!",
"description": "Izgleda, da še niste dodali nobenih kolutov. Za pomoč pri začetku si oglejte <helpPageLink>pomoč</helpPageLink>."
},
"help": {
"help": "Pomoč",
"resources": {
"vendor": "Podjetja, ki proizvajajo filament.",
"filament": "Znamke filamentov. Imajo lastnosti, kot so ime, material, barva, premer in drugo.",
"spool": "Posamezni fizični koluti določenega filamenta."
},
"description": "<title>Pomoč</title><p>Tukaj je nekaj nasvetov za začetek.</p><p>Kolutar hrani 3 različne vrste podatkov:</p><itemsHelp/><p>Če želite v bazo podatkov dodati nov kolut, najprej ustvarite objekt <filamentCreateLink>Filament</filamentCreateLink>. Ko to storite, nadaljujte z ustvarjanjem objekta <spoolCreateLink>Kolut</spoolCreateLink> za ta določen kolut. Če kasneje pridobite dodatne kolute istega filamenta, preprosto ustvarite dodatne kolutne objekte in ponovno uporabite isti filamentni objekt.</p><p>Po želji lahko ustvarite tudi objekt <vendorCreateLink>Proizvajalec</vendorCreateLink> za podjetje, ki proizvaja filament, če želite slediti tem informacijam. </p><p>Imate možnost povezati druge storitve za 3D-tiskalnike s Kolutarjem, na primer Moonraker, ki lahko samodejno spremlja porabo filamenta in posodablja objekte koluta za vas. Navodila za nastavitev najdete v Kolutarjevi <readmeLink>README</readmeLink> datoteki.</p>"
},
"table": {
"actions": "Dejanja"
},
"documentTitle": {
"default": "Kolutar",
"suffix": " | Kolutar",
"home": {
"list": "Dom | Kolutar"
},
"help": {
"list": "Pomoč | Kolutar"
},
"locations": {
"list": "Lokacije | Kolutar"
},
"filament": {
"list": "Filamenti | Kolutar",
"show": "Prikaži filament #{{id}} | Kolutar",
"edit": "Uredi filament #{{id}} | Kolutar",
"create": "Ustvari filament | Kolutar",
"clone": "Podvoji filament #{{id}} | Kolutar"
},
"spool": {
"list": "Koluti | Kolutar",
"show": "Prikaži kolut #{{id}} | Kolutar",
"edit": "Uredi kolut #{{id}} | Kolutar",
"create": "Ustvari kolut | Kolutar",
"clone": "Podvoji kolut #{{id}} | Kolutar"
},
"vendor": {
"list": "Proizvajalci | Kolutar",
"show": "Prikaži proizvajalca #{{id}} | Kolutar",
"edit": "Uredi proizvajalca #{{id}} | Kolutar",
"create": "Ustvari proizvajalca | Kolutar",
"clone": "Podvoji proizvajalca #{{id}} | Kolutar"
}
},
"locations": {
"locations": "Lokacije",
"new_location": "Nova lokacija",
"no_location": "Brez lokacije",
"no_locations_help": "Na tej strani lahko kolute razvrstite po lokacijah. Za začetek dodajte nekaj kolutov."
},
"sponsor": "Sponzorirajte me na GitHubu",
"theme": {
"label": "Tema",
"system": "Sistem",
"light": "Svetla",
"dark": "Temna"
}
}
+153 -39
View File
@@ -28,7 +28,8 @@
"notAccessTitle": "Du har inte tillgång till denna sida",
"hideColumns": "Dölj kolumner",
"clearFilters": "Rensa filter",
"saveAndAdd": "Spara och Lägg till"
"saveAndAdd": "Spara och Lägg till",
"continue": "Fortsätt"
},
"warnWhenUnsavedChanges": "Är du säker på att du vill lämna sidan? Du har osparade ändringar.",
"notifications": {
@@ -42,7 +43,8 @@
"editSuccess": "Lyckades ändra {{resource}}",
"editError": "Det gick inte att ändra {{resource}} (statuskod: {{statusCode}})",
"importProgress": "Importerar: {{processed}}/{{total}}",
"saveSuccessful": "Sparning lyckades!"
"saveSuccessful": "Sparning lyckades!",
"validationError": "Valideringsfel: {{error}}"
},
"loading": "Laddar",
"version": "Version",
@@ -86,29 +88,57 @@
"none": "Ingen",
"border": "Ram",
"grid": "Rutnät"
}
},
"defaultSettings": "Standard",
"newSetting": "Ny",
"settingsName": "Inställningsnamn",
"deleteSettingsConfirm": "Är du säker på att du vill radera den nuvarande inställningen?",
"itemCopies": "Kopior",
"settings": "Inställningar",
"addSettings": "Skapa inställning",
"saveSetting": "Spara inställningar",
"duplicateSettings": "Duplicera",
"deleteSettings": "Radera nuvarande inställning",
"saveAsImage": "Spara som bild"
},
"qrcode": {
"button": "Skriv ut QR-koder",
"title": "QR-kod utskrift",
"button": "Skriv ut etiketter",
"title": "Etikettutskrift",
"spoolWeight": "Spolvikt: {{weight}}",
"lotNr": "Batch: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Textstorlek",
"showSpoolmanIcon": "Visa Spoolman-ikon",
"showContent": "Etikett",
"showVendor": "Leverantör",
"showContent": "Visa text",
"showVendor": "Tillverkare",
"showLotNr": "Batchnummer",
"showSpoolWeight": "Spolvikt",
"showTemperatures": "Temperaturer",
"showSpoolComment": "Spolkommentar",
"showFilamentComment": "Filamentkommentar",
"showVendorComment": "Leverantörskommentar"
"showVendorComment": "Tillverkarkommentar",
"template": "Textmall",
"templateHelp": "Använd {} för att infoga värden från spolobjektet som text. Till exempel kommer {id} att ersättas med spolens id, eller {filament.material} att ersättas med filamentets material. Omslut text med dubbla asterisker ** för att göra den fetstilad. Klicka på knappen för att visa en lista över alla tillgängliga taggar.",
"showQRCode": "Visa QR-kod",
"showQRCodeMode": {
"no": "Nej",
"simple": "Enkel",
"withIcon": "Med logga"
},
"useHTTPUrl": {
"options": {
"default": "",
"url": ""
},
"preview": "",
"tooltip": "",
"label": ""
}
},
"spoolSelect": {
"title": "Välj spolar",
"description": "Välj spolar att skriva ut QR-koder för.",
"description": "Välj spolar att skriva ut etiketter för.",
"showArchived": "Tillåt arkiverade",
"noSpoolsSelected": "Du har inte valt några spolar.",
"selectAll": "Markera/avmarkera alla",
@@ -147,7 +177,12 @@
"comment": "Kommentar",
"archived": "Arkiverad",
"measured_weight": "Uppmätt vikt",
"weight_to_use": "Vikt"
"weight_to_use": "Vikt",
"price": "Pris",
"initial_weight": "Initial Vikt",
"spool_weight": "Tom Vikt",
"filament_internal": "Interna",
"filament_external": "Externa"
},
"fields_help": {
"used_weight": "Hur mycket filament som använts på spolen. En ny spole har värdet 0g.",
@@ -155,7 +190,11 @@
"lot_nr": "Tillverkarens batchnummer. Kan användas för att se till att en utskrift har jämn färg ifall flera spolar används.",
"weight_to_use": "Välj vilken typ av vikt du vill fylla i. Uppmätt vikt är endast tillgänglig om det valda filamentet har en spolvikt satt.",
"measured_weight": "Hur mycket filamentet och spolen väger tillsammans.",
"remaining_weight": "Hur mycket filament som finns kvar på spolen. För en ny spole så är detta det värdet som står på etiketten."
"remaining_weight": "Hur mycket filament som finns kvar på spolen. För en ny spole så är detta det värdet som står på etiketten.",
"price": "Pris för en hel rulle. Om det inte är inställt kommer priset för filamentet istället att antas.",
"initial_weight": "Ursprungsvikt av filament på spolen (nettovikt). Om du inte sätter detta så tas vikten från filamentobjektet.",
"spool_weight": "Vikten på en tom spole. Lämna tom för att ta värdet från filament- eller tillverkarobjektet.",
"external_filament": "Du har valt ett filament från den externa databasen. Ett filamentobjekt (och eventuellt ett tillverkarobjekt) kommer att skapas automatiskt när du skapar denna spole. Detta kan skapa duplicerade filamentobjekt om du redan har skapat ett filamentobjekt för denna filament."
},
"titles": {
"create": "Skapa spole",
@@ -164,22 +203,33 @@
"list": "Spolar",
"show": "Visa spole",
"archive": "Arkivera spole",
"show_title": "[Spole #{{id}}] {{name}}"
"show_title": "[Spole #{{id}}] {{name}}",
"adjust": "Justera Spolmängd"
},
"messages": {
"archive": "Är du säker på att du vill arkivera denna spole?"
},
"form": {
"new_location_prompt": "Fyll i en ny plats",
"spool_updated": "Den här spolen har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"spool_updated": "Den här spolen har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!",
"measurement_type": {
"length": "Längd",
"weight": "Vikt"
},
"measurement_type_label": "Mättyp",
"adjust_filament_value": "Mängd att konsumera",
"adjust_filament_help": "Här kan du direkt lägga till eller subtrahera filament från spolen. Ett positivt värde kommer att konsumera filament, ett negativt värde kommer att lägga till det."
},
"formats": {
"last_used": "Senast använd {{date}}"
}
},
"filament": {
"filament": "Filament",
"fields": {
"id": "ID",
"vendor_name": "Leverantör",
"vendor": "Leverantör",
"vendor_name": "Tillverkare",
"vendor": "Tillverkare",
"name": "Namn",
"material": "Material",
"price": "Pris",
@@ -193,15 +243,21 @@
"settings_extruder_temp": "Extrudertemp",
"settings_bed_temp": "Bäddtemp",
"color_hex": "Färg",
"spools": "Visa spolar"
"spools": "Visa spolar",
"single_color": "Enkel",
"multi_color": "Flerfärgad",
"coaxial": "Coextruderad",
"longitudinal": "Longitudinell",
"external_id": "Externt ID"
},
"fields_help": {
"name": "Filamentets namn, för att skilja detta filament från andra från samma leverantör. Bör innehålla färgen till exempel.",
"name": "Filamentets namn, för att skilja detta filament från andra från samma tillverkare. Bör innehålla färgen till exempel.",
"material": "T.ex. PLA, ABS, PETG, etc.",
"price": "Pris för en full spole i systemets valuta.",
"price": "Pris för en full spole.",
"weight": "Vikten på en full spole (nettovikt). Detta ska inte inkludera vikten på spolen själv, bara filamentet. Det är vad som vanligtvis står på förpackningen.",
"spool_weight": "Vikten på en tom spole. Används för att beräkna filamentvikten från en vägning.",
"article_number": "T.ex. EAN, UPC, etc."
"article_number": "T.ex. EAN, UPC, etc.",
"multi_color_direction": "Filament kan ha flera färger på två sätt: antingen genom coextrudering, så som två-färgs filament med konsekventa multifärger, eller genom longitudinella färgförändringar, så som gradientfilament som skiftar färger längs spolen."
},
"titles": {
"create": "Skapa filament",
@@ -212,45 +268,94 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Det här filamentet har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"filament_updated": "Det här filamentet har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!",
"import_external": "Importera från externt",
"import_external_description": "Välj ett filament i listan för att automatiskt fylla dess detaljer i formuläret. Detta kommer att skriva över alla data som du har angett i formuläret.<br><br>Ett tillverkarobjekt skapas automatiskt när du klickar på Ok, om det behövs."
},
"buttons": {
"add_spool": "Skapa Spole"
}
},
"vendor": {
"vendor": "Leverantörer",
"vendor": "Tillverkare",
"fields": {
"id": "ID",
"name": "Namn",
"registered": "Registrerad",
"comment": "Kommentar"
"comment": "Kommentar",
"empty_spool_weight": "Tom spolvikt",
"external_id": "Externt ID"
},
"titles": {
"create": "Skapa leverantör",
"clone": "Klona leverantör",
"edit": "Ändra leverantör",
"list": "Leverantörer",
"show": "Visa leverantör",
"show_title": "[Leverantör #{{id}}] {{name}}"
"create": "Skapa tillverkare",
"clone": "Klona tillverkare",
"edit": "Ändra tillverkare",
"list": "Tillverkare",
"show": "Visa tillverkare",
"show_title": "[Tillverkare #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Den här leverantören har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"vendor_updated": "Den här tillverkaren har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
},
"fields_help": {
"empty_spool_weight": "Vikten av en tom spool från denna tillverkare."
}
},
"home": {
"home": "Hem"
"home": "Hem",
"welcome": "",
"description": ""
},
"table": {
"actions": "Åtgärder"
},
"settings": {
"settings": "Inställningar",
"header": "Inställningar",
"generic": {
"general": {
"tab": "Allmänt",
"currency": {
"label": "Valuta"
},
"base_url": {
"tooltip": "",
"label": ""
},
"round_prices": {
"label": "",
"tooltip": ""
}
},
"extra_fields": {
"tab": "Extra Fält"
"tab": "Extra Fält",
"description": "<p>Här kan du lägga till extra anpassade fält för dina objekt.</p><p>När ett fält har lagts till kan du inte ändra dess nyckel eller typ, och för fält av typen \"val\" kan du inte ta bort val eller ändra flervalsläget. Om du tar bort ett fält kommer den associerade datan för alla enheter att raderas.</p><p>Nyckeln är vad andra program läser/skriver datan som, så om ditt anpassade fält ska integreras med ett tredjepartsprogram, se till att ställa in det korrekt. Standardvärdet tillämpas endast på nya objekt.</p><p>Extra fält kan inte sorteras eller filtreras i tabellvyn.</p>",
"params": {
"unit": "Enhet",
"default_value": "Standardvärde",
"key": "Nyckel",
"field_type": "Typ",
"name": "Namn",
"choices": "Val",
"order": "Ordning",
"multi_choice": "Flerval"
},
"field_type": {
"integer_range": "Heltalsintervall",
"float": "Decimaltal",
"float_range": "Decimaltalsintervall",
"datetime": "Datum och tid",
"boolean": "Booleansk",
"choice": "Val",
"text": "Text",
"integer": "Heltal"
},
"boolean_true": "Ja",
"boolean_false": "Nej",
"choices_missing_error": "Du kan inte ta bort val. Följande val saknas: {{choices}}",
"delete_confirm_description": "Detta kommer att radera fältet och all tillhörande data för alla objekt.",
"delete_confirm": "Ta bort fältet {{name}}?",
"non_unique_key_error": "Nyckeln måste vara unik.",
"key_not_changed": "Vänligen ändra nyckeln till något annat."
}
},
"documentTitle": {
@@ -274,14 +379,17 @@
"clone": "#{{id}} Klona spole | Spoolman"
},
"vendor": {
"list": "Leverantörer | Spoolman",
"show": "#{{id}} Visa leverantör | Spoolman",
"edit": "#{{id}} Redigera leverantör | Spoolman",
"create": "Skapa leverantör | Spoolman",
"clone": "#{{id}} Klona leverantör | Spoolman"
"list": "Tillverkare | Spoolman",
"show": "#{{id}} Visa tillverkare | Spoolman",
"edit": "#{{id}} Redigera tillverkare | Spoolman",
"create": "Skapa tillverkare | Spoolman",
"clone": "#{{id}} Klona tillverkare | Spoolman"
},
"help": {
"list": "Hjälp | Spoolman"
},
"locations": {
"list": ""
}
},
"help": {
@@ -290,8 +398,14 @@
"filament": "Märken av filament. De har egenskaper som namn, material, färg, diameter och mer.",
"spool": "Individuella fysiska spolar av ett specifikt filament."
},
"description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Leverantörs</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>",
"description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Tillverkar</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>",
"help": "Hjälp"
},
"kofi": "Dricksa mig på Ko-fi"
"kofi": "Dricksa mig på Ko-fi",
"locations": {
"new_location": "Ny plats",
"no_location": "Ingen plats",
"locations": "",
"no_locations_help": ""
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"buttons": {
"hideArchived": "காப்பகப்படுத்தப்பட்டதை மறைக்கவும்",
"create": "உருவாக்கு",
"save": "சேமி",
"saveAndAdd": "சேமித்து சேர்க்கவும்",
"logout": "விடுபதிகை",
"delete": "நீக்கு",
"edit": "தொகு",
"cancel": "ரத்துசெய்",
"confirm": "நீங்கள் உறுதியாக இருக்கிறீர்களா?",
"continue": "தொடரவும்",
"filter": "வடிப்பி",
"clear": "தெளிவான",
"refresh": "புதுப்பிப்பு",
"show": "காட்டு",
"undo": "செயல்தவிர்",
"import": "இறக்குமதி",
"clone": "நகலி",
"archive": "காப்பகம்",
"unArchive": "அன்கான்",
"showArchived": "காப்பகப்படுத்தப்பட்டதைக் காட்டு",
"notAccessTitle": "அணுக உங்களுக்கு இசைவு இல்லை",
"hideColumns": "நெடுவரிசைகளை மறைக்கவும்",
"clearFilters": "தெளிவான வடிப்பான்கள்"
},
"printing": {
"generic": {
"itemCopies": "உருப்படி நகல்கள்",
"title": "அச்சிடுதல்",
"description": "விரும்பிய அச்சு தளவமைப்பைப் பெற கீழேயுள்ள அமைப்புகளில் டியூன் செய்யுங்கள். அச்சுப்பொறிகளும் உங்கள் ஓஎச்வும் அதன் சொந்த ஓரங்கள் மற்றும் அளவிடுதலைப் பயன்படுத்தலாம் என்பதை நினைவில் கொள்ளுங்கள், எனவே இவை அனைத்தும் சரியானதாக இருப்பதற்கு முன்பு நீங்கள் சில சோதனை மற்றும் பிழை செய்ய வேண்டியிருக்கும். உண்மையான லேபிள்களில் அச்சிடுவதற்கு முன் வழக்கமான காகிதத்தில் அதை சோதிக்கவும்.",
"helpMargin": "உங்கள் சிட்டை காகிதம் மற்றும் அச்சுப்பொறியுடன் பொருந்தக்கூடிய வகையில் விளிம்புகள் கட்டமைக்கப்பட வேண்டும், இவற்றை மாற்றுவது முழு கட்டத்தின் அளவையும் பாதிக்கும்.",
"helpPrinterMargin": "உங்கள் அச்சுப்பொறி எவ்வளவு நெருக்கமாக அச்சிட முடியும் என்பதற்கு பாதுகாப்பான-மண்டலத்தை அமைக்க வேண்டும், இவற்றை மாற்றுவது முழு கட்டத்தையும் பாதிக்காது.",
"print": "அச்சிடுக",
"columns": "நெடுவரிசைகள்",
"rows": "வரிசைகள்",
"paperSize": "காகித அளவு",
"customSize": "தனிப்பயன்",
"dimensions": "பரிமாணங்கள்",
"showBorder": "எல்லையைக் காட்டு",
"previewScale": "முன்னோட்டம் அளவு",
"skipItems": "உருப்படிகளைத் தவிர்க்கவும்",
"contentSettings": "உள்ளடக்க அமைப்புகள்",
"layoutSettings": "தளவமைப்பு அமைப்புகள்",
"horizontalSpacing": "கிடைமட்ட இடைவெளி",
"verticalSpacing": "செங்குத்து இடைவெளி",
"marginLeft": "இடது விளிம்பு",
"marginRight": "வலது விளிம்பு",
"marginTop": "மேல் விளிம்பு",
"marginBottom": "கீழ் விளிம்பு",
"printerMarginLeft": "பாதுகாப்பான மண்டலம் இடது",
"printerMarginRight": "பாதுகாப்பான-மண்டல உரிமை",
"printerMarginTop": "பாதுகாப்பான-மண்டல மேல்",
"printerMarginBottom": "பாதுகாப்பான-மண்டல கீழே",
"borders": {
"none": "எதுவுமில்லை",
"border": "எல்லை",
"grid": "வலைவாய்"
},
"settings": "முன்னமைவுகள்",
"defaultSettings": "இயல்புநிலை",
"addSettings": "புதிய முன்னமைவைச் சேர்க்கவும்",
"newSetting": "புதிய",
"duplicateSettings": "நகல்",
"deleteSettings": "தற்போதைய முன்னமைவை நீக்கு",
"deleteSettingsConfirm": "இந்த முன்னமைவை நீக்க விரும்புகிறீர்களா?",
"settingsName": "முன்னமைக்கப்பட்ட பெயர்",
"saveAsImage": "படமாக சேமிக்கவும்",
"saveSetting": "முன்னமைவுகளை சேமிக்கவும்"
},
"qrcode": {
"button": "லேபிள்களை அச்சிடுக",
"title": "சிட்டை அச்சிடுதல்",
"template": "சிட்டை வார்ப்புரு",
"templateHelp": "ச்பூல் பொருளின் மதிப்புகளை உரையாக செருக {} ஐப் பயன்படுத்தவும். எடுத்துக்காட்டாக, {id} ச்பூல் ஐடியுடன் மாற்றப்படும், அல்லது {filament.material} ச்பூலின் பொருள் மூலம் மாற்றப்படும். ஒரு மதிப்பு காணவில்லை என்றால் அது \"?\" இதை அகற்ற {of இன் இரண்டாவது தொகுப்பு பயன்படுத்தப்படலாம். கூடுதலாக, மதிப்பு காணவில்லை எனில் {of இன் தொகுப்புகளுக்கு இடையில் எந்த உரையும் அகற்றப்படும். எடுத்துக்காட்டாக, {lot nr: {lot_nr} எச்பி ச்பூலில் நிறைய எண் இருந்தால் மட்டுமே லேபிளைக் காண்பிக்கும். தைரியமாக மாற்ற இரட்டை ஆச்டெரிக்ச் ** உடன் உரையை இணைக்கவும். கிடைக்கக்கூடிய அனைத்து குறிச்சொற்களின் பட்டியலையும் காண பொத்தானைக் சொடுக்கு செய்க.",
"textSize": "உரை அளவு சிட்டை",
"showContent": "சிட்டை அச்சு",
"useHTTPUrl": {
"label": "QR குறியீடு இணைப்பு",
"tooltip": "ச்பூல்மேனின் ச்கேனிங் அம்சத்திலிருந்து (இயல்புநிலை) ச்கேன் செய்யப்பட்டால் மட்டுமே செயல்படும் தனியுரிம இணைப்பைப் பயன்படுத்தும். அமைப்புகளில் குறிப்பிடப்பட்டுள்ள அடிப்படை முகவரி அல்லது தற்போதைய பக்க முகவரி ஐ அமைக்கவில்லை என்றால் முகவரி பயன்படுத்துகிறது.",
"options": {
"default": "இயல்புநிலை",
"url": "முகவரி"
},
"preview": "முன்னோட்டம்:"
},
"showQRCode": "QR குறியீட்டை அச்சிடுக",
"showQRCodeMode": {
"no": "இல்லை",
"simple": "எளிய",
"withIcon": "ஐகானுடன்"
}
},
"spoolSelect": {
"title": "ச்பூல்களைத் தேர்ந்தெடுக்கவும்",
"description": "லேபிள்களை அச்சிட ச்பூல்களைத் தேர்ந்தெடுக்கவும்.",
"showArchived": "காப்பகப்படுத்தப்பட்டதைக் காட்டு",
"noSpoolsSelected": "நீங்கள் எந்த ச்பூல்களையும் தேர்வு செய்யவில்லை.",
"selectAll": "அனைத்தையும் தேர்ந்தெடு/தேர்வுநீக்கு",
"selectedTotal_one": "{{count}} ச்பூல் தேர்ந்தெடுக்கப்பட்டது",
"selectedTotal_other": "{{count}} spools தேர்ந்தெடுக்கப்பட்டவை"
}
},
"spool": {
"titles": {
"create": "ச்பூலை உருவாக்கவும்",
"clone": "நகலி ச்பூல்",
"edit": "திருத்து ச்பூல்",
"list": "ச்பூல்கள்",
"show": "ச்பூல் காட்டு",
"show_title": "[ச்பூல் #{{id}}] {{name}}",
"archive": "காப்பக ச்பூல்",
"adjust": "ச்பூல் இழை சரிசெய்யவும்"
},
"messages": {
"archive": "இந்த ச்பூலை காப்பகப்படுத்த விரும்புகிறீர்களா?"
},
"spool": "ச்பூல்கள்",
"fields": {
"id": "ஐடி",
"filament_name": "தாள்",
"filament": "தாள்",
"remaining_weight": "மீதமுள்ள எடை",
"filament_internal": "உள்",
"filament_external": "வெளிப்புறம்",
"price": "விலை",
"material": "பொருள்",
"weight_to_use": "எடை",
"used_weight": "பயன்படுத்தப்பட்ட எடை",
"measured_weight": "அளவிடப்பட்ட எடை",
"used_length": "பயன்படுத்தப்பட்ட நீளம்",
"remaining_length": "மீதமுள்ள நீளம்",
"initial_weight": "தொடக்க எடை",
"spool_weight": "வெற்று எடை",
"location": "இடம்",
"lot_nr": "நிறைய இல்லை",
"first_used": "முதலில் பயன்படுத்தப்பட்டது",
"last_used": "கடைசியாக பயன்படுத்தப்பட்டது",
"registered": "பதிவுசெய்யப்பட்டது",
"comment": "கருத்து",
"archived": "காப்பகப்படுத்தப்பட்டது"
},
"fields_help": {
"price": "ஒரு முழு ச்பூலின் விலை. அமைக்கப்படாவிட்டால், அதற்கு பதிலாக இழைகளின் விலை ஏற்றுக்கொள்ளப்படும்.",
"weight_to_use": "என்ன எடை மதிப்பை உள்ளிட வேண்டும் என்பதைத் தேர்ந்தெடுக்கவும். தேர்ந்தெடுக்கப்பட்ட இழைக்கு ச்பூல் எடை அமைக்கப்பட்டால் மட்டுமே அளவிடப்பட்ட எடை கிடைக்கும்.",
"remaining_weight": "ச்பூலில் எவ்வளவு இழை விடப்பட்டுள்ளது. ஒரு புதிய ச்பூலுக்கு இது ச்பூல் எடையுடன் பொருந்த வேண்டும்.",
"used_weight": "ச்பூலில் இருந்து எவ்வளவு இழை பயன்படுத்தப்பட்டுள்ளது. ஒரு புதிய ச்பூல் 0 ஐயா பயன்படுத்தப்பட வேண்டும்.",
"measured_weight": "இழை மற்றும் ச்பூல் எவ்வளவு எடை கொண்டது.",
"initial_weight": "ச்பூலில் (நிகர எடை) இழைகளின் ஆரம்ப எடை. அமைக்காவிட்டால் இழை பொருளிலிருந்து எடையைப் பயன்படுத்தும்.",
"spool_weight": "ச்பூலின் காலியாக இருக்கும்போது அது எடை. இழை அல்லது உற்பத்தியாளரிடமிருந்து மதிப்பைப் பயன்படுத்த காலியாக விடவும்.",
"location": "உங்கள் ச்பூல்களை சேமிக்கும் பல இடங்கள் இருந்தால் ச்பூல் அமைந்துள்ள இடத்தில்.",
"lot_nr": "உற்பத்தியாளரின் நிறைய எண். பல ச்பூல்கள் பயன்படுத்தப்பட்டால் அச்சுக்கு நிலையான வண்ணம் இருப்பதை உறுதிப்படுத்த பயன்படுத்தலாம்.",
"external_filament": "வெளிப்புற தரவுத்தளத்திலிருந்து ஒரு இழை தேர்வு செய்துள்ளீர்கள். இந்த ச்பூலை நீங்கள் உருவாக்கும்போது ஒரு இழை பொருள் (மற்றும் ஒரு உற்பத்தியாளர் பொருள்) தானாக உருவாக்கப்படும். இந்த இழைக்கு நீங்கள் ஏற்கனவே ஒரு இழை பொருளை உருவாக்கியிருந்தால் இது நகல் இழை பொருள்களை உருவாக்க முடியும்."
},
"form": {
"measurement_type": {
"length": "நீளம்",
"weight": "எடை"
},
"measurement_type_label": "அளவீட்டு வகை",
"adjust_filament_help": "இங்கே நீங்கள் நேரடியாக ச்பூலில் இருந்து இழைகளைச் சேர்க்கலாம் அல்லது கழிக்கலாம். நேர்மறையான மதிப்பு இழையை நுகரும், எதிர்மறை மதிப்பு அதைச் சேர்க்கும்.",
"adjust_filament_value": "தொகையை உட்கொள்ளுங்கள்",
"new_location_prompt": "புதிய இடத்தை உள்ளிடவும்",
"spool_updated": "இந்த பக்கத்தைத் திறந்ததிலிருந்து இந்த ச்பூல் யாரோ/வேறு ஏதாவது புதுப்பிக்கப்பட்டுள்ளது. சேமிப்பது அந்த மாற்றங்களை மேலெழுதும்!"
},
"formats": {
"last_used": "கடைசியாக பயன்படுத்தப்பட்ட {{date}}"
}
},
"filament": {
"fields": {
"coaxial": "இணை",
"longitudinal": "நீளமான",
"external_id": "வெளிப்புற ஐடி",
"id": "ஐடி",
"vendor_name": "உற்பத்தியாளர்",
"vendor": "உற்பத்தியாளர்",
"name": "பெயர்",
"material": "பொருள்",
"price": "விலை",
"density": "அடர்த்தி",
"diameter": "விட்டம்",
"weight": "எடை",
"spool_weight": "ச்பூல் எடை",
"article_number": "கட்டுரை எண்",
"registered": "பதிவுசெய்யப்பட்டது",
"comment": "கருத்து",
"settings_extruder_temp": "எக்ச்ட்ரூடர் தற்காலிக",
"settings_bed_temp": "படுக்கை தற்காலிக",
"color_hex": "நிறம்",
"single_color": "ஒற்றை",
"multi_color": "பல",
"spools": "ச்பூல்களைக் காட்டு"
},
"fields_help": {
"article_number": "எ.கா. EAN, UPC, முதலியன.",
"material": "எ.கா. பி.எல்.ஏ, ஏபிஎச், பெட்சி, முதலியன.",
"name": "இழை பெயர், இந்த இழை வகையை ஒரே உற்பத்தியாளரிடமிருந்து வேறுபடுத்துவதற்கு. உதாரணமாக வண்ணம் இருக்க வேண்டும்.",
"price": "ஒரு முழு ச்பூலின் விலை.",
"weight": "ஒரு முழு ச்பூலின் இழை எடை (நிகர எடை). இதில் ச்பூலின் எடையை சேர்க்கக்கூடாது, இழை மட்டுமே. இது பொதுவாக பேக்கேசிங்கில் எழுதப்படுகிறது.",
"spool_weight": "வெற்று ச்பூலின் எடை. ஒரு ச்பூலின் அளவிடப்பட்ட எடையை தீர்மானிக்கப் பயன்படுகிறது.",
"multi_color_direction": "இழைகள் இரண்டு வழிகளில் பல வண்ணங்களைக் கொண்டிருக்கலாம்: சகாக்களின் மூலம், நிலையான பல வண்ணங்களுடன் இரட்டை-வண்ண இழைகள் போன்றவை, அல்லது ச்பூலில் வண்ணங்களை மாற்றும் சாய்வு இழைகள் போன்ற நீளமான வண்ண மாற்றங்கள் மூலம்."
},
"titles": {
"list": "இழைகள்",
"show": "இழை காட்டு",
"create": "இழை உருவாக்கு",
"clone": "நகலி இழை",
"edit": "இழை திருத்து",
"show_title": "[இழை #{{id}}] {{name}}"
},
"filament": "இழைகள்",
"form": {
"filament_updated": "இந்த பக்கத்தைத் திறந்ததிலிருந்து இந்த இழை யாரோ/வேறு ஏதாவது புதுப்பிக்கப்பட்டுள்ளது. சேமிப்பது அந்த மாற்றங்களை மேலெழுதும்!",
"import_external": "வெளிப்புறத்திலிருந்து இறக்குமதி",
"import_external_description": "அதன் விவரங்களை இழை உருவாக்கும் வடிவத்தில் தானாக விரிவுபடுத்தப் பட்டியலில் ஒரு இழை தேர்ந்தெடுக்கவும். இது நீங்கள் படிவத்தில் உள்ளிட்ட எந்தத் தரவையும் மேலெழுதும். <br><br> தேவைப்பட்டால், சரி என்பதைக் சொடுக்கு செய்யும்போது ஒரு உற்பத்தியாளர் பொருள் தானாக உருவாக்கப்படும்."
},
"buttons": {
"add_spool": "ச்பூல் சேர்க்கவும்"
}
},
"vendor": {
"fields": {
"name": "பெயர்",
"empty_spool_weight": "வெற்று ச்பூல் எடை",
"id": "ஐடி",
"external_id": "வெளிப்புற ஐடி",
"registered": "பதிவுசெய்யப்பட்டது",
"comment": "கருத்து"
},
"vendor": "உற்பத்தியாளர்கள்",
"fields_help": {
"empty_spool_weight": "இந்த உற்பத்தியாளரிடமிருந்து வெற்று ச்பூலின் எடை."
},
"titles": {
"create": "உற்பத்தியாளரை உருவாக்கவும்",
"clone": "நகலி உற்பத்தியாளர்",
"edit": "உற்பத்தியாளரைத் திருத்து",
"list": "உற்பத்தியாளர்கள்",
"show": "உற்பத்தியாளரைக் காட்டு",
"show_title": "[உற்பத்தியாளர் #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "இந்த பக்கத்தைத் திறந்ததிலிருந்து இந்த உற்பத்தியாளர் யாரோ/வேறு ஏதாவது புதுப்பிக்கப்பட்டார். சேமிப்பது அந்த மாற்றங்களை மேலெழுதும்!"
}
},
"help": {
"resources": {
"vendor": "இழை செய்யும் நிறுவனங்கள்.",
"filament": "இழைகளின் பிராண்டுகள். அவற்றில் பெயர், பொருள், நிறம், விட்டம் மற்றும் பல போன்ற பண்புகள் உள்ளன.",
"spool": "ஒரு குறிப்பிட்ட இழைகளின் தனிப்பட்ட உடல் ச்பூல்கள்."
},
"help": "உதவி",
"description": "<title> உதவி </title> <p> நீங்கள் தொடங்குவதற்கான சில குறிப்புகள் இங்கே. </p> <p> ச்பூல்மேன் 3 வெவ்வேறு வகையான தரவுகளை வைத்திருக்கிறார்: </p> <itemsHelp/> <p> தரவுத்தளத்தில் ஒரு புதிய ச்பூலைச் சேர்க்க, <filamentCreateLink>இழை</filamentCreateLink> பொருளை உருவாக்குவதன் மூலம் தொடங்கவும். அது முடிந்ததும், அந்தக் குறிப்பிட்ட ச்பூலுக்கான<spoolCreateLink> ச்பூல்</spoolCreateLink> பொருளை உருவாக்கத் தொடரவும்.</p><p> அதே இழைகளின் கூடுதல் ச்பூல்களைப் பின்னர் நீங்கள் பெற்றால், கூடுதல் ச்பூல் பொருள்களை <vendorCreateLink>உருவாக்கி</vendorCreateLink>அதே இழை பொருளை மீண்டும் பயன்படுத்தினால்.</p><p>இழை பயன்பாட்டைத் தானாகக் கண்காணித்து, உங்களுக்கான ச்பூல் பொருள்களைப் புதுப்பிக்கவும். அதை எவ்வாறு அமைப்பது என்பதற்கான வழிகாட்டுதலுக்கு குறித்த வழிகாட்டுதலுக்கு<readmeLink>ச்பூல்மேன் ரீட்மே</readmeLink> ஐப் பார்க்கவும். </p>"
},
"documentTitle": {
"vendor": {
"clone": "#{{id}} நகலி உற்பத்தியாளர் | ச்பூல்மேன்",
"list": "உற்பத்தியாளர்கள் | ச்பூல்மேன்",
"show": "#{{id}} உற்பத்தியாளரைக் காட்டு | ச்பூல்மேன்",
"edit": "#{{id}} உற்பத்தியாளரைத் திருத்து | ச்பூல்மேன்",
"create": "உற்பத்தியாளரை உருவாக்கு | ச்பூல்மேன்"
},
"default": "ச்பூல்மேன்",
"suffix": " | ச்பூல்மேன்",
"home": {
"list": "முகப்பு | ச்பூல்மேன்"
},
"help": {
"list": "உதவி | ச்பூல்மேன்"
},
"locations": {
"list": "இடங்கள் | ச்பூல்மேன்"
},
"filament": {
"list": "இழைகள் | ச்பூல்மேன்",
"show": "#{{id}} ஃபிலிமென்ட் காட்டு | ச்பூல்மேன்",
"edit": "#{{id}} Filuctient தாள் | ச்பூல்மேன்",
"create": "இழை உருவாக்கு | ச்பூல்மேன்",
"clone": "#{{id}} நகலி இழை | ச்பூல்மேன்"
},
"spool": {
"list": "ச்பூல்கள் | ச்பூல்மேன்",
"show": "#{{id}} காட்டு ச்பூல் | ச்பூல்மேன்",
"edit": "#{{id}} திருத்து ச்பூல் | ச்பூல்மேன்",
"create": "ச்பூலை உருவாக்கு | ச்பூல்மேன்",
"clone": "#{{id}} நகலி ச்பூல் | ச்பூல்மேன்"
}
},
"locations": {
"locations": "இருப்பிடங்கள்",
"new_location": "புதிய இடம்",
"no_location": "இடம் இல்லை",
"no_locations_help": "இந்த பக்கம் உங்கள் ச்பூல்களை இருப்பிடங்களில் ஒழுங்கமைக்க அனுமதிக்கிறது, தொடங்குவதற்கு சில ச்பூல்களைச் சேர்க்கவும்!"
},
"actions": {
"list": "பட்டியல்",
"create": "உருவாக்கு",
"edit": "தொகு",
"show": "காட்டு",
"clone": "நகலி"
},
"warnWhenUnsavedChanges": "நீங்கள் நிச்சயமாக வெளியேற விரும்புகிறீர்களா? நீங்கள் சேமிக்கப்படாத மாற்றங்கள் உள்ளன.",
"notifications": {
"success": "செய்",
"error": "பிழை (நிலை குறியீடு: {{statusCode}})",
"undoable": "நீங்கள் செயல்தவிர்க்க {{seconds}} வினாடிகள் உள்ளன.",
"createSuccess": "வெற்றிகரமாக உருவாக்கப்பட்டது {{resource}}",
"createError": "{{resource}} (நிலைக் குறியீடு: {{statusCode}}) உருவாக்கும் பிழை இருந்தது",
"deleteSuccess": "வெற்றிகரமாக நீக்கப்பட்டது {{resource}}",
"deleteError": "{{resource}} நீக்கும்போது பிழை (நிலைக் குறியீடு: {{statusCode}})",
"editSuccess": "வெற்றிகரமாக திருத்தப்பட்டது {{resource}}",
"editError": "{{resource}} திருத்தும்போது பிழை (நிலை குறியீடு: {{statusCode}})",
"importProgress": "இறக்குமதி: {{processed}}/{{total}}}",
"saveSuccessful": "வெற்றிகரமாக சேமிக்கவும்!",
"validationError": "சரிபார்ப்பு பிழை: {{error}}"
},
"kofi": "உதவிக்குறிப்பு மற்றும் ஆன்-ஃபை",
"loading": "ஏற்றுகிறது",
"version": "பதிப்பு",
"unknown": "தெரியவில்லை",
"yes": "ஆம்",
"no": "இல்லை",
"tags": {
"clone": "நகலி"
},
"dashboard": {
"title": "முகப்புப்பெட்டி"
},
"scanner": {
"title": "QR குறியீடு ச்கேனர்",
"description": "ச்பூல் பற்றிய விவரங்களைக் காண ஒரு ச்பூல்மேன் கியூஆர் குறியீட்டை ச்கேன் செய்யுங்கள்.",
"error": {
"notAllowed": "கேமராவை அணுக நீங்கள் அனுமதிக்கவில்லை.",
"insecureContext": "பக்கம் HTTPS க்கு மேல் வழங்கப்படவில்லை.",
"streamApiNotSupported": "உலாவி மீடியாச்ட்ரீம் பநிஇ ஆதரிக்காது.",
"notReadable": "கேமரா படிக்க முடியாது.",
"notFound": "கேமரா எதுவும் கண்டுபிடிக்கப்படவில்லை.",
"unknown": "அறியப்படாத பிழை ஏற்பட்டது. ({{error}})"
}
},
"home": {
"home": "வீடு",
"welcome": "உங்கள் ச்பூல்மேன் உதாரணத்திற்கு வருக!",
"description": "நீங்கள் இதுவரை எந்த ச்பூல்களையும் சேர்க்கவில்லை என்று தெரிகிறது. தொடங்குவதற்கு <helpPageLink>உதவி பக்கம்</helpPageLink> ஐப் பார்க்கவும்."
},
"table": {
"actions": "செயல்கள்"
},
"settings": {
"settings": "அமைப்புகள்",
"header": "அமைப்புகள்",
"general": {
"tab": "பொது",
"currency": {
"label": "நாணயம்"
},
"base_url": {
"label": "அடிப்படை முகவரி",
"tooltip": "QR குறியீடுகள் போன்ற அம்சங்களை உருவாக்கும்போது பயன்படுத்த வேண்டிய அடிப்படை முகவரி."
},
"round_prices": {
"label": "சுற்று விலைகள்",
"tooltip": "அருகிலுள்ள முழு எண்ணுக்கு வட்ட விலைகள்."
}
},
"extra_fields": {
"tab": "கூடுதல் புலங்கள்",
"description": "<p> இங்கே நீங்கள் உங்கள் நிறுவனங்களுக்குக் கூடுதல் தனிப்பயன் புலங்களைச் சேர்க்கலாம். நீங்கள் ஒரு புலத்தை அகற்றினால், அனைத்து நிறுவனங்களுக்கும் தொடர்புடைய தரவு நீக்கப்படும்.</p><p> மற்ற நிரல்கள் தரவைப் படிக்கிறோம்/எழுதுகின்றன என்பதுதான் முக்கியமானது, எனவே உங்கள் தனிப்பயன் புலம் மூன்றாம் தரப்பு நிரலுடன் ஒருங்கிணைக்க வேண்டுமானால், அதைச் சரியாக அமைக்க உறுதிசெய்க. </p><p>இயல்புநிலை மதிப்பு புதிய உருப்படிகளுக்கு மட்டுமே பயன்படுத்தப்படுகிறது. </p><p> கூடுதல் புலங்களை அட்டவணை காட்சிகளில் வரிசைப்படுத்தவோ வடிகட்டவோ முடியாது. </p>",
"params": {
"key": "விசை",
"name": "பெயர்",
"field_type": "வகை",
"unit": "அலகு",
"order": "ஒழுங்கு",
"default_value": "இயல்புநிலை மதிப்பு",
"choices": "தேர்வுகள்",
"multi_choice": "பல தேர்வு"
},
"field_type": {
"text": "உரை",
"integer": "முழு எண்",
"integer_range": "முழு எண் வரம்பு",
"float": "மிதவை",
"float_range": "மிதவை வரம்பு",
"datetime": "தேதிநேரம்",
"boolean": "பூலியன்",
"choice": "தேர்வு"
},
"boolean_true": "ஆம்",
"boolean_false": "இல்லை",
"choices_missing_error": "நீங்கள் தேர்வுகளை அகற்ற முடியாது. பின்வரும் தேர்வுகள் காணவில்லை: {{choices}}",
"non_unique_key_error": "முக்கியமானது தனித்துவமாக இருக்க வேண்டும்.",
"key_not_changed": "தயவுசெய்து விசையை வேறு ஏதாவது மாற்றவும்.",
"delete_confirm": "புலம் {{name}} ஐ நீக்கு?",
"delete_confirm_description": "இது அனைத்து நிறுவனங்களுக்கும் புலம் மற்றும் தொடர்புடைய அனைத்து தரவையும் நீக்கிவிடும்."
}
}
}
+405
View File
@@ -0,0 +1,405 @@
{
"spool": {
"fields": {
"registered": "ลงทะเบียน",
"remaining_length": "ความยาวคงเหลือ",
"lot_nr": "เลขล็อต",
"archived": "เก็บถาวร",
"remaining_weight": "น้ำหนักคงเหลือ",
"material": "วัสดุ",
"spool_weight": "น้ำหนักม้วนพลาสติกเปล่า",
"id": "ID",
"initial_weight": "น้ำหนักเริ่มต้น",
"measured_weight": "น้ำหนักที่วัดได้",
"used_weight": "น้ำหนักที่ใช้ไป",
"filament_external": "ภายนอก",
"price": "ราคา",
"last_used": "ใช้ครั้งล่าสุด",
"filament_internal": "ภายใน",
"used_length": "ความยาวที่ใช้",
"comment": "หมายเหตุ",
"filament_name": "เส้นพลาสติก",
"first_used": "ใช้ครั้งแรก",
"filament": "เส้นพลาสติก",
"weight_to_use": "น้ำหนัก",
"location": "สถานที่จัดเก็บ"
},
"titles": {
"show_title": "[ม้วนพลาสติก #{{id}}] {{name}}",
"archive": "เก็บถาวรม้วนพลาสติก",
"clone": "ทำซ้ำม้วนพลาสติก",
"create": "สร้างม้วนพลาสติก",
"adjust": "ปรับการใช้ม้วนพลาสติก",
"edit": "แก้ไขม้วนพลาสติก",
"list": "ม้วนพลาสติก",
"show": "แสดงม้วนพลาสติก"
},
"form": {
"adjust_filament_value": "ปริมาณที่ใช้",
"measurement_type": {
"length": "ความยาว",
"weight": "น้ำหนัก"
},
"new_location_prompt": "กรอกชื่อสถานที่เก็บใหม่",
"measurement_type_label": "ประเภทการวัด",
"spool_updated": "ม้วนพลาสติกนี้ได้รับการอัปเดตโดยผู้อื่นหลังจากที่คุณเปิดหน้านี้ การบันทึกจะเขียนทับการเปลี่ยนแปลง!",
"adjust_filament_help": "คุณสามารถเพิ่มหรือลดปริมาณเส้นพลาสติกจากม้วนพลาสติกได้ที่นี่ ค่าบวกจะเป็นการใช้เส้นพลาสติก ค่าลบจะเป็นการเติมกลับ"
},
"fields_help": {
"measured_weight": "น้ำหนักรวมของเส้นพลาสติกและม้วนพลาสติก",
"spool_weight": "น้ำหนักของม้วนพลาสติกเมื่อเปล่า หากเว้นว่างไว้จะใช้ค่าจากเส้นพลาสติกหรือจากผู้ผลิต",
"location": "ตำแหน่งที่เก็บม้วนพลาสติก หากคุณมีหลายที่เก็บ นี่คือที่ที่คุณจัดเก็บม้วนพลาสติก",
"initial_weight": "น้ำหนักเส้นพลาสติกในม้วนพลาสติก (สุทธิ) หากไม่ได้ตั้งค่า จะใช้น้ำหนักจากอ็อบเจ็กต์เส้นพลาสติก",
"used_weight": "ปริมาณเส้นพลาสติกที่ถูกใช้ไปจากม้วนพลาสติก ม้วนพลาสติกใหม่ควรเริ่มต้นที่ 0 กรัม",
"price": "ราคาของม้วนพลาสติกเต็ม หากไม่ได้ตั้งค่า จะใช้ราคาของเส้นพลาสติกแทน",
"remaining_weight": "ปริมาณเส้นพลาสติกที่เหลืออยู่ในม้วนพลาสติก สำหรับม้วนพลาสติกใหม่ควรมีค่าเท่ากับน้ำหนักของม้วนพลาสติกเต็ม",
"weight_to_use": "เลือกค่าน้ำหนักที่คุณต้องการระบุ น้ำหนักวัดได้จะใช้ได้เฉพาะเมื่อมีการตั้งค่าน้ำหนักม้วนพลาสติกเปล่าสำหรับเส้นพลาสติกที่เลือก",
"lot_nr": "เลขล็อตของผู้ผลิต ใช้เพื่อให้มั่นใจว่า การพิมพ์จะมีสีสม่ำเสมอ หากต้องใช้พลาสติกหลายม้วน",
"external_filament": "คุณได้เลือกเส้นพลาสติกจากฐานข้อมูลภายนอก อ็อบเจ็กต์เส้นพลาสติก (และอาจมีอ็อบเจ็กต์ผู้ผลิต) จะถูกสร้างโดยอัตโนมัติเมื่อคุณสร้างม้วนพลาสติก อาจทำให้เกิดเส้นพลาสติกซ้ำกัน ถ้าคุณได้สร้างเส้นพลาสติกนั้นไว้อยู่แล้ว"
},
"formats": {
"last_used": "ใช้ครั้งล่าสุด {{date}}"
},
"spool": "ม้วนพลาสติก",
"messages": {
"archive": "คุณแน่ใจหรือไม่ว่าต้องการเก็บถาวรม้วนพลาสติกนี้?"
}
},
"buttons": {
"hideArchived": "ซ่อนที่เก็บถาวร",
"saveAndAdd": "บันทึกและเพิ่ม",
"clearFilters": "ล้างตัวกรอง",
"delete": "ลบ",
"logout": "ออกจากระบบ",
"continue": "ดำเนินการต่อ",
"filter": "กรอง",
"clear": "ล้าง",
"refresh": "รีเฟรช",
"show": "แสดง",
"import": "นำเข้า",
"clone": "ทำซ้ำ",
"save": "บันทึก",
"confirm": "คุณแน่ใจหรือไม่?",
"unArchive": "ยกเลิกเก็บถาวร",
"notAccessTitle": "คุณไม่มีสิทธิ์เข้าถึง",
"create": "สร้าง",
"undo": "ย้อนกลับ",
"cancel": "ยกเลิก",
"hideColumns": "ซ่อนคอลัมน์",
"showArchived": "แสดงที่เก็บถาวร",
"edit": "แก้ไข",
"archive": "เก็บถาวร"
},
"actions": {
"clone": "ทำซ้ำ",
"show": "แสดง",
"edit": "แก้ไข",
"list": "รายการ",
"create": "สร้าง"
},
"printing": {
"generic": {
"columns": "คอลัมน์",
"printerMarginBottom": "Safe-Zone ล่าง",
"printerMarginLeft": "Safe-Zone ซ้าย",
"showBorder": "แสดงเส้นขอบ",
"newSetting": "ใหม่",
"deleteSettings": "ลบพรีเซ็ตปัจจุบัน",
"customSize": "กำหนดเอง",
"skipItems": "ข้ามรายการ",
"print": "พิมพ์",
"dimensions": "ขนาด",
"marginLeft": "ระยะขอบซ้าย",
"verticalSpacing": "ระยะห่างแนวตั้ง",
"marginTop": "ระยะขอบบน",
"layoutSettings": "การตั้งค่าเค้าโครง",
"marginRight": "ระยะขอบขวา",
"helpMargin": "การตั้งค่าขอบกระดาษควรกำหนดให้ตรงกับขนาดกระดาษและเครื่องพิมพ์ของคุณ การเปลี่ยนค่านี้จะมีผลต่อขนาดตารางทั้งหมด",
"helpPrinterMargin": "Safe-Zone ควรกำหนดเป็นค่าระยะห่างจากขอบกระดาษที่เครื่องพิมพ์สามารถพิมพ์ได้ การเปลี่ยนค่านี้จะไม่กระทบต่อขนาดตารางทั้งหมด",
"previewScale": "สเกลตัวอย่าง",
"itemCopies": "สำเนาต่อรายการ",
"borders": {
"grid": "ตาราง",
"none": "ไม่แสดง",
"border": "เส้นขอบ"
},
"settingsName": "ชื่อพรีเซ็ต",
"contentSettings": "การตั้งค่าข้อมูล",
"printerMarginRight": "Safe-Zone ขวา",
"saveAsImage": "บันทึกเป็นรูปภาพ",
"settings": "พรีเซ็ต",
"defaultSettings": "ค่าเริ่มต้น",
"duplicateSettings": "คัดลอกพรีเซ็ต",
"saveSetting": "บันทึกพรีเซ็ต",
"deleteSettingsConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบพรีเซ็ตนี้?",
"addSettings": "เพิ่มพรีเซ็ตใหม่",
"rows": "แถว",
"title": "การพิมพ์",
"horizontalSpacing": "ระยะห่างแนวนอน",
"printerMarginTop": "Safe-Zone บน",
"paperSize": "ขนาดกระดาษ",
"marginBottom": "ระยะขอบล่าง",
"description": "ปรับการตั้งค่าด้านล่างเพื่อให้ได้รูปแบบการพิมพ์ที่ต้องการ โปรดทราบว่าเครื่องพิมพ์และระบบปฏิบัติการของคุณอาจมีการตั้งค่าขอบกระดาษและการปรับขนาดของตัวเอง ดังนั้นคุณอาจต้องลองผิดลองถูกเล็กน้อยก่อนที่จะได้ผลลัพธ์ที่ถูกต้อง แนะนำให้ทดสอบบนกระดาษธรรมดาก่อนที่จะพิมพ์บนกระดาษสติกเกอร์จริง"
},
"qrcode": {
"textSize": "ขนาดข้อความฉลาก",
"useHTTPUrl": {
"options": {
"default": "ค่าเริ่มต้น",
"url": "URL"
},
"label": "ลิงก์ QR code",
"preview": "ตัวอย่าง:",
"tooltip": "จะใช้ลิงก์ภายในที่จะทำงานได้เฉพาะเมื่อสแกนจากฟีเจอร์สแกนของ Spoolman (ค่าเริ่มต้น) URL จะใช้จาก base URL ที่กำหนดไว้ใน settings หรือใช้ URL ของหน้าปัจจุบันหากไม่ได้ตั้งค่า"
},
"showQRCodeMode": {
"withIcon": "พร้อมไอคอน",
"no": "ไม่",
"simple": "ธรรมดา"
},
"template": "เทมเพลตฉลาก",
"button": "พิมพ์ฉลาก",
"title": "การพิมพ์ฉลาก",
"showQRCode": "พิมพ์ QR Code",
"showContent": "พิมพ์ฉลาก",
"templateHelp": "ใช้ {} เพื่อใส่ค่าจากอ็อบเจ็กต์ spool เป็นข้อความ ตัวอย่างเช่น {id} จะถูกแทนที่ด้วย id ของ spool หรือ {filament.material} จะถูกแทนที่ด้วยวัสดุของ spool หากไม่มีค่าจะแสดงเป็น \"?\" สามารถใช้ชุดเครื่องหมาย {} แบบซ้อนเพื่อลบได้ด้วย นอกจากนี้ยังสามารถลบข้อความหากค่านั้นหายไป ตัวอย่าง {Lot Nr: {lot_nr}} จะแสดงฉลากต่อเมื่อ spool มี lot number ใช้เครื่องหมาย ** ครอบข้อความเพื่อทำให้เป็นตัวหนา คลิกปุ่มเพื่อดูรายการแท็กทั้งหมดที่ใช้ได้"
},
"spoolSelect": {
"selectAll": "เลือก/ยกเลิกทั้งหมด",
"title": "เลือกม้วนพลาสติก",
"noSpoolsSelected": "คุณยังไม่ได้เลือกม้วนพลาสติก",
"selectedTotal_other": "เลือก {{count}} ม้วนพลาสติก",
"showArchived": "แสดงที่เก็บถาวร",
"description": "เลือกม้วนพลาสติกสำหรับพิมพ์ฉลาก"
}
},
"filament": {
"fields_help": {
"spool_weight": "น้ำหนักของม้วนพลาสติกเปล่า ใช้เพื่อคำนวณน้ำหนักที่วัดได้ของม้วนพลาสติก",
"material": "ตัวอย่างเช่น PLA, ABS, PETG, TPU, ASA, PC, PA เป็นต้น",
"article_number": "เช่น EAN, UPC, เป็นต้น",
"price": "ราคาของม้วนพลาสติกเต็ม",
"weight": "น้ำหนักของเส้นพลาสติกในม้วนพลาสติก (สุทธิ) ไม่รวมม้วนพลาสติกเปล่า เป็นค่าน้ำหนักที่มักระบุบนบรรจุภัณฑ์",
"name": "ชื่อเส้นพลาสติก เพื่อแยกความต่างระหว่างเส้นพลาสติกแบบเดียวกันจากผู้ผลิตเดียวกัน โดยทั่วไปควรระบุสีในชื่อด้วย",
"multi_color_direction": "เส้นพลาสติกหลายสีสามารถมีหลายสีได้ 2 รูปแบบ: แบบ coextrusion (เช่น เส้นพลาสติกสองสีพร้อมกัน) หรือแบบไล่สีตามความยาว (เช่น เส้นพลาสติกเปลี่ยนสีไปเรื่อยๆ)"
},
"fields": {
"comment": "หมายเหตุ",
"density": "ความหนาแน่น",
"weight": "น้ำหนัก",
"vendor": "ผู้ผลิต",
"settings_extruder_temp": "อุณหภูมิหัวพิมพ์",
"longitudinal": "ไล่สีตามความยาว",
"material": "วัสดุ",
"id": "ID",
"settings_bed_temp": "อุณหภูมิฐานพิมพ์",
"vendor_name": "ผู้ผลิต",
"diameter": "เส้นผ่านศูนย์กลาง",
"article_number": "รหัสสินค้า",
"multi_color": "หลายสี",
"external_id": "รหัสภายนอก",
"color_hex": "สี",
"price": "ราคา",
"spool_weight": "น้ำหนักม้วนพลาสติกเปล่า",
"coaxial": "เส้นประกบหลายสี",
"name": "ชื่อ",
"spools": "แสดงม้วนพลาสติก",
"registered": "ลงทะเบียน",
"single_color": "สีเดียว"
},
"titles": {
"edit": "แก้ไขเส้นพลาสติก",
"list": "เส้นพลาสติก",
"show": "แสดงเส้นพลาสติก",
"clone": "ทำซ้ำเส้นพลาสติก",
"show_title": "[เส้นพลาสติก #{{id}}] {{name}}",
"create": "สร้างเส้นพลาสติก"
},
"form": {
"filament_updated": "เส้นพลาสติกนี้ได้รับการอัปเดตโดยผู้อื่นหลังจากที่คุณเปิดหน้านี้ การบันทึกจะเขียนทับการเปลี่ยนแปลง!",
"import_external": "นำเข้าจากภายนอก",
"import_external_description": "เลือกเส้นพลาสติกในรายการเพื่อกรอกข้อมูลอัตโนมัติในฟอร์มนี้ การดำเนินการนี้จะเขียนทับข้อมูลที่คุณกรอกไว้<br><br>อ็อบเจ็กต์ผู้ผลิตจะถูกสร้างขึ้นโดยอัตโนมัติเมื่อคุณคลิกตกลง หากจำเป็น"
},
"buttons": {
"add_spool": "เพิ่มม้วนพลาสติก"
},
"filament": "เส้นพลาสติก"
},
"kofi": "สนับสนุนเราได้ที่ Ko-fi",
"scanner": {
"error": {
"notReadable": "ไม่สามารถอ่านข้อมูลจากกล้องได้",
"insecureContext": "หน้านี้ไม่ได้ถูกให้บริการผ่าน HTTPS",
"notAllowed": "คุณไม่ได้อนุญาตให้เข้าถึงกล้อง",
"streamApiNotSupported": "เบราว์เซอร์ไม่รองรับ MediaStream API",
"unknown": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ ({{error}})",
"notFound": "ไม่พบกล้อง"
},
"description": "สแกน Spoolman QR code เพื่อดูข้อมูลเกี่ยวกับม้วนพลาสติก",
"title": "เครื่องสแกน QR Code"
},
"documentTitle": {
"filament": {
"clone": "#{{id}} ทำซ้ำเส้นพลาสติก | Spoolman",
"edit": "#{{id}} แก้ไขเส้นพลาสติก | Spoolman",
"show": "#{{id}} แสดงเส้นพลาสติก | Spoolman",
"create": "สร้างเส้นพลาสติก | Spoolman",
"list": "เส้นพลาสติก | Spoolman"
},
"home": {
"list": "หน้าแรก | Spoolman"
},
"spool": {
"show": "#{{id}} แสดงม้วนพลาสติก | Spoolman",
"clone": "#{{id}} ทำซ้ำม้วนพลาสติก | Spoolman",
"create": "สร้างม้วนพลาสติก | Spoolman",
"list": "ม้วนพลาสติก | Spoolman",
"edit": "#{{id}} แก้ไขม้วนพลาสติก | Spoolman"
},
"vendor": {
"list": "ผู้ผลิต | Spoolman",
"show": "#{{id}} แสดงผู้ผลิต | Spoolman",
"edit": "#{{id}} แก้ไขผู้ผลิต | Spoolman",
"create": "สร้างผู้ผลิต | Spoolman",
"clone": "#{{id}} ทำซ้ำผู้ผลิต | Spoolman"
},
"help": {
"list": "วิธีใช้ | Spoolman"
},
"suffix": " | Spoolman",
"locations": {
"list": "สถานที่เก็บ | Spoolman"
},
"default": "Spoolman"
},
"notifications": {
"error": "เกิดข้อผิดพลาด (รหัสสถานะ: {{statusCode}})",
"undoable": "คุณเหลือเวลา {{seconds}} วินาทีในการย้อนกลับ",
"deleteSuccess": "ลบ {{resource}} สำเร็จ",
"editSuccess": "แก้ไข {{resource}} สำเร็จ",
"validationError": "เกิดข้อผิดพลาดในการตรวจสอบ: {{error}}",
"deleteError": "เกิดข้อผิดพลาดในการลบ {{resource}} (รหัสสถานะ: {{statusCode}})",
"success": "สำเร็จ",
"createSuccess": "สร้าง {{resource}} สำเร็จ",
"saveSuccessful": "บันทึกสำเร็จ!",
"editError": "เกิดข้อผิดพลาดในการแก้ไข {{resource}} (รหัสสถานะ: {{statusCode}})",
"createError": "เกิดข้อผิดพลาดในการสร้าง {{resource}} (รหัสสถานะ: {{statusCode}})",
"importProgress": "กำลังนำเข้า: {{processed}}/{{total}}"
},
"loading": "กำลังโหลด",
"version": "เวอร์ชัน",
"unknown": "ไม่ทราบ",
"yes": "ใช่",
"no": "ไม่",
"tags": {
"clone": "ทำซ้ำ"
},
"vendor": {
"titles": {
"create": "สร้างผู้ผลิต",
"list": "ผู้ผลิต",
"clone": "ทำซ้ำผู้ผลิต",
"show_title": "[ผู้ผลิต #{{id}}] {{name}}",
"edit": "แก้ไขผู้ผลิต",
"show": "แสดงผู้ผลิต"
},
"fields": {
"empty_spool_weight": "น้ำหนักม้วนพลาสติกเปล่า",
"id": "ID",
"comment": "หมายเหตุ",
"external_id": "รหัสภายนอก",
"registered": "ลงทะเบียน",
"name": "ชื่อ"
},
"fields_help": {
"empty_spool_weight": "น้ำหนักของม้วนพลาสติกเปล่าจากผู้ผลิตนี้"
},
"form": {
"vendor_updated": "ผู้ผลิตนี้ได้รับการอัปเดตโดยผู้อื่นหลังจากที่คุณเปิดหน้านี้ การบันทึกจะเขียนทับการเปลี่ยนแปลง!"
},
"vendor": "ผู้ผลิต"
},
"help": {
"resources": {
"vendor": "บริษัทที่เป็นผู้ผลิตเส้นพลาสติก",
"spool": "ม้วนพลาสติกจริงของเส้นพลาสติกเฉพาะแต่ละม้วน",
"filament": "แบรนด์ของเส้นพลาสติก ซึ่งมีคุณสมบัติต่างๆ เช่น ชื่อ วัสดุ สี เส้นผ่านศูนย์กลาง และอื่นๆ"
},
"help": "วิธีใช้",
"description": "<title>วิธีใช้</title><p>นี่คือเคล็ดลับบางส่วนเพื่อเริ่มต้นใช้งาน</p><p>Spoolman เก็บข้อมูล 3 ประเภทที่แตกต่างกัน:</p><itemsHelp/><p>ในการเพิ่มม้วนพลาสติกใหม่ลงในฐานข้อมูล ให้เริ่มด้วยการสร้างอ็อบเจ็กต์ <filamentCreateLink>Filament</filamentCreateLink> เมื่อเสร็จแล้วจึงสร้างอ็อบเจ็กต์ <spoolCreateLink>Spool</spoolCreateLink> สำหรับม้วนพลาสติกนั้น หากคุณได้ม้วนพลาสติกของเส้นพลาสติกเดิมเพิ่มในภายหลัง ก็เพียงสร้างอ็อบเจ็กต์ Spool ใหม่และใช้อ็อบเจ็กต์ Filament เดิมซ้ำได้</p><p>นอกจากนี้คุณยังสามารถสร้างอ็อบเจ็กต์ <vendorCreateLink>Manufacturer</vendorCreateLink> สำหรับบริษัทที่ผลิตเส้นพลาสติกได้หากต้องการติดตามข้อมูลนั้น</p><p>คุณสามารถเชื่อมต่อบริการเครื่องพิมพ์ 3D อื่นๆ เข้ากับ Spoolman เช่น Moonraker ซึ่งสามารถตรวจสอบการใช้เส้นพลาสติกโดยอัตโนมัติและอัปเดตอ็อบเจ็กต์ Spool ให้คุณ ดูคำแนะนำในการตั้งค่าได้ที่ <readmeLink>Spoolman README</readmeLink></p>"
},
"dashboard": {
"title": "แดชบอร์ด"
},
"settings": {
"settings": "การตั้งค่า",
"header": "การตั้งค่า",
"general": {
"tab": "ทั่วไป",
"currency": {
"label": "สกุลเงิน"
},
"base_url": {
"label": "URL พื้นฐาน",
"tooltip": "URL พื้นฐานที่ใช้สำหรับสร้างฟีเจอร์ต่างๆ เช่น QR code"
},
"round_prices": {
"label": "ปัดเศษราคา",
"tooltip": "ปัดเศษราคาเป็นจำนวนเต็มที่ใกล้ที่สุด"
}
},
"extra_fields": {
"params": {
"multi_choice": "เลือกได้หลายรายการ",
"key": "คีย์",
"name": "ชื่อ",
"field_type": "ประเภท",
"unit": "หน่วย",
"order": "ลำดับ",
"default_value": "ค่าเริ่มต้น",
"choices": "ตัวเลือก"
},
"delete_confirm_description": "สิ่งนี้จะลบฟิลด์และข้อมูลที่เกี่ยวข้องทั้งหมดของทุกเอนทิตี",
"field_type": {
"text": "ข้อความ",
"integer": "จำนวนเต็ม",
"integer_range": "ช่วงจำนวนเต็ม",
"float": "ทศนิยม",
"float_range": "ช่วงทศนิยม",
"datetime": "วันเวลา",
"boolean": "บูลีน",
"choice": "ตัวเลือก"
},
"tab": "ฟิลด์เพิ่มเติม",
"boolean_true": "ใช่",
"boolean_false": "ไม่",
"choices_missing_error": "คุณไม่สามารถลบตัวเลือกได้ ตัวเลือกต่อไปนี้หายไป: {{choices}}",
"non_unique_key_error": "คีย์ต้องไม่ซ้ำ",
"key_not_changed": "โปรดเปลี่ยนคีย์เป็นอย่างอื่น",
"delete_confirm": "ลบฟิลด์ {{name}} หรือไม่?",
"description": "<p>คุณสามารถเพิ่มฟิลด์ที่กำหนดเองเพิ่มเติมสำหรับเอนทิตีของคุณได้ที่นี่</p><p>เมื่อเพิ่มฟิลด์แล้ว คุณจะไม่สามารถเปลี่ยนคีย์หรือประเภทของฟิลด์ได้ และสำหรับฟิลด์ประเภทตัวเลือกคุณจะไม่สามารถลบตัวเลือกหรือเปลี่ยนสถานะตัวเลือกหลายรายการได้ หากคุณลบฟิลด์ ข้อมูลที่เกี่ยวข้องของทุกเอนทิตีจะถูกลบไปด้วย</p><p>คีย์คือสิ่งที่โปรแกรมอื่นจะใช้เพื่ออ่าน/เขียนข้อมูล ดังนั้นหากฟิลด์ที่คุณสร้างต้องการเชื่อมต่อกับโปรแกรมภายนอก กรุณาตั้งค่าให้ถูกต้อง ค่าเริ่มต้นจะถูกนำไปใช้เฉพาะกับรายการใหม่เท่านั้น</p><p>ฟิลด์เพิ่มเติมสามารถแสดงและจัดเรียงในมุมมองตารางได้ ฟิลด์ประเภทตัวเลือกและบูลีนยังมีตัวเลือกการกรองที่นั่นด้วย และฟิลด์เพิ่มเติมทุกประเภทรองรับการกรองหาค่าว่าง</p>",
"choices_reorder_hint": "ลาก หรือใช้ปุ่มลูกศร เพื่อจัดเรียงตัวเลือกนี้"
}
},
"locations": {
"new_location": "สถานที่เก็บใหม่",
"no_location": "ไม่มีสถานที่เก็บ",
"locations": "สถานที่เก็บ",
"no_locations_help": "หน้านี้ให้คุณจัดการม้วนพลาสติกตามสถานที่เก็บ เพิ่มม้วนพลาสติกเพื่อเริ่มต้น!"
},
"home": {
"home": "หน้าแรก",
"welcome": "ยินดีต้อนรับสู่ Spoolman ของคุณ!",
"description": "ดูเหมือนว่าคุณยังไม่ได้เพิ่มม้วนพลาสติกใดๆ ดูคำแนะนำการเริ่มต้นใช้งานได้ที่ <helpPageLink>หน้าวิธีใช้</helpPageLink>"
},
"warnWhenUnsavedChanges": "คุณแน่ใจหรือไม่ว่าต้องการออก? คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก",
"table": {
"actions": "การดำเนินการ"
},
"theme": {
"label": "ธีม",
"system": "ตามระบบ",
"light": "โหมดสว่าง",
"dark": "โหมดมืด"
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"actions": {
"list": "Liste",
"create": "Oluştur",
"edit": "Düzenle",
"show": "Göster",
"clone": "Kopyala"
},
"buttons": {
"create": "Oluştur",
"save": "Kaydet",
"saveAndAdd": "Kaydet ve Ekle",
"logout": "Çıkış yap",
"delete": "Sil",
"edit": "Düzenle",
"cancel": "İptal et",
"confirm": "Emin misiniz?",
"continue": "Devam et",
"filter": "Filtrele",
"clear": "Temizle",
"refresh": "Yenile",
"show": "Göster",
"undo": "Geri al",
"import": "İçe aktar",
"clone": "Kopyala",
"archive": "Arşivle",
"unArchive": "Arşivden çıkar",
"hideArchived": "Arşivlenenleri gizle",
"showArchived": "Arşivlenenleri göster",
"notAccessTitle": "Erişim için yetkiniz bulunmamakta",
"hideColumns": "Sütunları gizle",
"clearFilters": "Filtreleri temizle"
},
"warnWhenUnsavedChanges": "Çıkmak istediğinizden emin misiniz? Kaydedilmemiş değişiklikleriniz bulunmakta.",
"notifications": {
"success": "Başarılı",
"error": "Hata (durum kodu: {{statusCode}})",
"undoable": "Geri almak için {{seconds}} saniyeniz var.",
"createSuccess": "{{resource}} başarıyla oluşturuldu",
"createError": "{{resource}} oluşturulurken bir hata ile karşılaşıldı (durum kodu :{{statusCode}})",
"deleteSuccess": "{{resource}} başarılı şekilde silindi",
"deleteError": "{{resource}} silinirken hata ile karşılaşıldı(durum kodu:{{statusCode}})",
"editSuccess": "{{resource}} başarılı şekilde düzenlendi",
"editError": "{{resource}} düzenlenirken hata ile karşılaşıldı(durum kodu:{{statusCode}})",
"importProgress": "İçe aktarılıyor: {{processed}}/{{total}}",
"saveSuccessful": "Kayıt başarılı!",
"validationError": "Form kontrol hatası: {{error}}"
},
"kofi": "Ko-fi'den bana birşeyler ateşle",
"loading": "Yükleniyor",
"version": "Versiyon",
"unknown": "Bilinmeyen",
"yes": "Evet",
"no": "Hayır",
"tags": {
"clone": "Kopyala"
},
"dashboard": {
"title": "Gösterge paneli"
},
"printing": {
"generic": {
"title": "Yazdırılıyor",
"description": "İstenilen yazıcı düzenine sahip olmak için aşağıdaki ayarları düzenleyiniz. İşletim sisteminizin ve yazıcınızın kendi boşluk ve boyutlandırma ayarlarını uygulayabileceği nedeniyle doğru sonuca ulaşana kadar birkaç kez deneme yanılma gerçekleştirmeniz gerekebilir. Gerçek etikete basmadan önce normal kağıtta deneyiniz.",
"helpMargin": "Boşluklar seçtiğiniz etikete ve yazıcınıza bağlı olarak ayarlanmalıdır, bunlarda yapacağınız herhangi bir değişiklik tüm dizgeyi değiştirecektir.",
"helpPrinterMargin": "Güvenli-Bölge yazıcınızın sayfa kenarına ne kadar yakınlıkta yazabileceğine göre ayarlanmalıdır, bunu değiştirmek dizge üstünden değişikliğe neden olmayacaktır.",
"print": "Yazdır",
"columns": "Sütunlar",
"rows": "Satırlar",
"paperSize": "Sayfa Boyutu",
"customSize": "Özel",
"dimensions": "Boyutlar",
"showBorder": "Kenarlık Göster",
"previewScale": "Ölçeklendirmeyi Önizle",
"skipItems": "Öğeleri Atla",
"itemCopies": "Öğe Kopyası",
"contentSettings": "İçerik Ayarları",
"layoutSettings": "Düzen Ayarları",
"horizontalSpacing": "Yatay Boşluk",
"verticalSpacing": "Dikey Boşluk",
"marginLeft": "Sol Kenar Boşluğu",
"marginRight": "Sağ Kenar Boşluğu",
"marginTop": "Üst Kenar Boşluğu",
"marginBottom": "Alt Kenar Boşluğu",
"printerMarginLeft": "Sol Güvenli-Bölge",
"printerMarginRight": "Sağ Güvenli-Bölge",
"printerMarginTop": "Üst Güvenli-Bölge",
"printerMarginBottom": "Alt Güvenli-Bölge",
"borders": {
"none": "Kenarlık Yok",
"border": "Kenarlıklı",
"grid": "Izgara Görünümü"
},
"settings": "Kayıtlı Etiketler",
"defaultSettings": "Varsayılan",
"addSettings": "Yeni Kayıtlı Etiket Ekle",
"newSetting": "Yeni",
"duplicateSettings": "Mükerrer",
"deleteSettings": "Mevcut Kayıtlı Etiketi Sil",
"deleteSettingsConfirm": "Mevcut kayıtlı etiketi silmek istediğinizden emin misiniz?",
"settingsName": "Kayıtlı Etiket Adı",
"saveSetting": "Kaydet",
"saveAsImage": "Resim olarak kaydet"
},
"qrcode": {
"button": "Etiketleri Yazdır",
"title": "Etiket Yazdırılıyor",
"template": "Etiket Şablonu",
"templateHelp": "Makara nesnesinin değerlerini metin olarak eklemek için {} işaretini kullanın. Örneğin, {id} makaraya ait kimlik numarasıyla, {filament.material} ise makaradaki malzeme türüyle değiştirilecektir. Eğer bir değer eksikse, yerine \"?\" ibaresi konur. İkinci bir {} çifti kullanılarak bu durum önlenebilir. Ayrıca, iki {} çifti arasındaki herhangi bir metin, değer eksikse tamamen kaldırılır. Örneğin, {Lot Nr: {lot_nr}} ifadesi yalnızca makarada lot numarası varsa etiket olarak gösterilecektir. Metni kalın yapmak için çift yıldız işareti ** ile metni çevreleyin. Tüm kullanılabilir etiketlerin listesini görmek için butona tıklayın.",
"textSize": "Etiket Metin Boyutu",
"showContent": "Etiket Yazdır",
"useHTTPUrl": {
"label": "QR kod bağlantısı",
"tooltip": "Yalnızca Spoolman'ın tarama özelliği (varsayılan) ile tarandığında çalışacak özel bir bağlantı kullanılacaktır. URL, ayarlarda belirtilen temel URL'yi veya ayarlanmadıysa mevcut sayfanın URL'sini kullanır.",
"options": {
"default": "Varsayılan",
"url": "URL"
},
"preview": "Öngörünüm:"
},
"showQRCode": "QR kodu yazdır",
"showQRCodeMode": {
"no": "Hayır",
"simple": "Basit",
"withIcon": "Simge ile"
}
},
"spoolSelect": {
"title": "Makaraları Seçin",
"description": "Etiket yazdırılacak makaraları seçin.",
"showArchived": "Arşivlenmişleri göster",
"noSpoolsSelected": "Hiçbir makara seçimi yapmadınız.",
"selectAll": "Tümünü Seçin/Kaldırın",
"selectedTotal_one": "{{count}} makara seçildi",
"selectedTotal_other": "{{count}} makara seçildi"
}
},
"scanner": {
"title": "QR Kod Tarayıcı",
"description": "Makara hakkında bilgi almak için Spoolman QR kodunu taratınız.",
"error": {
"notAllowed": "Kameraya erişim yetkisi vermediniz.",
"insecureContext": "Bu sayfa HTTPS üstünden güvenli olarak gösterilmemektedir.",
"streamApiNotSupported": "Bu tarayıcı MediaStream API'sini desteklememektedir.",
"notReadable": "Kamera okunabilir değil.",
"notFound": "Kamera bulunamadı.",
"unknown": "Bilinmeyen hata oluştu.({{error}})"
}
},
"spool": {
"spool": "Makaralar",
"fields": {
"id": "ID",
"filament_name": "Filament",
"filament": "Filament",
"filament_internal": "Dahili",
"filament_external": "Harici",
"price": "Fiyat",
"material": "Malzeme",
"weight_to_use": "Ağırlık",
"used_weight": "Kullanılmış Ağırlık",
"remaining_weight": "Kalan Ağırlık",
"measured_weight": "Tartılmış Ağırlık",
"used_length": "Kullanılmış Uzunluk",
"remaining_length": "Kalan Uzunluk",
"initial_weight": "Başlangıç Ağırlığı",
"spool_weight": "Boş Makara Ağırlığı",
"location": "Konum",
"lot_nr": "Lot No",
"first_used": "İlk Kullanım Tarihi",
"last_used": "Son Kullanım Tarihi",
"registered": "Kayıtlı",
"comment": "Yorum",
"archived": "Arşivlendi"
},
"fields_help": {
"price": "Makaranın fiyatı eğer makara fiyatı belirlenmemişse filament fiyatı devralınacaktır.",
"weight_to_use": "Hangi ağırlık değerinin girileceğini seçin. Ölçülen ağırlık, yalnızca seçilen filamente ait makara ağırlığı ayarlanmışsa kullanılabilir.",
"used_weight": "Makaradan ne kadar filament kullanıldığını gösterir. Yeni bir makaranın kullanılmamış olması gerekir ve 0g kullanım göstermelidir.",
"remaining_weight": "Makara üzerinde ne kadar filament kaldığını gösterir. Yeni bir makara için bu değer, makara ağırlığıyla eşlit olmalıdır.",
"measured_weight": "Filament ve makaranın toplam ağırlığını gösterir.",
"initial_weight": "Makara üzerindeki filamentin ilk (net) ağırlığıdır. Ayarlanmadıysa, filament nesnesindeki ağırlık değeri kullanılır.",
"spool_weight": "Makaranın boş ağırlığıdır. Bu alan boş bırakılırsa, filamentten veya üreticiden gelen değer kullanılır.",
"location": "Makaralarınızı birden fazla konumda saklamanız durumunda makaranın bulunduğu yer bilgisidir.",
"lot_nr": "Üretici lot numarası. Birden fazla makara kullanımı durumunda tutarlı renk alınmasını sağlamak için kullanılabilir.",
"external_filament": "Dış veritabanından bir filament seçtiniz. Bu makarayı oluşturduğunuzda otomatik olarak bir filament nesnesi (ve muhtemelen bir üretici nesnesi) oluşturulacaktır. Eğer bu filament için zaten bir filament nesnesi oluşturduysanız, bu işlem tekrarlanan (çift) filament nesneleri yaratabilir."
},
"titles": {
"create": "Makara Oluştur",
"clone": "Makara Kopyala",
"edit": "Makara Düzenle",
"list": "Makaralar",
"show": "Makarayı Göster",
"show_title": "[Makara #{{id}}] {{name}}",
"archive": "Makarayı Arşivle",
"adjust": "Makara filamentini ayarla"
},
"form": {
"measurement_type": {
"length": "Uzunluk",
"weight": "Ağırlık"
},
"measurement_type_label": "Ölçüm Şekli",
"adjust_filament_help": "Burada makaradan filament doğrudan ekleyebilir veya çıkarabilirsiniz. Pozitif bir değer filamentin tüketildiğini, negatif bir değer ise filamentin eklendiğini gösterir.",
"adjust_filament_value": "Tüketim Miktarı",
"new_location_prompt": "Yeni konumu girin",
"spool_updated": "Bu makara, siz bu sayfayı açtığınızdan beri başka biri veya bir şey tarafından güncellendi. Kaydetme işlemi bu değişikliklerin üzerine yazacaktır!"
},
"messages": {
"archive": "Bu makarayı arşivlemek istediğinizden emin misiniz?"
},
"formats": {
"last_used": "Son kullanım tarihi {{date}}"
}
},
"filament": {
"filament": "Filamentler",
"fields": {
"id": "ID",
"vendor_name": "Üretici",
"vendor": "Üretici",
"name": "Ad",
"material": "Malzeme",
"price": "Fiyat",
"density": "Yoğunluk",
"diameter": "Çap",
"weight": "Ağırlık",
"spool_weight": "Makara Ağırlığı",
"article_number": "Artikel Numarası",
"registered": "Kayıtlı",
"comment": "Yorum",
"settings_extruder_temp": "Extruder Sıcaklığı",
"settings_bed_temp": "Tabla Sıcaklığı",
"color_hex": "Renk",
"single_color": "Tek Renk",
"multi_color": "Çok Renk",
"coaxial": "Koekstrüde edilmiş",
"longitudinal": "Boylamsal",
"external_id": "Harici ID",
"spools": "Makaraları Göster"
},
"fields_help": {
"name": "Filament adı, aynı üreticinin diğer filament türlerinden ayırt edilebilmesi için kullanılır. Örneğin, renk bilgisini içermelidir.",
"material": "Örn.PLA, ABS, PETG, gibi.",
"price": "Tam dolu makaranın fiyatı.",
"weight": "Dolu bir makaradaki filamentin ağırlığıdır (net ağırlık). Bu değer yalnızca filamentin ağırlığını içermelidir, makaranın kendisinin ağırlığı dahil edilmemelidir. Genellikle ambalaj üzerinde yazan değerdir.",
"spool_weight": "Boş bir makaranın ağırlığıdır. Makaradaki ölçülen ağırlığın hesaplanmasında kullanılır.",
"article_number": "Örn. EAN, UPC, gibi.",
"multi_color_direction": "Filamentler iki farklı şekilde birden fazla renge sahip olabilir: Birincisi, çift renkli filamentlerde olduğu gibi, koekstrüzyon yoluyla sabit çoklu renklerle üretilen filamentlerdir. İkincisi ise, makara boyunca renk geçişleri olan gradyan filamentlerde görülen boyuna (uzunlamasına) renk değişiklikleridir."
},
"titles": {
"create": "Filament Oluştur",
"clone": "Filament Kopyala",
"edit": "Filament Düzenle",
"list": "Filamentler",
"show": "Filament Göster",
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Bu filament, siz bu sayfayı açtıktan sonra başka biri veya bir şey tarafından güncellendi. Kaydetme işlemi bu değişikliklerin üzerine yazacaktır!",
"import_external": "Harici Kaynaktan İçeriye Aktar",
"import_external_description": "Listeden bir filament seçerek, filament oluşturma formunun detaylarının otomatik olarak doldurulmasını sağlayabilirsiniz. Bu işlem, formda daha önce girdiğiniz tüm verilerin üzerine yazacaktır.<br><br>Gerekirse, \"Tamam\" butonuna tıkladığınızda otomatik olarak bir Üretici nesnesi oluşturulacaktır."
},
"buttons": {
"add_spool": "Makara Ekle"
}
},
"vendor": {
"vendor": "Üreticiler",
"fields": {
"id": "ID",
"name": "İsim",
"empty_spool_weight": "Boş Makara Ağırlığı",
"external_id": "Harici ID",
"registered": "Kayıtlı",
"comment": "Yorum"
},
"fields_help": {
"empty_spool_weight": "Bu üreticiden boş bir makaranın ağırlığı."
},
"titles": {
"create": "Üretici Oluştur",
"clone": "Üretici Kopyala",
"edit": "Üretici Düzenle",
"list": "Üreticiler",
"show": "Üretici Göster",
"show_title": "[Üretici#{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Bu üretici, siz bu sayfayı açtıktan sonra başka biri veya bir şey tarafından güncellendi. Kaydetme işlemi bu değişikliklerin üzerine yazacaktır!"
}
},
"home": {
"home": "Ana Sayfa",
"welcome": "Spoolman sunucunuza hoşgeldiniz!",
"description": "Görünüşe göre henüz hiç makara eklememişsiniz. Başlamak için yardım almak isterseniz <helpPageLink>Yardım sayfasına</helpPageLink> göz atabilirsiniz."
},
"help": {
"help": "Yardım",
"resources": {
"filament": "Filament markalarıdır. İsim, malzeme, renk, çap gibi çeşitli özelliklere sahiptirler.",
"spool": "Belirli bir filamente ait bireysel fiziksel makaralardır.",
"vendor": "Filamenti üreten şirketlerdir."
},
"description": "<title>Yardım</title><p>Başlamak için bazı ipuçları:</p><p>Spoolman üç farklı veri türünü tutar:</p><itemsHelp/><p>Veritabanına yeni bir makara eklemek için önce bir <filamentCreateLink>Filament</filamentCreateLink> nesnesi oluşturun. Bu işlem tamamlandıktan sonra, ilgili makara için bir <spoolCreateLink>Makara</spoolCreateLink> nesnesi oluşturun. Daha sonra aynı filamentten ek makaralar edinirseniz, sadece yeni Makara nesneleri oluşturup mevcut Filament nesnesini kullanabilirsiniz.</p><p>İsterseniz, filamentin üreticisi olan firma için bir <vendorCreateLink>Üretici</vendorCreateLink> nesnesi oluşturarak bu bilgiyi de takip edebilirsiniz.</p><p>Ayrıca, filament kullanımını otomatik olarak takip edip Makara nesnelerini güncelleyebilen Moonraker gibi diğer 3D yazıcı servislerini Spoolman ile bağlama seçeneğiniz vardır. Kurulum için <readmeLink>Spoolman README</readmeLink> dokümanına bakabilirsiniz.</p>"
},
"table": {
"actions": "İşlemler"
},
"settings": {
"settings": "Ayarlar",
"header": "Ayarlar",
"general": {
"tab": "Genel",
"currency": {
"label": "Para Birimi"
},
"base_url": {
"label": "Temel URL",
"tooltip": "QR kodları gibi ek özellikler kullanılırken kullanılacak temel URL."
},
"round_prices": {
"label": "Fiyatları Yuvarla",
"tooltip": "Fiyatları en yakın tam sayıya yuvarla."
}
},
"extra_fields": {
"tab": "Ekstra Alanlar",
"description": "<p>Burada varlıklarınıza ekstra özel alanlar ekleyebilirsiniz.</p><p>Bir alan eklendikten sonra, anahtarını (key) veya türünü değiştiremezsiniz; seçim türündeki alanlarda ise seçenekleri kaldıramaz veya çoklu seçim durumunu değiştiremezsiniz. Bir alanı silerseniz, o alana bağlı tüm varlıkların verileri silinir.</p><p>Anahtar, diğer programların veriyi okuduğu/yazdığı isimdir; bu nedenle, özel alanınız üçüncü taraf bir programla entegrasyon için kullanılacaksa doğru şekilde ayarlandığından emin olun. Varsayılan değer yalnızca yeni öğelere uygulanır.</p><p>Ekstra alanlar tablo görünümünde sıralanamaz veya filtrelenemez.</p>",
"params": {
"key": "Anahtar",
"name": "İsim",
"field_type": "Tür",
"unit": "Birim",
"order": "Sipariş",
"default_value": "Varsayılan Değer",
"choices": "Seçenekler",
"multi_choice": "Çoklu Seçim"
},
"field_type": {
"text": "Metin",
"integer": "Tam Sayı",
"integer_range": "Tam Sayı Aralık",
"float": "Ondalıklı Sayı",
"float_range": "Ondalıklı Sayı Aralığı",
"datetime": "Tarih+Saat",
"boolean": "İkili Değer",
"choice": "Seçim Listesi"
},
"boolean_true": "Evet",
"boolean_false": "Hayır",
"choices_missing_error": "Seçenekleri kaldıramazsınız. Aşağıdaki seçenekler eksik: {{choices}}",
"non_unique_key_error": "Anahtar benzersiz olmalıdır.",
"key_not_changed": "Lütfen anahtarı başka bir şeyle değiştirin.",
"delete_confirm": "{{name}} alanı silinsin mi?",
"delete_confirm_description": "Bu işlem alanı ve ona bağlı tüm varlıkların verilerini silecektir."
}
},
"documentTitle": {
"default": "Spoolman",
"suffix": " | Spoolman",
"home": {
"list": "Ana Sayfa | Spoolman"
},
"help": {
"list": "Yardım | Spoolman"
},
"locations": {
"list": "Konumlar | Spoolman"
},
"filament": {
"list": "Filamentler | Spoolman",
"show": "#{{id}} Filament Görüntüle | Spoolman",
"edit": "#{{id}} Filament Düzenle | Spoolman",
"create": "Filament Oluştur | Spoolman",
"clone": "#{{id}} Filament Kopyala | Spoolman"
},
"spool": {
"list": "Makaralar | Spoolman",
"show": "#{{id}} Makara Göster | Spoolman",
"edit": "#{{id}} Makara Düzenle | Spoolman",
"create": "Makara Oluştur | Spoolman",
"clone": "#{{id}}Makara Kopyala | Spoolman"
},
"vendor": {
"list": "Üreticiler | Spoolman",
"show": "#{{id}} Üretici Göster | Spoolman",
"edit": "#{{id}} Üretici Düzenle | Spoolman",
"create": "Üretici Oluştur | Spoolman",
"clone": "#{{id}} Üretici Kopyala | Spoolman"
}
},
"locations": {
"locations": "Konumlar",
"new_location": "Yeni Konum",
"no_location": "Konum Yok",
"no_locations_help": "Bu sayfa, makaralarınızı konumlara göre düzenlemenizi sağlar. Başlamak için birkaç makara ekleyin!"
}
}
+142 -17
View File
@@ -21,7 +21,8 @@
"notAccessTitle": "У Вас бракує прав для доступу",
"clone": "Клонувати",
"unArchive": "Розархівувати",
"saveAndAdd": "Зберегти та додати"
"saveAndAdd": "Зберегти та додати",
"continue": "Продовжити"
},
"spool": {
"fields": {
@@ -42,7 +43,11 @@
"registered": "Додано",
"first_used": "Вперше Використано",
"remaining_weight": "Залишок Ваги",
"price": ""
"price": "Ціна",
"filament_external": "Зовнішній",
"filament_internal": "Внутрішній",
"initial_weight": "Початкова вага",
"spool_weight": "Порожня вага"
},
"spool": "Котушки",
"titles": {
@@ -52,11 +57,19 @@
"archive": "Архівувати Котушку",
"show": "Відобразити Котушку",
"create": "Створити Котушку",
"clone": "Клонувати Котушку"
"clone": "Клонувати Котушку",
"adjust": "Відрегулюйте філамент з котушки"
},
"form": {
"spool_updated": "Ця котушка була оновлена кимось/чимось після відкриття цієї сторінки. Збереження перезапише ці зміни!",
"new_location_prompt": "Введіть нове розташування"
"new_location_prompt": "Введіть нове розташування",
"adjust_filament_value": "Споживана кількість",
"measurement_type": {
"length": "Довжина",
"weight": "Вага"
},
"measurement_type_label": "Вид вимірювання",
"adjust_filament_help": ""
},
"fields_help": {
"used_weight": "Скільки філаменту було використано з котушки. Для нової котушки слід використовувати 0g.",
@@ -65,10 +78,16 @@
"lot_nr": "Номер партії виробника. Може використовуватися для гарантування рівномірного кольору друку при використанні декількох котушок.",
"remaining_weight": "Скільки філаменту залишилося на котушці. Для нової котушки це має відповідати вазі філаменту на котушці.",
"location": "Де розташована котушка, якщо у вас є кілька місць для зберігання котушок.",
"price": ""
"price": "Ціна цілої котушки. Якщо не вказано, замість неї буде використано ціну філамента.",
"spool_weight": "",
"initial_weight": "",
"external_filament": ""
},
"messages": {
"archive": "Ви впевнені що хочете архівувати цю котушку?"
},
"formats": {
"last_used": ""
}
},
"actions": {
@@ -109,14 +128,25 @@
"printerMarginBottom": "Безпечна зона знизу",
"printerMarginTop": "Безпечна зона зверху",
"printerMarginRight": "Безпечна зона справа",
"printerMarginLeft": "Безпечна зона зліва"
"printerMarginLeft": "Безпечна зона зліва",
"settings": "Передналаштування",
"defaultSettings": "За замовчуванням",
"addSettings": "Додати нове передналаштування",
"newSetting": "Новий",
"itemCopies": "Копії елементів",
"deleteSettings": "Видалити поточне передналаштування",
"duplicateSettings": "Дублікат",
"deleteSettingsConfirm": "Ви впевнені, що бажаєте видалити це передналаштування?",
"settingsName": "Назва передналаштування",
"saveSetting": "Зберегти передналаштування",
"saveAsImage": "Зберегти як зображення"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} обрана котушка",
"selectedTotal_few": "{{count}} обрані котушки",
"selectedTotal_many": "{{count}} обраних котушок",
"showArchived": "Показати Архівні",
"description": "Виберіть котушки для друку QR-кодів.",
"description": "Виберіть котушки для друку етикеток",
"title": "Виберіть котушки",
"selectAll": "Вибрати/Відмінити Всі",
"noSpoolsSelected": "Ви не вибрали жодної котушки."
@@ -130,14 +160,31 @@
"showContent": "Друк етикеток",
"showVendorComment": "Примітка про виробника",
"showSpoolComment": "Примітка про котушку",
"textSize": "Розмір тексту",
"title": "Друк QR-кодів",
"textSize": "Розмір тексту етикетки",
"title": "Друк етикеток",
"showSpoolmanIcon": "Відобразити іконку Spoolman",
"showLotNr": "Номер партії",
"bedTemp": "ТС: {{temp}}",
"lotNr": "Партія номер: {{lot}}",
"showFilamentComment": "Примітка про філамент",
"button": "Друкувати QR-коди"
"button": "Друкувати етикетки",
"useHTTPUrl": {
"label": "Посилання на QR-код",
"preview": "Попередній перегляд:",
"options": {
"default": "За замовчуванням",
"url": "URL"
},
"tooltip": ""
},
"template": "Шаблон етикетки",
"showQRCode": "Надрукувати QR-код",
"showQRCodeMode": {
"no": "Ні",
"simple": "Простий",
"withIcon": "З іконкою"
},
"templateHelp": "Використовуйте {} для вставки значень об'єкту котушки у вигляді тексту. Наприклад, {id} буде замінено ідентифікатором котушки, або {filament.material} буде замінено значенням матеріалу котушки. Відсутне значення буде замінено на \"?\". Щоб видалити його, можна використати ще одні {}. Крім того, будь-який текст між {} буде видалено, якщо значення відсутнє. Наприклад, {Lot Nr: {lot_nr}} покаже напис тільки якщо котушка має номер партії. Загорніть текст подвійними зірочками **, щоб зробити його жирним. Натисніть кнопку, щоб переглянути список усіх доступних тегів."
}
},
"notifications": {
@@ -150,7 +197,9 @@
"importProgress": "Імпортування: {{processed}}/{{total}}",
"createError": "Трапилась помилка при створенні {{resource}} (код помилки: {{statusCode}})",
"editSuccess": "Успішно змінено {{resource}}",
"deleteError": "Помилка при видаленні {{resource}} (код помилки: {{statusCode}})"
"deleteError": "Помилка при видаленні {{resource}} (код помилки: {{statusCode}})",
"saveSuccessful": "Успішно збережено!",
"validationError": "Помилка перевірки: {{error}}"
},
"no": "Ні",
"yes": "Так",
@@ -186,7 +235,12 @@
"article_number": "Артикул",
"comment": "Коментар",
"registered": "Додано",
"spools": "Показати котушки"
"spools": "Показати котушки",
"single_color": "Одноколірний",
"external_id": "Зовнішній ID",
"multi_color": "Багатокольоровий",
"coaxial": "Коекструдтваний",
"longitudinal": ""
},
"titles": {
"show_title": "[Філамент #{{id}}] {{name}}",
@@ -197,16 +251,22 @@
"create": "Створити Філамент"
},
"fields_help": {
"price": "Ціна всієї котушки у валюті, встановленій в системі.",
"price": "Ціна всієї котушки.",
"material": "Наприклад PLA, ABS, PETG, тощо.",
"name": "Назва філаменту, щоб відрізнити цей філамент поміж інших від того самого виробника. Вона повинна містити, наприклад, колір.",
"article_number": "Напр. EAN, UPC тощо.",
"spool_weight": "Вага порожньої котушки. Використовується для визначення виміряної ваги котушки.",
"weight": "Вага повної котушки філаменту (нетто). Ця вага не повинна включати вагу самої котушки, а тільки філаменту. Зазвичай це вказується на упаковці."
"weight": "Вага повної котушки філаменту (нетто). Ця вага не повинна включати вагу самої котушки, а тільки філаменту. Зазвичай це вказується на упаковці.",
"multi_color_direction": ""
},
"filament": "Філаменти",
"form": {
"filament_updated": "Цей філамент хтось/щось оновив після того, як ви відкрили цю сторінку. Збереження перезапише ці зміни!"
"filament_updated": "Цей філамент хтось/щось оновив після того, як ви відкрили цю сторінку. Збереження перезапише ці зміни!",
"import_external": "Імпортувати з зовнішнього джерела",
"import_external_description": ""
},
"buttons": {
"add_spool": "Додати Котушку"
}
},
"unknown": "Невідомий",
@@ -230,11 +290,16 @@
"comment": "Коментар",
"name": "Назва",
"registered": "Додано",
"id": "ІД"
"id": "ІД",
"empty_spool_weight": "Вага пустої котушки",
"external_id": "Зовнішній ID"
},
"vendor": "Виробники",
"form": {
"vendor_updated": "Цей виробник був оновлений кимось/чимось після відкриття цієї сторінки. Збереження перезапише ці зміни!"
},
"fields_help": {
"empty_spool_weight": ""
}
},
"documentTitle": {
@@ -266,6 +331,9 @@
},
"help": {
"list": "Довідка | Spoolman"
},
"locations": {
"list": ""
}
},
"table": {
@@ -282,6 +350,63 @@
},
"kofi": "Задонатити на Ko-fi",
"home": {
"home": "Домашня"
"home": "Домашня",
"welcome": "",
"description": ""
},
"settings": {
"header": "Налаштування",
"general": {
"currency": {
"label": "Валюта"
},
"tab": "Загальне",
"base_url": {
"label": "Базова URL-адреса",
"tooltip": ""
},
"round_prices": {
"label": "",
"tooltip": ""
}
},
"extra_fields": {
"params": {
"key": "Ключ",
"name": "Ім'я",
"field_type": "Тип",
"default_value": "Стандартне значення",
"choices": "Вибір",
"multi_choice": "Вибрати кілька значень",
"unit": "Одиниця",
"order": "Замовлення"
},
"field_type": {
"text": "Текст",
"integer_range": "Цілочисловий діапазон",
"float": "Число з рухомою комою",
"float_range": "Діапазон чисел з рухомою комою",
"boolean": "Логічний тип даних",
"integer": "Ціле число",
"datetime": "Дата та час",
"choice": "Можливісь вибору"
},
"boolean_false": "Ні",
"non_unique_key_error": "Ключ має бути унікальним.",
"tab": "Додаткові поля",
"boolean_true": "Так",
"key_not_changed": "Змініть ключ на інший будь ласка.",
"delete_confirm": "Видалити поле {{name}}?",
"delete_confirm_description": "Це призведе до видалення поля та усіх пов'язаних з ним даних для усіх об'єктів.",
"choices_missing_error": "Ви не можете видалити обрані варіанти. Наступні варіанти відсутні: {{choices}}",
"description": "<p>Тут ви можете додати додаткові користувацькі поля до своїх об’єктів.</p><p>Після додавання поля ви не можете змінити його ключ або тип, а для полів типу вибору ви не можете видалити варіанти або змінити стан кількох варіантів. Якщо ви вилучите поле, пов’язані дані для всіх об’єктів буде видалено.</p><p>Ключ – це те, як інші програми читають/записують дані, тож якщо ваше спеціальне поле має інтегруватися зі сторонніми програми, переконайтеся, що ви правильно її налаштували. Значення за замовчуванням застосовується лише до нових елементів.</p><p>Додаткові поля не можна сортувати чи фільтрувати в режимі перегляду таблиці.</p>"
},
"settings": "Налаштування"
},
"locations": {
"new_location": "",
"no_location": "",
"locations": "",
"no_locations_help": ""
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"actions": {
"clone": "複製",
"show": "顯示",
"list": "列表",
"create": "建立",
"edit": "編輯"
},
"buttons": {
"create": "建立",
"save": "儲存",
"saveAndAdd": "儲存並新增",
"logout": "登出",
"delete": "刪除",
"edit": "編輯",
"cancel": "取消",
"confirm": "您確定嗎?",
"continue": "繼續",
"filter": "篩選",
"clear": "清除",
"refresh": "重新整理",
"show": "檢視",
"undo": "復原",
"import": "匯入",
"clone": "複製",
"archive": "封存",
"unArchive": "取消封存",
"hideArchived": "隱藏已封存",
"showArchived": "檢視已封存",
"notAccessTitle": "您沒有存取的權限",
"hideColumns": "隱藏欄",
"clearFilters": "清除篩選"
},
"warnWhenUnsavedChanges": "您確定要離開嗎?您尚有未儲存的變更。",
"notifications": {
"success": "成功",
"error": "錯誤 ( 狀態碼:{{statusCode}} )",
"undoable": "您有 {{seconds}} 秒可以還原。",
"createSuccess": "成功建立 {{resource}}",
"createError": "建立 {{resource}} 時發生錯誤(狀態碼:{{statusCode}})",
"deleteSuccess": "成功刪除 {{resource}}",
"deleteError": "刪除 {{resource}} 時發生錯誤(狀態碼:{{statusCode}})",
"editSuccess": "成功編輯{{resource}}",
"editError": "編輯 {{resource}} 時發生錯誤(狀態碼:{{statusCode}})",
"importProgress": "匯入中:{{processed}}/{{total}}",
"saveSuccessful": "儲存成功!",
"validationError": "驗證錯誤: {{error}}"
},
"kofi": "在Ko-fi上打賞我",
"loading": "讀取中",
"version": "版本",
"unknown": "未知",
"yes": "是",
"no": "否",
"tags": {
"clone": "複製"
},
"dashboard": {
"title": "儀表板"
},
"printing": {
"generic": {
"title": "列印中",
"description": "調整以下設定以取得所需的列印版面。請注意,印表機與作業系統可能會套用自己的邊距和縮放比例,因此可能需要多次測試。列印實際標籤前,請先使用一般紙張測試。",
"helpMargin": "邊距應設定為與您的標籤紙和印表機相符,調整這些將影響整個網格的大小。",
"rows": "列",
"paperSize": "紙張大小",
"customSize": "自訂",
"dimensions": "尺寸",
"showBorder": "顯示外框",
"previewScale": "預覽縮放比例",
"skipItems": "跳過項目",
"itemCopies": "每項份數",
"contentSettings": "內容設定",
"layoutSettings": "佈局設定",
"horizontalSpacing": "水平間距",
"marginLeft": "左邊界",
"marginRight": "右邊界",
"marginBottom": "下邊界",
"printerMarginLeft": "安全區域左側",
"printerMarginRight": "安全區域右側",
"printerMarginTop": "安全區域上側",
"printerMarginBottom": "安全區域下側",
"borders": {
"border": "邊框",
"grid": "格線",
"none": "無"
},
"defaultSettings": "預設",
"addSettings": "新增預設定",
"newSetting": "新增",
"settings": "預設集",
"settingsName": "預設集名稱",
"duplicateSettings": "複製",
"deleteSettings": "刪除目前預設定",
"deleteSettingsConfirm": "確定要刪除此預設集嗎?",
"saveSetting": "儲存預設集",
"saveAsImage": "另存為圖片",
"print": "列印",
"columns": "欄",
"verticalSpacing": "垂直間距",
"marginTop": "上邊界",
"helpPrinterMargin": "安全區域應設定為印表機能列印的距離紙張的邊界,更改此選項不影響整個網格。"
},
"qrcode": {
"title": "標籤列印中",
"template": "標籤範本",
"button": "列印標籤",
"templateHelp": "使用 {} 插入線捲物件的數值。例如,{id} 會被替換為線捲 ID,而 {filament.material} 則會替換為該線捲的材質。若數值缺失,會以「?」代替。您可以使用第二組 {} 來移除此符號;此外,若數值缺失,兩組 {} 之間的任何文字也都會被一併移除。例如,{批號:{lot_nr}} 僅會在該線捲含有批號資訊時顯示標籤。使用雙星號 ** 包圍文字可設為粗體。點擊按鈕可查看所有可用標籤的清單。",
"textSize": "標籤文字大小",
"showContent": "列印標籤",
"useHTTPUrl": {
"label": "QR 碼連結",
"preview": "預覽:",
"options": {
"default": "預設",
"url": "URL"
},
"tooltip": "將使用專屬連結,僅在使用 Spoolman 的掃描功能掃描時有效(預設)。URL 使用設定中指定的基礎 URL,若未設置則使用目前頁面的 URL。"
},
"showQRCodeMode": {
"no": "否",
"withIcon": "包含圖示",
"simple": "簡易"
},
"showQRCode": "列印QR碼"
},
"spoolSelect": {
"title": "選擇料盤",
"description": "選擇要列印標籤的料盤。",
"showArchived": "檢視已封存",
"noSpoolsSelected": "您尚未選擇任何料盤。",
"selectAll": "選擇/取消全選",
"selectedTotal_other": "已選擇 {{count}} 個線軸"
}
},
"scanner": {
"description": "掃描 Spoolman QR 碼以檢視線軸的詳細資料。",
"error": {
"notAllowed": "您未被允許存取攝影機。",
"streamApiNotSupported": "瀏覽器不支援MediaStream API。",
"notReadable": "攝影機不可讀取。",
"notFound": "未找到攝影機。",
"insecureContext": "此頁面未通過 HTTPS 提供服務。",
"unknown": "發生未知的錯誤。({{error}})"
},
"title": "QR 碼掃描器"
},
"spool": {
"spool": "線軸",
"fields": {
"id": "ID",
"filament": "線材",
"filament_internal": "內部",
"used_weight": "已用重量",
"remaining_weight": "剩餘重量",
"measured_weight": "實測重量",
"used_length": "已用長度",
"remaining_length": "剩餘長度",
"initial_weight": "初始重量",
"spool_weight": "空料盤重量",
"location": "位置",
"lot_nr": "批號",
"first_used": "首次使用",
"last_used": "上次使用",
"registered": "登記於",
"filament_name": "線材",
"filament_external": "外部",
"price": "價格",
"weight_to_use": "重量",
"comment": "備註",
"archived": "封存",
"material": "材料"
},
"fields_help": {
"price": "完整線盤的價格。如果未設定,則會假設為線材的價格。",
"used_weight": "料盤所使用的線材重量。一個新料盤應該為0克。",
"measured_weight": "線材和料盤的重量為多少。",
"initial_weight": "料盤上線材的初始重量(淨重)。如果未設置,將使用線材物件中的重量。",
"spool_weight": "線盤的空盤重量。留空時將使用線材或製造商提供的數值。",
"location": "如果您的料盤存於多個位置,請填寫料盤所在位置。",
"lot_nr": "製造商的批號。如果使用多個線盤,可用於確保列印的顏色一致。",
"weight_to_use": "選擇要輸入的重量值。只有在選擇的線材設置了料盤重量時,才會顯示測量重量。",
"remaining_weight": "料盤所剩餘的線材。對於新的料盤來說應等於料盤重量。",
"external_filament": "您已從外部資料庫選擇了一種線材。在建立此線盤時,系統將自動建立一個線材(以及可能的製造商)。如果您之前已為該線材建立過資料,這可能會產生重複的線材。"
},
"titles": {
"create": "建立線盤",
"clone": "複製線盤",
"edit": "編輯線盤",
"list": "線盤",
"show": "檢視線盤",
"adjust": "調整料盤線材",
"show_title": "[線盤 #{{id}}] {{name}}",
"archive": "封存線盤"
},
"form": {
"measurement_type_label": "測量類型",
"adjust_filament_help": "此處您可直接增加或減少料盤上的線材,正數為減少線材數量,負數為增加線材數量。",
"adjust_filament_value": "消耗量",
"measurement_type": {
"length": "長度",
"weight": "重量"
},
"new_location_prompt": "輸入一個新位置",
"spool_updated": "此線盤已被其他人/程式更新過。若儲存將覆蓋這些變更!"
},
"messages": {
"archive": "您確定要封存此線盤嗎?"
},
"formats": {
"last_used": "上次使用{{date}}"
}
},
"filament": {
"fields": {
"vendor": "製造商",
"name": "名稱",
"material": "材料",
"price": "價格",
"density": "密度",
"diameter": "線徑",
"weight": "重量",
"spool_weight": "料盤重量",
"article_number": "產品編號",
"comment": "備註",
"color_hex": "顏色",
"multi_color": "多色",
"coaxial": "混色",
"settings_extruder_temp": "擠出機溫度",
"settings_bed_temp": "床台溫度",
"external_id": "內部ID",
"spools": "檢視料盤",
"longitudinal": "縱向",
"single_color": "單色",
"id": "ID",
"vendor_name": "製造商",
"registered": "已登記"
},
"fields_help": {
"material": "例如:PLA、ABS、PETG等。",
"name": "線材名稱,用來區分相同製造商的不同線材類型。例如應該包含顏色資訊。",
"article_number": "例如EAN、UPC等。",
"price": "全新料盤價格。",
"spool_weight": "空線盤的重量,用於計算整捲線的實際重量。",
"multi_color_direction": "線材的顏色可通過兩種方式呈現多樣性:一種是共擠技術,例如雙色線材,具有穩定的多色效果;另一種是沿線材長度的顏色變化,例如漸變線材,顏色會隨著線材捲的長度逐漸轉變。",
"weight": "整捲線材的重量(淨重)。此重量不應包含線盤本身的重量,僅限於線材的重量,通常會標示在包裝上。"
},
"titles": {
"create": "建立線材",
"clone": "複製線材",
"edit": "編輯線材",
"list": "線材",
"show": "檢視線材",
"show_title": "[線材 #{{id}}] {{name}}"
},
"form": {
"import_external": "從外部匯入",
"filament_updated": "自您開啟此頁面以後,此線材已被其他人/事物更新。儲存將覆蓋這些變更!",
"import_external_description": "在列表中選擇一種線材,即可自動填入線材建立表單中的相關詳細資訊。此操作將覆蓋您已輸入的所有資料。<br><br>如果需要,當您點擊“確定”時,將自動建立一個製造商對象。"
},
"buttons": {
"add_spool": "增加料盤"
},
"filament": "線材"
},
"vendor": {
"vendor": "製造商",
"fields": {
"name": "名稱",
"empty_spool_weight": "空料盤重量",
"external_id": "外部ID",
"comment": "備註",
"id": "ID",
"registered": "已登記"
},
"fields_help": {
"empty_spool_weight": "此製造商空線盤的重量。"
},
"titles": {
"create": "建立製造商",
"edit": "編輯製造商",
"show_title": "[製造商 #{{id}}] {{name}}",
"show": "檢視製造商",
"clone": "複製製造商",
"list": "製造商"
},
"form": {
"vendor_updated": "此製造商已被其他人/程式更新過。若儲存將覆蓋這些變更!"
}
},
"documentTitle": {
"vendor": {
"create": "建立製造商 | Spoolman",
"clone": "#{{id}} 複製製造商 | Spoolman",
"list": "製造商 | Spoolman",
"edit": "#{{id}} 編輯製造商 | Spoolman",
"show": "#{{id}} 檢視製造商 | Spoolman"
},
"default": "Spoolman",
"suffix": " | Spoolman",
"home": {
"list": "主頁 | Spoolman"
},
"help": {
"list": "說明 | Spoolman"
},
"filament": {
"list": "線材 | Spoolman",
"edit": "#{{id}} 編輯線材 | Spoolman",
"show": "#{{id}} 檢視線材 | Spoolman",
"create": "建立線材 | Spoolman",
"clone": "#{{id}} 複製線材 | Spoolman"
},
"spool": {
"create": "建立線盤 | Spoolman",
"clone": "#{{id}} 複製線盤 | Spoolman",
"show": "#{{id}} 檢視線盤 | Spoolman",
"edit": "#{{id}} 編輯線盤 | Spoolman",
"list": "線盤 | Spoolman"
},
"locations": {
"list": "位置| Spoolman"
}
},
"home": {
"home": "主頁",
"welcome": "歡迎來到您的 Spoolman!",
"description": "您似乎還沒新增任何線捲。請參閱 <helpPageLink>說明頁面</helpPageLink> 以了解如何開始使用。"
},
"help": {
"help": "說明",
"resources": {
"filament": "線材的品牌。它們具有名稱、材料、顏色、線徑等屬性。",
"spool": "特定線材的單個實體線盤。",
"vendor": "製造線材的公司。"
},
"description": "<title>說明</title><p>這裡是一些入門提示。</p><p>Spoolman 儲存三種類型的資料:</p><itemsHelp/><p>要將新線材加入資料庫,首先需要建立一個 <filamentCreateLink>線材</filamentCreateLink> 物件。完成後,接著為該特定線材建立一個 <spoolCreateLink>線盤</spoolCreateLink> 物件。如果之後您再獲得相同線材的其他線盤,只需生成更多的線盤物件,並重複使用相同的線材物件。</p><p>如果您希望追蹤製造商資訊,也可以選擇建立一個 <vendorCreateLink>製造商</vendorCreateLink> 物件。</p><p>您還可以將其他3D列印服務與Spoolman連接,例如Moonraker,它可以自動監控線材使用情況並為您更新線盤物件。請參閱 <readmeLink>Spoolman README</readmeLink> 以獲取設置指南。</p>"
},
"table": {
"actions": "操作"
},
"settings": {
"settings": "設定",
"header": "設定",
"general": {
"tab": "一般",
"currency": {
"label": "貨幣"
},
"base_url": {
"tooltip": "生成 QR 碼等功能時使用的基本 URL。",
"label": "基本URL"
},
"round_prices": {
"label": "價格四捨五入",
"tooltip": "將價格四捨五入至最接近的整數。"
}
},
"extra_fields": {
"tab": "額外欄位",
"params": {
"key": "鍵值",
"name": "名稱",
"field_type": "類型",
"unit": "單位",
"order": "順序",
"default_value": "預設值",
"multi_choice": "多選項",
"choices": "選項"
},
"field_type": {
"text": "文字",
"float_range": "小數範圍",
"datetime": "日期時間",
"boolean": "布林",
"choice": "選項",
"float": "小數",
"integer": "整數",
"integer_range": "整數範圍"
},
"boolean_true": "是",
"boolean_false": "否",
"choices_missing_error": "您無法刪除選項。以下選項缺失:{{choices}}",
"key_not_changed": "請將鍵值更改為其他名稱。",
"delete_confirm": "刪除欄位 {{name}}?",
"description": "<p>在此,您可以為您的實體加入額外的自訂欄位。</p><p>一旦加入欄位,您將無法更改其鍵值或類型,對於選擇類型欄位,您也無法刪除選項或更改多選狀態。如果刪除欄位,所有實體的相關資料將被刪除。</p><p>鍵值是其他程式讀取/寫入資料的方式,因此如果您的自訂欄位需要與第三方程式整合,請確保正確設置。預設值僅適用於新品項。</p><p>額外的欄位無法在表格視圖中排序或過濾。</p>",
"non_unique_key_error": "鍵值必須為唯一。",
"delete_confirm_description": "這將刪除該欄位及所有與之關聯的所有實體資料。"
}
},
"locations": {
"locations": "位置",
"no_location": "未設定位置",
"no_locations_help": "這個頁面讓您將料盤按位置進行整理,請從增加一些線盤來開始!",
"new_location": "新增位置"
},
"sponsor": "在 GitHub 上贊助專案"
}
+220 -87
View File
@@ -21,19 +21,20 @@
"undo": "撤销",
"import": "导入",
"clone": "克隆",
"archive": "存档",
"unArchive": "取消存档",
"archive": "归档",
"unArchive": "解除归档",
"hideArchived": "隐藏存档",
"showArchived": "显示存档",
"showArchived": "显示已归档项",
"notAccessTitle": "您没有访问权限",
"hideColumns": "隐藏列",
"clearFilters": "清除筛选器",
"saveAndAdd": "保存并添加"
"saveAndAdd": "保存并添加",
"continue": "继续"
},
"warnWhenUnsavedChanges": "您确定要离开吗?您有未保存的更改。",
"notifications": {
"success": "操作成功",
"error": "出现错误 (状态码:{{statusCode}})",
"error": "发生错误 (状态码:{{statusCode}})",
"undoable": "您有 {{seconds}} 秒可以撤销。",
"createSuccess": "成功创建 {{resource}}",
"createError": "创建 {{resource}} 时出现错误 (状态码:{{statusCode}})",
@@ -41,7 +42,9 @@
"deleteError": "删除 {{resource}} 时出现错误 (状态码:{{statusCode}})",
"editSuccess": "成功修改 {{resource}}",
"editError": "修改 {{resource}} 时出现错误 (状态码:{{statusCode}})",
"importProgress": "导入中: {{processed}}/{{total}}"
"importProgress": "导入中: {{processed}}/{{total}}",
"saveSuccessful": "保存成功!",
"validationError": "校验错误:{{error}}"
},
"loading": "加载中",
"version": "版本",
@@ -57,96 +60,128 @@
"printing": {
"generic": {
"title": "打印中",
"description": "调整以下设置以获得所需的打印布局。请记住,打印机和您的操作系统可能会应用自己的边距和缩放,因此您可能需要多次尝试后才能正确打印。在实际标签上打印之前,请在一张普通纸上进行测试。",
"helpMargin": "页边距应设置为与您的标签纸和打印机相匹配,更改这些将影响整个网格的大小。",
"helpPrinterMargin": "安全区应设置为打印机可以打印的离纸张边缘距离,更改这些不会影响整个网格。",
"description": "在下方微调设置以获取理想的打印布局。请注意,打印机驱动和操作系统可能会自动应用边距与缩放,因此您可能需要进行几次试打印。建议在正式打印前先用普通纸测试。",
"helpMargin": "页边距应与您的标签纸和打印机相匹配,修改此项会影响整个标签网格的位置。",
"helpPrinterMargin": "安全打印区域应设置为打印机允许贴近纸张边缘打印的极限距离,修改此项不会改变标签网格的位置。",
"print": "打印",
"columns": "行",
"columns": "列数",
"rows": "列",
"paperSize": "纸张尺寸",
"customSize": "自定义",
"dimensions": "尺寸标注",
"customSize": "自定义尺寸",
"dimensions": "尺寸规格",
"showBorder": "显示边框",
"previewScale": "预览比例",
"skipItems": "跳过项目",
"skipItems": "跳过初始格子数",
"contentSettings": "内容设置",
"layoutSettings": "布局设置",
"horizontalSpacing": "水平间距",
"verticalSpacing": "垂直间距",
"marginLeft": "左边界",
"marginRight": "右边界",
"marginTop": "上边界",
"marginBottom": "下边界",
"printerMarginLeft": "安全区左侧",
"printerMarginRight": "安全区右侧",
"printerMarginTop": "安全区顶部",
"printerMarginBottom": "安全区底部",
"horizontalSpacing": "列间距 (水平)",
"verticalSpacing": "行间距 (垂直)",
"marginLeft": "左边距",
"marginRight": "右边距",
"marginTop": "上边距",
"marginBottom": "下边距",
"printerMarginLeft": "左侧安全区域",
"printerMarginRight": "右侧安全区域",
"printerMarginTop": "顶部安全区域",
"printerMarginBottom": "底部安全区域",
"borders": {
"none": "无",
"border": "外边框",
"grid": "格子"
}
},
"defaultSettings": "默认",
"settings": "预设",
"saveSetting": "保存预设",
"settingsName": "预设名称",
"addSettings": "添加新预设",
"deleteSettingsConfirm": "确定删除此预设吗?",
"saveAsImage": "保存为图片",
"newSetting": "新增",
"itemCopies": "单个标签打印份数",
"duplicateSettings": "复制",
"deleteSettings": "删除当前预设"
},
"qrcode": {
"button": "打印二维码",
"title": "打印二维码",
"button": "打印标签",
"title": "打印标签",
"spoolWeight": "料盘重量:{{weight}}",
"lotNr": "批号:{{lot}}",
"bedTemp": "热床温度:{{temp}}",
"extruderTemp": "挤出温度:{{temp}}",
"textSize": "内容文本大小",
"textSize": "标签文本大小",
"showSpoolmanIcon": "显示Spoolman图标",
"showVendor": "供应商",
"showVendor": "制造商",
"showContent": "打印标签",
"showLotNr": "批号",
"showSpoolWeight": "料盘重量",
"showTemperatures": "温度",
"showSpoolComment": "料盘备注",
"showFilamentComment": "耗材备注",
"showVendorComment": "供应商备注"
"showVendorComment": "制造商备注",
"template": "标签模版",
"showQRCode": "打印二维码",
"showQRCodeMode": {
"no": "否",
"withIcon": "含图标",
"simple": "简单"
},
"templateHelp": "使用 {} 将料盘的值插入为文本。例如,{id} 将被替换为料盘的ID,或者 {filament.material} 将被替换为料盘的材质。如果某个值缺失,将用 \"?\" 替代。可以使用第二组 {} 来删除缺失值相关的内容。此外,任意位于两组 {} 之间的文本,如果值缺失,将被一同删除。例如,{Lot Nr: {lot_nr}} 只有在料盘存在批号时才会显示标签内容。用双星号 ** 包裹文本可以将其加粗。点击按钮查看所有可用的标签列表。",
"useHTTPUrl": {
"tooltip": "将使用专有链接,该链接仅在通过 Spoolman 的内置扫描功能扫描时生效(默认)。URL 将使用设置中指定的基准 URL;若未设置,则使用当前页面的 URL。",
"label": "二维码编码内容",
"options": {
"default": "默认(Spoolman 专属格式)",
"url": "标准网页 URL"
},
"preview": "预览:"
}
},
"spoolSelect": {
"title": "选择料盘",
"description": "选择要打印二维码的料盘。",
"description": "选择要打印标签的料盘。",
"showArchived": "显示存档",
"noSpoolsSelected": "您尚未选择任何料盘。",
"selectAll": "选择/取消选择全部",
"selectedTotal_other": "已选择{{count}}个料盘"
"selectedTotal_other": "已选择 {{count}} 卷耗材"
}
},
"scanner": {
"title": "扫码二维码",
"description": "扫描Spoolman二维码,查看有关料盘的详细信息。",
"title": "二维码扫描器",
"description": "扫描 Spoolman 专属二维码以快速查看该耗材卷的详细信息。",
"error": {
"notAllowed": "您不被允许访问相机。",
"insecureContext": "该页面不是通过HTTPS传输。",
"streamApiNotSupported": "浏览器不支持MediaStream API。",
"notAllowed": "您尚未授权允许使用摄像头。",
"insecureContext": "当前页面未通过安全的 HTTPS 协议提供服务。",
"streamApiNotSupported": "当前浏览器不支持 MediaStream API 视频流。",
"notReadable": "相机不可读。",
"notFound": "没有找到相机。",
"unknown": "发生未知错误。({{error}})"
"notFound": "未检测到可用摄像头。",
"unknown": "发生未知错误。({{error}})"
}
},
"spool": {
"spool": "料盘",
"spool": "耗材卷",
"fields": {
"id": "编号",
"id": "编号 (ID)",
"filament_name": "耗材",
"filament": "耗材",
"material": "材料",
"filament": "耗材规格",
"material": "材质",
"weight_to_use": "重量",
"used_weight": "已用重量",
"remaining_weight": "剩余重量",
"measured_weight": "净重",
"used_length": "已用长度",
"remaining_length": "剩余长度",
"location": "位置",
"location": "存放位置",
"lot_nr": "批号",
"first_used": "首次使用",
"last_used": "最后使用",
"registered": "登记时间",
"registered": "录入时间",
"comment": "备注",
"archived": "存档",
"price": ""
"archived": "已归档",
"price": "采购价格",
"spool_weight": "空料盘重量",
"initial_weight": "初始重量",
"filament_internal": "内部",
"filament_external": "外部"
},
"fields_help": {
"weight_to_use": "请选择要输入的重量值。净重只在所选的耗材设置了料盘重量时可用。",
@@ -155,7 +190,10 @@
"measured_weight": "耗材和料盘的重量分别是多少。",
"location": "如果您有多个存放料盘的地方,请填写料盘所在的位置。",
"lot_nr": "制造商的批号。如果使用多个料盘可以使用批号进行控制,以确保打印颜色一致。",
"price": ""
"price": "整卷料盘的价格。如果未设置,则默认使用该耗材的价格。",
"spool_weight": "料盘没料时的重量。留空时使用耗材或者制造商的相关参数。",
"external_filament": "您已从外部数据库中选择了一种耗材。在创建这个料盘时,将自动创建一个耗材对象(可能还包括一个制造商对象)。如果您已经为这种耗材创建了一个耗材对象,这可能会导致耗材对象重复。",
"initial_weight": "料盘上耗材的初始重量(净重)。如果未设置将使用耗材的相关参数。"
},
"titles": {
"create": "创建料盘",
@@ -164,44 +202,61 @@
"list": "料盘列表",
"show": "查看料盘",
"archive": "料盘存档",
"show_title": "[料盘 #{{id}}] {{name}}"
"show_title": "[料盘 #{{id}}] {{name}}",
"adjust": "调整线盘耗材"
},
"messages": {
"archive": "您确定要存档此料盘吗?"
},
"form": {
"new_location_prompt": "输入一个新位置",
"spool_updated": "自您打开此页面以来,此料盘已被其他人/事物更新。保存将覆盖这些更改!"
"spool_updated": "自您打开此页面以来,此料盘已被其他人/事物更新。保存将覆盖这些更改!",
"measurement_type": {
"length": "长度",
"weight": "重量"
},
"measurement_type_label": "测量类型",
"adjust_filament_help": "在这里你可以直接增加或减少线盘中的耗材。正数是减少耗材数量,负数是增加耗材数量。",
"adjust_filament_value": "消耗量"
},
"formats": {
"last_used": "上次使用 {{date}}"
}
},
"filament": {
"filament": "耗材",
"filament": "耗材规格",
"fields": {
"id": "编号",
"vendor_name": "供应商",
"vendor": "供应商",
"id": "ID",
"vendor_name": "制造商",
"vendor": "厂商",
"name": "名称",
"material": "材料",
"material": "材质",
"price": "价格",
"density": "密度",
"diameter": "直径",
"weight": "重量",
"diameter": "线径",
"weight": "耗材净重",
"spool_weight": "料盘重量",
"article_number": "产品编号",
"registered": "登记时间",
"registered": "录入时间",
"comment": "备注",
"settings_extruder_temp": "挤出头温度",
"settings_bed_temp": "热床温度",
"color_hex": "颜色",
"spools": "显示料盘"
"spools": "显示料盘",
"external_id": "外部ID",
"multi_color": "多色",
"longitudinal": "纵向渐变",
"coaxial": "双色共挤",
"single_color": "单色"
},
"fields_help": {
"name": "耗材名称,用于区分来自同一供应商的不同耗材类型。例如包含颜色信息。",
"name": "耗材名称,用于区分来自同一制造商的不同耗材类型。例如在耗材名称中包含颜色信息。",
"material": "例如PLA、ABS、PETG等。",
"price": "一整卷耗材的价格。",
"weight": "一整卷耗材的重量(净重)。不应包括料盘本身的重量,仅计算耗材的重量。通常在包装上注明。",
"spool_weight": "空料盘的重量。使用它来计算耗材的重量。",
"article_number": "例如EAN、UPC等。"
"article_number": "例如EAN、UPC等。",
"multi_color_direction": "耗材可以通过两种方式实现多种颜色:共挤出:例如双色耗材,具有稳定的多色组合。纵向颜色变化:例如渐变色耗材,颜色沿着料盘逐渐变化。"
},
"titles": {
"create": "创建耗材",
@@ -212,27 +267,37 @@
"show_title": "[耗材 #{{id}}] {{name}}"
},
"form": {
"filament_updated": "自您打开此页面以来,此耗材已被其他人/事物更新。保存将覆盖这些更改!"
"filament_updated": "自您打开此页面以来,此耗材已被其他人/事物更新。保存将覆盖这些更改!",
"import_external_description": "在列表中选择一种耗材以自动填充耗材创建表单中的详细信息。这将覆盖您在表单中输入的任何数据。<br><br>如果有必要,在您点击“确定”时将自动创建一个制造商对象。",
"import_external": "从外部导入"
},
"buttons": {
"add_spool": "添加料盘"
}
},
"vendor": {
"vendor": "供应商",
"vendor": "制造商",
"fields": {
"id": "编号",
"name": "名称",
"registered": "登记时间",
"comment": "备注"
"id": "ID",
"name": "厂商名称",
"registered": "录入时间",
"comment": "备注",
"external_id": "外部ID",
"empty_spool_weight": "空料盘重量"
},
"titles": {
"create": "创建供应商",
"clone": "克隆供应商",
"edit": "编辑供应商",
"list": "供应商列表",
"show": "查看供应商",
"show_title": "[供应商 #{{id}}] {{name}}"
"create": "创建制造商",
"clone": "克隆制造商",
"edit": "编辑制造商",
"list": "制造商列表",
"show": "查看制造商",
"show_title": "[制造商 #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "自您打开此页面以来,此供应商已被其他人/事物更新。保存将覆盖这些更改!"
"vendor_updated": "自您打开此页面以来,此制造商已被其他人/事物更新。保存将覆盖这些更改!"
},
"fields_help": {
"empty_spool_weight": "来自此制造商的空料盘重量。"
}
},
"table": {
@@ -256,30 +321,98 @@
"clone": "#{{id}} 克隆料盘 | Spoolman"
},
"vendor": {
"list": "供应商 | Spoolman",
"show": "#{{id}} 查看供应商 | Spoolman",
"edit": "#{{id}} 编辑供应商 | Spoolman",
"create": "创建供应商 | Spoolman",
"clone": "#{{id}} 克隆供应商 | Spoolman"
"list": "制造商 | Spoolman",
"show": "#{{id}} 查看制造商 | Spoolman",
"edit": "#{{id}} 编辑制造商 | Spoolman",
"create": "创建制造商 | Spoolman",
"clone": "#{{id}} 克隆制造商 | Spoolman"
},
"home": {
"list": ""
"list": "主页 | Spoolman"
},
"help": {
"list": ""
"list": "帮助 | Spoolman"
},
"locations": {
"list": "位置 | Spoolman"
}
},
"kofi": "",
"kofi": "在Ko-fi上给我打赏一点小费",
"home": {
"home": ""
"home": "主页",
"welcome": "欢迎使用您的 Spoolman 实例!",
"description": "看来您尚未添加任何耗材。请参阅 <helpPageLink> 帮助页面 </helpPageLink> ,了解入门指南。"
},
"help": {
"help": "",
"help": "帮助",
"resources": {
"filament": "",
"vendor": "",
"spool": ""
"filament": "耗材品种。它们具有名称、材料、颜色、直径等属性。",
"vendor": "制造耗材的公司。",
"spool": "特定耗材的单个物理卷轴。"
},
"description": ""
"description": "<title>帮助</title><p>以下是一些入门提示。</p><p>Spoolman保存了3种不同类型的数据:</p><itemsHelp/><p>要向数据库添加新的料盘,请先创建一个 <filamentCreateLink>耗材</filamentCreateLink> 对象。完成后,继续为该特定耗材创建一个 <spoolCreateLink>料盘</spoolCreateLink> 对象。如果以后获取相同耗材的其他料盘,只需生成其他的料盘对象并重复使用同一个耗材对象。</p><p>同时,如果您希望跟踪该信息,还可以为制造耗材的公司生成一个 <vendorCreateLink>制造商</vendorCreateLink> 对象。</p><p>您可以选择将其他3D打印机服务链接到 Spoolman,例如 Moonraker,它可以自动监视耗材使用情况并为您更新料盘对象。请参阅 <readmeLink>Spoolman README</readmeLink> 获取有关如何设置的指导。</p>"
},
"settings": {
"settings": "设置",
"extra_fields": {
"tab": "额外字段",
"field_type": {
"integer_range": "整数范围",
"float": "浮点数",
"float_range": "浮点数范围",
"boolean": "布尔类型",
"datetime": "日期时间",
"text": "文本",
"integer": "整数",
"choice": "选项"
},
"delete_confirm_description": "这将删除该字段以及所有实体的关联数据。",
"description": "<p>您可以在此处为不同的实体对象添加自定义扩展字段。</p><p>字段一旦添加,其键名 (Key) 和数据类型将无法修改;对于单选/多选字段,已有的选项将无法删除,也无法更改其多选状态。如果您删除某个字段,所有对应实体中存储的该字段数据都将被永久抹除。</p><p>键名 (Key) 是第三方程序读取/写入数据时使用的唯一标识。如果您需要将自定义字段与第三方程序集成,请务必设置正确的键名。默认值仅应用于新建的项目。</p><p>扩展字段可以显示在表格视图中并支持排序。单选框与布尔值字段还提供相应的筛选选项,且所有扩展字段类型均支持筛选空值。</p>",
"params": {
"name": "名称",
"field_type": "类型",
"order": "顺序",
"default_value": "默认值",
"multi_choice": "多选",
"choices": "选择项",
"key": "键",
"unit": "单位"
},
"boolean_true": "是",
"boolean_false": "否",
"choices_missing_error": "您无法删除选择。以下选项丢失:{{choices}}",
"non_unique_key_error": "键必须是唯一的。",
"key_not_changed": "请将键改成其他的。",
"delete_confirm": "删除字段 {{name}}?",
"choices_reorder_hint": "拖动或使用方向键来重新排列此选项的顺序。"
},
"header": "系统设置",
"general": {
"tab": "常规设置",
"currency": {
"label": "货币单位"
},
"base_url": {
"label": "基础URL",
"tooltip": "生成功能(例如二维码)时使用的基础 URL。"
},
"round_prices": {
"label": "取整",
"tooltip": "将价格四舍五入到最接近的整数。"
}
}
},
"locations": {
"no_locations_help": "此页面允许您在不同位置管理料盘,添加一些料盘即可开始!",
"locations": "位置",
"new_location": "新位置",
"no_location": "没有位置"
},
"sponsor": "在 GitHub 上赞助项目",
"theme": {
"label": "主题",
"system": "系统",
"light": "明亮",
"dark": "暗黑"
}
}
-22
View File
@@ -1,22 +0,0 @@
{
"name": "Spoolman",
"short_name": "Spoolman",
"description": "Keep track of your inventory of 3D-printer filament spools.",
"icons": [
{
"purpose": "maskable",
"sizes": "512x512",
"src": "icon512_maskable.png",
"type": "image/png"
},
{
"purpose": "any",
"sizes": "512x512",
"src": "icon512_rounded.png",
"type": "image/png"
}
],
"background_color": "#1F1F1F",
"theme_color": "#DC7734",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env node
import { readdirSync, readFileSync, statSync } from "fs";
import { dirname, join } from "path";
import process from "process";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = join(__dirname, "../public/locales");
const I18N_FILE = join(__dirname, "../src/i18n.ts");
const REFERENCE_LOCALE = "en";
const MIN_TRANSLATED_RATIO = 0.5; // A locale must be at least 50% translated to be included
/**
* Recursively collect all leaf values of a translation object, keyed by their
* dotted path. Only string leaves are considered translatable.
*/
function collectLeaves(obj, prefix, out) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === "object") {
collectLeaves(value, path, out);
} else if (typeof value === "string") {
out[path] = value;
}
}
return out;
}
function loadLocale(locale) {
const commonFilePath = join(LOCALES_DIR, locale, "common.json");
return collectLeaves(JSON.parse(readFileSync(commonFilePath, "utf8")), "", {});
}
/**
* Ratio of keys in `locale` that are present, non-empty, and different from the
* English reference. Missing keys and untranslated (identical) strings count as
* not translated.
*/
function translatedRatio(reference, locale) {
const refKeys = Object.keys(reference);
if (refKeys.length === 0) return 0;
let translated = 0;
for (const key of refKeys) {
const value = locale[key];
if (typeof value === "string" && value.trim() !== "" && value !== reference[key]) {
translated++;
}
}
return translated / refKeys.length;
}
function getLocaleFolders() {
return readdirSync(LOCALES_DIR).filter((folder) => {
const folderPath = join(LOCALES_DIR, folder);
const commonFilePath = join(folderPath, "common.json");
return statSync(folderPath).isDirectory() && statSync(commonFilePath, { throwIfNoEntry: false })?.isFile();
});
}
function getDeclaredLanguages() {
const i18nContent = readFileSync(I18N_FILE, "utf8");
const languageMatches = [...i18nContent.matchAll(/\["(.*?)"\]:/g)];
return languageMatches.map((match) => match[1]);
}
function main() {
const reference = loadLocale(REFERENCE_LOCALE);
const declaredLocales = new Set(getDeclaredLanguages());
// Locales that are sufficiently translated but not yet declared in i18n.ts
const missingLocales = [];
for (const locale of getLocaleFolders()) {
if (locale === REFERENCE_LOCALE || declaredLocales.has(locale)) continue;
const ratio = translatedRatio(reference, loadLocale(locale));
if (ratio >= MIN_TRANSLATED_RATIO) {
missingLocales.push({ locale, ratio });
}
}
if (missingLocales.length > 0) {
console.error("❌ The following locales are at least 50% translated but missing from src/i18n.ts:");
missingLocales.forEach(({ locale, ratio }) =>
console.error(` - ${locale} (${Math.round(ratio * 100)}% translated)`),
);
console.error("⚠️ Please add them to the `languages` object in i18n.ts.");
console.log("Template:");
for (const { locale } of missingLocales) {
console.log(`["${locale}"]: {
name: "",
fullCode: "",
djs: () => import("dayjs/locale/${locale.toLowerCase()}"),
},`);
}
process.exit(1);
}
console.log("✅ All sufficiently translated locales are declared in i18n.ts.");
process.exit(0);
}
main();
+31 -24
View File
@@ -5,27 +5,30 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ErrorComponent } from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router-v6";
import dataProvider from "./components/dataProvider";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { ColorModeContextProvider } from "./contexts/color-mode";
import {
FileOutlined,
HighlightOutlined,
HomeOutlined,
QuestionOutlined,
TableOutlined,
ToolOutlined,
UserOutlined,
} from "@ant-design/icons";
import { ConfigProvider } from "antd";
import React from "react";
import { Locale } from "antd/es/locale";
import { languages } from "./i18n";
import loadable from "@loadable/component";
import SpoolmanNotificationProvider from "./components/notificationProvider";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
import { ConfigProvider } from "antd";
import { Locale } from "antd/es/locale";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import dataProvider from "./components/dataProvider";
import { Favicon } from "./components/favicon";
import { SpoolmanLayout } from "./components/layout";
import liveProvider from "./components/liveProvider";
import SpoolmanNotificationProvider from "./components/notificationProvider";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { languages } from "./i18n";
import { getAPIURL, getBasePath } from "./utils/url";
interface ResourcePageProps {
resource: "spools" | "filaments" | "vendors";
@@ -38,7 +41,7 @@ const LoadableResourcePage = loadable(
{
fallback: <div>Page is Loading...</div>,
cacheKey: (props: ResourcePageProps) => `${props.resource}-${props.page}-${props.mode ?? ""}`,
}
},
);
interface LoadablePageProps {
@@ -60,8 +63,8 @@ function App() {
};
// Fetch the antd locale using dynamic imports
const [antdLocale, setAntdLocale] = React.useState<Locale | undefined>();
React.useEffect(() => {
const [antdLocale, setAntdLocale] = useState<Locale | undefined>();
useEffect(() => {
const fetchLocale = async () => {
const locale = await import(
`./../node_modules/antd/es/locale/${languages[i18n.language].fullCode.replace("-", "_")}.js`
@@ -84,23 +87,16 @@ function App() {
}
return (
<BrowserRouter>
<BrowserRouter basename={getBasePath() + "/"}>
<RefineKbarProvider>
<ColorModeContextProvider>
<ConfigProvider
locale={antdLocale}
theme={{
token: {
colorPrimary: "#dc7734",
},
}}
>
<ConfigProvider locale={antdLocale}>
<Refine
dataProvider={dataProvider(import.meta.env.VITE_APIURL)}
dataProvider={dataProvider(getAPIURL())}
notificationProvider={SpoolmanNotificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
liveProvider={liveProvider(import.meta.env.VITE_APIURL)}
liveProvider={liveProvider(getAPIURL())}
resources={[
{
name: "home",
@@ -146,6 +142,14 @@ function App() {
icon: <UserOutlined />,
},
},
{
name: "locations",
list: "/locations",
meta: {
canDelete: false,
icon: <TableOutlined />,
},
},
{
name: "settings",
list: "/settings",
@@ -190,6 +194,7 @@ function App() {
/>
<Route path="edit/:id" element={<LoadableResourcePage resource="spools" page="edit" />} />
<Route path="show/:id" element={<LoadableResourcePage resource="spools" page="show" />} />
<Route path="print" element={<LoadablePage name="printing" />} />
</Route>
<Route path="/filament">
<Route index element={<LoadableResourcePage resource="filaments" page="list" />} />
@@ -219,6 +224,7 @@ function App() {
</Route>
<Route path="/settings/*" element={<LoadablePage name="settings" />} />
<Route path="/help" element={<LoadablePage name="help" />} />
<Route path="/locations" element={<LoadablePage name="locations" />} />
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
@@ -227,6 +233,7 @@ function App() {
<UnsavedChangesNotifier />
<DocumentTitleHandler />
<ReactQueryDevtools />
<Favicon url={getBasePath() + "/favicon.svg"} />
</Refine>
</ConfigProvider>
</ColorModeContextProvider>
+257 -49
View File
@@ -1,19 +1,18 @@
import { Button, Col, Dropdown, Row, Space, Spin } from "antd";
import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
import { DateField, TextField } from "@refinedev/antd";
import { UseQueryResult } from "@tanstack/react-query";
import { Button, Col, DatePicker, Dropdown, Input, InputNumber, Row, Space, Spin } from "antd";
import { ColumnFilterItem, ColumnType, FilterDropdownProps } from "antd/es/table/interface";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { AlignType } from "rc-table/lib/interface";
import { Link } from "react-router";
import { getFiltersForField, typeFilters } from "../utils/filtering";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { TableState } from "../utils/saveload";
import { getSortOrderForField, typeSorters } from "../utils/sorting";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { DateField, TextField } from "@refinedev/antd";
import Icon from "@ant-design/icons";
import SpoolIcon from "../icon_spool.svg?react";
import { useTranslate } from "@refinedev/core";
import { enrichText } from "../utils/parsing";
import { UseQueryResult } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { Field, FieldType } from "../utils/queryFields";
import SpoolIcon from "./spoolIcon";
dayjs.extend(utc);
@@ -28,6 +27,156 @@ const FilterDropdownLoading = () => {
);
};
function TextFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) {
const hasValue = selectedKeys.length > 0;
return (
<>
<div style={{ padding: "8px" }}>
<Input
placeholder="Search..."
value={selectedKeys[0] as string}
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => confirm()}
/>
</div>
<div className="ant-table-filter-dropdown-btns">
<Button
type="link"
size="small"
disabled={!hasValue}
onClick={() => {
clearFilters?.();
confirm();
}}
>
Reset
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
OK
</Button>
</div>
</>
);
}
function NumberRangeFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
precision,
}: FilterDropdownProps & { precision?: number }) {
const current = selectedKeys[0] as string | undefined;
let minVal: number | null = null;
let maxVal: number | null = null;
if (current && current.includes(":")) {
const parts = current.split(":", 2);
minVal = parts[0] ? Number(parts[0]) : null;
maxVal = parts[1] ? Number(parts[1]) : null;
}
const updateKeys = (min: number | null, max: number | null) => {
if (min === null && max === null) {
setSelectedKeys([]);
} else {
setSelectedKeys([`${min !== null ? min : ""}:${max !== null ? max : ""}`]);
}
};
const hasValue = selectedKeys.length > 0;
return (
<>
<div style={{ padding: "8px", display: "flex", flexDirection: "column", gap: 4 }}>
<InputNumber
placeholder="Min..."
value={minVal}
precision={precision ?? 0}
onChange={(value) => updateKeys(value, maxVal)}
/>
<InputNumber
placeholder="Max..."
value={maxVal}
precision={precision ?? 0}
onChange={(value) => updateKeys(minVal, value)}
/>
</div>
<div className="ant-table-filter-dropdown-btns">
<Button
type="link"
size="small"
disabled={!hasValue}
onClick={() => {
clearFilters?.();
confirm();
}}
>
Reset
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
OK
</Button>
</div>
</>
);
}
function DateTimeRangeFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) {
const current = selectedKeys[0] as string | undefined;
let fromVal: dayjs.Dayjs | null = null;
let toVal: dayjs.Dayjs | null = null;
if (current && current.includes("|")) {
const parts = current.split("|", 2);
fromVal = parts[0] ? dayjs(parts[0]) : null;
toVal = parts[1] ? dayjs(parts[1]) : null;
}
const updateKeys = (from: dayjs.Dayjs | null, to: dayjs.Dayjs | null) => {
if (from === null && to === null) {
setSelectedKeys([]);
} else {
setSelectedKeys([`${from ? from.utc().toISOString() : ""}|${to ? to.utc().toISOString() : ""}`]);
}
};
const hasValue = selectedKeys.length > 0;
return (
<>
<div style={{ padding: "8px", display: "flex", flexDirection: "column", gap: 4 }}>
<DatePicker
showTime={{ use12Hours: false }}
format="YYYY-MM-DD HH:mm:ss"
placeholder="From..."
value={fromVal}
onChange={(date) => updateKeys(date, toVal)}
/>
<DatePicker
showTime={{ use12Hours: false }}
format="YYYY-MM-DD HH:mm:ss"
placeholder="To..."
value={toVal}
onChange={(date) => updateKeys(fromVal, date)}
/>
</div>
<div className="ant-table-filter-dropdown-btns">
<Button
type="link"
size="small"
disabled={!hasValue}
onClick={() => {
clearFilters?.();
confirm();
}}
>
Reset
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
OK
</Button>
</div>
</>
);
}
interface Entity {
id: number;
}
@@ -41,10 +190,11 @@ export interface Action {
interface BaseColumnProps<Obj extends Entity> {
id: string | string[];
dataId?: keyof Obj & string;
dataId?: (keyof Obj & string) | string; // Allow string values for custom fields
i18ncat?: string;
i18nkey?: string;
title?: string;
align?: AlignType;
sorter?: boolean;
t: (key: string) => string;
navigate: (link: string) => void;
@@ -53,6 +203,7 @@ interface BaseColumnProps<Obj extends Entity> {
width?: number;
actions?: (record: Obj) => Action[];
transform?: (value: unknown) => unknown;
render?: (rawValue: string | undefined, record: Obj) => React.ReactNode;
}
interface FilteredColumnProps {
@@ -61,6 +212,7 @@ interface FilteredColumnProps {
allowMultipleFilters?: boolean;
onFilterDropdownOpen?: () => void;
loadingFilters?: boolean;
filterDropdown?: (props: FilterDropdownProps) => React.ReactNode;
}
interface CustomColumnProps<Obj> {
@@ -68,13 +220,13 @@ interface CustomColumnProps<Obj> {
render?: (value: any, record: Obj, index: number) => React.ReactNode;
onCell?: (
data: Obj,
index?: number
index?: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => React.HTMLAttributes<any> | React.TdHTMLAttributes<any>;
}
function Column<Obj extends Entity>(
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>,
): ColumnType<Obj> | undefined {
const t = props.t;
const navigate = props.navigate;
@@ -87,6 +239,7 @@ function Column<Obj extends Entity>(
const columnProps: ColumnType<Obj> = {
dataIndex: props.id,
align: props.align,
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
filterMultiple: props.allowMultipleFilters ?? true,
width: props.width ?? undefined,
@@ -96,10 +249,8 @@ function Column<Obj extends Entity>(
// Sorting
if (props.sorter) {
columnProps.sorter = true;
columnProps.sortOrder = getSortOrderForField(
typeSorters<Obj>(props.tableState.sorters),
props.dataId ?? (props.id as keyof Obj)
);
const sortField = props.dataId ?? (Array.isArray(props.id) ? props.id.join(".") : props.id);
columnProps.sortOrder = getSortOrderForField(typeSorters<Obj>(props.tableState.sorters), sortField);
}
// Filter
@@ -109,14 +260,23 @@ function Column<Obj extends Entity>(
if (props.loadingFilters) {
columnProps.filterDropdown = <FilterDropdownLoading />;
}
columnProps.onFilterDropdownOpenChange = (open) => {
if (open && props.onFilterDropdownOpen) {
props.onFilterDropdownOpen();
}
columnProps.filterDropdownProps = {
onOpenChange: (open) => {
if (open && props.onFilterDropdownOpen) {
props.onFilterDropdownOpen();
}
},
};
if (props.dataId) {
columnProps.key = props.dataId;
}
} else if (props.filterDropdown) {
columnProps.filterDropdown = props.filterDropdown;
columnProps.filteredValue = props.filteredValue;
columnProps.filterMultiple = false;
if (props.dataId) {
columnProps.key = props.dataId;
}
}
// Render
@@ -170,7 +330,7 @@ export function SortedColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
}
export function RichColumn<Obj extends Entity>(
props: Omit<BaseColumnProps<Obj>, "transform"> & { transform?: (value: unknown) => string }
props: Omit<BaseColumnProps<Obj>, "transform"> & FilteredColumnProps & { transform?: (value: unknown) => string },
) {
return Column({
...props,
@@ -182,7 +342,7 @@ export function RichColumn<Obj extends Entity>(
}
interface FilteredQueryColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[]>;
filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[], unknown>;
allowMultipleFilters?: boolean;
}
@@ -195,7 +355,7 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -203,11 +363,12 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
}
filters.push({
text: "<empty>",
value: "<empty>",
value: "",
});
const typedFilters = typeFilters<Obj>(props.tableState.filters);
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj));
const filterField = props.dataId ?? (Array.isArray(props.id) ? props.id.join(".") : props.id);
const filteredValue = getFiltersForField(typedFilters, filterField);
const onFilterDropdownOpen = () => {
query.refetch();
@@ -216,7 +377,7 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
return Column({ ...props, filters, filteredValue, onFilterDropdownOpen, loadingFilters: query.isLoading });
}
interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj>, FilteredColumnProps {
unit: string;
maxDecimals?: number;
minDecimals?: number;
@@ -226,6 +387,7 @@ interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
return Column({
...props,
align: "right",
render: (rawValue) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
if (value === null || value === undefined) {
@@ -245,7 +407,7 @@ export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>)
});
}
export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj> & FilteredColumnProps) {
return Column({
...props,
render: (rawValue) => {
@@ -255,18 +417,19 @@ export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
hidden={!value}
value={dayjs.utc(value).local()}
title={dayjs.utc(value).local().format()}
format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
/>
);
},
});
}
export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Action[]): ColumnType<Obj> | undefined {
const t = useTranslate();
export function ActionsColumn<Obj extends Entity>(
title: string,
actionsFn: (record: Obj) => Action[],
): ColumnType<Obj> | undefined {
return {
title: t("table.actions"),
title,
responsive: ["lg"],
render: (_, record) => {
const buttons = actionsFn(record).map((action) => {
@@ -295,7 +458,7 @@ export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Ac
}
interface SpoolIconColumnProps<Obj extends Entity> extends FilteredQueryColumnProps<Obj> {
color: (record: Obj) => string | undefined;
color: (record: Obj) => string | { colors: string[]; vertical: boolean } | undefined;
}
export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<Obj>) {
@@ -307,7 +470,7 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -319,7 +482,8 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
});
const typedFilters = typeFilters<Obj>(props.tableState.filters);
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj));
const filterField = props.dataId ?? (Array.isArray(props.id) ? props.id.join(".") : props.id);
const filteredValue = getFiltersForField(typedFilters, filterField);
const onFilterDropdownOpen = () => {
query.refetch();
@@ -342,19 +506,12 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
},
render: (rawValue, record: Obj) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
const colorStr = props.color(record);
const colorObj = props.color(record);
return (
<Row wrap={false} justify="space-around" align="middle">
{colorStr && (
{colorObj && (
<Col flex="none">
<Icon
component={SpoolIcon}
style={{
color: "#" + colorStr,
fontSize: 42,
marginRight: 0,
}}
/>
<SpoolIcon color={colorObj} />
</Col>
)}
<Col flex="auto">{value}</Col>
@@ -390,13 +547,46 @@ export function NumberRangeColumn<Obj extends Entity>(props: NumberColumnProps<O
});
}
// Helper function to create filter items for custom fields (boolean and choice only)
function createCustomFieldFilters(field: Field): ColumnFilterItem[] {
const filters: ColumnFilterItem[] = [];
if (field.field_type === FieldType.boolean) {
// "No" maps to empty ("") because the backend treats unset and false as equivalent
return [
{ text: "Yes", value: "true" },
{ text: "No / Not set", value: "" },
];
}
// For choice fields, add each choice as a filter option
if (field.field_type === FieldType.choice && field.choices) {
field.choices.forEach((choice) => {
filters.push({
text: choice,
value: `"${choice}"`,
});
});
}
filters.push({ text: "<empty>", value: "" });
return filters;
}
export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProps<Obj>, "id"> & { field: Field }) {
const field = props.field;
const fieldId = `extra.${field.key}`;
const typedFilters = typeFilters<Obj>(props.tableState.filters);
const filteredValue = getFiltersForField(typedFilters, fieldId);
const commonProps = {
...props,
id: ["extra", field.key],
title: field.name,
sorter: false,
sorter: true,
dataId: fieldId,
transform: (value: unknown) => {
if (value === null || value === undefined) {
return undefined;
@@ -408,12 +598,16 @@ export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProp
if (field.field_type === FieldType.integer) {
return NumberColumn({
...commonProps,
filterDropdown: (p: FilterDropdownProps) => <NumberRangeFilterDropdown {...p} precision={0} />,
filteredValue,
unit: field.unit ?? "",
maxDecimals: 0,
});
} else if (field.field_type === FieldType.float) {
return NumberColumn({
...commonProps,
filterDropdown: (p: FilterDropdownProps) => <NumberRangeFilterDropdown {...p} precision={3} />,
filteredValue,
unit: field.unit ?? "",
minDecimals: 0,
maxDecimals: 3,
@@ -421,12 +615,16 @@ export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProp
} else if (field.field_type === FieldType.integer_range) {
return NumberRangeColumn({
...commonProps,
filterDropdown: (p: FilterDropdownProps) => <NumberRangeFilterDropdown {...p} precision={0} />,
filteredValue,
unit: field.unit ?? "",
maxDecimals: 0,
});
} else if (field.field_type === FieldType.float_range) {
return NumberRangeColumn({
...commonProps,
filterDropdown: (p: FilterDropdownProps) => <NumberRangeFilterDropdown {...p} precision={3} />,
filteredValue,
unit: field.unit ?? "",
minDecimals: 0,
maxDecimals: 3,
@@ -434,14 +632,20 @@ export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProp
} else if (field.field_type === FieldType.text) {
return RichColumn({
...commonProps,
filterDropdown: TextFilterDropdown,
filteredValue,
});
} else if (field.field_type === FieldType.datetime) {
return DateColumn({
...commonProps,
filterDropdown: DateTimeRangeFilterDropdown,
filteredValue,
});
} else if (field.field_type === FieldType.boolean) {
return Column({
...commonProps,
filters: createCustomFieldFilters(field),
filteredValue,
render: (rawValue) => {
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
let text;
@@ -458,6 +662,8 @@ export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProp
} else if (field.field_type === FieldType.choice && !field.multi_choice) {
return Column({
...commonProps,
filters: createCustomFieldFilters(field),
filteredValue,
render: (rawValue) => {
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
return <TextField value={value} />;
@@ -466,6 +672,8 @@ export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProp
} else if (field.field_type === FieldType.choice && field.multi_choice) {
return Column({
...commonProps,
filters: createCustomFieldFilters(field),
filteredValue,
render: (rawValue) => {
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
return <TextField value={(value as string[] | undefined)?.join(", ")} />;
+24 -6
View File
@@ -1,14 +1,16 @@
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
import { DataProvider } from "@refinedev/core";
import { axiosInstance } from "@refinedev/simple-rest";
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
import { getCustomFieldFilters } from "../utils/filtering";
import { isCustomField } from "../utils/queryFields";
type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";
const dataProvider = (
apiUrl: string,
httpClient: AxiosInstance = axiosInstance
httpClient: AxiosInstance = axiosInstance,
): Omit<Required<DataProvider>, "createMany" | "updateMany" | "deleteMany"> => ({
getList: async ({ resource, meta, pagination, sorters, filters }) => {
const url = `${apiUrl}/${resource}`;
@@ -19,26 +21,36 @@ const dataProvider = (
if (pagination && pagination.mode == "server") {
const pageSize = pagination.pageSize ?? 10;
const offset = ((pagination.current ?? 1) - 1) * pageSize;
const offset = ((pagination.currentPage ?? 1) - 1) * pageSize;
queryParams["limit"] = pageSize;
queryParams["offset"] = offset;
}
if (sorters && sorters.length > 0) {
// Map all sorters, including custom field sorters
queryParams["sort"] = sorters
.map((sort) => {
const field = sort.field;
// Custom field sorters are already in the correct format (extra.field_key)
return `${field}:${sort.order}`;
})
.join(",");
}
if (filters && filters.length > 0) {
// Process regular filters
filters.forEach((filter) => {
if (!("field" in filter)) {
throw Error("Filter must be a LogicalFilter.");
}
const field = filter.field;
// Skip custom fields, they'll be handled separately
if (typeof field === "string" && isCustomField(field)) {
return;
}
if (filter.value.length > 0) {
const filterValueArray = Array.isArray(filter.value) ? filter.value : [filter.value];
@@ -54,6 +66,14 @@ const dataProvider = (
queryParams[field] = filterValue;
}
});
// Process custom field filters
const customFieldFilters = getCustomFieldFilters(filters);
Object.entries(customFieldFilters).forEach(([key, values]) => {
if (values.length > 0) {
queryParams[`extra.${key}`] = values.map((value) => (value === "<empty>" ? "" : value)).join(",");
}
});
}
const { data, headers } = await httpClient[requestMethod](`${url}`, {
@@ -61,8 +81,6 @@ const dataProvider = (
params: queryParams,
});
// console.log(url, requestMethod, queryParams, data, headers)
return {
data,
total: parseInt(headers["x-total-count"]) ?? 100,
+2 -2
View File
@@ -1,8 +1,8 @@
import { DatePicker } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import advancedFormat from "dayjs/plugin/advancedFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
+15 -11
View File
@@ -1,13 +1,13 @@
import { DateField, TextField } from "@refinedev/antd";
import { Field, FieldType } from "../utils/queryFields";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import { enrichText } from "../utils/parsing";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Checkbox, Form, Input, InputNumber, Select, Typography } from "antd";
import { FormItemProps, Rule } from "antd/es/form";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { DateTimePicker } from "./dateTimePicker";
import { InputNumberRange } from "./inputNumberRange";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
dayjs.extend(utc);
@@ -165,14 +165,14 @@ export function ExtraFieldFormItem(props: { field: Field; setDefaultValue?: bool
* @returns
*/
export function ParsedExtras<T extends { extra?: { [key: string]: string } }>(
obj: T
obj: T,
): Omit<T, "extra"> & { extra?: { [key: string]: unknown } } {
if (obj.extra) {
const newExtra: { [key: string]: unknown } = {};
Object.entries(obj.extra).forEach(([key, value]) => {
try {
newExtra[key] = JSON.parse(value);
} catch (e) {
} catch {
newExtra[key] = value;
}
});
@@ -187,16 +187,20 @@ export function ParsedExtras<T extends { extra?: { [key: string]: string } }>(
/**
* Convert the JSON-parsed value extra key-values of an entity to their string values.
*
* A field the user left empty has no value, which travels as an explicit null: the API
* merges `extra` per key, so a key simply left out of the map would keep whatever it
* already held instead of being cleared.
* @param obj
* @returns
*/
export function StringifiedExtras<T extends { extra?: { [key: string]: unknown } }>(
obj: T
): Omit<T, "extra"> & { extra?: { [key: string]: string } } {
obj: T,
): Omit<T, "extra"> & { extra?: { [key: string]: string | null } } {
if (obj.extra) {
const newExtra: { [key: string]: string } = {};
const newExtra: { [key: string]: string | null } = {};
Object.entries(obj.extra).forEach(([key, value]) => {
newExtra[key] = JSON.stringify(value);
newExtra[key] = value === undefined ? null : JSON.stringify(value);
});
return {
...obj,
+20
View File
@@ -0,0 +1,20 @@
import { useEffect } from "react";
/**
* Renders a favicon element in the head of the HTML document with the specified URL.
*
* @param {string} props.url - The URL of the favicon image.
* @return {JSX.Element} - An empty JSX element.
*/
export function Favicon(props: { url: string }) {
useEffect(() => {
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!link) {
link = document.createElement("link") as HTMLLinkElement;
link.rel = "icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
link.href = props.url;
}, [props.url]);
return <></>;
}
@@ -0,0 +1,72 @@
import { useTranslate } from "@refinedev/core";
import { Form, Modal, Select } from "antd";
import { Trans } from "react-i18next";
import { formatFilamentLabel } from "../pages/spools/functions";
import { searchMatches } from "../utils/filtering";
import { ExternalFilament, useGetExternalDBFilaments } from "../utils/queryExternalDB";
export function FilamentImportModal(props: {
isOpen: boolean;
onImport: (filament: ExternalFilament) => void;
onClose: () => void;
}) {
const [form] = Form.useForm();
const t = useTranslate();
const externalFilaments = useGetExternalDBFilaments();
const filamentOptions =
externalFilaments.data?.map((item) => {
return {
label: formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type,
),
value: item.id,
item: item,
};
}) ?? [];
filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
return (
<Modal
title={t("filament.form.import_external")}
open={props.isOpen}
onOk={() => form.submit()}
onCancel={() => props.onClose()}
>
<Form
layout="vertical"
form={form}
onFinish={(values) => {
const filament = filamentOptions.find((item) => item.value === values.filament)?.item;
if (!filament) {
throw new Error("Filament not found");
}
props.onImport(filament);
props.onClose();
form.resetFields();
}}
>
<p>
<Trans
i18nKey={"filament.form.import_external_description"}
components={{
br: <br />,
}}
/>
</p>
<Form.Item name="filament" rules={[{ required: true }]}>
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
/>
</Form.Item>
</Form>
</Modal>
);
}
+19 -15
View File
@@ -1,28 +1,33 @@
import { DownOutlined } from "@ant-design/icons";
import type { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd";
import { useGetLocale, useSetLocale } from "@refinedev/core";
import { Button, Dropdown, Layout as AntdLayout, MenuProps, Space, Switch, theme } from "antd";
import { DesktopOutlined, DownOutlined, MoonOutlined, SunOutlined } from "@ant-design/icons";
import type { RefineThemedLayoutHeaderProps } from "@refinedev/antd";
import { useGetLocale, useSetLocale, useTranslate } from "@refinedev/core";
import { Layout as AntdLayout, Button, Dropdown, MenuProps, Segmented, Space, theme } from "antd";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
import { ColorModeContext, ThemePreference } from "../../contexts/color-mode";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
const { useToken } = theme;
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) => {
export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => {
const { token } = useToken();
const t = useTranslate();
const locale = useGetLocale();
const changeLanguage = useSetLocale();
const { mode, setMode } = useContext(ColorModeContext);
const { preference, setPreference } = useContext(ColorModeContext);
const currentLocale = locale();
const themeOptions = [
{ value: "system", icon: <DesktopOutlined />, title: t("theme.system") },
{ value: "light", icon: <SunOutlined />, title: t("theme.light") },
{ value: "dark", icon: <MoonOutlined />, title: t("theme.dark") },
];
const menuItems: MenuProps["items"] = [...(Object.keys(languages) || [])].sort().map((lang: string) => ({
key: lang,
onClick: () => changeLanguage(lang),
icon: <span className={"fi fi-" + languages[lang].countryCode} style={{ marginRight: 8 }} />,
label: languages[lang].name,
}));
@@ -52,17 +57,16 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) =>
>
<Button type="text">
<Space>
<span className={"fi fi-" + languages[currentLocale ?? "en"].countryCode} />
{languages[currentLocale ?? "en"].name}
<DownOutlined />
</Space>
</Button>
</Dropdown>
<Switch
checkedChildren="🌛"
unCheckedChildren="🔆"
onChange={() => setMode(mode === "light" ? "dark" : "light")}
defaultChecked={mode === "dark"}
<Segmented
aria-label={t("theme.label")}
options={themeOptions}
value={preference}
onChange={(value) => setPreference(value as ThemePreference)}
/>
<QRCodeScannerModal />
</Space>
+22 -26
View File
@@ -1,12 +1,18 @@
import { ThemedLayoutV2, ThemedSiderV2, ThemedTitleV2 } from "@refinedev/antd";
import { Header } from "./header";
import { Footer } from "antd/es/layout/layout";
import { Version } from "./version";
import { Button } from "antd";
import Logo from "../icon.svg?react";
import { ThemedLayout, ThemedSider, ThemedTitle } from "@refinedev/antd";
import { useTranslate } from "@refinedev/core";
import { Button } from "antd";
import { Footer } from "antd/es/layout/layout";
import Logo from "../icon.svg?react";
import { Header } from "./header";
import { Version } from "./version";
const SpoolmanFooter: React.FC = () => {
const SponsorHeart = () => (
<svg viewBox="0 0 16 16" width="1.2em" height="1.2em" fill="#db61a2" aria-hidden="true">
<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002Z" />
</svg>
);
const SpoolmanFooter = () => {
const t = useTranslate();
return (
@@ -25,19 +31,13 @@ const SpoolmanFooter: React.FC = () => {
</div>
<div>
<Button
icon={
<img
src="/kofi_s_logo_nolabel.png"
style={{
height: "1.6em",
}}
/>
}
icon={<SponsorHeart />}
type="text"
href="https://ko-fi.com/donkie"
href="https://github.com/sponsors/Donkie"
target="_blank"
rel="noopener noreferrer"
>
{t("kofi")}
{t("sponsor")}
</Button>
</div>
</div>
@@ -45,21 +45,17 @@ const SpoolmanFooter: React.FC = () => {
);
};
interface SpoolmanLayoutProps {
children: React.ReactNode;
}
export const SpoolmanLayout: React.FC<SpoolmanLayoutProps> = ({ children }) => (
<ThemedLayoutV2
export const SpoolmanLayout = ({ children }: { children: React.ReactNode }) => (
<ThemedLayout
Header={() => <Header sticky />}
Sider={() => (
<ThemedSiderV2
<ThemedSider
fixed
Title={({ collapsed }) => <ThemedTitleV2 collapsed={collapsed} text="Spoolman" icon={<Logo />} />}
Title={({ collapsed }) => <ThemedTitle collapsed={collapsed} text="Spoolman" icon={<Logo />} />}
/>
)}
Footer={() => <SpoolmanFooter />}
>
{children}
</ThemedLayoutV2>
</ThemedLayout>
);
+1 -1
View File
@@ -59,7 +59,7 @@ function subscribeSingle(
channel: string,
resource: string,
callback: (event: LiveEvent) => void,
id?: BaseKey
id?: BaseKey,
) {
// Verify that WebSockets are supported
if (!("WebSocket" in window)) {
+12 -17
View File
@@ -1,8 +1,9 @@
import { LiveEvent } from "@refinedev/core";
import React from "react";
import { useEffect, useState } from "react";
import { getAPIURL } from "../utils/url";
import liveProvider from "./liveProvider";
const liveProviderInstance = liveProvider(import.meta.env.VITE_APIURL);
const liveProviderInstance = liveProvider(getAPIURL());
/**
* Hook that subscribes to live updates for the items in the dataSource
@@ -13,31 +14,25 @@ export function useLiveify<Data extends { id: number }>(
resource: string,
dataSource: Data[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformPayload: (payload: any) => Data
transformPayload: (payload: any) => Data,
) {
// TODO: The hooks in this function is quite janky, and should be refactored to be more efficient
// New state that holds the dataSource with updated values from the live provider
const [updatedDataSource, setUpdatedDataSource] = React.useState<Data[]>(dataSource);
const [updatedDataSource, setUpdatedDataSource] = useState<Data[]>(dataSource);
// If the original dataSource changes, update the updatedDataSource
React.useEffect(() => {
useEffect(() => {
setUpdatedDataSource(dataSource);
}, [dataSource]);
// Create a constant reference to itemIds. This is to prevent the useEffect below from triggering extra times.
const itemIds = dataSource.map((item) => item.id);
const [prevItemIds, setPrevItemIds] = React.useState<number[]>(itemIds);
if (JSON.stringify(itemIds) !== JSON.stringify(prevItemIds)) {
setPrevItemIds(itemIds);
}
// Subscribe to changes for all items in the dataSource
React.useEffect(() => {
useEffect(() => {
const itemIds = dataSource.map((item) => item.id);
const subscription = liveProviderInstance?.subscribe({
channel: `${resource}-list`,
params: {
resource: resource,
ids: prevItemIds,
ids: itemIds,
subscriptionType: "useList",
},
types: ["update"],
@@ -45,7 +40,7 @@ export function useLiveify<Data extends { id: number }>(
setUpdatedDataSource((prev) =>
prev.map((item) => {
return item.id === event.payload.data.id ? transformPayload(event.payload.data) : item;
})
}),
);
},
});
@@ -56,7 +51,7 @@ export function useLiveify<Data extends { id: number }>(
liveProviderInstance?.unsubscribe(subscription);
}
};
}, [resource, prevItemIds, transformPayload]);
}, [resource, dataSource, transformPayload]);
return updatedDataSource;
}
@@ -0,0 +1,83 @@
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
import { Badge, Button, ColorPicker, Space } from "antd";
function generateRandomColor() {
return "000000".replace(/0/g, function () {
return (~~(Math.random() * 16)).toString(16);
});
}
function generateInitialColors(num: number) {
const colors = [];
for (let i = 0; i < num; i++) {
colors.push(generateRandomColor());
}
return colors;
}
/**
* An Ant Design compatible form input for multiple color pickers
* The value is a comma separated list of hex values, without hashtags
* @param props
* @returns
*/
export function MultiColorPicker(props: {
value?: string | null | undefined;
onChange?: (value: string | null | undefined) => void;
min?: number;
max?: number;
}) {
const values = props.value ? props.value.split(",") : generateInitialColors(props.min ?? 0);
if (!props.value && props.onChange) {
// Update value immediately
props.onChange(values.join(","));
}
const pickers = values.map((value, idx) => (
<Badge
key={idx}
count={
values.length > (props.min ?? 0) ? (
<span className="ant-badge-count">
<CloseOutlined
onClick={() => {
// Remove this picker
if (props.onChange) {
props.onChange(values.filter((v, i) => i !== idx).join(","));
}
}}
/>
</span>
) : (
<></>
)
}
>
<ColorPicker
value={value}
onChange={(clr) => {
if (props.onChange) {
props.onChange(values.map((v, i) => (i === idx ? clr.toHex() : v)).join(","));
}
}}
/>
</Badge>
));
return (
<>
<Space direction="horizontal" size="middle" style={{ marginTop: "1em" }}>
{pickers}
{values.length < (props.max ?? Infinity) && (
<Button
icon={<PlusOutlined />}
onClick={() => {
if (props.onChange) {
props.onChange(values.concat(generateRandomColor()).join(","));
}
}}
/>
)}
</Space>
</>
);
}
+36 -3
View File
@@ -1,7 +1,40 @@
import { NotificationProvider } from "@refinedev/core";
import { message } from "antd";
import { UndoOutlined } from "@ant-design/icons";
import { NotificationProvider, OpenNotificationParams } from "@refinedev/core";
import { Button, Progress, message } from "antd";
import { UndoableNotification } from "@refinedev/antd/src/components/undoableNotification";
type UndoableNotificationProps = {
notificationKey: OpenNotificationParams["key"];
message: OpenNotificationParams["message"];
cancelMutation: OpenNotificationParams["cancelMutation"];
undoableTimeout: OpenNotificationParams["undoableTimeout"];
};
const UndoableNotification = ({ message, cancelMutation, undoableTimeout }: UndoableNotificationProps) => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: "-7px",
}}
>
<Progress
type="circle"
percent={(undoableTimeout ?? 0) * 20}
format={(time) => time && time / 20}
size={50}
strokeColor="#1890ff"
status="normal"
/>
<span style={{ marginLeft: 8, width: "100%" }}>{message}</span>
<Button
style={{ flexShrink: 0 }}
onClick={cancelMutation}
disabled={undoableTimeout === 0}
icon={<UndoOutlined />}
></Button>
</div>
);
const SpoolmanNotificationProvider: NotificationProvider = {
open: ({ key, message: content, type, cancelMutation, undoableTimeout }) => {
+4 -5
View File
@@ -1,6 +1,5 @@
import React from "react";
import { Typography } from "antd";
import { NumberFieldProps } from "@refinedev/antd/dist/components/fields/types";
import { Typography } from "antd";
const { Text } = Typography;
@@ -17,12 +16,12 @@ type Props = NumberFieldProps & {
*
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/fields/number} for more details.
*/
export const NumberFieldUnit: React.FC<Props> = ({ value, locale, options, ...rest }) => {
export const NumberFieldUnit = ({ value, locale, options, unit }: Props) => {
const number = Number(value);
return (
<Text {...rest}>
{toLocaleStringSupportsOptions() ? number.toLocaleString(locale, options) : number} {rest.unit}
<Text>
{toLocaleStringSupportsOptions() ? number.toLocaleString(locale, options) : number} {unit}
</Text>
);
};
+52 -17
View File
@@ -1,15 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { Tooltip } from "antd";
import { ColumnFilterItem } from "antd/es/table/interface";
import { IFilament } from "../pages/filaments/model";
import { IVendor } from "../pages/vendors/model";
import { ColumnFilterItem } from "antd/es/table/interface";
import { getAPIURL } from "../utils/url";
export function useSpoolmanFilamentFilter(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, ColumnFilterItem[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -30,27 +31,66 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
} else {
name = `${filament.name ?? "<unknown>"}`;
}
const tooltipParts: React.ReactNode[] = [];
if (filament.color_hex) {
tooltipParts.push(
<div
key="color"
style={{
borderRadius: ".4em",
width: "1.4em",
height: "1.4em",
backgroundColor: "#" + filament.color_hex,
}}
></div>,
);
}
if (filament.material) {
tooltipParts.push(<div key="material">{filament.material}</div>);
}
if (filament.weight) {
tooltipParts.push(<div key="weight">{filament.weight}g</div>);
}
return {
text: name,
text: (
<Tooltip
title={
<div
style={{
display: "flex",
flexDirection: "row",
gap: ".5em",
alignContent: "center",
}}
>
{tooltipParts}
</div>
}
>
{name}
</Tooltip>
),
value: filament.id,
sortId: name,
};
})
// Remove duplicates
.filter((item, index, self) => self.findIndex((t) => t.value === item.value) === index)
// Sort by name
.sort((a, b) => a.text.localeCompare(b.text));
.sort((a, b) => a.sortId.localeCompare(b.sortId));
return names;
},
});
}
export function useSpoolmanFilamentNames(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, string[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -74,12 +114,11 @@ export function useSpoolmanFilamentNames(enabled: boolean = false) {
}
export function useSpoolmanVendors(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IVendor[], unknown, string[]>({
enabled: enabled,
queryKey: ["vendors"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/vendor");
const response = await fetch(getAPIURL() + "/vendor");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -96,12 +135,11 @@ export function useSpoolmanVendors(enabled: boolean = false) {
}
export function useSpoolmanMaterials(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["materials"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/material");
const response = await fetch(getAPIURL() + "/material");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -114,12 +152,11 @@ export function useSpoolmanMaterials(enabled: boolean = false) {
}
export function useSpoolmanArticleNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["articleNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/article-number");
const response = await fetch(getAPIURL() + "/article-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -132,12 +169,11 @@ export function useSpoolmanArticleNumbers(enabled: boolean = false) {
}
export function useSpoolmanLotNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["lotNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/lot-number");
const response = await fetch(getAPIURL() + "/lot-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -150,12 +186,11 @@ export function useSpoolmanLotNumbers(enabled: boolean = false) {
}
export function useSpoolmanLocations(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["locations"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/location");
const response = await fetch(getAPIURL() + "/location");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -1,129 +0,0 @@
import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd";
import { useSavedState } from "../../utils/saveload";
import PrintingDialog from "./printingDialog";
import { useTranslate } from "@refinedev/core";
interface QRCodeData {
value: string;
label?: JSX.Element;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
visible: boolean;
items: QRCodeData[];
onCancel: () => void;
extraSettings?: JSX.Element;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({ visible, items, onCancel, extraSettings }) => {
const t = useTranslate();
const [showContent, setShowContent] = useSavedState("print-showContent", true);
const [textSize, setTextSize] = useSavedState("print-textSize", 3);
const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState("print-showSpoolmanIcon", true);
const elements = items.map((item) => {
return (
<div className="print-qrcode-item">
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
</div>
{showContent && <div className="print-qrcode-title">{item.label ?? item.value}</div>}
</div>
);
});
return (
<PrintingDialog
visible={visible}
title={t("printing.qrcode.title")}
items={elements}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch checked={showContent} onChange={(checked) => setShowContent(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={2}
max={7}
value={textSize}
step={0.1}
onChange={(value) => {
setTextSize(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
setTextSize(value ?? 5);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolmanIcon")}>
<Switch checked={showSpoolmanIcon} onChange={(checked) => setShowSpoolmanIcon(checked)} />
</Form.Item>
{extraSettings}
</>
}
style={`
.print-page .print-qrcode-item {
display: flex;
width: 100%;
max-height: 100%;
justify-content: center;
}
.print-page .print-qrcode-container {
max-width: ${showContent ? "50%" : "100%"};
display: flex;
}
.print-page .print-qrcode {
width: auto !important;
height: auto !important;
}
.print-page .print-qrcode-title {
flex: 1 1 auto;
font-size: ${textSize}mm;
color: #000;
overflow: hidden;
}
.print-page canvas, .print-page svg {
/* display: block; */
object-fit: contain;
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
`}
onCancel={onCancel}
/>
);
};
export default QRCodePrintingDialog;
@@ -1,135 +0,0 @@
import { Form, Switch } from "antd";
import { IFilament } from "../../pages/filaments/model";
import { ISpool } from "../../pages/spools/model";
import QRCodePrintingDialog from "./qrCodePrintingDialog";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
interface SpoolQRCodePrintingDialog {
visible: boolean;
items: ISpool[];
onCancel: () => void;
}
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ visible, items, onCancel }) => {
const t = useTranslate();
const [showVendor, setShowVendor] = useSavedState("print-showVendor", true);
const [showLotNr, setShowLotNr] = useSavedState("print-showLotNr", true);
const [showSpoolWeight, setShowSpoolWeight] = useSavedState("print-showSpoolWeight", true);
const [showTemperatures, setShowTemperatures] = useSavedState("print-showTemperatures", true);
const [showSpoolComment, setShowSpoolComment] = useSavedState("print-showSpoolComment", false);
const [showFilamentComment, setShowFilamentComment] = useSavedState("print-showFilamentComment", false);
const [showVendorComment, setShowVendorComment] = useSavedState("print-showVendorComment", false);
const formatFilament = (filament: IFilament) => {
let vendorPrefix = "";
if (showVendor && filament.vendor) {
vendorPrefix = `${filament.vendor.name} - `;
}
let name = filament.name;
if (!name) {
name = `Filament #${filament.id}`;
}
return `${vendorPrefix}${name}`;
};
return (
<QRCodePrintingDialog
visible={visible}
onCancel={onCancel}
items={items.map(function (spool) {
const temps = [];
if (spool.filament.settings_extruder_temp) {
temps.push(t("printing.qrcode.extruderTemp", { temp: `${spool.filament.settings_extruder_temp} °C` }));
}
if (spool.filament.settings_bed_temp) {
temps.push(t("printing.qrcode.bedTemp", { temp: `${spool.filament.settings_bed_temp} °C` }));
}
const tempLine = temps.join(" - ");
return {
value: `web+spoolman:s-${spool.id}`,
label: (
<p
style={{
padding: "1mm 1mm 1mm 0",
}}
>
<b>{formatFilament(spool.filament)}</b>
<br />
<b>
#{spool.id}
{spool.filament.material && <> - {spool.filament.material}</>}
</b>
{showSpoolWeight && (
<>
<br />
{t("printing.qrcode.spoolWeight", { weight: `${spool.filament.spool_weight ?? "?"} g` })}
</>
)}
{showTemperatures && tempLine && (
<>
<br />
{tempLine}
</>
)}
{showLotNr && (
<>
<br />
{t("printing.qrcode.lotNr", { lot: spool.lot_nr ?? "?" })}
</>
)}
{showSpoolComment && spool.comment && (
<>
<br />
{spool.comment}
</>
)}
{showFilamentComment && spool.filament.comment && (
<>
<br />
{spool.filament.comment}
</>
)}
{showVendorComment && spool.filament.vendor?.comment && (
<>
<br />
{spool.filament.vendor.comment}
</>
)}
</p>
),
errorLevel: "H",
};
})}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showVendor")}>
<Switch checked={showVendor} onChange={(checked) => setShowVendor(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolWeight")}>
<Switch checked={showSpoolWeight} onChange={(checked) => setShowSpoolWeight(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showTemperatures")}>
<Switch checked={showTemperatures} onChange={(checked) => setShowTemperatures(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showLotNr")}>
<Switch checked={showLotNr} onChange={(checked) => setShowLotNr(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolComment")}>
<Switch checked={showSpoolComment} onChange={(checked) => setShowSpoolComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showFilamentComment")}>
<Switch checked={showFilamentComment} onChange={(checked) => setShowFilamentComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showVendorComment")}>
<Switch checked={showVendorComment} onChange={(checked) => setShowVendorComment(checked)} />
</Form.Item>
</>
}
/>
);
};
export default SpoolQRCodePrintingDialog;
+43 -28
View File
@@ -1,50 +1,52 @@
import React, { useState } from "react";
import { FloatButton, Modal, Space } from "antd";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { useNavigate } from "react-router-dom";
import { CameraOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
import { FloatButton, Modal, Space } from "antd";
import { useState } from "react";
import { useNavigate } from "react-router";
const QRCodeScannerModal: React.FC = () => {
const QRCodeScannerModal = () => {
const [visible, setVisible] = useState(false);
const [lastError, setLastError] = useState<string | null>(null);
const t = useTranslate();
const navigate = useNavigate();
const onScan = (result: string) => {
const onScan = (detectedCodes: IDetectedBarcode[]) => {
if (detectedCodes.length === 0) {
return;
}
const result = detectedCodes[0].rawValue;
// Check for the spoolman ID format
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/);
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
if (match && match.groups) {
setVisible(false);
navigate(`/spool/show/${match.groups.id}`);
}
const fullURLmatch = result.match(/^https?:\/\/[^/]+\/spool\/show\/(?<id>[0-9]+)$/i);
if (fullURLmatch && fullURLmatch.groups) {
setVisible(false);
navigate(`/spool/show/${fullURLmatch.groups.id}`);
}
};
return (
<>
<FloatButton type="primary" onClick={() => setVisible(true)} icon={<CameraOutlined />} shape="circle" />
<Modal open={visible} destroyOnClose onCancel={() => setVisible(false)} footer={null} title={t("scanner.title")}>
<Modal open={visible} destroyOnHidden onCancel={() => setVisible(false)} footer={null} title={t("scanner.title")}>
<Space direction="vertical" style={{ width: "100%" }}>
<p>{t("scanner.description")}</p>
<QrScanner
viewFinder={
lastError
? () => (
<div
style={{
position: "absolute",
textAlign: "center",
width: "100%",
top: "50%",
}}
>
<p>{lastError}</p>
</div>
)
: undefined
}
onDecode={onScan}
onError={(error: Error) => {
<Scanner
constraints={{
facingMode: "environment",
}}
onScan={onScan}
// Accept common 2D matrix codes, not just QR, so labels generated with e.g.
// Data Matrix or Aztec codes can be scanned too (issue #887). Payloads that
// don't match the spoolman format are ignored, so widening this is harmless.
formats={["qr_code", "micro_qr_code", "rm_qr_code", "data_matrix", "aztec", "pdf417"]}
onError={(err: unknown) => {
const error = err as Error;
console.error(error);
if (error.name === "NotAllowedError") {
setLastError(t("scanner.error.notAllowed"));
@@ -63,7 +65,20 @@ const QRCodeScannerModal: React.FC = () => {
setLastError(t("scanner.error.unknown", { error: error.name }));
}
}}
/>
>
{lastError && (
<div
style={{
position: "absolute",
textAlign: "center",
width: "100%",
top: "50%",
}}
>
<p>{lastError}</p>
</div>
)}
</Scanner>
</Space>
</Modal>
</>
Loaded 100 of 566 files, more files were not shown because too many files have changed in this diff. Show more