From 37b9bbfaaf934f9c2c1824b556ba4e98e4c60ce6 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sat, 20 Jun 2026 20:45:22 +0200 Subject: [PATCH] fix(print-modal): disable printers between dispatch-accept and PRINT_START MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported off-list by a corporate supporter running a multi-operator farm shift. Backend already rejects double-sends with HTTP 409 (see background_dispatch._dispatch at lines 283-290), so no double-print was possible, but PrinterSelector consulted only PrinterStatus.state ({IDLE, FINISH, FAILED}) to decide whether a printer was selectable. PRINT_START is the only signal that flips the printer out of IDLE, so during the upload + print-command + firmware-ack window the card stayed clickable and operators only found out after submitting. New useDispatchedPrinterIds hook subscribes to the existing background-dispatch WebSocket event (already consumed for the toast overlay), collects printer_ids from dispatched_jobs + active_jobs, and exposes them via useSyncExternalStore so every PrinterSelector instance shares one snapshot. OR'd into isPrinterBusy; badge label flips to "Dispatching..." instead of a misleading "Idle". No backend change — the reservation already exists, the UI just didn't reflect it. No behaviour change for add-to-queue / edit-queue modes (disableBusy stays false for those). --- CHANGELOG.md | 2 + .../hooks/useDispatchedPrinterIds.test.ts | 105 +++++++ .../components/PrintModal/PrinterSelector.tsx | 9 + frontend/src/hooks/useDispatchedPrinterIds.ts | 85 ++++++ .../{index-BUDdM_BW.js => index-BdzWqWAS.js} | 260 +++++++++--------- static/index.html | 2 +- 6 files changed, 332 insertions(+), 131 deletions(-) create mode 100644 frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts create mode 100644 frontend/src/hooks/useDispatchedPrinterIds.ts rename static/assets/{index-BUDdM_BW.js => index-BdzWqWAS.js} (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06f2ec65..0d38a7a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ All notable changes to Bambuddy will be documented in this file. - **Push notification for "Printer offline" now actually fires (#1752, reported by @saint-hh)** — The notification provider's `on_printer_offline` toggle has shipped since the notifications feature landed: schema field, DB column, `notification_template.py` entry, and the dispatcher `NotificationService.on_printer_offline(printer_id, printer_name, db)` are all in place. What was missing was the caller — nothing in the codebase actually invoked the dispatcher when a printer went offline. The reporter (P2S, smart-plug-cuts-power scenario) confirmed turning the toggle on did nothing; only the print-failure notification fired when power was restored, via the firmware's `gcode_state=FAILED` report on MQTT reconnect. **Why the toggle was orphan:** every other provider event (`on_print_start`, `on_print_complete`, `on_print_progress`, `on_printer_error`, etc.) has a clear call site under `main.py::on_printer_status_change` or alongside the print-lifecycle hooks. The offline event was the only edge-triggered toggle without one — the dispatcher and template predated the wiring step and were silently shipped. Both upstream offline-trigger paths (`smart_plug_manager` → `printer_manager.mark_printer_offline()` and `bambu_mqtt.py::check_staleness` after the 30s STALE_RECONNECT_COOLDOWN) route through `_on_status_change` already and reach `on_printer_status_change`; the handler just didn't act on the disconnect edge. **Fix:** edge detection in `on_printer_status_change` watches `state.connected` against the previous observation per printer (`_printer_last_connected: dict[int, bool]`). On the True → False transition it schedules `_maybe_notify_printer_offline(printer_id)` as a background asyncio task; on the next True observation it cancels any pending task. The helper sleeps `_PRINTER_OFFLINE_NOTIFY_DEBOUNCE_SECONDS = 60.0` then re-checks `printer_manager.is_connected(printer_id)` — only fires the notification if the printer is still offline. **Why 60s debounce:** sized against `bambu_mqtt.py::STALE_RECONNECT_COOLDOWN = 30s` — a single stale-trigger + reconnect cycle isn't enough to fire, only a real outage that survives one full cooldown notifies. Transient MQTT blips (WiFi roam, broker reload, brief packet loss) recover within the window and the cancellation path kicks in. **Edge-case handling:** initial observation with no prior connected state doesn't fire (covers Bambuddy startup with an already-offline printer); a False → False repeat doesn't reschedule (the in-flight task stays in place rather than resetting the clock on every status callback, which would otherwise mean the notification never fires); the task entry pops from `_printer_offline_notify_tasks` in the finally block whether the notification fired, the printer reconnected, or the task was cancelled mid-await. **No symmetric `on_printer_online` event:** the reporter explicitly noted the "printer lost power and interrupted the print" notification already fires when power is restored — that's the print-failure notification, triggered by the firmware reporting `gcode_state=FAILED` for the interrupted print on MQTT reconnect. That covers the "printer is back" channel without a new toggle. If the user then resumes the print, no print_start notification fires (Bambuddy's `bambu_mqtt.py:3039` explicitly suppresses `is_new_print` for PAUSE → RUNNING to prevent duplicates when resuming from pause), but that's a separate scope from offline-detection. **Tests:** 9 new cases in `test_printer_offline_notification.py` split across two classes. `TestMaybeNotifyPrinterOffline` pins the debounced helper: fires notification when still offline at end of window, doesn't fire when printer reconnected during debounce, doesn't fire when the printer disappeared from the DB (uninstall mid-window), clears `_printer_offline_notify_tasks[printer_id]` after run. `TestOfflineEdgeDetection` pins the edge logic inside `on_printer_status_change`: first observation (connected) doesn't schedule, first observation (disconnected) doesn't schedule (the no-prior-True case — important for startup), True → False schedules a task, reconnect cancels the pending task, repeated False observations don't replace the in-flight task. Full backend suite still green; ruff clean. +- **PrintModal printer picker no longer offers a printer between dispatch-accept and PRINT_START (reported off-list by a corporate user running multi-operator farm shifts)** — Operator picks a printer in the reprint modal, hits Send, Bambuddy accepts the dispatch and begins FTP upload + sending the print command. The printer hasn't reported `gcode_state=RUNNING` yet — it's still IDLE on its own MQTT status. A second operator opening the modal during this window sees the same printer as available and submits a second job. The backend correctly rejects the second submit with HTTP 409 (`background_dispatch._dispatch` rejects when `_queued_jobs` or `_active_jobs` already holds the printer_id), so no double-print is possible, but the operator only finds out after they click Send — wasted minutes per attempt on a busy floor. **Root cause:** `PrinterSelector.tsx::isPrinterBusy` consulted only `PrinterStatus.state` against `AVAILABLE_STATES = {IDLE, FINISH, FAILED}`. PRINT_START is the only signal that flips the printer out of IDLE, and there's a real wall-clock window (upload time + print command + firmware ack) between dispatch acceptance and that flip. The dispatch-queue state — already broadcast as a WebSocket `background_dispatch` push including `dispatched_jobs[].printer_id` and `active_jobs[].printer_id` — was being consumed by `ToastContext` for the progress overlay but never read by the picker. **Fix:** new `frontend/src/hooks/useDispatchedPrinterIds.ts` exposes `Set` of printer_ids with a queued or active dispatch, populated from the same `background-dispatch` window event the ToastContext listens for. Module-level singleton + `useSyncExternalStore` so every `PrinterSelector` instance sees the same snapshot and a modal opened mid-batch picks up the latest state without a refetch. Reference-stable snapshot (size + membership check) keeps `useSyncExternalStore`'s Object.is comparison from re-rendering on every WS push that doesn't change the set. `PrinterSelector.tsx::isPrinterBusy` ORs the set into the existing connected/state check — printer disabled the instant dispatch is accepted, re-enabled when the dispatch finishes (or fails) and disappears from the next state payload. `getPrinterStateLabel` returns `"Dispatching..."` for the badge so operators see the in-flight state instead of a misleading "Idle" on a now-disabled card. Hardcoded English label is consistent with the existing labels in that function (`"Idle"`, `"Printing"`, `"Paused"` are all hardcoded, no i18n key). **What this is NOT:** a backend change (the reservation Mike asked about already exists at `background_dispatch.py:283-290`); a behaviour change for `add-to-queue` / `edit-queue-item` modes (those don't set `disableBusy=true`, so the busy-OR remains dormant for the card click handler — the badge label still flips, which is informative); a guarantee against the WS-not-yet-connected race (a fresh page load that opens the modal before the WS initial-state push lands still sees an empty set for ~1 frame; same race as today, much shorter window). **Tests:** 8 new cases in `useDispatchedPrinterIds.test.ts` pin the contract — empty initial set, picks up `dispatched_jobs` printer_ids, picks up `active_jobs` printer_ids, unions both lists, clears when subsequent event reports zero jobs, ignores non-numeric `printer_id` (defensive against payload drift), reference-stable snapshot when content doesn't change, shared state across hook instances. Existing 84 PrintModal + PrinterSelector cases still green — the new code path is dormant until a `background-dispatch` window event fires, which existing tests don't trigger. `npm run build` clean, ESLint clean. + - **Completion notification reported the whole project's duration and material usage when only one plate of a multi-plate 3MF was printed (#1785)** — Reporter (H2D) noticed the Discord on-print-complete message stated the full multi-plate project's totals (e.g. "6h 12m" / "370 g") even though only a single plate had been started, while every Bambuddy surface (print queue card, archive card, statistics) correctly showed the per-plate values. Root cause traced to the 3MF parser at `services/archive.py:200-264`, which sums per-plate `prediction` (slicer time estimate) and `weight` across every `` of a multi-plate file and stores those file-level totals on the `PrintArchive.print_time_seconds` / `PrintArchive.filament_used_grams` columns. That summing was added by #1593 to fix the archive card under-reporting on multi-plate files, and is correct for the archive-level "whole project" headline. The queue UI already re-reads the 3MF per-plate at `print_queue.py:272-285` (using `extract_filament_usage_from_3mf` / `_extract_print_time_from_3mf`) and substitutes the plate's actual values — which is why everything inside Bambuddy displays per-plate correctly. The notification path at `main.py::_background_notifications` read the archive's columns and `extra_data.filament_slots` directly with **no plate-aware override**, so the dispatched template variables (`{{duration}}`, `{{filament_grams}}`, `{{filament_details}}`) consistently rendered the project-wide sum. For material grams this was unconditionally wrong on multi-plate single-plate prints; for duration it depended on whether the real elapsed `actual_time_seconds` was populated (the `actual_time_seconds or print_time_seconds` fallback chain only landed on the summed estimate when the timestamps weren't usable). **Fix:** new `_scope_notification_archive_data_to_plate(archive_data, file_path, plate_id, status, progress, base_dir)` helper in `main.py` mirrors what the queue UI does — when `plate_id` is set on the just-completed print, re-read the 3MF, sum the plate's `` entries for the actual grams, read the plate's `` for the estimate, and replace `archive_data["actual_filament_grams"]` + `archive_data["print_time_seconds"]` + `archive_data["filament_slots"]` accordingly. `notify_plate_id` is captured from the existing `_print_plate_ids` register at the same point that already pops it (around `main.py:4183`), so no extra bookkeeping is added to the print-start path — the queue dispatcher and direct-Print path both already register plate_id there. **Partial-print scaling preserved:** the helper applies the same `progress / 100` scale factor to the per-plate grams + per-slot weights as the pre-existing summed-totals branch did, so a 50%-cancelled plate-2 print still reports "half the plate's grams," not the whole plate. **Fail-open on every error path:** missing `plate_id` (single-plate file / archive-only flow), missing `archive.file_path`, the 3MF file having been deleted between print completion and notification firing, a corrupt zip, or a plate index outside the file's range — all return `archive_data` unchanged so the notification still ships with the project-level numbers it would have shown before this fix. Same defensiveness shape as the helper-loaders the queue route relies on. **Hoisted `extract_print_time_from_3mf` into `utils/threemf_tools.py`** so the notification path can reuse the queue UI's logic without importing from a routes module (the route's `_extract_print_time_from_3mf` becomes a one-line alias). Identical signature + return shape, so the queue's existing call sites keep working without changes. **Tests:** 10 new cases. `test_threemf_tools.py::TestExtractPrintTimeFrom3mf` (7 cases): plate-N prediction returned for plate_id=N, first plate when no plate_id passed, None for plate_id outside range, None for unparseable prediction, None for missing slice_info / invalid zip / missing file. `test_notification_plate_scope.py::TestScopeNotificationArchiveDataToPlate` (10 cases): plate-2 of 3 collapses summed 370g/3h into plate's 120g/60min; plate-1 and plate-3 scope correctly; partial-print at progress=50 halves grams + per-slot weights but keeps full slicer estimate; no plate_id / no file_path / missing file / corrupt zip / out-of-range plate_id all return the input unchanged so the notification still sends; single-plate file with plate_id=1 is a clean no-op (the parser's sum already collapses to plate-1's values, no double-scaling). Full backend `pytest -n 30` 4086/4086 in 50s; ruff clean. **Scope clarification:** the archive card / project rollup / statistics surfaces stay on the summed totals (the original #1593 contract) — only the completion notification path now plate-scopes, mirroring the queue card precedent. Print Logs entries continue to use the per-run filament helper (#1378 / #1390) which already reads from `usage_results` + scales by progress, so this fix doesn't touch them. ## [0.2.4.7] - 2026-06-14 diff --git a/frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts b/frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts new file mode 100644 index 000000000..86f5e4e1c --- /dev/null +++ b/frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts @@ -0,0 +1,105 @@ +/** + * Tests for useDispatchedPrinterIds — the hook that exposes printer IDs with + * a queued/active background-dispatch job so PrinterSelector can grey them + * out between dispatch-accepted and the printer's PRINT_START report. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { + useDispatchedPrinterIds, + __resetDispatchedPrinterIdsForTests, +} from '../../hooks/useDispatchedPrinterIds'; + +function fire(detail: Record) { + act(() => { + window.dispatchEvent(new CustomEvent('background-dispatch', { detail })); + }); +} + +describe('useDispatchedPrinterIds', () => { + beforeEach(() => { + __resetDispatchedPrinterIdsForTests(); + }); + + afterEach(() => { + __resetDispatchedPrinterIdsForTests(); + }); + + it('returns an empty set initially', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + expect(result.current.size).toBe(0); + }); + + it('picks up printer IDs from dispatched_jobs', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ + dispatched_jobs: [ + { job_id: 1, printer_id: 42, printer_name: 'Farm-A' }, + ], + active_jobs: [], + }); + expect(result.current.has(42)).toBe(true); + expect(result.current.size).toBe(1); + }); + + it('picks up printer IDs from active_jobs', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ + dispatched_jobs: [], + active_jobs: [ + { job_id: 1, printer_id: 7, printer_name: 'Farm-B' }, + ], + }); + expect(result.current.has(7)).toBe(true); + }); + + it('unions both lists', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ + dispatched_jobs: [{ job_id: 1, printer_id: 1 }], + active_jobs: [{ job_id: 2, printer_id: 2 }], + }); + expect(result.current.size).toBe(2); + expect(result.current.has(1)).toBe(true); + expect(result.current.has(2)).toBe(true); + }); + + it('clears printers when subsequent event reports no jobs', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ dispatched_jobs: [{ job_id: 1, printer_id: 9 }], active_jobs: [] }); + expect(result.current.has(9)).toBe(true); + fire({ dispatched_jobs: [], active_jobs: [] }); + expect(result.current.size).toBe(0); + }); + + it('ignores jobs without a numeric printer_id', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ + dispatched_jobs: [ + { job_id: 1, printer_id: 'not-a-number' }, + { job_id: 2 }, + { job_id: 3, printer_id: 5 }, + ], + active_jobs: [], + }); + expect(result.current.size).toBe(1); + expect(result.current.has(5)).toBe(true); + }); + + it('keeps snapshot reference stable when content is unchanged', () => { + const { result } = renderHook(() => useDispatchedPrinterIds()); + fire({ dispatched_jobs: [{ printer_id: 1 }], active_jobs: [] }); + const first = result.current; + fire({ dispatched_jobs: [{ printer_id: 1 }], active_jobs: [] }); + expect(result.current).toBe(first); + }); + + it('shares state across hook instances', () => { + const a = renderHook(() => useDispatchedPrinterIds()); + const b = renderHook(() => useDispatchedPrinterIds()); + fire({ dispatched_jobs: [{ printer_id: 11 }], active_jobs: [] }); + expect(a.result.current.has(11)).toBe(true); + expect(b.result.current.has(11)).toBe(true); + }); +}); diff --git a/frontend/src/components/PrintModal/PrinterSelector.tsx b/frontend/src/components/PrintModal/PrinterSelector.tsx index b88eb8c28..e5dc69517 100644 --- a/frontend/src/components/PrintModal/PrinterSelector.tsx +++ b/frontend/src/components/PrintModal/PrinterSelector.tsx @@ -12,6 +12,7 @@ import { Users, } from 'lucide-react'; import { api, type PrinterStatus } from '../../api/client'; +import { useDispatchedPrinterIds } from '../../hooks/useDispatchedPrinterIds'; import { getColorName } from '../../utils/colors'; import { normalizeColorForCompare, @@ -255,7 +256,14 @@ export function PrinterSelector({ return map; }, [activePrinters, statusQueries]); + // Printers with a queued/active background dispatch — accepted by Bambuddy + // but not yet reflected in PrinterStatus.state (which only flips on + // PRINT_START from the printer itself). Backend rejects double-sends with + // 409 anyway; this just stops the operator from picking them in the modal. + const dispatchedPrinterIds = useDispatchedPrinterIds(); + const isPrinterBusy = (printerId: number): boolean => { + if (dispatchedPrinterIds.has(printerId)) return true; const status = printerStatusMap.get(printerId); if (!status) return false; // Unknown state — don't block if (!status.connected) return true; @@ -263,6 +271,7 @@ export function PrinterSelector({ }; const getPrinterStateLabel = (printerId: number): string | null => { + if (dispatchedPrinterIds.has(printerId)) return 'Dispatching...'; const status = printerStatusMap.get(printerId); if (!status) return null; if (!status.connected) return 'Offline'; diff --git a/frontend/src/hooks/useDispatchedPrinterIds.ts b/frontend/src/hooks/useDispatchedPrinterIds.ts new file mode 100644 index 000000000..50a007bbc --- /dev/null +++ b/frontend/src/hooks/useDispatchedPrinterIds.ts @@ -0,0 +1,85 @@ +/** + * Subscribes to background-dispatch WebSocket events and returns the set of + * printer IDs that currently have a queued or active dispatch job. + * + * Used by PrinterSelector to disable printers between the moment Bambuddy + * accepts a dispatch (FTP upload, print command) and the moment the printer + * itself reports PRINT_START. The backend already rejects double-sends with + * HTTP 409, but the UI gap still let operators pick a printer the server would + * refuse — surfaced by a corporate user running multi-operator farm shifts. + * + * Module-level state + useSyncExternalStore so every PrinterSelector instance + * sees the same snapshot, and component mounts mid-batch pick up the latest + * state without re-fetching. + */ +import { useSyncExternalStore } from 'react'; + +interface DispatchEventJob { + printer_id?: unknown; +} + +interface DispatchEventDetail { + dispatched_jobs?: DispatchEventJob[]; + active_jobs?: DispatchEventJob[]; + total?: number; + dispatched?: number; + processing?: number; +} + +const EMPTY: ReadonlySet = new Set(); +let currentSet: ReadonlySet = EMPTY; +const subscribers = new Set<() => void>(); +let attached = false; + +function recompute(detail: DispatchEventDetail): ReadonlySet { + const next = new Set(); + for (const job of detail.dispatched_jobs ?? []) { + if (typeof job.printer_id === 'number') next.add(job.printer_id); + } + for (const job of detail.active_jobs ?? []) { + if (typeof job.printer_id === 'number') next.add(job.printer_id); + } + return next; +} + +function handleEvent(event: Event) { + const detail = (event as CustomEvent).detail ?? {}; + const next = recompute(detail); + // Keep reference stable when content didn't change — useSyncExternalStore + // compares snapshots via Object.is and re-renders on any new reference. + if (next.size === currentSet.size && [...next].every((id) => currentSet.has(id))) { + return; + } + currentSet = next; + subscribers.forEach((cb) => cb()); +} + +function ensureAttached() { + if (attached || typeof window === 'undefined') return; + window.addEventListener('background-dispatch', handleEvent); + attached = true; +} + +const subscribe = (callback: () => void): (() => void) => { + ensureAttached(); + subscribers.add(callback); + return () => { + subscribers.delete(callback); + }; +}; + +const getSnapshot = (): ReadonlySet => currentSet; + +export function useDispatchedPrinterIds(): ReadonlySet { + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} + +/** Test-only helper — resets the module-level singleton between tests. */ +export function __resetDispatchedPrinterIdsForTests(): void { + currentSet = EMPTY; + subscribers.clear(); + if (attached && typeof window !== 'undefined') { + window.removeEventListener('background-dispatch', handleEvent); + attached = false; + } +} diff --git a/static/assets/index-BUDdM_BW.js b/static/assets/index-BdzWqWAS.js similarity index 87% rename from static/assets/index-BUDdM_BW.js rename to static/assets/index-BdzWqWAS.js index 727bfffe9..0b77e10c9 100644 --- a/static/assets/index-BUDdM_BW.js +++ b/static/assets/index-BdzWqWAS.js @@ -41,11 +41,11 @@ mW→W için 0.001, kW→W için 1000 çarpanı kullanın.`,mqttEnergyHint:`JSON Wh→kWh için 0.001, MWh→kWh için 1000 çarpanı kullanın.`,mqttStateHint:`JSON yolu JSON yükten değer çıkarır. Ham değerler için boş bırakın. ON değeri: "ON" anlamına gelen tam string. Otomatik algılama için boş bırakın (ON, true, 1).`,restControl:`Kontrol`,restOnUrl:`AÇ URL'si`,restOffUrl:`KAPAT URL'si`,restOnBody:`AÇ İstek Gövdesi`,restOffBody:`KAPAT İstek Gövdesi`,restMethod:`HTTP Yöntemi`,restHeaders:`Özel Başlıklar (JSON)`,restStatusUrl:`Durum URL'si`,restStatusPath:`Durum JSON Yolu`,restStatusOnValue:`ON Değeri`,restPowerUrl:`Güç URL'si`,restPowerPath:`Güç JSON Yolu`,restPowerMultiplier:`Güç Çarpanı`,restEnergyUrl:`Enerji URL'si`,restEnergyPath:`Enerji JSON Yolu`,restEnergyMultiplier:`Enerji Çarpanı`,restUrlRequired:`REST prizleri için en az bir URL (ON veya OFF) gerekli`,restHeadersHint:`örn. {"Authorization": "Bearer your-token"}`,restBodyHint:`örn. ON, {"state": "on"}`,restStatusHint:`Mevcut durumu sorgulamak için URL`,restPathHint:`örn. state veya data.power.status`,restPowerUrlHint:`Güç verisi için ayrı URL (boşsa Durum URL'sini kullanır)`,restEnergyUrlHint:`Enerji verisi için ayrı URL (boşsa Durum URL'sini kullanır)`,restEnergyHint:`Her değer kendi URL'sini kullanabilir veya Durum URL'sine geri dönebilir. Birim dönüşümü için çarpanları kullanın (örn. Wh'yi kWh'ye dönüştürmek için 0.001).`,testConnection:`Bağlantıyı Test Et`,connectionSuccess:`Bağlantı başarılı`,noSwitchesInSwitchbar:`Anahtar çubuğunda anahtar yok`,enableSwitchbarHint:`Ayarlar > Akıllı Prizler'de "Anahtar Çubuğunda Göster"i etkinleştirin`},notifications:{providerTypes:{callmebot:`CallMeBot/WhatsApp`,ntfy:`ntfy`,pushover:`Pushover`,telegram:`Telegram`,email:`E-posta`,discord:`Discord`,webhook:`Webhook`,homeassistant:`Home Assistant`},providerDescriptions:{email:`SMTP e-posta bildirimleri`,telegram:`Telegram botu üzerinden bildirimler`,discord:`Webhook üzerinden Discord kanalına gönder`,ntfy:`Ücretsiz, kendi barındırılabilir push bildirimleri`,pushover:`Basit, güvenilir push bildirimleri`,callmebot:`CallMeBot üzerinden ücretsiz WhatsApp bildirimleri`,webhook:`Herhangi bir URL'ye genel HTTP POST`,homeassistant:`Home Assistant gösterge panelinde kalıcı bildirimler`},lastSuccess:`Son: {{date}}`,error:`Hata`,printer:`Yazıcı:`,allPrinters:`Tüm yazıcılar`,sendTestNotification:`Test Bildirimi Gönder`,eventSettings:`Olay Ayarları`,enabled:`Etkin`,sendFromProvider:`Bu sağlayıcıdan bildirim gönder`,printEvents:`Baskı Olayları`,printerStatus:`Yazıcı Durumu`,amsAlarms:`AMS Alarmları`,amsHtAlarms:`AMS-HT Alarmları`,printQueue:`Baskı Kuyruğu`,start:`Başlangıç`,plateCheck:`Plaka Kontrolü`,complete:`Tamamlandı`,failed:`Başarısız`,stopped:`Durduruldu`,progress:`İlerleme`,offline:`Çevrimdışı`,lowFilament:`Az Filament`,maintenance:`Bakım`,amsHumidity:`AMS Nemi`,amsTemp:`AMS Sıcaklığı`,amsHtHumidity:`AMS-HT Nemi`,amsHtTemp:`AMS-HT Sıcaklığı`,bedCooled:`Tabla Soğudu`,firstLayer:`İlk Katman`,quiet:`Sessiz`,digest:`Özet {{time}}`,printStarted:`Baskı Başladı`,plateNotEmpty:`Plaka Boş Değil`,plateNotEmptyDescription:`Baskıdan önce nesneler algılandı`,printCompleted:`Baskı Tamamlandı`,bedCooledLabel:`Tabla Soğudu`,bedCooledDescription:`Baskıdan sonra tabla eşiğin altına soğudu`,firstLayerCompleteLabel:`İlk Katman Tamamlandı`,firstLayerCompleteDescription:`İlk katman bittiğinde anlık görüntüyle bildir`,missingSpoolAssignmentLabel:`Eksik Makara Ataması`,missingSpoolAssignmentDescription:`Baskı başladığında ve gerekli tepsilerin atanmış makarası olmadığında bildir`,printFailed:`Baskı Başarısız`,printStopped:`Baskı Durduruldu`,progressMilestones:`İlerleme Kilometre Taşları`,progressMilestonesDescription:`%25, %50, %75'te bildir`,printerOffline:`Yazıcı Çevrimdışı`,printerError:`Yazıcı Hatası`,lowFilamentLabel:`Az Filament`,maintenanceDue:`Bakım Zamanı`,maintenanceDueDescription:`Bakım gerektiğinde bildir`,amsHumidityHigh:`AMS Nemi Yüksek`,amsHumidityHighDescription:`Normal AMS nemi eşiği aşıyor`,amsTemperatureHigh:`AMS Sıcaklığı Yüksek`,amsTemperatureHighDescription:`Normal AMS sıcaklığı eşiği aşıyor`,amsHtHumidityHigh:`AMS-HT Nemi Yüksek`,amsHtHumidityHighDescription:`AMS-HT nemi eşiği aşıyor`,amsHtTemperatureHigh:`AMS-HT Sıcaklığı Yüksek`,amsHtTemperatureHighDescription:`AMS-HT sıcaklığı eşiği aşıyor`,inventoryAlerts:`Envanter Uyarıları`,stockReorderAlert:`Yeniden Sipariş Uyarısı`,stockReorderAlertDescription:`SKU yeniden sipariş noktasına ulaştı`,stockBreakAlert:`Stok Tükenme Uyarısı`,stockBreakAlertDescription:`Yenileme gelmeden stok tükenecek`,jobAdded:`İş Eklendi`,jobAddedDescription:`İş kuyruğa eklendi`,jobAssigned:`İş Atandı`,jobAssignedDescription:`Model tabanlı iş yazıcıya atandı`,jobStarted:`İş Başladı`,jobStartedDescription:`Kuyruk işi yazdırmaya başladı`,jobWaiting:`İş Bekliyor`,jobWaitingDescription:`İş filament veya yazıcı için bekliyor`,jobSkipped:`İş Atlandı`,jobSkippedDescription:`İş atlandı (önceki başarısız oldu)`,jobFailed:`İş Başarısız`,jobFailedDescription:`İş başlatılamadı`,queueComplete:`Kuyruk Tamamlandı`,queueCompleteDescription:`Tüm kuyruk işleri bitti`,quietHours:`Sessiz Saatler`,noNotificationsDuring:`Bu saatler arasında bildirim yok`,editProviderToChangeQuietHours:`Sessiz saatleri değiştirmek için sağlayıcıyı düzenleyin`,dailyDigest:`Günlük Özet`,batchNotifications:`Bildirimleri tek bir günlük özette topla`,sendAt:`{{time}}'de gönder`,editProviderToChangeDigestTime:`Özet saatini değiştirmek için sağlayıcıyı düzenleyin`,edit:`Düzenle`,deleteProvider:`Bildirim Sağlayıcısını Sil`,deleteConfirm:`"{{name}}" silmek istediğinizden emin misiniz? Bu geri alınamaz.`,delete:`Sil`,addTitle:`Bildirim Sağlayıcısı Ekle`,editTitle:`Bildirim Sağlayıcısını Düzenle`,nameLabel:`Ad *`,namePlaceholder:`Bildirimlerim`,providerTypeLabel:`Sağlayıcı Türü *`,configuration:`Yapılandırma`,testConfiguration:`Yapılandırmayı Test Et`,printerFilter:`Yazıcı Filtresi`,onlyFromPrinter:`Yalnızca bu yazıcının olayları için bildirim gönder`,quietHoursDnd:`Sessiz Saatler (Rahatsız Etme)`,quietStart:`Başlangıç`,quietEnd:`Bitiş`,dailyDigestLabel:`Günlük Özet`,sendDigestAt:`Özeti şu saatte gönder`,digestCollected:`Olaylar toplanacak ve bu saatte tek bir özet olarak gönderilecek`,notificationEvents:`Bildirim Olayları`,progressPercent:`(%25, %50, %75)`,bedCooledAfterPrint:`(baskı tamamlandıktan sonra)`,eventPriority:{sectionTitle:`ntfy Önceliği`,helpNtfy:`Her etkin olay için bir öncelik seçin. ntfy uyarıları kademelendirmek için bunları kullanır (ses, görünürlük, push davranışı). Burada ayarlanmamış seviyeler ntfy sunucu varsayılanını kullanır.`,min:`Min`,low:`Düşük`,default:`Varsayılan`,high:`Yüksek`,urgent:`Acil`},cancel:`İptal`,save:`Kaydet`,add:`Ekle`,nameRequired:`Ad gerekli`,fieldRequired:`{{field}} gerekli`,phoneNumber:`Telefon Numarası`,apiKey:`API Anahtarı`,serverUrl:`Sunucu URL`,topic:`Konu`,authToken:`Yetkilendirme Belirteci`,userKey:`Kullanıcı Anahtarı`,appToken:`Uygulama Belirteci`,priority:`Öncelik`,botToken:`Bot Belirteci`,chatId:`Sohbet ID`,smtpServer:`SMTP Sunucusu`,smtpPort:`SMTP Portu`,security:`Güvenlik`,authentication:`Kimlik Doğrulama`,username:`Kullanıcı Adı`,password:`Parola`,fromEmail:`Gönderen E-posta`,toEmail:`Alıcı E-posta`,webhookUrl:`Webhook URL`,payloadFormat:`Yük Formatı`,authorization:`Yetkilendirme`,titleFieldName:`Başlık Alanı Adı`,messageFieldName:`Mesaj Alanı Adı`,editTemplate:`Şablonu Düzenle: {{name}}`,titleLabel:`Başlık`,bodyLabel:`Gövde`,titlePlaceholder:`Bildirim başlığı...`,bodyPlaceholder:`Bildirim gövdesi...`,availableVariables:`Kullanılabilir Değişkenler`,clickToInsert:`Gövdede imleç konumuna eklemek için tıklayın`,livePreview:`Canlı Önizleme`,hide:`Gizle`,show:`Göster`,loadingPreview:`Önizleme yükleniyor...`,enterTemplateContent:`Önizlemeyi görmek için şablon içeriğini girin`,titlePreview:`Başlık:`,bodyPreview:`Gövde:`,resetToDefault:`Varsayılana Sıfırla`,titleRequired:`Başlık gerekli`,bodyRequired:`Gövde gerekli`,notificationLog:`Bildirim Günlüğü`,showFailedOnly:`Yalnızca başarısız`,last24Hours:`Son 24 saat`,last7Days:`Son 7 gün`,last30Days:`Son 30 gün`,last90Days:`Son 90 gün`,justNow:`Az önce`,noFailedNotifications:`Başarısız bildirim yok`,noNotificationsLogged:`Kaydedilen bildirim yok`,unknownProvider:`Bilinmeyen Sağlayıcı`,logTitle:`Başlık`,logMessage:`Mesaj`,logError:`Hata`,logProvider:`Sağlayıcı: {{type}}`,logTime:`Saat: {{time}}`,refresh:`Yenile`,clearOld:`Eskileri Temizle`,statsSummary:`Son {{days}} gün:`,statsNotifications:`bildirim`,statsSent:`{{count}} gönderildi`,statsFailed:`{{count}} başarısız`,eventTypes:{print_start:`Baskı Başladı`,print_complete:`Baskı Tamamlandı`,print_failed:`Baskı Başarısız`,print_stopped:`Baskı Durduruldu`,print_progress:`İlerleme`,printer_offline:`Yazıcı Çevrimdışı`,printer_error:`Yazıcı Hatası`,filament_low:`Az Filament`,maintenance_due:`Bakım Zamanı`,test:`Test`},userEmail:{title:`Bildirimler`,emailNotifications:`E-posta Bildirimleri`,emailNotificationsDesc:`Kendi baskı işleriniz için e-posta bildirimleri alın. E-postalar Gelişmiş Kimlik Doğrulamada yapılandırılan sistem SMTP ayarları kullanılarak gönderilir.`,sendingTo:`Bildirimler şuraya gönderilecek`,noEmailWarning:`Hesabınızın e-posta adresi yok. Eklemek için bir yöneticiyle iletişime geçin.`,printJobNotifications:`Baskı İşi Bildirimleri`,printJobNotificationsDesc:`Gönderdiğiniz baskı işleri için hangi olayların e-posta bildirimlerini tetikleyeceğini seçin.`,printJobStarts:`Baskı İşi Başlar`,printJobStartsDesc:`Baskı işiniz başladığında bildirim alın.`,printJobFinishes:`Baskı İşi Biter`,printJobFinishesDesc:`Baskı işiniz başarıyla tamamlandığında bildirim alın.`,printErrors:`Baskı Hataları`,printErrorsDesc:`Baskı işiniz başarısız olduğunda veya bir hatayla karşılaştığında bildirim alın.`,printJobStops:`Baskı İşi Durur`,printJobStopsDesc:`Baskı işiniz iptal edildiğinde veya durdurulduğunda bildirim alın.`,saveSuccess:`Bildirim tercihleri kaydedildi.`,saveError:`Bildirim tercihleri kaydedilemedi.`}},richTextEditor:{bold:`Kalın`,italic:`İtalik`,underline:`Altı Çizili`,bulletList:`Madde İşaretli Liste`,numberedList:`Numaralı Liste`,alignLeft:`Sola Hizala`,alignCenter:`Ortaya Hizala`,alignRight:`Sağa Hizala`,addLink:`Bağlantı Ekle`,removeLink:`Bağlantıyı Kaldır`},externalLinks:{title:`Kenar çubuğu bağlantıları`,sidebarLayout:`Kenar çubuğu`,sidebarLayoutDescription:`Yerleşik sayfaları gösterin veya gizleyin, harici bağlantılar ekleyin ve kenar çubuğu gezinmesini yeniden sıralamak için öğeleri sürükleyin.`,systemPages:`Bambuddy sayfaları`,externalLinks:`Harici bağlantılar`,visibleInSidebar:`Kenar çubuğunda görünür`,hiddenFromSidebar:`Kenar çubuğunda gizli`,requiredInSidebar:`Kenar çubuğunda gerekli`,hidePage:`Sayfayı gizle`,showPage:`Sayfayı göster`,settingsCannotBeHidden:`Ayarlar gizlenemez`,noLinksConfigured:`Yapılandırılmış harici bağlantı yok`,deleteLink:`Bağlantıyı Sil`,removeCustomIcon:`Özel simgeyi kaldır`,openInNewTab:`Yeni sekmede aç`,placeholders:{linkName:`Bağlantım`}},keyboardShortcuts:{title:`Klavye Kısayolları`,navigation:`Navigasyon`,archivesSection:`Arşivler`,kProfilesSection:`K-Profilleri`,generalSection:`Genel`,shortcuts:{goToPrinters:`Yazıcılara Git`,goToArchives:`Arşivlere Git`,goToQueue:`Kuyruğa Git`,goToStats:`İstatistiklere Git`,goToProfiles:`Bulut Profillerine Git`,goToSettings:`Ayarlara Git`,focusSearch:`Aramaya odaklan`,openUploadModal:`Yükleme modalini aç`,clearSelection:`Seçimi temizle / odağı kaldır`,contextMenu:`Kartlarda bağlam menüsü`,refreshProfiles:`Profilleri yenile`,newProfile:`Yeni profil`,exitSelectionMode:`Seçim modundan çık`,showHelp:`Bu yardımı göster`},footer:`Kapatmak için Esc'ye basın veya dışarı tıklayın`},notificationLog:{title:`Bildirim Günlüğü`,events:{printStarted:`Baskı Başladı`,printComplete:`Baskı Tamamlandı`,printFailed:`Baskı Başarısız`,printStopped:`Baskı Durduruldu`,progress:`İlerleme`,printerOffline:`Yazıcı Çevrimdışı`,printerError:`Yazıcı Hatası`,lowFilament:`Az Filament`,maintenanceDue:`Bakım Zamanı`,test:`Test`},timeAgo:{justNow:`Az önce`,minutesAgo:`{{minutes}}dk önce`,hoursAgo:`{{hours}}sa önce`}},restoreBackup:{title:`Yedeği Geri Yükle`,restoring:`Geri yükleniyor...`,restoreComplete:`Geri Yükleme Tamamlandı`,restoreFailed:`Geri Yükleme Başarısız`,importSettings:`Ayarları bir yedek dosyasından içe aktar`,pleaseWait:`Verileriniz geri yüklenirken lütfen bekleyin`,clickToSelect:`Yedek dosyası seçmek için tıklayın (.json veya .zip)`,howDuplicateHandling:`Yinelenen yönetiminin nasıl çalıştığı:`,categories:{printers:`Yazıcılar`,smartPlugs:`Akıllı Prizler`,notificationProviders:`Bildirim Sağlayıcıları`,filaments:`Filamentler`,archives:`Arşivler`,pendingUploads:`Bekleyen Yüklemeler`,settingsTemplates:`Ayarlar ve Şablonlar`},matchingInfo:{printers:`seri numarasına göre eşleşti`,smartPlugs:`IP adresine göre eşleşti`,notificationProviders:`ada göre eşleşti`,filaments:`ad + tür + markaya göre eşleşti`,archives:`içerik hash'ine göre eşleşti`,pendingUploads:`dosya adına göre eşleşti`,settingsTemplates:`her zaman üzerine yazılır`},replaceExisting:`Mevcut veriyi değiştir`,keepExisting:`Mevcut veriyi koru`,replaceDescription:`Zaten mevcut olan öğeleri yedek verisiyle üzerine yaz`,keepDescription:`Yalnızca zaten mevcut olmayan öğeleri geri yükle`,caution:`Dikkat:`,cautionText:`Üzerine yazma, mevcut yapılandırmalarınızı yedek verisiyle değiştirecek. Güvenlik için yazıcı erişim kodları asla üzerine yazılmaz.`,itemsRestored:`Geri Yüklenen Öğeler`,itemsSkipped:`Atlanan Öğeler`,restored:`Geri yüklendi`,skipped:`Atlandı (zaten mevcut)`,filesLabel:`Dosyalar (3MF, küçük resimler, vb.)`,newApiKeysGenerated:`Yeni API Anahtarları Oluşturuldu`,newApiKeysWarning:`Bu anahtarlar yalnızca bir kez gösterilir. Şimdi kopyalayın!`,processingBackup:`Yedek dosyası işleniyor...`,noDataFound:`Yedek dosyasında geri yüklenecek veri bulunamadı.`,failedToRestore:`Yedek geri yüklenemedi. Lütfen dosya formatını kontrol edin.`},backupExport:{title:`Yedeği Dışa Aktar`,selectData:`Dahil edilecek veriyi seçin`,selectAll:`Tümünü Seç`,selectNone:`Hiçbirini Seçme`,categoryDescriptions:{settings:`Dil, tema, güncelleme tercihleri`,notifications:`ntfy, Pushover, Discord, vb.`,templates:`Özel mesaj şablonları`,smartPlugs:`Tasmota priz yapılandırmaları`,externalLinks:`Harici servislere kenar çubuğu bağlantıları`,printers:`Yazıcı bilgisi (erişim kodları hariç)`,plateDetection:`Boş plaka referans görüntüleri`,filaments:`Filament türleri ve maliyetleri`,maintenance:`Özel bakım programları`,archives:`Tüm baskı verileri + dosyalar (3MF, küçük resimler, fotoğraflar)`,projects:`Projeler, BOM öğeleri ve ekler`,pendingUploads:`İnceleme bekleyen sanal yazıcı yüklemeleri`,apiKeys:`Webhook API anahtarları (içe aktarımda yeni anahtarlar oluşturulur)`},requiresPrinters:`Yazıcıların seçili olması gerekir`,zipFileWarning:`ZIP dosyası oluşturulacak.`,zipFileDescription:`Tüm 3MF dosyalarını, küçük resimleri, zaman atlamalı videoları ve fotoğrafları içerir. Bu biraz zaman alabilir ve büyük bir dosya oluşturabilir.`,includeAccessCodes:`Erişim Kodlarını Dahil Et`,includeAccessCodesDescription:`Başka bir makineye aktarmak için`,includeAccessCodesWarning:`Erişim kodları düz metin olarak dahil edilecek. Bu yedek dosyasını güvende tutun!`,categoriesSelected:`{{selectedCount}} kategori seçildi`},pendingUploads:{placeholders:{notes:`Bu baskı hakkında notlar ekleyin...`},discardUpload:`Yüklemeyi At`,archiveAllUploads:`Tüm Yüklemeleri Arşivle`,discardAllUploads:`Tüm Yüklemeleri At`,archive:`Arşivle`,timeAgo:{justNow:`Az önce`,minutesAgo:`{{minutes}}dk önce`,hoursAgo:`{{hours}}sa önce`,daysAgo:`{{days}}g önce`}},apiBrowser:{placeholders:{requestBody:`JSON istek gövdesi...`,searchEndpoints:`Uç noktalarda ara...`}},configureAmsSlot:{title:`AMS Yuvasını Yapılandır`,slotConfigured:`Yuva Yapılandırıldı!`,configuringSlot:`Yuva yapılandırılıyor:`,slotLabel:`{{ams}} Yuva {{slot}}`,searchPresets:`Ön ayarlarda ara...`,colorPlaceholder:`Renk adı veya hex (örn., kahverengi, FF8800)`,clearCustomColor:`Özel rengi temizle`,noCloudPresets:`Bulut ön ayarı yok. Senkronize etmek için Bambu Cloud'a giriş yapın.`,noPresetsAvailable:`Kullanılabilir ön ayar yok. Bambu Cloud'a giriş yapın veya yerel profilleri içe aktarın.`,noMatchingPresets:`Eşleşen ön ayar bulunamadı.`,custom:`Özel`,builtin:`Yerleşik`,orcaCloud:`Orca Cloud`,bambuCloud:`Bambu Cloud`,settingsSentToPrinter:`Ayarlar yazıcıya gönderildi`,filamentProfile:`Filament Profili`,kProfileLabel:`K Profili (Basınç İlerlemesi)`,filteringFor:`Şu için filtreleniyor: {{material}}`,noKProfile:`K profili yok (varsayılan 0.020 kullan)`,noMatchingKProfiles:`Eşleşen K profili bulunamadı. Varsayılan K=0.020 kullanılacak.`,selectFilamentFirst:`Önce bir filament profili seçin`,kFromCalibration:`Yazıcı kalibrasyonundan K={{value}}`,customColorLabel:`Özel Renk (isteğe bağlı)`,presetColors:`{{name}} renkleri:`,showLessColors:`Daha az renk göster`,showMoreColors:`Daha fazla renk göster`,clear:`Temizle`,hexLabel:`Hex: #{{hex}}`,resetting:`Sıfırlanıyor...`,resetSlot:`Yuvayı Sıfırla`,cancel:`İptal`,configuring:`Yapılandırılıyor...`,configureSlot:`Yuvayı Yapılandır`},githubBackup:{title:`Git Yedekleme`,history:`Geçmiş`,downloadBackup:`Yedeği İndir`,restoreBackup:`Yedeği Geri Yükle`,noBackupsYet:`Henüz yedek yok`},emailSettings:{placeholders:{fromName:`BamBuddy`}},tagManagement:{searchTags:`Etiketlerde ara...`,renameTag:`Etiketi yeniden adlandır`,deleteTag:`Etiketi sil`},notificationTemplates:{placeholders:{title:`Bildirim başlığı...`,body:`Bildirim gövdesi...`}},batchTag:{placeholders:{newTag:`Yeni etiket girin...`}},photoGallery:{deletePhoto:`Fotoğrafı Sil`},filamentHoverCard:{copySpoolUuid:`Makara UUID'sini kopyala`},kProfilesView:{hasNote:`Notu var`,copyProfile:`Profili kopyala`},layout:{openMenu:`Menüyü aç`,noPermissionSystemInfo:`Sistem bilgisini görüntüleme izniniz yok`},dashboard:{dragToReorder:`Yeniden sıralamak için sürükleyin`,hideWidget:`Widget'ı gizle`},notificationProviderCard:{deleteNotificationProvider:`Bildirim Sağlayıcısını Sil`},fileManagerModal:{closeFileManager:`Dosya yöneticisini kapat`,sortFiles:`Dosyaları sırala`,goToParentFolder:`Üst klasöre git`,threeView:`3B Görünüm`},embeddedCameraViewer:{refreshStream:`Akışı yenile`,close:`Kapat`,zoomOut:`Uzaklaştır`,resetZoom:`Yakınlaştırmayı sıfırla`,zoomIn:`Yakınlaştır`,dragToResize:`Yeniden boyutlandırmak için sürükleyin`},timelapseViewer:{skipBack5s:`5 sn geri al`,skipForward5s:`5 sn ileri al`},notificationProviders:{descriptions:{email:`SMTP e-posta bildirimleri`,telegram:`Telegram botu üzerinden bildirimler`,discord:`Webhook üzerinden Discord kanalına gönder`,ntfy:`Ücretsiz, kendi barındırılabilir push bildirimleri`,pushover:`Basit, güvenilir push bildirimleri`,callmebot:`CallMeBot üzerinden ücretsiz WhatsApp bildirimleri`,webhook:`Herhangi bir URL'ye genel HTTP POST`}},logViewer:{searchPlaceholder:`Mesaj veya günlükçü adı ara...`,noLogEntries:`Günlük kaydı bulunamadı`},switchbarPopover:{noSwitchesInSwitchbar:`Anahtar çubuğunda anahtar yok`},projectPageModal:{placeholders:{title:`Başlık`,designer:`Tasarımcı`,license:`Lisans`,description:`Açıklama girin...`,profileTitle:`Profil Başlığı`,profileDescription:`Profil açıklaması...`}},spoolmanSettings:{},time:{unknown:`-`,waiting:`Bekliyor`,justNow:`Az önce`,now:`Şimdi`,minsAgo:`{{count}}dk önce`,inMins:`{{count}}dk sonra`,hoursAgo:`{{count}}sa önce`,inHours:`{{count}}sa sonra`,daysAgo:`{{count}}g önce`,inDays:`{{count}}g sonra`},spoolbuddy:{nav:{dashboard:`Gösterge Paneli`,ams:`AMS`,inventory:`Envanter`,writeTag:`Yaz`,settings:`Ayarlar`},status:{nfcReady:`NFC Hazır`,nfcOff:`NFC Kapalı`,offline:`Çevrimdışı`,online:`Çevrimiçi`,noPrinters:`Yazıcı yok`,deviceOffline:`Cihaz Çevrimdışı`,waitingConnection:`Cihaz bağlantısı bekleniyor...`,systemReady:`Sistem Hazır`,status:`Durum`},dashboard:{readyToScan:`Taramaya hazır`,idleMessage:`Tanımlamak için bir makarayı tartıya yerleştirin`,nfcHint:`NFC etiketi otomatik olarak okunacak`,device:`Cihaz`,syncWeight:`Ağırlığı Senkronize Et`,weightSynced:`Senkronize edildi!`,unknownTag:`Bilinmeyen Etiket`,newTag:`Yeni Etiket Algılandı`,onScale:`tartıda`,linkSpool:`Makaraya Bağla`,linkTagTitle:`Etiketi Makaraya Bağla`,linkTag:`Etiketi Bağla`,selectSpool:`Bu etiketi bağlamak için bir makara seçin:`,noUntagged:`Etiketsiz makara bulunamadı`,tagDetected:`Etiket algılandı`,noTag:`Etiket yok`,tagId:`Etiket`,grossWeight:`Brüt ağırlık`,spoolSize:`Makara boyutu`,close:`Kapat`,currentSpool:`Mevcut Makara`,plateReady:`Plaka hazır: {{name}}`,plateReadyLabel:`Temizlenmeye hazır plakalar`,plateClearAction:`Temizle`,plateClearedToast:`Plaka temizlendi olarak işaretlendi`,plateClearFailed:`Plaka temizlendi olarak işaretlenemedi`},modal:{spoolDetected:`Makara Algılandı`,assignToAms:`AMS'e Ata`,syncWeight:`Ağırlığı Senkronize Et`,weightSynced:`Senkronize edildi!`,syncing:`Senkronize ediliyor...`,newTagDetected:`Yeni Etiket Algılandı`,addToInventory:`Envantere Ekle`,assignToAmsTitle:`AMS'e Ata`,selectSlot:`Bir yuva seçin`,assign:`Ata`,assigning:`Atanıyor...`,assignSuccess:`Atandı!`,assignPendingInsert:`Atandı. Makarayı yerleştirdiğinizde yuva yapılandırılacak.`,assignError:`Makara atanamadı. Lütfen tekrar deneyin.`,noPrinterSelected:`Bir yazıcı seçin...`,noAmsDetected:`Bu yazıcıda AMS algılanmadı`,slot:`Yuva`},weight:{noReading:`Okuma yok`,stable:`Kararlı`,measuring:`Ölçülüyor...`,tare:`Dara`,calibrate:`Kalibre Et`},spool:{remaining:`Kalan`,material:`Malzeme`,brand:`Marka`,color:`Renk`,coreWeight:`Çekirdek`,labelWeight:`Etiket`,scaleWeight:`Tartı`,netWeight:`Net`,lastUsed:`Son kullanım`},ams:{noData:`AMS algılanmadı`,connectAms:`Filament yuvalarını görmek için bir AMS bağlayın`,noPrinter:`Yazıcı seçilmedi`,selectPrinter:`Üst çubuktan bir yazıcı seçin`,printerDisconnected:`Yazıcı bağlantısı kesildi`,humidity:`Nem`,level:`Seviye`,active:`Aktif`,slot:`Yuva`,empty:`Boş`},inventory:{search:`Makaralarda ara...`,empty:`Envanterde makara yok`,noResults:`Eşleşen makara yok`,spools:`makara`,addSpool:`Makara Ekle`},settings:{tabDevice:`Cihaz`,tabDisplay:`Görüntü`,tabScale:`Tartı`,tabUpdates:`Güncellemeler`,nfcReader:`NFC Okuyucu`,type:`Tür`,connection:`Bağlantı`,notConnected:`N/A`,deviceInfo:`Cihaz Bilgisi`,hostname:`Ana Bilgisayar`,uptime:`Çalışma Süresi`,systemConfig:`Arka Uç ve Kimlik Doğrulama`,backendUrl:`Bambuddy Arka Uç URL'si`,apiToken:`API Belirteci`,apiTokenPlaceholder:`API belirtecini girin`,saveConfig:`Yapılandırmayı Kaydet`,systemQueued:`Yapılandırma kuyruğa alındı.`,nfcDiagnostic:`NFC Tanılaması`,scaleDiagnostic:`Tartı Tanılaması`,readTagDiagnostic:`Etiket Okuma Tanılaması`,testNfc:`Okuyucuyu test et`,testScale:`Doğruluğu test et`,testReadTag:`Etiket oku`,systemFieldsRequired:`Arka uç URL'si gerekli.`,brightness:`Parlaklık`,saved:`Kaydedildi`,noBacklight:`DSI arka ışık algılanmadı. Parlaklık kontrolü DSI ekran gerektirir.`,screenBlank:`Ekran Kapanma Zaman Aşımı`,screenBlankDesc:`Hareketsizlik sonrası ekran kapanır. Uyandırmak için dokunun.`,displayNote:`Parlaklık bir yazılım filtresi olarak uygulanır.`,scaleCalibration:`Tartı Kalibrasyonu`,currentWeight:`Mevcut ağırlık`,tareOffset:`Dara`,calFactor:`Faktör`,knownWeight:`Bilinen ağırlık`,calStep1:`Tartıdan tüm öğeleri kaldırın ve Sıfır Ayarla'ya basın.`,calStep2:`Bilinen ağırlığı tartıya yerleştirin.`,setZero:`Sıfır Ayarla`,calibrateNow:`Kalibre Et`,calibrated:`Kalibre edildi`,tareSet:`Dara komutu gönderildi. Cihaz bekleniyor...`,tareComplete:`Dara tamam!`,tareTimedOut:`Dara zaman aşımına uğradı — SpoolBuddy daemon çalışıyor mu?`,tareFailed:`Dara komutu gönderilemedi`,zeroSet:`Sıfır noktası ayarlandı. Bilinen ağırlığı tartıya yerleştirin.`,calibrationDone:`Kalibrasyon tamamlandı!`,calibrationFailed:`Kalibrasyon başarısız`,lastCalibrated:`Son kalibrasyon`,stable:`Kararlı`,settling:`Sabitleniyor...`,firmware:`Firmware`,scale:`Tartı`,noDevice:`SpoolBuddy cihazı bulunamadı`,daemonVersion:`Daemon Sürümü`,currentVersion:`Mevcut`,versionPending:`Daemon bekleniyor...`,checking:`Kontrol ediliyor...`,checkUpdates:`Güncellemeleri Kontrol Et`,updateAvailable:`Güncelleme mevcut`,updateInstructions:`SSH üzerinden güncelle: yükseltmek için SpoolBuddy yükleme betiğini çalıştırın.`,upToDate:`Güncel`,includeBeta:`Beta sürümleri dahil et`},writeTag:{tabExisting:`Mevcut Makara`,tabNew:`Yeni Makara`,tabReplace:`Etiketi Değiştir`,searchPlaceholder:`Malzeme, renk, marka ile ara...`,noUntaggedSpools:`Etiketsiz makara yok`,noTaggedSpools:`Etiketli makara yok`,selectSpool:`Bir makara seçin, ardından okuyucuya boş bir NTAG yerleştirin`,placeTag:`Okuyucuya bir NTAG yerleştirin`,tagReady:`Etiket algılandı — yazmaya hazır`,writeTag:`Etiketi Yaz`,replaceTag:`Etiketi Değiştir`,writing:`Etiket yazılıyor...`,waiting:`SpoolBuddy bekleniyor...`,writeSuccess:`Etiket başarıyla yazıldı!`,writeFailed:`Yazma başarısız`,queueFailed:`Yazma komutu kuyruğa alınamadı`,tryAgain:`Tekrar Dene`,cancel:`İptal`,replaceWarning:`Eski etiket bağlantısı kaldırılacak. Yeni etiket yerini alacak.`,deviceOffline:`SpoolBuddy çevrimdışı`,material:`Malzeme`,colorName:`Renk Adı`,color:`Renk`,brand:`Marka`,weight:`Ağırlık (g)`,createSpool:`Makara Oluştur`,creating:`Oluşturuluyor...`,spoolCreated:`Makara oluşturuldu! Yazmaya hazır.`,createFailed:`Makara oluşturulamadı`,incompleteDataWarning:`Etiket eksik Spoolman verisiyle yazıldı`},quickMenu:{printerPower:`Yazıcı Gücü`,systemControls:`Sistem`,restartDaemon:`Daemon'u Yeniden Başlat`,restartBrowser:`Tarayıcıyı Yeniden Başlat`,reboot:`Yeniden Başlat`,shutdown:`Kapat`,swipeToClose:`Kapatmak için aşağı kaydır`,confirmTitle:`Onayla`,confirmShutdown:`SpoolBuddy'yi kapatmak istediğinizden emin misiniz? Tekrar açmak için fiziksel erişime ihtiyacınız olacak.`,confirmReboot:`SpoolBuddy'yi yeniden başlatmak istediğinizden emin misiniz?`,confirmRestartDaemon:`SpoolBuddy daemon yeniden başlatılsın mı? NFC ve tartı geçici olarak kullanılamayacak.`,confirmRestartBrowser:`Kiosk tarayıcısı yeniden başlatılsın mı? Ekran kısa bir süre kararacak.`,confirm:`Onayla`,confirmPlugOn:`{{name}} açılsın mı?`,confirmPlugOff:`{{name}} kapatılsın mı?`,turnOn:`Aç`,turnOff:`Kapat`}},diagnostic:{modalTitle:`Bağlantı tanılaması — {{name}}`,running:`Tanılama çalışıyor...`,runningElapsed:`Tanılama çalışıyor... ({{elapsed}}s)`,waitingForReportHint:`Yazıcının durum raporu yayınlamasını bekliyor — bu işlem en fazla {{max}} saniye sürebilir.`,runFailed:`Tanılama çalıştırılamadı: {{error}}`,retry:`Tekrar çalıştır`,runButton:`Tanılamayı çalıştır`,sectionTitle:`Bağlantı Tanılaması`,sectionDescription:`Bir yazıcının neden bağlanmadığını veya yazdırmadığını kontrol edin — port erişilebilirliği, LAN geliştirici modu, Docker ağ modu ve kimlik bilgileri.`,noPrinters:`Yapılandırılmış yazıcı yok.`,overall:{ok:`Sorun bulunamadı — yazıcı bağlantısı sağlıklı görünüyor.`,warnings:`Yazıcı çalışmalı, ancak bazı şeyler dikkat gerektiriyor.`,problems:`Yazıcının neden bağlanmadığını veya yazdırmadığını açıklayan sorunlar bulundu.`},check:{port_mqtt:{title:`Kontrol portu (MQTT 8883)`,pass:`Erişilebilir — yazıcı kontrol bağlantılarını kabul ediyor.`,fail:`Port 8883 erişilemez. Yazıcı kapalı, farklı bir IP adresinde veya bir güvenlik duvarı engelliyor. Yazıcı IP'sini ve hiçbir şeyin port 8883'ü engellemediğini doğrulayın.`},port_ftps:{title:`Dosya aktarım portu (FTPS 990)`,pass:`Erişilebilir — baskı dosyaları gönderme çalışacak.`,warn:`Port 990 erişilemez. İzleme yine çalışabilir, ancak yazıcıya baskı gönderme başarısız olacak. Port 990'ın engellenmediğinden emin olun.`},external_storage:{title:`Gönderilen dosyaları harici depolamada sakla (kurulum adımı 4)`,pass:`Yazıcı bu seçeneğin açık olduğunu bildiriyor — gönderilen dosyalar SD kartta saklanacak ve arşivler küçük resim ve dilimleyici meta verileri içerecek.`,fail:`Yazıcı bu seçeneğin kapalı olduğunu bildiriyor. "Gönderilen dosyaları harici depolamada sakla" seçeneğini etkinleştirin — yeni donanım yazılımlarında (P2S 01.02 / Bambu Studio 2.6+) düğme yazıcının Baskı Ayarları'nda; eski sürümlerde Bambu Studio / OrcaSlicer'in Cihaz sekmesindedir. Bu seçenek olmadan, arşivlenen her baskıda küçük resim ve dilimleyici meta verisi olmayacak.`,skip:`Kontrol edilmedi — etkin bir MQTT bağlantısı gerekli. Bu ayarın yalnızca dilimleyicide bulunduğu eski dilimleyicilerde yazıcı bunu bildirmez, bu nedenle seçenek kapalı olsa bile bu kontrol geçer — kurulum adımı 4'ü manuel olarak doğrulayın.`},port_rtsps:{title:`Kamera portu (RTSPS 322)`,pass:`Erişilebilir — kamera akışı çalışacak.`,warn:`Port 322 erişilemez. Canlı kamera görünümü çalışmayacak. Bu, baskıyı etkilemez.`},network_mode:{title:`Docker ağ modu`,pass:`Ana bilgisayar ağ modunda çalışıyor.`,warn:`Bambuddy, Docker köprü ağı kullanılarak çalışıyor. Yazıcı keşfi ve Sanal Yazıcı, ana bilgisayar ağ modu gerektirir — konteyneri "network_mode: host" ile yeniden oluşturun.`,skip:`Docker'da çalışmıyor — uygulanamaz.`},subnet:{title:`Ağ alt ağı`,pass:`Yazıcı ve Bambuddy aynı alt ağda.`,warn:`Yazıcı ({{printer_ip}}) ve Bambuddy ({{host_ip}}) farklı alt ağlarda. Alt ağlar arasında yönlendirme yapılandırılmazsa birbirlerine erişemeyebilirler.`,skip:`Alt ağ belirlenemedi — atlandı.`},mqtt_auth:{title:`Yazıcı kimlik bilgileri`,pass:`Yazıcı bağlantıyı kabul etti.`,fail:`Yazıcı erişilebilir ancak bağlantıyı reddetti. Büyük olasılıkla erişim kodu veya seri numarası yanlış. Erişim kodu, Geliştirici Modu her açılıp kapatıldığında değişir — yazıcı ekranından yeniden kopyalayın.`,skip:`Kontrol edilmedi — yazıcıya erişilemedi.`},developer_mode:{title:`LAN Geliştirici Modu`,pass:`Geliştirici Modu etkin.`,fail:`Yazıcıda Geliştirici Modu KAPALI. Yazıcının LAN ayarlarında etkinleştirin — ve OK ile onaylayın. Bu olmadan baskılar başlamayacak.`,skip:`Kontrol edilemedi — yazıcıya canlı bir bağlantı gerektirir.`},printer_publishing:{title:`Yazıcı durum yayını yapıyor`,pass:`Yazıcı durum güncellemelerini yayınlıyor — AMS, filamentler ve K profilleri dilimleyiciye doğru şekilde yansıtılacak.`,fail:`MQTT aracısı bağlantıyı kabul etti, ancak yazıcı hiç durum raporu yayınlamadı. Bu neredeyse her zaman yanlış ya da büyük/küçük harf hatalı bir seri numarasından kaynaklanır — device//report konusu büyük/küçük harfe duyarlıdır. Yazıcı ayarlarındaki seri numarasını cihazın ekranındaki ile karşılaştırarak doğrulayın.`,skip:`Kontrol edilemedi — yazıcıya canlı bir bağlantı gerektirir.`}}},systemHealth:{sectionTitle:`Sistem Sağlığı`,sectionDescription:`Bir destek talebine dönüşmeden önce genellikle kendi başınıza düzeltebileceğiniz bilinen sorunlar için son günlükleri tarar.`,rescan:`Yeniden tara`,clean:`Son {{times}} günlük kaydında bilinen sorun bulunamadı.`,logUnavailable:`Dosya günlüğü devre dışı, bu nedenle günlükler taranamıyor. Bu kontrolü kullanmak için dosya günlüğünü etkinleştirin.`,learnMore:`Nasıl düzeltilir`,fixLabel:`Düzeltme:`,occurrences:`{{times}}× görüldü — son {{lastSeen}}'de`,category:{layer8:`Bunu düzeltebilirsiniz`,environment:`Ortam`,bug:`Lütfen bunu bildirin`},signature:{"ftp-auth-rejected":{name:`Yazıcı erişim kodunu reddetti`,cause:`Yazıcı dosya aktarım girişini reddetti. Erişim kodu yanlış veya Geliştirici Modu açılıp kapatıldıktan sonra değişti.`,fix:`Erişim kodunu yazıcı ekranından (LAN ayarları) yeniden kopyalayın ve Bambuddy'deki yazıcının ayarlarında güncelleyin.`},"ftp-connection-timeout":{name:`Dosya aktarım bağlantısı zaman aşımına uğradı`,cause:`Bambuddy yazıcının dosya aktarım portuna (FTPS 990) erişemedi. Port engellendi veya yazıcı kapalı ya da başka bir alt ağda.`,fix:`Bambuddy ile yazıcı arasında hiçbir şeyin port 990'ı engellemediğinden ve her ikisinin de aynı ağda olduğundan emin olun.`},"ftp-ssl-error":{name:`Güvenli dosya aktarım el sıkışması başarısız`,cause:`Yazıcının dosya aktarım sunucusuyla TLS el sıkışması başarısız oldu. Bu genellikle bir güvenlik duvarı veya eski yazıcı firmware'idir.`,fix:`Yazıcı firmware'ini güncelleyin ve port 990'daki bağlantıyı hiçbir güvenlik duvarı veya proxy'nin engellemediğini kontrol edin.`},"mqtt-connection-flapping":{name:`Yazıcı bağlantısı sürekli düşüyor`,cause:`Kontrol bağlantısı (MQTT 8883) tekrar tekrar bağlantısı kesiliyor ve yeniden bağlanıyor — genellikle zayıf bir ağ yolu veya kısmen engellenmiş bir port.`,fix:`Yazıcıdaki Wi-Fi sinyalini kontrol edin, kablolu bağlantı tercih edin ve port 8883'ün güvenilir bir şekilde erişilebilir olduğundan emin olun.`},"camera-connection-refused":{name:`Kamera akışına erişilemez`,cause:`Canlı kameraya RTSPS 322 portunda erişilemedi. Port engellendi veya yazıcıda kamera veya LAN canlı görünümü kapalı.`,fix:`Yazıcıda kamerayı ve LAN canlı görünümünü etkinleştirin ve port 322'nin engellenmediğinden emin olun. Bu, baskıyı etkilemez.`},"database-locked":{name:`Veritabanı yazma çekişmesi`,cause:`SQLite veritabanı yük altında "veritabanı kilitli" hatalarıyla karşılaşıyor — aynı anda birkaç yazıcı çalıştırırken yaygındır.`,fix:`Bambuddy'yi harici bir PostgreSQL veritabanına geçirin. Dokümantasyondaki PostgreSQL kılavuzuna bakın.`}}},vpDiagnostic:{title:`Kurulum kontrolü — {{name}}`,runButton:`Kurulum kontrolünü çalıştır`,running:`Kurulum kontrolü çalışıyor...`,runFailed:`Kurulum kontrolü çalıştırılamadı: {{error}}`,retry:`Tekrar çalıştır`,overall:{ok:`Tüm kontroller geçti — bu sanal yazıcı doğru şekilde kurulmuş.`,warnings:`Sanal yazıcı çalışmalı, ancak bazı şeyler dikkat gerektiriyor.`,problems:`Dilimleyicinin bu sanal yazıcıyı neden göremediğini veya kullanamadığını açıklayan sorunlar bulundu.`},check:{enabled:{title:`Sanal yazıcı etkin`,fail:`Bu sanal yazıcı kapatılmış. Keşfedilebilir hale getirmek için açın.`},running:{title:`Servisler çalışıyor`,fail:`Sanal yazıcı etkin ancak servisleri çalışmıyor. Bambuddy günlüğünü kontrol edin — bir bind IP çakışması veya bir izin hatası genellikle onları durdurur.`},bind_interface:{title:`Bind ağ arayüzü`,fail:`Bind arayüzü ayarlanmamış veya artık bu ana bilgisayarda mevcut değil. Bind Arayüzü açılır menüsünde mevcut bir arayüz seçin.`},access_code:{title:`Erişim kodu ayarlandı`,fail:`Erişim kodu ayarlanmamış. Dilimleyiciye burada ayarladığınız aynı 8 karakterli erişim kodunun verilmesi gerekir.`},target_printer:{title:`Hedef yazıcı`,fail:`Hedef yazıcı seçilmemiş. Proxy modu iletmek için gerçek bir yazıcıya ihtiyaç duyar.`,warn:`Hedef yazıcı şu anda çevrimdışı — yeniden bağlandığında proxy devam edecek.`},port_ftps:{title:`Dosya yükleme servisi (port {{port}})`,fail:`Bind IP'sinin {{port}} portunda hiçbir şey dinlemiyor, bu nedenle dilimleyici dosya yükleyemez. Bu arayüzde bir port çakışması genellikle nedendir.`},port_mqtt:{title:`Kontrol servisi (port {{port}})`,fail:`Bind IP'sinin {{port}} portunda hiçbir şey dinlemiyor, bu nedenle dilimleyici bağlanamaz veya durum gösteremez.`},port_bind:{title:`Keşif servisi (port {{port}})`,fail:`Bind IP'sinin {{port}} portunda hiçbir şey dinlemiyor, bu nedenle dilimleyicinin keşif el sıkışması başarısız oluyor.`},certificate:{title:`TLS sertifikası`,pass:`Sertifika hazır. Bambuddy CA sertifikasının (yukarıda) dilimleyicinizin güven deposuna içe aktarıldığından emin olun.`,fail:`Bu sanal yazıcı için TLS sertifikası eksik. Bambuddy veri dizininin yazılabilir olduğunu kontrol edin.`}}},bugReport:{title:`Hata Bildir`,description:`Açıklama`,descriptionPlaceholder:`Ne ters gitti? Lütfen sorunu tanımlayın...`,email:`E-posta (isteğe bağlı)`,emailPlaceholder:`sizin@email.com`,emailPrivacy:`Sağlanırsa, e-postanız GitHub sorunundaki daraltılmış bir bölümde dahil edilecek, böylece bakımcı takip edebilir.`,screenshot:`Ekran Görüntüsü`,uploadOrPaste:`Bir görüntü yükleyin, yapıştırın veya sürükleyin`,dataCollectedSummary:`Rapora hangi veriler dahil edilir?`,dataIncluded:`Dahil:`,dataIncludedList:`Uygulama sürümü, OS, mimari, Python sürümü, veritabanı istatistikleri (yalnızca sayılar), yazıcı modelleri, nozul sayıları, firmware sürümleri, bağlantı durumu, entegrasyon durumu (Spoolman, MQTT, HA), hassas olmayan ayarlar, ağ arayüzü sayısı, Docker ayrıntıları, bağımlılık sürümleri.`,dataNeverIncluded:`Asla dahil edilmez:`,dataNeverIncludedList:`Yazıcı adları, seri numaraları, erişim kodları, parolalar, IP adresleri, e-posta adresleri, API anahtarları, belirteçler, webhook URL'leri, ana bilgisayar adları veya kullanıcı adları.`,submit:`Gönder`,startLogging:`Hata Ayıklama Kaydını Başlat`,stepEnableLogging:`Hata ayıklama kaydı etkin`,stepReproduce:`Sorunu şimdi yeniden oluşturun`,stepStopLogging:`Durdur ve raporu gönder`,stopAndSubmit:`Durdur ve Gönder`,maxDuration:`{{minutes}} dk sonra otomatik durur`,stoppingLogs:`Günlükler toplanıyor ve gönderiliyor...`,submitting:`Hata raporu gönderiliyor...`,submittingStepConnection:`Yazıcı bağlantı kontrolleri çalıştırılıyor`,submittingStepVirtualPrinters:`Sanal yazıcı kurulum kontrolleri çalıştırılıyor`,submittingStepLogScan:`Son günlüklerde bilinen sorunlar taranıyor`,submittingStepSubmit:`Rapor GitHub'a gönderiliyor`,submitSuccess:`Hata raporu başarıyla gönderildi!`,submitFailed:`Hata raporu gönderilemedi`,diagnosticChecking:`Yazıcı bağlantıları kontrol ediliyor...`,diagnosticHealthy:`Bağlantı kontrolü başarılı — yazıcılarınızda sorun bulunamadı.`,diagnosticSummary:`{{total}} yazıcıdan {{problems}} tanesinde bağlantı sorunu var`,diagnosticIntro:`Bir veya daha fazla yazıcının sorununuza neden olabilecek bir bağlantı sorunu var. Düzeltmeyi görmek için aşağıda bir yazıcıyı genişletin — bunu çözmek, hata raporu olmadan sorunu çözebilir. Yine de aşağıda bir rapor gönderebilirsiniz.`,logHealthSummary:`Günlüklerinizde bilinen sorunlar bulundu`,logHealthIntro:`Son günlükler bilinen sorunlarla eşleşiyor. Aşağıdaki düzeltmeleri kontrol edin — bunları çözmek, hata raporu olmadan sorununuzu çözebilir. Yine de aşağıda bir rapor gönderebilirsiniz.`,thankYou:`Teşekkürler!`,submitted:`Hata raporunuz gönderildi.`,viewIssue:`Sorunu Görüntüle`,unexpectedError:`Beklenmedik bir hata oluştu`},failureDetection:{title:`AI Başarısızlık Algılama`,description:`Baskıları kendi barındırılan bir Obico ML API ile izle ve algılanan başarısızlıklara otomatik olarak yanıt ver.`,mlUrl:`Obico ML API URL'si`,mlUrlHint:`Kendi barındırılan Obico ml_api konteynerinizin temel URL'si (örn. http://192.168.1.10:3333).`,test:`Test`,testSuccess:`ML API erişilebilir ve sağlıklı.`,testFailed:`ML API'ye erişilemedi.`,sensitivity:`Hassasiyet`,sensitivityLow:`Düşük (daha az yanlış pozitif)`,sensitivityMedium:`Orta (dengeli)`,sensitivityHigh:`Yüksek (erken algıla, daha fazla yanlış pozitif)`,sensitivityHint:`Uyarıları ve başarısızlıkları tetikleyen güven eşiklerini ayarlar.`,action:`Algılanan başarısızlıkta eylem`,actionNotify:`Yalnızca bildir`,actionPause:`Baskıyı duraklat`,actionPauseOff:`Duraklat ve gücü kes`,pollInterval:`Sorgulama aralığı (saniye)`,pollIntervalHint:`Baskı yaparken her yazıcının ne sıklıkta kontrol edileceği. Minimum 5sn, maksimum 120sn.`,externalUrlMissing:`Harici URL ayarlanmamış.`,externalUrlHint:`ML API, kamera anlık görüntüsünü URL üzerinden alır. ML API konteyneri Bambuddy'ye erişebilsin diye Genel ayarlarda Harici URL'yi ayarlayın.`,perPrinterTitle:`İzlenen Yazıcılar`,perPrinterHint:`Algılama servisinin hangi yazıcıları izleyeceğini seçin.`,monitorAll:`Tüm bağlı yazıcıları izle`,statusTitle:`Durum`,serviceRunning:`Servis çalışıyor`,thresholds:`Düşük / Yüksek eşikler`,activePrinters:`Aktif baskılar`,noActivePrints:`Şu anda çalışan baskı yok.`,historyTitle:`Son Algılamalar`,noHistory:`Henüz algılama yok.`},makerworld:{title:`MakerWorld`,description:`Bambu Handy uygulamasına gitmeden — doğrudan Bambuddy'den içe aktarmak ve yazdırmak için bir MakerWorld model URL'si yapıştırın.`,pasteUrlHeader:`MakerWorld'den İçe Aktar`,pasteUrlPlaceholder:`https://makerworld.com/en/models/… veya herhangi bir MakerWorld bağlantısını yapıştırın`,resolveButton:`Çöz`,signInRequiredTitle:`İndirme için Bambu Cloud girişi gerekli`,signInRequiredBody:`Model ayrıntılarına anonim olarak göz atabilirsiniz, ancak MakerWorld 3MF dosyalarını indirmek için bir Bambu Cloud hesabı gerektirir.`,openCloudSettings:`Bulut ayarlarını aç`,untitledModel:`Adsız model`,byCreator:`{{name}} tarafından`,downloadsCount:`{{count}} indirme`,licensePrefix:`Lisans`,alreadyImported:`Zaten kütüphanede`,openOnMakerworld:`MakerWorld'de aç`,alreadyInLibrary:`Bu model zaten kütüphanenizde — Dosya Yöneticisi → MakerWorld'de bulun`,importSuccess:`{{filename}} içe aktarıldı — Dosya Yöneticisi → MakerWorld'e kaydedildi`,platesHeader:`Plakalar ({{count}})`,plateDefaultName:`Plaka {{n}}`,materialCount:`{{count}} filament`,amsRequired:`AMS gerekli`,slicedFor:`{{printer}} için dilimlendi`,alsoCompatible:`Ayrıca uyumlu olarak işaretlendi: {{printers}}`,importToLibrary:`Kaydet`,sliceIn:`Kaydet ve {{slicer}}'de Dilimle`,disclaimer:`MakerWorld entegrasyonu topluluk tarafından belgelenen API uç noktalarını kullanır. Bambuddy, MakerWorld veya Bambu Lab ile ilişkili veya onaylı değildir.`,lastImportSuccess:`Kütüphanenize içe aktarıldı`,lastImportAlreadyInLibrary:`Zaten kütüphanenizde`,viewInLibrary:`Dosya Yöneticisinde Görüntüle`,openInBambuStudio:`Bambu Studio'da Aç`,openInOrcaSlicer:`OrcaSlicer'da Aç`,importTo:`Dosya yöneticisine içe aktar`,recentImportsHeader:`Son içe aktarımlar`,phaseResolving:`Çözülüyor`,phaseDownloading:`İndiriliyor`,folderAuto:`MakerWorld (varsayılan)`,importAll:`Tümünü içe aktar`,importAllProgress:`{{current}}/{{total}} içe aktarılıyor`,openGallery:`Görüntü galerisini aç`,galleryPrev:`Önceki görüntü`,galleryNext:`Sonraki görüntü`,deleteImport:`Kütüphaneden kaldır`,importDeleting:`Kaldırılıyor…`,importDeleted:`Kütüphaneden kaldırıldı`,confirmDelete:`{{filename}} kütüphaneden kaldırılsın mı? Bu, yerel dosyayı siler ancak plaka MakerWorld'den yeniden içe aktarılabilir.`,errors:{resolveFailed:`O MakerWorld URL'si çözülemedi.`,downloadFailed:`İndirme başarısız. Lütfen tekrar deneyin.`,deleteFailed:`Dosya kütüphaneden kaldırılamadı.`}},gcodeViewer:{back:`Geri`,backToArchives:`Baskı Arşivlerine Dön`,backToFiles:`Dosya Yöneticisine Dön`},libraryTrash:{title:`Çöp Kutusu`,headerButton:`Çöp`,headerTooltip:`Çöpe taşınan dosyaları görüntüle`,backToFiles:`Dosya Yöneticisine Dön`,subtitleAdmin:`Silinen dosyalar burada {{days}} gün kalır, ardından otomatik silinir. Bu görünüm tüm kullanıcılar için çöpteki dosyaları gösterir.`,subtitleUser:`Silinen dosyalar burada {{days}} gün kalır, ardından otomatik silinir.`,loading:`Çöp yükleniyor…`,loadError:`Çöp yüklenemedi.`,empty:`Çöp boş.`,summary:`{{count}} dosya · {{size}}`,emptyTrash:`Çöpü boşalt`,restore:`Geri Yükle`,purgeNow:`Şimdi sil`,autoPurgeIn:`{{when}} içinde otomatik silinir`,days:`gün`,retentionLabel:`Otomatik sil`,selectAll:`Tümünü seç`,selectOne:`{{filename}} seç`,selectionCount:`{{count}} seçildi`,bulkRestore:`Seçilenleri geri yükle`,bulkPurge:`Seçilenleri sil`,col:{filename:`Dosya`,folder:`Klasör`,size:`Boyut`,deleted:`Çöpe taşındı`,autoPurge:`Otomatik silinir`,owner:`Sahip`,actions:`İşlemler`},confirm:{purgeTitle:`Kalıcı olarak silinsin mi?`,purgeBody:`{{filename}} diskten silinecek ve geri yüklenemeyecek.`,emptyTitle:`Çöp boşaltılsın mı?`,emptyBody:`Tüm {{count}} dosya diskten silinecek. Bu geri alınamaz.`,bulkPurgeTitle:`Seçili dosyalar kalıcı olarak silinsin mi?`,bulkPurgeBody:`Seçili {{count}} dosya diskten silinecek ve geri yüklenemeyecek.`,cta:`Kalıcı olarak sil`},toast:{restored:`Dosya geri yüklendi.`,restoreFailed:`Dosya geri yüklenemedi.`,purged:`Dosya kalıcı olarak silindi.`,purgeFailed:`Dosya silinemedi.`,emptied:`{{count}} dosya çöpten silindi.`,emptyFailed:`Çöp boşaltılamadı.`,retentionSaved:`Otomatik silme {{days}} gün olarak ayarlandı.`,retentionFailed:`Saklama ayarı kaydedilemedi.`,bulkRestored:`{{count}} dosya geri yüklendi.`,bulkPurged:`{{count}} dosya silindi.`}},libraryPurge:{title:`Eski dosyaları temizle`,headerButton:`Eskileri temizle`,headerTooltip:`Eski dosyaları toplu olarak çöpe taşı`,description:`Kütüphanenizden eski dosyaları tek seferde temizleyin. Baskı geçmişi olan dosyalar son baskı tarihlerine göre, hiç yazdırılmamış dosyalar yükleme tarihlerine göre yaşlandırılır.`,ageLabel:`Şundan eski dosyaları taşı`,days:`gün`,includeNeverPrinted:`Hiç yazdırılmamış dosyaları dahil et`,effectsTitle:`Temizle'ye tıkladığınızda ne olur`,effect1:`Eşleşen dosyalar Çöp'e taşınır — henüz diskten silinmezler.`,effect2:`Saklama penceresi sona erene kadar Çöp'ten herhangi bir zamanda geri yükleyebilirsiniz.`,effect3:`Saklama sonrası, çöp süpürücü onları kalıcı olarak diskten kaldırır.`,effect4:`Harici (bağlı) klasörlerdeki dosyalar atlanır — Bambuddy asla sahibi olmadığı bayt'ları silmez.`,previewLoading:`Kaç dosyanın eşleştiği kontrol ediliyor…`,previewFailed:`Temizleme önizlenemedi.`,previewSummary:`{{count}} dosya · {{size}} çöpe taşınacak`,andMore:`…ve {{count}} daha`,warning:`Çöpe taşınan dosyalar saklama penceresi sona erene kadar depolama alanını kullanmaya devam eder. Diski hemen boşaltmak için sonradan Çöp'ü boşaltın.`,confirmCta:`{{count}} dosyayı çöpe taşı`,purging:`Çöpe taşınıyor…`,toast:{success:`{{count}} dosya çöpe taşındı.`,failed:`Dosyalar temizlenemedi.`}},libraryAutoPurge:{enableLabel:`Eski dosyaları otomatik temizle`,enableDescription:`Yönetici temizliğini günde bir kez çalıştırır. Dosyalar önce Çöp'e gider — hemen silinmezler.`,ageLabel:`Şundan eski dosyaları otomatik temizle`,ageDescription:`Minimum 7 gün, maksimum 10 yıl. Manuel Temizle düğmesiyle aynı yaş kuralını kullanır.`,days:`gün`,includeNeverPrinted:`Hiç yazdırılmamış dosyaları dahil et`,saveFailed:`Otomatik temizleme ayarları kaydedilemedi.`},archivePurge:{headerButton:`Eskileri temizle`,headerTooltip:`Eski arşivleri toplu olarak sil`,title:`Eski arşivleri temizle`,description:`Eski baskı geçmişini temizleyin. Her arşiv en son baskı tamamlamasına göre yaşlandırılır — bir arşivi yeniden yazdırmak yaşını yeniler, bu nedenle aktif çalışma asla temizlenmez.`,ageLabel:`Son şu kadar günde yazdırılmamış arşivleri sil`,days:`gün`,effectsTitle:`Temizle'ye tıkladığınızda ne olur`,effect1:`Eşleşen her arşiv listelerden gizlenir ve dosyaları diskten kaldırılır (3MF, küçük resim, zaman atlamalı video, kaynak 3MF, F3D tasarım dosyası, fotoğraflar).`,effect2:`Arşiv satırı veritabanında kalır, böylece Hızlı İstatistikler filament, süre, maliyet ve enerji katkısını korur — tek arşiv silme varsayılanı ile aynı.`,effect3:`Hızlı İstatistik katkısını da kaldırmak için aşağıdaki "Ayrıca istatistiklerden kaldır"ı işaretleyin (tek arşiv silme seçeneğiyle eşleşir). O yol geri alınamaz.`,effect4:`Bir arşivi yeniden yazdırmak yaş saatini yeniler, bu nedenle hâlâ kullandığınız arşivler güvendedir.`,purgeStatsLabel:`Ayrıca istatistiklerden kaldır`,purgeStatsHint:`Eşleşen arşivleri Hızlı İstatistiklerden (filament, süre, maliyet, enerji) düşürür. Bu olmadan, Hızlı İstatistikler her katkıyı korur ve yalnızca dosyalar diskten ayrılır.`,previewLoading:`Kaç arşivin eşleştiği kontrol ediliyor…`,previewFailed:`Temizleme önizlenemedi.`,previewSummary:`{{count}} arşiv · {{size}} kaldırılacak`,andMore:`…ve {{count}} daha`,warning:`Dosyalar diskten kaldırılır ve geri yüklenemez. Devam etmeden önce saklamak istediğiniz her şeyi indirin veya favoriye ekleyin.`,confirmCta:`{{count}} arşivi kaldır`,purging:`Kaldırılıyor…`,toast:{success:`{{count}} arşiv kaldırıldı.`,failed:`Arşivler temizlenemedi.`}},archiveAutoPurge:{enableLabel:`Eski arşivleri otomatik temizle`,enableDescription:`Günde bir kez, eşik içinde yazdırılmamışsa arşivleri listelerden gizler ve dosyalarını diskten kaldırır. Bir arşivi yeniden yazdırmak saati sıfırlar.`,ageLabel:`Son şu kadar günde yazdırılmamış arşivleri otomatik sil`,ageDescription:`Minimum 7 gün, maksimum 10 yıl. En son baskı tamamlamasına göre — bir arşivi yeniden yazdırmak yaşını yeniler. 3MF, küçük resim, zaman atlamalı video, kaynak 3MF, F3D ve fotoğrafları kaldırır.`,days:`gün`,purgeStatsLabel:`Ayrıca istatistiklerden kaldır`,purgeStatsDescription:`Etkinleştirildiğinde, günlük süpürücü her temizlenen arşivi de Hızlı İstatistiklerden (filament, süre, maliyet, enerji) düşürür. Varsayılan kapalı — Hızlı İstatistikler katkıyı korur, yalnızca dosyalar diskten ayrılır.`,runNow:`Arşivleri şimdi temizle`,saveFailed:`Otomatik temizleme ayarları kaydedilemedi.`},cameraTokens:{title:`Kamera API Belirteçleri`,navTitle:`Kamera API belirteçleri`,description:`Kamera akışını Home Assistant, Frigate, kiosklar veya kararlı bir URL'ye ihtiyaç duyan başka herhangi bir araca gömmek için uzun ömürlü belirteçler. Her belirteç yalnızca kamera akışı içindir ve herhangi bir zamanda iptal edilebilir.`,loading:`Yükleniyor…`,confirmRevoke:{title:`Bu belirteç iptal edilsin mi?`,body:`"{{name}}" kullanan herhangi bir cihaz hemen erişimini kaybedecek. Bu geri alınamaz.`,cancel:`İptal`,confirm:`İptal Et`},create:{title:`Yeni belirteç oluştur`,nameLabel:`Belirteç adı`,namePlaceholder:`örn. Home Assistant`,daysLabel:`Sona erene kadar gün`,submit:`Oluştur`,hint:`Maksimum ömür 365 gün. Belirteç değeri oluşturmada yalnızca bir kez gösterilir — şimdi kopyalayın.`},created:{title:`Belirteç oluşturuldu — şimdi kopyalayın`,warning:`Bu, bu belirtecin görünür olacağı tek seferdir. Bu iletişim kutusunu kapattıktan sonra onu bir daha asla görüntüleyemezsiniz.`,copy:`Kopyala`,dismiss:`Kaydettim`},list:{myTitle:`Belirteçlerim`,allTitle:`Tüm kullanıcılar (yönetici görünümü)`,empty:`Henüz belirteç yok.`,name:`Ad`,owner:`Sahip`,prefix:`Önek`,created:`Oluşturuldu`,expires:`Sona Erer`,lastUsed:`Son kullanım`,revoke:`İptal Et`,expired:`Süresi Doldu`},toast:{created:`Belirteç oluşturuldu`,createFailed:`Belirteç oluşturulamadı`,revoked:`Belirteç iptal edildi`,revokeFailed:`Belirteç iptal edilemedi`,loadFailed:`Belirteçler yüklenemedi`,copied:`Panoya kopyalandı`,copyFailed:`Kopyalama başarısız — manuel olarak seçip kopyalayın`}},forecast:{title:`Tahmin`,noSpools:`Aktif makara bulunamadı. Tahmin verisini görmek için envanterinize makara ekleyin.`,noUsageData:`Kullanım verisi mevcut değil — stok zaman çizelgesi öngörülemiyor.`,sku:`SKU`,material:`Malzeme`,stock:`Stok`,dailyRate:`Oran`,daysLeft:`Kalan Gün`,emptyBy:`Şu Tarihte Boş`,reorderBy:`Şu Tarihte Sipariş`,actions:`İşlemler`,trend:`Trend`,estimated:`Tahmini`,noData:`Veri yok`,timeframe:`Zaman Aralığı`,chartTitle:`Öngörülen Stok — İlk 5 Malzeme`,dashedLinesROP:`Kesik çizgiler = yeniden sipariş noktaları`,stockLevel:`Stok Seviyesi`,reorderPoint:`Yeniden Sipariş Noktası`,safetyMargin:`Güvenlik Marjı`,trendLegend:`Trend (geçmiş tabanlı, %95 servis seviyesi)`,estimatedLegend:`Tahmini (ağırlık farkı)`,noDataLegend:`Veri yok`,ropLabel:`ROP`,ssLabel:`SS`,safetyStockLegend:`Güvenlik stoku`,stockArrivalLegend:`Stok varışı`,stockoutLegend:`Stok tükenmesi`,alertCount_one:`{{count}} uyarı`,alertCount_other:`{{count}} uyarı`,order:`Sipariş`,save:`Kaydet`,cancel:`İptal`,settingsSaved:`Ayarlar kaydedildi`,failedSaveSettings:`Ayarlar kaydedilemedi`,globalLeadTimeSaved:`Global teslim süresi kaydedildi`,globalLeadTime:`Global teslim süresi`,globalLeadTimeHint:`Global teslim süresi tabanı — tüm SKU'lar için yeniden sipariş noktası hesaplamasında kullanılır`,skuLeadTimeOverride:`SKU Teslim Süresi Geçersiz Kılma`,skuLeadTimeHint:`0 = global teslim süresini kullan. Bu SKU için geçersiz kılmak için >0 ayarlayın.`,safetyMarginLabel:`Güvenlik Marjı`,effectiveLeadTime:`Etkili Teslim Süresi`,effectiveLeadTimeHint:`max(global {{global}}g, SKU {{sku}}g)`,reorderPointHint:`d̄ × LT + güvenlik marjı — stok bu seviyeye geldiğinde sipariş ver`,safetyMarginHint:`İstatistiksel güvenlik stoku (z=1.65 × σ × √LT) + kullanıcı tanımlı tampon`,safetyMarginHintDays:`İstatistiksel güvenlik stokunun üzerine eklenen tampon.{{approx}}`,safetyMarginHintDaysApprox:` ≈ mevcut oranda {{g}}g.`,safetyMarginHintG:`İstatistiksel güvenlik stokunun üzerine eklenen sabit ağırlık tamponu.{{approx}}`,safetyMarginHintGApprox:` ≈ mevcut oranda {{days}}g.`,individualSpools:`Bireysel makaralar`,labelWeight:`Etiket`,spoolCount_one:`{{count}} makara`,spoolCount_other:`{{count}} makara`,stockBreakRisk:`Stok tükenme riski`,stockBreakDetail:`{{days}}g kaldı, teslim süresi {{lt}}g.`,stockBreakBefore:`Yenileme öncesi stok tükenmesi`,reorderNow:`Şimdi sipariş ver`,reorderTriggerPassed:`Tetikleyici tarih {{date}} geçti.`,shoppingList:`Alışveriş Listesi`,shoppingListItems_one:`({{count}} öğe)`,shoppingListItems_other:`({{count}} öğe)`,shoppingListEmpty:`Alışveriş listesi boş. Öğe eklemek için herhangi bir satırdaki sepet simgesine tıklayın.`,addToCart:`Alışveriş listesine ekle`,alertsSnoozed:`Bu SKU için uyarıları sustur`,alertsEnabled:`Bu SKU için uyarıları aç`,addedToCart:`Alışveriş listesine eklendi`,failedAddItem:`Öğe eklenemedi`,listView:`Liste`,logisticsView:`Lojistik`,qty:`Adet`,weight:`Ağırlık`,leadTime:`Teslim Süresi`,expectedRestock:`Beklenen Yenileme`,status:`Durum`,note:`Not`,pending:`Beklemede`,purchased:`Satın Alındı`,received:`Teslim Alındı`,markPurchased:`Satın alındı olarak işaretle`,markReceived:`Teslim alındı olarak işaretle — makaraları Stok envanterine ekler`,resetToPending:`Beklemeye sıfırla`,remove:`Kaldır`,clearAll:`Tümünü temizle`,downloadCsv:`CSV`,addToCartTitle:`Alışveriş Listesine Ekle`,byQuantity:`Miktara Göre`,byDuration:`Süreye Göre`,numberOfSpools:`Makara sayısı`,lastHowManyDays:`Kaç gün dayanmalı?`,noUsageQty:`Kullanım verisi yok — miktar 1 olarak ayarlandı.`,noteOptional:`Not (isteğe bağlı)`,notePlaceholder:`örn. X projesi için, acil…`,addNSpools_one:`{{count}} makara ekle`,addNSpools_other:`{{count}} makara ekle`,onArrival:`Varışta`,stockBreakIn:`{{days}}g içinde stok tükenmesi.`,stockRunsOutBefore:`{{lt}}g teslim süresi dolmadan önce stok tükenir.`,atRate:`{{rate}}g/gün oranında size gerekli`,moreSpools_one:`{{count}} daha makara`,moreSpools_other:`{{count}} daha makara`,bridgeGap:`boşluğu kapatmak için.`,noReadAccess:`Envanter tahminlerini görüntüleme izniniz yok.`,noWriteAccess:`Tahmin ayarlarını değiştirme izniniz yok.`}}}},fallbackLng:`en`,supportedLngs:[`en`,`de`,`es`,`fr`,`ja`,`it`,`ko`,`pt-BR`,`tr`,`zh-CN`,`zh-TW`],detection:{order:[`localStorage`,`navigator`,`htmlTag`],lookupLocalStorage:`bambutrack_language`,caches:[`localStorage`]},interpolation:{escapeValue:!1},react:{useSuspense:!1}});var Ct=[{code:`en`,name:`English`,nativeName:`English`},{code:`de`,name:`German`,nativeName:`Deutsch`},{code:`es`,name:`Spanish`,nativeName:`Español`},{code:`fr`,name:`French`,nativeName:`Français`},{code:`ja`,name:`Japanese`,nativeName:`日本語`},{code:`it`,name:`Italian`,nativeName:`Italiano`},{code:`ko`,name:`Korean`,nativeName:`한국어`},{code:`pt-BR`,name:`Portuguese (Brazil)`,nativeName:`Português (Brasil)`},{code:`zh-CN`,name:`Chinese (Simplified)`,nativeName:`简体中文`},{code:`zh-TW`,name:`Chinese (Traditional)`,nativeName:`繁體中文`},{code:`tr`,name:`Turkish`,nativeName:`Türkçe`}],wt=`modulepreload`,Tt=function(e){return`/`+e},Et={},Dt=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=Tt(t,n),t in Et)return;Et[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:wt,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},Ot=`popstate`;function kt(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function At(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return Ft(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:It(t)}return Rt(t,n,null,e)}function jt(e,t){if(e===!1||e==null)throw Error(t)}function Mt(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function Nt(){return Math.random().toString(36).substring(2,10)}function Pt(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function Ft(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?Lt(t):t,state:n,key:t&&t.key||r||Nt(),mask:i}}function It({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function Lt(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function Rt(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=kt(e)?e:Ft(h.location,e,t);n&&n(r,e),l=u()+1;let d=Pt(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=kt(e)?e:Ft(h.location,e,t);n&&n(r,e),l=u();let i=Pt(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return zt(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(Ot,d),c=e,()=>{i.removeEventListener(Ot,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function zt(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),jt(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:It(t);return i=i.replace(/ $/,`%20`),!n&&i.startsWith(`//`)&&(i=r+i),new URL(i,r)}function Bt(e,t,n=`/`){return Vt(e,t,n,!1)}function Vt(e,t,n,r,i){let a=sn((typeof t==`string`?Lt(t):t).pathname||`/`,n);if(a==null)return null;let o=i??Ut(e),s=null,c=on(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;jt(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=gn([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(jt(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),Wt(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:en(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of Gt(e.path))a(e,t,!0,n)}),t}function Gt(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=Gt(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function Kt(e){e.sort((e,t)=>e.score===t.score?tn(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var qt=/^:[\w-]+$/,Jt=3,Yt=2,Xt=1,Zt=10,Qt=-2,$t=e=>e===`*`;function en(e,t){let n=e.split(`/`),r=n.length;return n.some($t)&&(r+=Qt),t&&(r+=Yt),n.filter(e=>!$t(e)).reduce((e,t)=>e+(qt.test(t)?Jt:t===``?Xt:Zt),r)}function tn(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function nn(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function an(e,t=!1,n=!0){Mt(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function on(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return Mt(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function sn(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var cn=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ln(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?Lt(e):e,a;return n?(n=hn(n),a=n.startsWith(`/`)?un(n.substring(1),`/`):un(n,t)):a=t,{pathname:a,search:yn(r),hash:bn(i)}}function un(e,t){let n=_n(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function dn(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function fn(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function pn(e){let t=fn(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function mn(e,t,n,r=!1){let i;typeof e==`string`?i=Lt(e):(i={...e},jt(!i.pathname||!i.pathname.includes(`?`),dn(`?`,`pathname`,`search`,i)),jt(!i.pathname||!i.pathname.includes(`#`),dn(`#`,`pathname`,`hash`,i)),jt(!i.search||!i.search.includes(`#`),dn(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=ln(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var hn=e=>e.replace(/\/\/+/g,`/`),gn=e=>hn(e.join(`/`)),_n=e=>e.replace(/\/+$/,``),vn=e=>_n(e).replace(/^\/*/,`/`),yn=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,bn=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,xn=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Sn(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Cn(e){return gn(e.map(e=>e.route.path).filter(Boolean))||`/`}var wn=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Tn(e,t){let n=e;if(typeof n!=`string`||!cn.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(wn)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=sn(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{Mt(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var En=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(En);var Dn=[`GET`,...En];new Set(Dn);var On=y.createContext(null);On.displayName=`DataRouter`;var kn=y.createContext(null);kn.displayName=`DataRouterState`;var An=y.createContext(!1);function jn(){return y.useContext(An)}var Mn=y.createContext({isTransitioning:!1});Mn.displayName=`ViewTransition`;var Nn=y.createContext(new Map);Nn.displayName=`Fetchers`;var Pn=y.createContext(null);Pn.displayName=`Await`;var Fn=y.createContext(null);Fn.displayName=`Navigation`;var In=y.createContext(null);In.displayName=`Location`;var Ln=y.createContext({outlet:null,matches:[],isDataRoute:!1});Ln.displayName=`Route`;var Rn=y.createContext(null);Rn.displayName=`RouteError`;var zn=`REACT_ROUTER_ERROR`,Bn=`REDIRECT`,Vn=`ROUTE_ERROR_RESPONSE`;function Hn(e){if(e.startsWith(`${zn}:${Bn}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Un(e){if(e.startsWith(`${zn}:${Vn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new xn(t.status,t.statusText,t.data)}catch{}}function Wn(e,{relative:t}={}){jt(Gn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=y.useContext(Fn),{hash:i,pathname:a,search:o}=tr(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:gn([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Gn(){return y.useContext(In)!=null}function Kn(){return jt(Gn(),`useLocation() may be used only in the context of a component.`),y.useContext(In).location}var qn=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Jn(e){y.useContext(Fn).static||y.useLayoutEffect(e)}function Yn(){let{isDataRoute:e}=y.useContext(Ln);return e?br():Xn()}function Xn(){jt(Gn(),`useNavigate() may be used only in the context of a component.`);let e=y.useContext(On),{basename:t,navigator:n}=y.useContext(Fn),{matches:r}=y.useContext(Ln),{pathname:i}=Kn(),a=JSON.stringify(pn(r)),o=y.useRef(!1);return Jn(()=>{o.current=!0}),y.useCallback((r,s={})=>{if(Mt(o.current,qn),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=mn(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:gn([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Zn=y.createContext(null);function Qn(){return y.useContext(Zn)}function $n(e){let t=y.useContext(Ln).outlet;return y.useMemo(()=>t&&y.createElement(Zn.Provider,{value:e},t),[t,e])}function er(){let{matches:e}=y.useContext(Ln);return e[e.length-1]?.params??{}}function tr(e,{relative:t}={}){let{matches:n}=y.useContext(Ln),{pathname:r}=Kn(),i=JSON.stringify(pn(n));return y.useMemo(()=>mn(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function nr(e,t){return rr(e,t)}function rr(e,t,n){jt(Gn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=y.useContext(Fn),{matches:i}=y.useContext(Ln),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Sr(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. -Please change the parent to .`)}let u=Kn(),d;if(t){let e=typeof t==`string`?Lt(t):t;jt(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):Bt(e,{pathname:p});Mt(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Mt(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=ur(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:gn([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:gn([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?y.createElement(In.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function ir(){let e=yr(),t=Sn(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=y.createElement(y.Fragment,null,y.createElement(`p`,null,`💿 Hey developer 👋`),y.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,y.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,y.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),y.createElement(y.Fragment,null,y.createElement(`h2`,null,`Unexpected Application Error!`),y.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?y.createElement(`pre`,{style:i},n):null,o)}var ar=y.createElement(ir,null),or=class extends y.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Un(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:y.createElement(Ln.Provider,{value:this.props.routeContext},y.createElement(Rn.Provider,{value:e,children:this.props.component}));return this.context?y.createElement(cr,{error:e},t):t}};or.contextType=An;var sr=new WeakMap;function cr({children:e,error:t}){let{basename:n}=y.useContext(Fn);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Hn(t.digest);if(e){let r=sr.get(t);if(r)throw r;let i=Tn(e.location,n);if(wn&&!sr.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw sr.set(t,n),n}return y.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function lr({routeContext:e,match:t,children:n}){let r=y.useContext(On);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),y.createElement(Ln.Provider,{value:e},n)}function ur(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);jt(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Cn(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||ar,o&&(s<0&&c===0?(Sr(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?y.createElement(n.route.Component,null):n.route.element?n.route.element:e,y.createElement(lr,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?y.createElement(or,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function dr(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function fr(e){let t=y.useContext(On);return jt(t,dr(e)),t}function pr(e){let t=y.useContext(kn);return jt(t,dr(e)),t}function mr(e){let t=y.useContext(Ln);return jt(t,dr(e)),t}function hr(e){let t=mr(e),n=t.matches[t.matches.length-1];return jt(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function gr(){return hr(`useRouteId`)}function _r(){let e=pr(`useNavigation`);return y.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function vr(){let{matches:e,loaderData:t}=pr(`useMatches`);return y.useMemo(()=>e.map(e=>Ht(e,t)),[e,t])}function yr(){let e=y.useContext(Rn),t=pr(`useRouteError`),n=hr(`useRouteError`);return e===void 0?t.errors?.[n]:e}function br(){let{router:e}=fr(`useNavigate`),t=hr(`useNavigate`),n=y.useRef(!1);return Jn(()=>{n.current=!0}),y.useCallback(async(r,i={})=>{Mt(n.current,qn),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var xr={};function Sr(e,t,n){!t&&!xr[e]&&(xr[e]=!0,Mt(!1,n))}y.memo(Cr);function Cr({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return rr(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function wr({to:e,replace:t,state:n,relative:r}){jt(Gn(),` may be used only in the context of a component.`);let{static:i}=y.useContext(Fn);Mt(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=y.useContext(Ln),{pathname:o}=Kn(),s=Yn(),c=mn(e,pn(a),o,r===`path`),l=JSON.stringify(c);return y.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Tr(e){return $n(e.context)}function Er(e){jt(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Dr({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){jt(!Gn(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=y.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=Lt(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=y.useMemo(()=>{let e=sn(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return Mt(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:y.createElement(Fn.Provider,{value:c},y.createElement(In.Provider,{children:t,value:h}))}function Or({children:e,location:t}){return nr(kr(e),t)}y.Component;function kr(e,t=[]){let n=[];return y.Children.forEach(e,(e,r)=>{if(!y.isValidElement(e))return;let i=[...t,r];if(e.type===y.Fragment){n.push.apply(n,kr(e.props.children,i));return}jt(e.type===Er,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),jt(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=kr(e.props.children,i)),n.push(a)}),n}var Ar=`get`,jr=`application/x-www-form-urlencoded`;function Mr(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Nr(e){return Mr(e)&&e.tagName.toLowerCase()===`button`}function Pr(e){return Mr(e)&&e.tagName.toLowerCase()===`form`}function Fr(e){return Mr(e)&&e.tagName.toLowerCase()===`input`}function Ir(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Lr(e,t){return e.button===0&&(!t||t===`_self`)&&!Ir(e)}function Rr(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function zr(e,t){let n=Rr(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Br=null;function Vr(){if(Br===null)try{new FormData(document.createElement(`form`),0),Br=!1}catch{Br=!0}return Br}var Hr=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Ur(e){return e!=null&&!Hr.has(e)?(Mt(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${jr}"`),null):e}function Wr(e,t){let n,r,i,a,o;if(Pr(e)){let o=e.getAttribute(`action`);r=o?sn(o,t):null,n=e.getAttribute(`method`)||Ar,i=Ur(e.getAttribute(`enctype`))||jr,a=new FormData(e)}else if(Nr(e)||Fr(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a