diff --git a/CHANGELOG.md b/CHANGELOG.md index 267396385..f144d9561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to Bambuddy will be documented in this file. - **Orca Cloud profile sync now connects by approving a code instead of the copy-paste sign-in** — Connecting Bambuddy to Orca Cloud used to mean opening an OAuth sign-in in a new tab, watching it redirect to a `localhost` URL that fails to load, then copying that dead URL out of the address bar and pasting it back into Bambuddy. That dance existed only because Orca's auth backend (Supabase) accepts no redirect target other than `localhost`, and the deliberately-broken redirect page confused nearly everyone who reached it. OrcaSlicer has since shipped a first-class external-app pairing API (the OAuth 2.0 Device Authorization Grant, RFC 8628), so the flow is now: click **Connect**, approve a short code on your Orca Cloud settings page, and Bambuddy pairs itself — no redirect, no paste, no client secret, and it behaves identically from a LAN IP, `localhost`, or behind a reverse proxy. Bambuddy requests **read-only** access (it only lists and views your Orca Cloud profiles), keeps the pairing alive with the API's rotating refresh tokens (validated end-to-end against Orca's staging and production servers), and stores nothing beyond the issued token pair. The profile list and detail views are unchanged, so nothing downstream of the connect step looks different. The old paste-based sign-in and the email/password fallback are removed. Points at production Orca Cloud by default; `ORCA_CLOUD_API_BASE` overrides the endpoint for testing. ### Fixed +- **Every SpoolBuddy screen crashed the moment a text field was focused (#2616, reporters @MartinNYHC, @agentdr8)** — Tapping the Search box on the SpoolBuddy inventory, or the Search / Color Name / Brand fields on the write-tag New Spool tab, blanked the UI with a minified React error #130 ("Element type is invalid… but got: object"). It hit both internal and Spoolman inventories, so it was not data-specific. **Root cause.** The SpoolBuddy shell mounts an on-screen keyboard (`VirtualKeyboard`) that pops up on `focusin` for any text input — which is why every field on every SpoolBuddy page tripped it, while the main app (no on-screen keyboard) was fine. That component does `import Keyboard from 'react-simple-keyboard'`, a CommonJS package, and under the current bundler's CJS→ESM interop the default import resolves to the module **namespace object** (`{ KeyboardReact, default }`) rather than the component itself. Rendering that object as `` put an object where an element type belongs, and React threw. (The discrepancy is interop-specific: the test runner hands back the real component, so it only manifested in the browser build — which is why it needed a runtime, not a type, fix.) **Fix.** A small `resolveInteropDefault` helper unwraps such an interop-wrapped default: it returns the value as-is when it's already a usable element type (function/class, tag string, or a `$$typeof`-marked forwardRef/memo/lazy) and otherwise falls through to `.default` and named exports. `VirtualKeyboard` resolves the real `react-simple-keyboard` component through it, so the keyboard renders under any interop shape. Covered by unit tests for the resolver against the object shape, a named-only export, a forwardRef object, and a bare component, plus a render test that mounts the keyboard on input focus. - **The streaming overlay (`/overlay`) showed nothing in OBS when login was enabled (#2613, reporter @MartinNYHC)** — With authentication on, the overlay page worked when opened in a browser where you were already signed in, but stayed blank in OBS. The reporter suspected their Cloudflare/remote setup; it was unrelated. **Root cause.** The `/overlay/{id}` *route* renders without a login, but every piece of data it draws is auth-gated — printer status and name (`PRINTERS_READ`), one setting (`SETTINGS_READ`), and the camera stream (a camera-stream token). In your own browser those ride the JWT from local storage and the app-wide stream-token sync; OBS is a fresh browser with **no session**, so the status calls 401'd and the overlay never populated (the same would happen in any private/incognito window — remote access was never the cause). Unlike the Cam Wall (`/camwall?token=…`), the overlay had no token mode, and a long-lived token couldn't help because the JWT-gated status/settings endpoints reject it. **Fix.** The overlay is now a self-contained kiosk surface. A new **Streaming Overlay** long-lived-token scope is offered under Settings → API Keys (with a ready-made `/overlay/{id}?token=…` URL copied once on creation); the overlay page reads `?token=` from the URL and, in that mode, authenticates its status and camera calls with the token instead of a JWT (and skips the WebSocket, falling back to its existing 2 s poll). A new token-authenticated `GET /printers/{id}/overlay-status` returns exactly the fields the overlay draws — name, camera rotation, live print state, and the one setting — and nothing else. The scope is deliberately **separate from `camwall`**: the overlay names the file on screen, which the Cam Wall is trusted never to expose, so folding it in would have silently widened every existing wall token. The logged-in path (opening the overlay while signed in) is unchanged. Docs updated to explain the token and stop claiming the overlay needs no authentication. Covered by backend tests (scope boundaries in both directions — an overlay token can't reach the Cam Wall feed and a camwall/camera-stream token can't reach the overlay feed — plus the payload shape, disconnected-printer shape, and revoked/absent/garbage-token rejection) and frontend tests (kiosk mode reads the token feed and carries the token to the camera, never touches the JWT-only status endpoint or a socket; the mint UI offers the scope and hands over the assembled OBS URL). - **Reassigning a queue item while it was dispatching split it across two printers (#2615, reporter @Jostxxl)** — Editing a queue item's printer while its FTP upload was already in flight left the queue row pointing at one printer while the archive, expected-print registration, and the physical `project_file` command had gone to another. On a farm this made the reassigned-to printer look broken (marked `printing` but never sent the job), left the row permanently inconsistent, and could trigger a duplicate dispatch after a restart. **Root cause.** A queue row stays `status='pending'` for the entire (multi-minute) FTP upload — status only flips to `printing` at the very end. The edit route only blocked non-`pending` rows, so a `PATCH` during the upload window was accepted; the in-flight dispatch kept using the printer it had snapshotted at the start, while the DB row's `printer_id` changed underneath it. The existing #1853 CAS guards *cancellation* mid-dispatch, not *reassignment*. **Fix.** A `dispatching_at` claim is stamped atomically on the row (`WHERE status='pending' AND dispatching_at IS NULL`) the moment the scheduler begins dispatching, before any slow I/O, and cleared when dispatch ends. While it's held, both edit routes reject changes — the single-item `PATCH` returns **409** (re-checked immediately before the write to close the read-modify-write gap), and bulk edits skip the row — and the scheduler's selection query won't re-pick it. Startup reconciliation clears any claim orphaned by a crash mid-dispatch (no dispatch coroutine survives a restart, so every claim present at boot is stale), so a stale token can never wedge an item out of the queue. The row stays `pending` throughout, so no status-consumer, UI, completion, or reconciliation path had to change. To move a dispatching item, cancel it first (the coordinated escape) and re-queue. New column `print_queue.dispatching_at` (nullable timestamp, dialect-safe DDL — SQLite `DATETIME` / Postgres `TIMESTAMP`). Covered by scheduler tests (claim is exclusive, fails on non-pending rows, releases on every exit, skips an already-claimed row, startup clears stale claims) and API tests (reassign returns 409 with `printer_id` unchanged, bulk skips the claimed row, an unclaimed pending row still edits normally). - **A single plate printed from a multi-plate 3MF recorded the whole file's filament in statistics (#2614, reporter @Jostxxl)** — Dispatching one selected plate of a sliced multi-plate 3MF through the queue could log the **entire file's** filament against that one plate. The reporter's `heart 3.gcode.3mf` has 22 plates totalling ~12.0 kg; every completed plate recorded `12006.49 g`, so 13 runs inflated lifetime/user/project/filament stats by ~156 kg from one file. **Root cause.** The per-run value written to `PrintLogEntry.filament_used_grams` prefers the AMS-tracked spool delta, but when the tracker measured nothing (no inventory assignment on the printer) a *completed* run fell back to `PrintArchive.filament_used_grams` — which is deliberately the **sum over every plate** of the source 3MF (correct for the archive card and project rollup, #1593). The archive's `plate_id` (persisted by #2603) was never consulted on this path, so the whole-file total was copied verbatim; `cost` had the same defect, falling back to the whole-file `archive.cost`. **Forward fix.** When the archive carries a `plate_id` and its 3MF is on disk, the completed-run fallback now uses that plate's own slicer estimate (`extract_plate_metadata_from_3mf`, the same plate-scoped parse the inventory tracker uses), and scales cost by the plate's share of the whole. The tracker-measured path is unchanged (measured spool deltas still win) and single-plate archives are unaffected (plate value equals the whole-file value). **Backfill.** A startup migration repairs rows already written: for completed print-log entries whose stored grams **exactly equal** the linked archive's whole-file value (the mis-copy signature) and whose archive has a `plate_id` and an on-disk 3MF, it recomputes the plate-scoped grams + cost. The exact-match guard means tracker-measured rows (a rounded spool-delta sum) and partial-progress rows (scaled to progress) are never touched; it's idempotent (a corrected row no longer matches) and data-only, identical on SQLite and Postgres. Logs how many rows and how many grams of over-count it removed. Covered by unit tests for the forward helper (plate scoping, cost scaling, fallbacks when there's no plate_id / no file / unreadable estimate) and the backfill (mis-copy repaired, tracker/partial rows untouched, missing-3MF skipped, single-plate not relabelled, idempotent). diff --git a/frontend/src/__tests__/components/VirtualKeyboard.test.tsx b/frontend/src/__tests__/components/VirtualKeyboard.test.tsx new file mode 100644 index 000000000..e40a67204 --- /dev/null +++ b/frontend/src/__tests__/components/VirtualKeyboard.test.tsx @@ -0,0 +1,43 @@ +/** + * Regression for #2616. react-simple-keyboard ships as CommonJS; under the + * bundler's CJS interop the default import can arrive as the module namespace + * object rather than the Keyboard component, so rendering throws + * React #130 ("Element type is invalid ... got: object"). The on-screen keyboard + * mounts on every SpoolBuddy screen the instant a text input is focused, so the + * crash hit inventory search and the write-tag New Spool fields alike. + */ + +import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import { VirtualKeyboard } from '../../components/VirtualKeyboard'; + +// focusin schedules a 100ms scrollIntoView on the focused input; jsdom doesn't +// implement it, so stub it or the deferred call throws an unhandled error after +// the test completes. +beforeEach(() => { + Element.prototype.scrollIntoView = vi.fn(); +}); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('VirtualKeyboard (#2616)', () => { + it('renders the keyboard when a text input is focused (no invalid-element-type crash)', () => { + render( +
+ + +
, + ); + + const input = screen.getByPlaceholderText('Search spools...'); + // The shell listens on document focusin, so drive a real focus event. + fireEvent.focusIn(input); + + // A key from the layout must be on screen — proves resolved to a + // real component instead of throwing on an object element type. + expect(screen.getByText('q')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/utils/interopDefault.test.ts b/frontend/src/__tests__/utils/interopDefault.test.ts new file mode 100644 index 000000000..06a6167d7 --- /dev/null +++ b/frontend/src/__tests__/utils/interopDefault.test.ts @@ -0,0 +1,46 @@ +/** + * Unit tests for resolveInteropDefault (#2616). + * + * The browser build resolved react-simple-keyboard's CommonJS default import to + * the module namespace object ({ KeyboardReact, default }) instead of the + * component, so threw React #130 ("got: object"). vitest's own interop + * happens to hand back the component, so a render test can't catch the + * regression — these assert the resolver directly against both shapes. + */ + +import { describe, it, expect } from 'vitest'; +import { resolveInteropDefault } from '../../utils/interopDefault'; + +const Comp = function Keyboard() { + return null; +}; + +describe('resolveInteropDefault', () => { + it('returns a bare function component unchanged', () => { + expect(resolveInteropDefault(Comp)).toBe(Comp); + }); + + it('unwraps the CJS interop namespace object via .default (the #2616 shape)', () => { + const moduleObject = { default: Comp, KeyboardReact: Comp }; + expect(resolveInteropDefault(moduleObject, ['KeyboardReact'])).toBe(Comp); + }); + + it('falls back to a named export when there is no .default', () => { + const moduleObject = { KeyboardReact: Comp }; + expect(resolveInteropDefault(moduleObject, ['KeyboardReact'])).toBe(Comp); + }); + + it('leaves a forwardRef/memo object (with $$typeof) untouched', () => { + const forwardRefLike = { $$typeof: Symbol.for('react.forward_ref'), render: Comp }; + expect(resolveInteropDefault(forwardRefLike)).toBe(forwardRefLike); + }); + + it('returns a string tag unchanged', () => { + expect(resolveInteropDefault('div')).toBe('div'); + }); + + it('returns the value unchanged when nothing usable is found', () => { + const opaque = { something: 1 }; + expect(resolveInteropDefault(opaque, ['KeyboardReact'])).toBe(opaque); + }); +}); diff --git a/frontend/src/components/VirtualKeyboard.tsx b/frontend/src/components/VirtualKeyboard.tsx index f9d6406f7..7dc098f39 100644 --- a/frontend/src/components/VirtualKeyboard.tsx +++ b/frontend/src/components/VirtualKeyboard.tsx @@ -1,7 +1,20 @@ import { useEffect, useRef, useState, useCallback } from 'react'; -import Keyboard from 'react-simple-keyboard'; +import KeyboardImport from 'react-simple-keyboard'; import 'react-simple-keyboard/build/css/index.css'; import './VirtualKeyboard.css'; +import { resolveInteropDefault } from '../utils/interopDefault'; + +// react-simple-keyboard is published as CommonJS. Depending on the bundler's +// CJS->ESM interop, the default import arrives either as the Keyboard component +// itself or as the module namespace object ({ KeyboardReact, default }). Under +// the current Vite build (and Node's ESM loader) it's the latter, so rendering +// puts an object where an element type belongs and React throws +// "Element type is invalid ... got: object" (#130) — crashing every SpoolBuddy +// screen the instant a text input is focused and this keyboard mounts (#2616). +// Resolve the real component defensively so it renders under any interop shape. +// The TYPE of the default import is already the component (from the .d.ts), so +// the cast keeps JSX + ref typing intact while fixing only the runtime value. +const Keyboard = resolveInteropDefault(KeyboardImport, ['KeyboardReact']); const FOCUSABLE_TYPES = new Set(['text', 'password', 'email', 'search', 'url', 'number']); diff --git a/frontend/src/utils/interopDefault.ts b/frontend/src/utils/interopDefault.ts new file mode 100644 index 000000000..4217e745f --- /dev/null +++ b/frontend/src/utils/interopDefault.ts @@ -0,0 +1,39 @@ +/** + * Unwrap a default import that a bundler's CommonJS->ESM interop may have + * wrapped in a module namespace object. + * + * Some CommonJS packages set `module.exports = { default: X, Named: X }`. + * Depending on the bundler (and differing between the browser build, the test + * runner, and Node's own ESM loader), `import X from 'pkg'` can hand you that + * whole object instead of `X`. Rendering such an object as a React component + * throws "Element type is invalid ... got: object" (React error #130) — see + * #2616, where react-simple-keyboard's default import arrived as the namespace + * object and crashed every SpoolBuddy screen on input focus. + * + * This returns the value unchanged when it is already a usable React element + * type (a function/class component, a tag string, or an object carrying a React + * `$$typeof` marker such as forwardRef/memo/lazy). Otherwise it tries `.default` + * and then each of `fallbackKeys` in order, returning the first usable one, and + * finally falls back to the original value. + */ +export function resolveInteropDefault(value: unknown, fallbackKeys: string[] = []): T { + if (isRenderableType(value)) return value as T; + + if (value !== null && typeof value === 'object') { + const obj = value as Record; + if (isRenderableType(obj.default)) return obj.default as T; + for (const key of fallbackKeys) { + if (isRenderableType(obj[key])) return obj[key] as T; + } + } + + return value as T; +} + +/** True when `v` is something React can render as an element type. */ +function isRenderableType(v: unknown): boolean { + if (typeof v === 'function' || typeof v === 'string') return true; + // forwardRef / memo / lazy / context objects are valid element types and are + // distinguished from a plain interop wrapper by their React `$$typeof` marker. + return typeof v === 'object' && v !== null && '$$typeof' in v; +} diff --git a/static/assets/index-BLUpUiDA.js b/static/assets/index-Cqi3-E-p.js similarity index 89% rename from static/assets/index-BLUpUiDA.js rename to static/assets/index-Cqi3-E-p.js index 9b4b1ace3..bb4bc79d8 100644 --- a/static/assets/index-BLUpUiDA.js +++ b/static/assets/index-Cqi3-E-p.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 (bugün)`,restEnergyTotalPath:`Enerji JSON yolu (toplam sayaç)`,restEnergyTotalMultiplier:`Toplam sayaç çarpanı`,restEnergyTotalPathHint:`örn. aenergy.total`,restEnergyTotalHint:`Birçok priz — tüm Shelly'ler dahil — yalnızca hiç sıfırlanmayan bir toplam sayaç bildirir. Bu değer yukarıdaki alana değil, buraya girilmelidir: bugünün tüketimi olarak okunursa gece yarısı sıfırlanmaz, Dün ve Toplam da boş kalır. Bambuddy Bugün ve Dün değerlerini bundan hesaplar; bunun için bir iki günlük ölçüm gerekir. Shelly watt-saat bildirdiğinden çarpan olarak 0.001 kullanın.`,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ı`,aiFailureDetection:`AI Hata Tespiti`,aiFailureDetectionDescription:`Obico AI olası bir baskı hatası tespit ettiğinde bildir`,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`,pushoverRetry:`Acil yeniden deneme (sn)`,pushoverExpire:`Acil sona erme (sn)`,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.`,skip_unsupported_model:`Bu modelde SD yuvası var ancak seçeneği açmanın bir yolu yok — mevcut P1 serisi bellenim, Bambu Studio'da bu anahtarı göstermiyor ve yazıcının ekranı yok. Burada düzeltilecek bir şey yok; Bambu Lab bellenim desteği ekleyene kadar arşivlenen baskılarda küçük resimler ve dilimleyici meta verileri eksik olabilir.`},port_rtsps:{title:`Kamera portu ({{protocol}} {{port}})`,pass:`Erişilebilir — kamera akışı çalışacak.`,warn:`Port {{port}} 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.`,signInExpiredTitle:`Bambu Cloud oturumu sona erdi`,signInExpiredBody:`Bambuddy oturumunuz açık kalmaya devam ediyor, ancak Bambu Lab kayıtlı belirteci artık kabul etmediği için indirmeler başarısız olacak. Bambu Cloud oturumunu yeniden açın.`,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:{scope:{camera_stream:`Kamera akışı`,camwall:`Kamera duvarı`,overlay:`Yayın bindirmesi`},title:`Kamera API Belirteçleri`,navTitle:`Kamera API belirteçleri`,description:`Kamera akışını Home Assistant, Frigate, kiosk ekranları veya sabit bir adres gerektiren başka araçlara gömmek için uzun ömürlü belirteçler. Kapsam oluşturma sırasında seçilir; belirteç istediğiniz zaman 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:{scopeLabel:`Kapsam`,hintCameraStream:`Kamera akışı belirteci yalnızca kamera akışlarını ve anlık görüntüleri alabilir. Home Assistant, Frigate veya tek bir kamerayı gömen her şey için kullanın.`,hintCamWall:`Kamera duvarı belirteci, oturum açmadan bir ekranda /camwall adresini açar. Her yazıcının adını ve durumunu, ayrıca kamera akışlarını görebilir. Dosya adlarını, adresleri veya erişim kodlarını göremez.`,hintOverlay:`Yayın bindirmesi belirteci, oturum açmadan bir ekranda /overlay/{printerId} adresini açar — OBS veya herhangi bir canlı yayın için. Bir yazıcının kamera akışını ve ekranda gösterilen dosya adı dahil canlı yazdırma durumunu görebilir. Adresleri veya erişim kodlarını göremez.`,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:{camWallUrlTitle:`Bu ekran için kamera duvarı adresi`,camWallUrlHint:`Bu adresi ekranda açın. Adresi okuyabilen herkes duvarı izleyebilir, bu yüzden onu bir anahtar gibi görün; ekranın erişimini kesmek için belirteci iptal edin.`,overlayUrlTitle:`OBS için bindirme adresi`,overlayUrlHint:`Bunu OBS'ye Tarayıcı Kaynağı olarak ekleyin. /overlay/1 içindeki sayıyı yazıcınızın numarasıyla değiştirin (Yazıcılar sayfasındaki adresinden). Adresi okuyabilen herkes yayını izleyebilir, bu yüzden onu bir anahtar gibi görün — erişimi kesmek için belirteci iptal edin.`,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:{scope:`Kapsam`,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.`}}}},Tt=[`en`,`de`,`es`,`fr`,`ja`,`it`,`ko`,`pt-BR`,`tr`,`zh-CN`,`zh-TW`],Et=`bambuddy_appliance_locale_consumed`;De.use(Ct).use(Ke).init({resources:wt,fallbackLng:`en`,supportedLngs:Tt,detection:{order:[`localStorage`,`navigator`,`htmlTag`],lookupLocalStorage:`bambutrack_language`,caches:[`localStorage`]},interpolation:{escapeValue:!1},react:{useSuspense:!1}});function Dt(){if(typeof window>`u`||!window.localStorage)return;let e=window.localStorage;typeof e.getItem!=`function`||typeof e.setItem!=`function`||e.getItem(Et)||fetch(`/api/v1/system/appliance`).then(e=>e.ok?e.json():null).then(t=>{!t||typeof t.locale!=`string`||Tt.includes(t.locale)&&(De.changeLanguage(t.locale),e.setItem(Et,`1`))}).catch(()=>{})}Dt();var Ot=[{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`}],kt=`modulepreload`,At=function(e){return`/`+e},jt={},Mt=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=At(t,n),t in jt)return;jt[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`:kt,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)})},G=`popstate`;function Nt(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function Pt(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return zt(``,{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:Bt(t)}return Ht(t,n,null,e)}function Ft(e,t){if(e===!1||e==null)throw Error(t)}function It(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function Lt(){return Math.random().toString(36).substring(2,10)}function Rt(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 zt(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?Vt(t):t,state:n,key:t&&t.key||r||Lt(),mask:i}}function Bt({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 Vt(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 Ht(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=Nt(e)?e:zt(h.location,e,t);n&&n(r,e),l=u()+1;let d=Rt(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=Nt(e)?e:zt(h.location,e,t);n&&n(r,e),l=u();let i=Rt(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 Ut(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(G,d),c=e,()=>{i.removeEventListener(G,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 Ut(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),Ft(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:Bt(t);return i=i.replace(/ $/,`%20`),!n&&i.startsWith(`//`)&&(i=r+i),new URL(i,r)}function Wt(e,t,n=`/`){return Gt(e,t,n,!1)}function Gt(e,t,n,r,i){let a=dn((typeof t==`string`?Vt(t):t).pathname||`/`,n);if(a==null)return null;let o=i??qt(e),s=null,c=un(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;Ft(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=bn([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(Ft(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),Jt(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:an(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of Yt(e.path))a(e,t,!0,n)}),t}function Yt(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=Yt(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 Xt(e){e.sort((e,t)=>e.score===t.score?on(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var Zt=/^:[\w-]+$/,Qt=3,$t=2,en=1,tn=10,nn=-2,rn=e=>e===`*`;function an(e,t){let n=e.split(`/`),r=n.length;return n.some(rn)&&(r+=nn),t&&(r+=$t),n.filter(e=>!rn(e)).reduce((e,t)=>e+(Zt.test(t)?Qt:t===``?en:tn),r)}function on(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 sn(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 ln(e,t=!1,n=!0){It(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 un(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return It(!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 dn(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 fn=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function pn(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?Vt(e):e,a;return n?(n=yn(n),a=n.startsWith(`/`)?mn(n.substring(1),`/`):mn(n,t)):a=t,{pathname:a,search:Cn(r),hash:wn(i)}}function mn(e,t){let n=xn(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function hn(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 gn(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function _n(e){let t=gn(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function vn(e,t,n,r=!1){let i;typeof e==`string`?i=Vt(e):(i={...e},Ft(!i.pathname||!i.pathname.includes(`?`),hn(`?`,`pathname`,`search`,i)),Ft(!i.pathname||!i.pathname.includes(`#`),hn(`#`,`pathname`,`hash`,i)),Ft(!i.search||!i.search.includes(`#`),hn(`#`,`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=pn(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var yn=e=>e.replace(/\/\/+/g,`/`),bn=e=>yn(e.join(`/`)),xn=e=>e.replace(/\/+$/,``),Sn=e=>xn(e).replace(/^\/*/,`/`),Cn=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,wn=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Tn=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 En(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Dn(e){return bn(e.map(e=>e.route.path).filter(Boolean))||`/`}var On=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function kn(e,t){let n=e;if(typeof n!=`string`||!fn.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(On)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=dn(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{It(!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 An=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(An);var jn=[`GET`,...An];new Set(jn);var Mn=y.createContext(null);Mn.displayName=`DataRouter`;var Nn=y.createContext(null);Nn.displayName=`DataRouterState`;var Pn=y.createContext(!1);function Fn(){return y.useContext(Pn)}var In=y.createContext({isTransitioning:!1});In.displayName=`ViewTransition`;var Ln=y.createContext(new Map);Ln.displayName=`Fetchers`;var Rn=y.createContext(null);Rn.displayName=`Await`;var zn=y.createContext(null);zn.displayName=`Navigation`;var Bn=y.createContext(null);Bn.displayName=`Location`;var Vn=y.createContext({outlet:null,matches:[],isDataRoute:!1});Vn.displayName=`Route`;var Hn=y.createContext(null);Hn.displayName=`RouteError`;var Un=`REACT_ROUTER_ERROR`,Wn=`REDIRECT`,Gn=`ROUTE_ERROR_RESPONSE`;function Kn(e){if(e.startsWith(`${Un}:${Wn}:{`))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 qn(e){if(e.startsWith(`${Un}:${Gn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Tn(t.status,t.statusText,t.data)}catch{}}function Jn(e,{relative:t}={}){Ft(Yn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=y.useContext(zn),{hash:i,pathname:a,search:o}=ar(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:bn([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Yn(){return y.useContext(Bn)!=null}function Xn(){return Ft(Yn(),`useLocation() may be used only in the context of a component.`),y.useContext(Bn).location}var Zn=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Qn(e){y.useContext(zn).static||y.useLayoutEffect(e)}function $n(){let{isDataRoute:e}=y.useContext(Vn);return e?wr():er()}function er(){Ft(Yn(),`useNavigate() may be used only in the context of a component.`);let e=y.useContext(Mn),{basename:t,navigator:n}=y.useContext(zn),{matches:r}=y.useContext(Vn),{pathname:i}=Xn(),a=JSON.stringify(_n(r)),o=y.useRef(!1);return Qn(()=>{o.current=!0}),y.useCallback((r,s={})=>{if(It(o.current,Zn),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=vn(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:bn([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var tr=y.createContext(null);function nr(){return y.useContext(tr)}function rr(e){let t=y.useContext(Vn).outlet;return y.useMemo(()=>t&&y.createElement(tr.Provider,{value:e},t),[t,e])}function ir(){let{matches:e}=y.useContext(Vn);return e[e.length-1]?.params??{}}function ar(e,{relative:t}={}){let{matches:n}=y.useContext(Vn),{pathname:r}=Xn(),i=JSON.stringify(_n(n));return y.useMemo(()=>vn(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function or(e,t){return sr(e,t)}function sr(e,t,n){Ft(Yn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=y.useContext(zn),{matches:i}=y.useContext(Vn),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||``;Er(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=Xn(),d;if(t){let e=typeof t==`string`?Vt(t):t;Ft(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})):Wt(e,{pathname:p});It(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),It(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=mr(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:bn([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:bn([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(Bn.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function cr(){let e=Cr(),t=En(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 lr=y.createElement(cr,null),ur=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=qn(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:y.createElement(Vn.Provider,{value:this.props.routeContext},y.createElement(Hn.Provider,{value:e,children:this.props.component}));return this.context?y.createElement(fr,{error:e},t):t}};ur.contextType=Pn;var dr=new WeakMap;function fr({children:e,error:t}){let{basename:n}=y.useContext(zn);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Kn(t.digest);if(e){let r=dr.get(t);if(r)throw r;let i=kn(e.location,n);if(On&&!dr.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 dr.set(t,n),n}return y.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function pr({routeContext:e,match:t,children:n}){let r=y.useContext(Mn);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),y.createElement(Vn.Provider,{value:e},n)}function mr(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);Ft(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:Dn(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||lr,o&&(s<0&&c===0?(Er(`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(pr,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?y.createElement(ur,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function hr(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function gr(e){let t=y.useContext(Mn);return Ft(t,hr(e)),t}function _r(e){let t=y.useContext(Nn);return Ft(t,hr(e)),t}function vr(e){let t=y.useContext(Vn);return Ft(t,hr(e)),t}function yr(e){let t=vr(e),n=t.matches[t.matches.length-1];return Ft(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function br(){return yr(`useRouteId`)}function xr(){let e=_r(`useNavigation`);return y.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function Sr(){let{matches:e,loaderData:t}=_r(`useMatches`);return y.useMemo(()=>e.map(e=>Kt(e,t)),[e,t])}function Cr(){let e=y.useContext(Hn),t=_r(`useRouteError`),n=yr(`useRouteError`);return e===void 0?t.errors?.[n]:e}function wr(){let{router:e}=gr(`useNavigate`),t=yr(`useNavigate`),n=y.useRef(!1);return Qn(()=>{n.current=!0}),y.useCallback(async(r,i={})=>{It(n.current,Zn),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Tr={};function Er(e,t,n){!t&&!Tr[e]&&(Tr[e]=!0,It(!1,n))}y.memo(Dr);function Dr({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return sr(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Or({to:e,replace:t,state:n,relative:r}){Ft(Yn(),` may be used only in the context of a component.`);let{static:i}=y.useContext(zn);It(!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(Vn),{pathname:o}=Xn(),s=$n(),c=vn(e,_n(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 kr(e){return rr(e.context)}function Ar(e){Ft(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function jr({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){Ft(!Yn(),`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=Vt(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=y.useMemo(()=>{let e=dn(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 It(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(zn.Provider,{value:c},y.createElement(Bn.Provider,{children:t,value:h}))}function Mr({children:e,location:t}){return or(Nr(e),t)}y.Component;function Nr(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,Nr(e.props.children,i));return}Ft(e.type===Ar,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),Ft(!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=Nr(e.props.children,i)),n.push(a)}),n}var Pr=`get`,Fr=`application/x-www-form-urlencoded`;function Ir(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Lr(e){return Ir(e)&&e.tagName.toLowerCase()===`button`}function Rr(e){return Ir(e)&&e.tagName.toLowerCase()===`form`}function zr(e){return Ir(e)&&e.tagName.toLowerCase()===`input`}function Br(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Vr(e,t){return e.button===0&&(!t||t===`_self`)&&!Br(e)}function Hr(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 Ur(e,t){let n=Hr(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Wr=null;function Gr(){if(Wr===null)try{new FormData(document.createElement(`form`),0),Wr=!1}catch{Wr=!0}return Wr}var Kr=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function qr(e){return e!=null&&!Kr.has(e)?(It(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Fr}"`),null):e}function Jr(e,t){let n,r,i,a,o;if(Rr(e)){let o=e.getAttribute(`action`);r=o?dn(o,t):null,n=e.getAttribute(`method`)||Pr,i=qr(e.getAttribute(`enctype`))||Fr,a=new FormData(e)}else if(Lr(e)||zr(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a