diff --git a/CHANGELOG.md b/CHANGELOG.md
index 581bcc43a..2835c4c31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
### New Features
- **External Links: Open in New Tab** ([#338](https://github.com/maziggy/bambuddy/issues/338)) — External sidebar links can now optionally open in a new browser tab instead of an iframe. Sites behind reverse proxies (Traefik, nginx) that send `X-Frame-Options: SAMEORIGIN` or CSP `frame-ancestors` headers block iframe embedding, causing "refused to connect" errors. A new "Open in new tab" toggle in the add/edit link modal lets users choose per-link. Keyboard shortcuts (number keys) also respect the setting. Defaults to iframe (existing behavior) for backward compatibility.
+- **Print Queue: Clear Plate Confirmation** — When a print finishes or fails and more items are queued, the printer card now shows a "Clear Plate & Start Next" button. The scheduler no longer auto-starts the next print while the printer is in FINISH or FAILED state — the user must confirm the build plate has been cleared first. This prevents prints from starting on a dirty plate. The button respects the `printers:control` permission and is available in all supported languages (en/de/ja).
### Improved
- **Additional Currency Options** ([#329](https://github.com/maziggy/bambuddy/issues/329), [#333](https://github.com/maziggy/bambuddy/issues/333)) — Added 17 additional currencies to the cost tracking dropdown: HKD, INR, KRW, SEK, NOK, DKK, PLN, BRL, TWD, SGD, NZD, MXN, CZK, THB, ZAR, RUB.
diff --git a/README.md b/README.md
index f9eb6d842..cd31720e0 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
- Per-printer AMS mapping (individual slot configuration for print farms)
- Scheduled prints (date/time)
- Queue Only mode (stage without auto-start)
+- Clear plate confirmation between queued prints
- Smart plug integration (Tasmota, Home Assistant, MQTT)
- MQTT smart plugs: Subscribe to Zigbee2MQTT, Shelly, or any MQTT topic for energy monitoring
- Energy consumption tracking (per-print kWh and cost)
diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py
index b54072c96..b2d50bca9 100644
--- a/backend/app/api/routes/printers.py
+++ b/backend/app/api/routes/printers.py
@@ -1821,6 +1821,37 @@ async def stop_print(
return {"success": True, "message": "Print stop command sent"}
+@router.post("/{printer_id}/clear-plate")
+async def clear_plate(
+ printer_id: int,
+ _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
+ db: AsyncSession = Depends(get_db),
+):
+ """Acknowledge that the build plate has been cleared after a finished/failed print.
+
+ Sets a plate-cleared flag so the scheduler can start the next queued print.
+ No MQTT command is sent to the printer — the scheduler's start_print command
+ will override the FINISH/FAILED state when it sends the next job.
+ """
+ result = await db.execute(select(Printer).where(Printer.id == printer_id))
+ printer = result.scalar_one_or_none()
+ if not printer:
+ raise HTTPException(404, "Printer not found")
+
+ if not printer_manager.is_connected(printer_id):
+ raise HTTPException(400, "Printer not connected")
+
+ state = printer_manager.get_status(printer_id)
+ if not state or state.state not in ("FINISH", "FAILED"):
+ raise HTTPException(
+ 400, f"Printer is not in FINISH or FAILED state (current: {state.state if state else 'unknown'})"
+ )
+
+ printer_manager.set_plate_cleared(printer_id)
+
+ return {"success": True, "message": "Plate cleared, next print will start shortly"}
+
+
@router.post("/{printer_id}/print/pause")
async def pause_print(
printer_id: int,
diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py
index cc838a5f4..045178765 100644
--- a/backend/app/services/print_scheduler.py
+++ b/backend/app/services/print_scheduler.py
@@ -694,9 +694,11 @@ class PrintScheduler:
if not state:
return False
- # Printer is idle if state is IDLE, FINISH, FAILED, or unknown
- # FAILED means previous print failed, printer is ready for new print
- return state.state in ("IDLE", "FINISH", "FAILED", "unknown")
+ # IDLE = ready for next print
+ # FINISH/FAILED = ready only if user confirmed plate is cleared
+ return state.state == "IDLE" or (
+ state.state in ("FINISH", "FAILED") and printer_manager.is_plate_cleared(printer_id)
+ )
async def _get_smart_plug(self, db: AsyncSession, printer_id: int) -> SmartPlug | None:
"""Get the smart plug associated with a printer."""
@@ -1034,6 +1036,9 @@ class PrintScheduler:
item.status = "printing"
item.started_at = datetime.utcnow()
await db.commit()
+
+ # Consume the plate-cleared flag now that we're starting a print
+ printer_manager.consume_plate_cleared(item.printer_id)
logger.info("Queue item %s: Status set to 'printing', sending print command...", item.id)
# Start the print with AMS mapping, plate_id and print options
diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py
index 65b5e2998..f6b3bf3c9 100644
--- a/backend/app/services/printer_manager.py
+++ b/backend/app/services/printer_manager.py
@@ -100,6 +100,8 @@ class PrinterManager:
self._loop: asyncio.AbstractEventLoop | None = None
# Track who started the current print (Issue #206)
self._current_print_user: dict[int, dict] = {} # {printer_id: {"user_id": int, "username": str}}
+ # Track plate-cleared acknowledgments for queue flow
+ self._plate_cleared: set[int] = set() # printer_ids where user confirmed plate is cleared
def get_printer(self, printer_id: int) -> PrinterInfo | None:
"""Get printer info by ID."""
@@ -117,6 +119,18 @@ class PrinterManager:
"""Clear the current print user when print completes (Issue #206)."""
self._current_print_user.pop(printer_id, None)
+ def set_plate_cleared(self, printer_id: int):
+ """Mark that user has cleared the build plate for this printer."""
+ self._plate_cleared.add(printer_id)
+
+ def is_plate_cleared(self, printer_id: int) -> bool:
+ """Check if user has confirmed the plate is cleared."""
+ return printer_id in self._plate_cleared
+
+ def consume_plate_cleared(self, printer_id: int):
+ """Clear the plate-cleared flag (called when scheduler starts next print)."""
+ self._plate_cleared.discard(printer_id)
+
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
"""Set the event loop for async callbacks."""
self._loop = loop
diff --git a/backend/tests/unit/test_scheduler_clear_plate.py b/backend/tests/unit/test_scheduler_clear_plate.py
new file mode 100644
index 000000000..ba72026aa
--- /dev/null
+++ b/backend/tests/unit/test_scheduler_clear_plate.py
@@ -0,0 +1,122 @@
+"""Tests for the clear plate queue flow in the print scheduler."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from backend.app.services.print_scheduler import PrintScheduler
+from backend.app.services.printer_manager import PrinterManager
+
+
+class TestPrinterManagerPlateCleared:
+ """Test the plate-cleared flag management in PrinterManager."""
+
+ @pytest.fixture
+ def manager(self):
+ return PrinterManager()
+
+ def test_plate_cleared_initially_false(self, manager):
+ """No printers should have plate cleared by default."""
+ assert not manager.is_plate_cleared(1)
+ assert not manager.is_plate_cleared(999)
+
+ def test_set_plate_cleared(self, manager):
+ """Setting plate cleared should make is_plate_cleared return True."""
+ manager.set_plate_cleared(1)
+ assert manager.is_plate_cleared(1)
+ assert not manager.is_plate_cleared(2)
+
+ def test_consume_plate_cleared(self, manager):
+ """Consuming plate cleared should reset the flag."""
+ manager.set_plate_cleared(1)
+ assert manager.is_plate_cleared(1)
+ manager.consume_plate_cleared(1)
+ assert not manager.is_plate_cleared(1)
+
+ def test_consume_plate_cleared_idempotent(self, manager):
+ """Consuming when not set should not raise."""
+ manager.consume_plate_cleared(1) # Should not raise
+ assert not manager.is_plate_cleared(1)
+
+ def test_set_plate_cleared_multiple_printers(self, manager):
+ """Plate cleared should be tracked per printer."""
+ manager.set_plate_cleared(1)
+ manager.set_plate_cleared(3)
+ assert manager.is_plate_cleared(1)
+ assert not manager.is_plate_cleared(2)
+ assert manager.is_plate_cleared(3)
+
+ def test_consume_only_affects_target_printer(self, manager):
+ """Consuming plate cleared for one printer should not affect others."""
+ manager.set_plate_cleared(1)
+ manager.set_plate_cleared(2)
+ manager.consume_plate_cleared(1)
+ assert not manager.is_plate_cleared(1)
+ assert manager.is_plate_cleared(2)
+
+
+class TestSchedulerIdleCheckWithPlateCleared:
+ """Test _is_printer_idle with plate-cleared flag interactions."""
+
+ @pytest.fixture
+ def scheduler(self):
+ return PrintScheduler()
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_idle_state_is_idle(self, mock_pm, scheduler):
+ """Printer in IDLE state should be considered idle."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="IDLE")
+ assert scheduler._is_printer_idle(1) is True
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_running_state_not_idle(self, mock_pm, scheduler):
+ """Printer in RUNNING state should not be idle."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="RUNNING")
+ assert scheduler._is_printer_idle(1) is False
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_finish_state_not_idle_without_plate_cleared(self, mock_pm, scheduler):
+ """Printer in FINISH state should NOT be idle without plate cleared."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="FINISH")
+ mock_pm.is_plate_cleared.return_value = False
+ assert scheduler._is_printer_idle(1) is False
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_finish_state_idle_with_plate_cleared(self, mock_pm, scheduler):
+ """Printer in FINISH state should be idle when plate is cleared."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="FINISH")
+ mock_pm.is_plate_cleared.return_value = True
+ assert scheduler._is_printer_idle(1) is True
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_failed_state_not_idle_without_plate_cleared(self, mock_pm, scheduler):
+ """Printer in FAILED state should NOT be idle without plate cleared."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="FAILED")
+ mock_pm.is_plate_cleared.return_value = False
+ assert scheduler._is_printer_idle(1) is False
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_failed_state_idle_with_plate_cleared(self, mock_pm, scheduler):
+ """Printer in FAILED state should be idle when plate is cleared."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = MagicMock(state="FAILED")
+ mock_pm.is_plate_cleared.return_value = True
+ assert scheduler._is_printer_idle(1) is True
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_disconnected_printer_not_idle(self, mock_pm, scheduler):
+ """Disconnected printer should never be idle."""
+ mock_pm.is_connected.return_value = False
+ assert scheduler._is_printer_idle(1) is False
+
+ @patch("backend.app.services.print_scheduler.printer_manager")
+ def test_no_status_not_idle(self, mock_pm, scheduler):
+ """Printer with no status should not be idle."""
+ mock_pm.is_connected.return_value = True
+ mock_pm.get_status.return_value = None
+ assert scheduler._is_printer_idle(1) is False
diff --git a/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx b/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx
new file mode 100644
index 000000000..7f41f845b
--- /dev/null
+++ b/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx
@@ -0,0 +1,177 @@
+/**
+ * Tests for the PrinterQueueWidget clear plate behavior.
+ *
+ * When the printer is in FINISH or FAILED state and has pending queue items,
+ * the widget shows a "Clear Plate & Start Next" button instead of the
+ * passive queue link. After clicking, it shows a confirmation state.
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../utils';
+import { PrinterQueueWidget } from '../../components/PrinterQueueWidget';
+import { http, HttpResponse } from 'msw';
+import { server } from '../mocks/server';
+
+const mockQueueItems = [
+ {
+ id: 1,
+ printer_id: 1,
+ archive_id: 1,
+ position: 1,
+ status: 'pending',
+ archive_name: 'First Print',
+ printer_name: 'X1 Carbon',
+ print_time_seconds: 3600,
+ scheduled_time: null,
+ },
+ {
+ id: 2,
+ printer_id: 1,
+ archive_id: 2,
+ position: 2,
+ status: 'pending',
+ archive_name: 'Second Print',
+ printer_name: 'X1 Carbon',
+ print_time_seconds: 7200,
+ scheduled_time: null,
+ },
+];
+
+describe('PrinterQueueWidget - Clear Plate', () => {
+ beforeEach(() => {
+ server.use(
+ http.get('/api/v1/queue/', ({ request }) => {
+ const url = new URL(request.url);
+ const printerId = url.searchParams.get('printer_id');
+ if (printerId === '1') {
+ return HttpResponse.json(mockQueueItems);
+ }
+ return HttpResponse.json([]);
+ }),
+ http.post('/api/v1/printers/:id/clear-plate', () => {
+ return HttpResponse.json({ success: true, message: 'Plate cleared' });
+ })
+ );
+ });
+
+ describe('clear plate button visibility', () => {
+ it('shows clear plate button when printer state is FINISH', async () => {
+ render(
{t('queue.nextInQueue')}
++ {nextItem?.archive_name || `Archive #${nextItem?.archive_id}`} +
+Next in queue
+{t('queue.nextInQueue')}
{nextItem?.archive_name || `Archive #${nextItem?.archive_id}`}
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 3b6b763df..4ca9d5749 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -697,6 +697,10 @@ export default { dragToReorder: 'Ziehen zum Neuordnen (nur Sofort)', reorderHint: 'Position betrifft nur Sofort-Elemente. Geplante Elemente werden zur festgelegten Zeit ausgeführt.', addedBy: 'Hinzugefügt von {{name}}', + nextInQueue: 'Nächster in der Warteschlange', + clearPlate: 'Druckplatte freigeben & Nächsten starten', + clearPlateSuccess: 'Druckplatte freigegeben — bereit für nächsten Druck', + plateReady: 'Druckplatte freigegeben — bereit für nächsten Druck', // Sections sections: { currentlyPrinting: 'Aktuell druckend', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 67333422b..3818be507 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -697,6 +697,10 @@ export default { dragToReorder: 'Drag to reorder (ASAP only)', reorderHint: 'Position only affects ASAP items. Scheduled items run at their set time.', addedBy: 'Added by {{name}}', + nextInQueue: 'Next in queue', + clearPlate: 'Clear Plate & Start Next', + clearPlateSuccess: 'Plate cleared — ready for next print', + plateReady: 'Plate cleared — ready for next print', // Sections sections: { currentlyPrinting: 'Currently Printing', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 1fc98971c..27b605ad9 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -769,6 +769,10 @@ export default { itemCount: '{{count}}件', dragToReorder: 'ドラッグして並べ替え(ASAPのみ)', addedBy: '{{username}}が追加', + nextInQueue: '次のキュー', + clearPlate: 'プレートをクリアして次を開始', + clearPlateSuccess: 'プレートをクリアしました — 次の印刷の準備完了', + plateReady: 'プレートをクリアしました — 次の印刷の準備完了', sections: { currentlyPrinting: '印刷中', queued: 'キュー中', diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index ae35f88c9..c6d5af6f0 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -2399,10 +2399,8 @@ function PrinterCard({s?.[0]===h[0]&&!r.context?(this.logger.warn(`It seems you are nesting recursively key: ${h[0]} in key: ${n[0]}`),null):this.translate(...h,n),r)),r.interpolation&&this.interpolator.reset()}const o=r.postProcess||this.options.postProcess,l=yn(o)?[o]:o;return e!=null&&l?.length&&r.applyPostProcessor!==!1&&(e=V9.handle(l,e,n,this.options&&this.options.postProcessPassResolved?{i18nResolved:{...i,usedParams:this.getUsedParamsDetails(r)},...r}:r,this)),e}resolve(e,n={}){let r,i,s,o,l;return yn(e)&&(e=[e]),e.forEach(c=>{if(this.isValidLookup(r))return;const u=this.extractFromKey(c,n),d=u.key;i=d;let h=u.namespaces;this.options.fallbackNS&&(h=h.concat(this.options.fallbackNS));const p=n.count!==void 0&&!yn(n.count),m=p&&!n.ordinal&&n.count===0,b=n.context!==void 0&&(yn(n.context)||typeof n.context=="number")&&n.context!=="",y=n.lngs?n.lngs:this.languageUtils.toResolveHierarchy(n.lng||this.language,n.fallbackLng);h.forEach(x=>{this.isValidLookup(r)||(l=x,!yU[`${y[0]}-${x}`]&&this.utils?.hasLoadedNamespace&&!this.utils?.hasLoadedNamespace(l)&&(yU[`${y[0]}-${x}`]=!0,this.logger.warn(`key "${i}" for languages "${y.join(", ")}" won't get resolved as namespace "${l}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),y.forEach(v=>{if(this.isValidLookup(r))return;o=v;const S=[d];if(this.i18nFormat?.addLookupKeys)this.i18nFormat.addLookupKeys(S,d,v,x,n);else{let T;p&&(T=this.pluralResolver.getSuffix(v,n.count,n));const M=`${this.options.pluralSeparator}zero`,A=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(p&&(n.ordinal&&T.indexOf(A)===0&&S.push(d+T.replace(A,this.options.pluralSeparator)),S.push(d+T),m&&S.push(d+M)),b){const k=`${d}${this.options.contextSeparator||"_"}${n.context}`;S.push(k),p&&(n.ordinal&&T.indexOf(A)===0&&S.push(k+T.replace(A,this.options.pluralSeparator)),S.push(k+T),m&&S.push(k+M))}}let N;for(;N=S.pop();)this.isValidLookup(r)||(s=N,r=this.getResource(v,x,N,n))}))})}),{res:r,usedKey:i,exactUsedKey:s,usedLng:o,usedNS:l}}isValidLookup(e){return e!==void 0&&!(!this.options.returnNull&&e===null)&&!(!this.options.returnEmptyString&&e==="")}getResource(e,n,r,i={}){return this.i18nFormat?.getResource?this.i18nFormat.getResource(e,n,r,i):this.resourceStore.getResource(e,n,r,i)}getUsedParamsDetails(e={}){const n=["defaultValue","ordinal","context","replace","lng","lngs","fallbackLng","ns","keySeparator","nsSeparator","returnObjects","returnDetails","joinArrays","postProcess","interpolation"],r=e.replace&&!yn(e.replace);let i=r?e.replace:e;if(r&&typeof e.count<"u"&&(i.count=e.count),this.options.interpolation.defaultVariables&&(i={...this.options.interpolation.defaultVariables,...i}),!r){i={...i};for(const s of n)delete i[s]}return i}static hasDefaultValue(e){const n="defaultValue";for(const r in e)if(Object.prototype.hasOwnProperty.call(e,r)&&n===r.substring(0,n.length)&&e[r]!==void 0)return!0;return!1}}class vU{constructor(e){this.options=e,this.supportedLngs=this.options.supportedLngs||!1,this.logger=dc.create("languageUtils")}getScriptPartFromCode(e){if(e=G0(e),!e||e.indexOf("-")<0)return null;const n=e.split("-");return n.length===2||(n.pop(),n[n.length-1].toLowerCase()==="x")?null:this.formatLanguageCode(n.join("-"))}getLanguagePartFromCode(e){if(e=G0(e),!e||e.indexOf("-")<0)return e;const n=e.split("-");return this.formatLanguageCode(n[0])}formatLanguageCode(e){if(yn(e)&&e.indexOf("-")>-1){let n;try{n=Intl.getCanonicalLocales(e)[0]}catch{}return n&&this.options.lowerCaseLng&&(n=n.toLowerCase()),n||(this.options.lowerCaseLng?e.toLowerCase():e)}return this.options.cleanCode||this.options.lowerCaseLng?e.toLowerCase():e}isSupportedCode(e){return(this.options.load==="languageOnly"||this.options.nonExplicitSupportedLngs)&&(e=this.getLanguagePartFromCode(e)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(e)>-1}getBestMatchFromCodes(e){if(!e)return null;let n;return e.forEach(r=>{if(n)return;const i=this.formatLanguageCode(r);(!this.options.supportedLngs||this.isSupportedCode(i))&&(n=i)}),!n&&this.options.supportedLngs&&e.forEach(r=>{if(n)return;const i=this.getScriptPartFromCode(r);if(this.isSupportedCode(i))return n=i;const s=this.getLanguagePartFromCode(r);if(this.isSupportedCode(s))return n=s;n=this.options.supportedLngs.find(o=>{if(o===s)return o;if(!(o.indexOf("-")<0&&s.indexOf("-")<0)&&(o.indexOf("-")>0&&s.indexOf("-")<0&&o.substring(0,o.indexOf("-"))===s||o.indexOf(s)===0&&s.length>1))return o})}),n||(n=this.getFallbackCodes(this.options.fallbackLng)[0]),n}getFallbackCodes(e,n){if(!e)return[];if(typeof e=="function"&&(e=e(n)),yn(e)&&(e=[e]),Array.isArray(e))return e;if(!n)return e.default||[];let r=e[n];return r||(r=e[this.getScriptPartFromCode(n)]),r||(r=e[this.formatLanguageCode(n)]),r||(r=e[this.getLanguagePartFromCode(n)]),r||(r=e.default),r||[]}toResolveHierarchy(e,n){const r=this.getFallbackCodes((n===!1?[]:n)||this.options.fallbackLng||[],e),i=[],s=o=>{o&&(this.isSupportedCode(o)?i.push(o):this.logger.warn(`rejecting language code not found in supportedLngs: ${o}`))};return yn(e)&&(e.indexOf("-")>-1||e.indexOf("_")>-1)?(this.options.load!=="languageOnly"&&s(this.formatLanguageCode(e)),this.options.load!=="languageOnly"&&this.options.load!=="currentOnly"&&s(this.getScriptPartFromCode(e)),this.options.load!=="currentOnly"&&s(this.getLanguagePartFromCode(e))):yn(e)&&s(this.formatLanguageCode(e)),r.forEach(o=>{i.indexOf(o)<0&&s(this.formatLanguageCode(o))}),i}}const wU={zero:0,one:1,two:2,few:3,many:4,other:5},_U={select:t=>t===1?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class _se{constructor(e,n={}){this.languageUtils=e,this.options=n,this.logger=dc.create("pluralResolver"),this.pluralRulesCache={}}addRule(e,n){this.rules[e]=n}clearCache(){this.pluralRulesCache={}}getRule(e,n={}){const r=G0(e==="dev"?"en":e),i=n.ordinal?"ordinal":"cardinal",s=JSON.stringify({cleanedCode:r,type:i});if(s in this.pluralRulesCache)return this.pluralRulesCache[s];let o;try{o=new Intl.PluralRules(r,{type:i})}catch{if(!Intl)return this.logger.error("No Intl support, please use an Intl polyfill!"),_U;if(!e.match(/-|_/))return _U;const c=this.languageUtils.getLanguagePartFromCode(e);o=this.getRule(c,n)}return this.pluralRulesCache[s]=o,o}needsPlural(e,n={}){let r=this.getRule(e,n);return r||(r=this.getRule("dev",n)),r?.resolvedOptions().pluralCategories.length>1}getPluralFormsOfKey(e,n,r={}){return this.getSuffixes(e,r).map(i=>`${n}${i}`)}getSuffixes(e,n={}){let r=this.getRule(e,n);return r||(r=this.getRule("dev",n)),r?r.resolvedOptions().pluralCategories.sort((i,s)=>wU[i]-wU[s]).map(i=>`${this.options.prepend}${n.ordinal?`ordinal${this.options.prepend}`:""}${i}`):[]}getSuffix(e,n,r={}){const i=this.getRule(e,r);return i?`${this.options.prepend}${r.ordinal?`ordinal${this.options.prepend}`:""}${i.select(n)}`:(this.logger.warn(`no plural rule found for: ${e}`),this.getSuffix("dev",n,r))}}const SU=(t,e,n,r=".",i=!0)=>{let s=fse(t,e,n);return!s&&i&&yn(n)&&(s=fj(t,n,r),s===void 0&&(s=fj(e,n,r))),s},uT=t=>t.replace(/\$/g,"$$$$");class Sse{constructor(e={}){this.logger=dc.create("interpolator"),this.options=e,this.format=e?.interpolation?.format||(n=>n),this.init(e)}init(e={}){e.interpolation||(e.interpolation={escapeValue:!0});const{escape:n,escapeValue:r,useRawValueToEscape:i,prefix:s,prefixEscaped:o,suffix:l,suffixEscaped:c,formatSeparator:u,unescapeSuffix:d,unescapePrefix:h,nestingPrefix:p,nestingPrefixEscaped:m,nestingSuffix:b,nestingSuffixEscaped:y,nestingOptionsSeparator:x,maxReplaces:v,alwaysFormat:S}=e.interpolation;this.escape=n!==void 0?n:pse,this.escapeValue=r!==void 0?r:!0,this.useRawValueToEscape=i!==void 0?i:!1,this.prefix=s?_p(s):o||"{{",this.suffix=l?_p(l):c||"}}",this.formatSeparator=u||",",this.unescapePrefix=d?"":h||"-",this.unescapeSuffix=this.unescapePrefix?"":d||"",this.nestingPrefix=p?_p(p):m||_p("$t("),this.nestingSuffix=b?_p(b):y||_p(")"),this.nestingOptionsSeparator=x||",",this.maxReplaces=v||1e3,this.alwaysFormat=S!==void 0?S:!1,this.resetRegExp()}reset(){this.options&&this.init(this.options)}resetRegExp(){const e=(n,r)=>n?.source===r?(n.lastIndex=0,n):new RegExp(r,"g");this.regexp=e(this.regexp,`${this.prefix}(.+?)${this.suffix}`),this.regexpUnescape=e(this.regexpUnescape,`${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`),this.nestingRegexp=e(this.nestingRegexp,`${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`)}interpolate(e,n,r,i){let s,o,l;const c=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{},u=m=>{if(m.indexOf(this.formatSeparator)<0){const v=SU(n,c,m,this.options.keySeparator,this.options.ignoreJSONStructure);return this.alwaysFormat?this.format(v,void 0,r,{...i,...n,interpolationkey:m}):v}const b=m.split(this.formatSeparator),y=b.shift().trim(),x=b.join(this.formatSeparator).trim();return this.format(SU(n,c,y,this.options.keySeparator,this.options.ignoreJSONStructure),x,r,{...i,...n,interpolationkey:y})};this.resetRegExp();const d=i?.missingInterpolationHandler||this.options.missingInterpolationHandler,h=i?.interpolation?.skipOnVariables!==void 0?i.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:m=>uT(m)},{regex:this.regexp,safeValue:m=>this.escapeValue?uT(this.escape(m)):uT(m)}].forEach(m=>{for(l=0;s=m.regex.exec(e);){const b=s[1].trim();if(o=u(b),o===void 0)if(typeof d=="function"){const x=d(e,s,i);o=yn(x)?x:""}else if(i&&Object.prototype.hasOwnProperty.call(i,b))o="";else if(h){o=s[0];continue}else this.logger.warn(`missed to pass in variable ${b} for interpolating ${e}`),o="";else!yn(o)&&!this.useRawValueToEscape&&(o=mU(o));const y=m.safeValue(o);if(e=e.replace(s[0],y),h?(m.regex.lastIndex+=o.length,m.regex.lastIndex-=s[0].length):m.regex.lastIndex=0,l++,l>=this.maxReplaces)break}}),e}nest(e,n,r={}){let i,s,o;const l=(c,u)=>{const d=this.nestingOptionsSeparator;if(c.indexOf(d)<0)return c;const h=c.split(new RegExp(`${d}[ ]*{`));let p=`{${h[1]}`;c=h[0],p=this.interpolate(p,o);const m=p.match(/'/g),b=p.match(/"/g);((m?.length??0)%2===0&&!b||b.length%2!==0)&&(p=p.replace(/'/g,'"'));try{o=JSON.parse(p),u&&(o={...u,...o})}catch(y){return this.logger.warn(`failed parsing options string in nesting for key ${c}`,y),`${c}${d}${p}`}return o.defaultValue&&o.defaultValue.indexOf(this.prefix)>-1&&delete o.defaultValue,c};for(;i=this.nestingRegexp.exec(e);){let c=[];o={...r},o=o.replace&&!yn(o.replace)?o.replace:o,o.applyPostProcessor=!1,delete o.defaultValue;const u=/{.*}/.test(i[1])?i[1].lastIndexOf("}")+1:i[1].indexOf(this.formatSeparator);if(u!==-1&&(c=i[1].slice(u).split(this.formatSeparator).map(d=>d.trim()).filter(Boolean),i[1]=i[1].slice(0,u)),s=n(l.call(this,i[1].trim(),o),o),s&&i[0]===e&&!yn(s))return s;yn(s)||(s=mU(s)),s||(this.logger.warn(`missed to resolve ${i[1]} for nesting ${e}`),s=""),c.length&&(s=c.reduce((d,h)=>this.format(d,h,r.lng,{...r,interpolationkey:i[1].trim()}),s.trim())),e=e.replace(i[0],s),this.regexp.lastIndex=0}return e}}const Nse=t=>{let e=t.toLowerCase().trim();const n={};if(t.indexOf("(")>-1){const r=t.split("(");e=r[0].toLowerCase().trim();const i=r[1].substring(0,r[1].length-1);e==="currency"&&i.indexOf(":")<0?n.currency||(n.currency=i.trim()):e==="relativetime"&&i.indexOf(":")<0?n.range||(n.range=i.trim()):i.split(";").forEach(o=>{if(o){const[l,...c]=o.split(":"),u=c.join(":").trim().replace(/^'+|'+$/g,""),d=l.trim();n[d]||(n[d]=u),u==="false"&&(n[d]=!1),u==="true"&&(n[d]=!0),isNaN(u)||(n[d]=parseInt(u,10))}})}return{formatName:e,formatOptions:n}},NU=t=>{const e={};return(n,r,i)=>{let s=i;i&&i.interpolationkey&&i.formatParams&&i.formatParams[i.interpolationkey]&&i[i.interpolationkey]&&(s={...s,[i.interpolationkey]:void 0});const o=r+JSON.stringify(s);let l=e[o];return l||(l=t(G0(r),i),e[o]=l),l(n)}},kse=t=>(e,n,r)=>t(G0(n),r)(e);class Mse{constructor(e={}){this.logger=dc.create("formatter"),this.options=e,this.init(e)}init(e,n={interpolation:{}}){this.formatSeparator=n.interpolation.formatSeparator||",";const r=n.cacheInBuiltFormats?NU:kse;this.formats={number:r((i,s)=>{const o=new Intl.NumberFormat(i,{...s});return l=>o.format(l)}),currency:r((i,s)=>{const o=new Intl.NumberFormat(i,{...s,style:"currency"});return l=>o.format(l)}),datetime:r((i,s)=>{const o=new Intl.DateTimeFormat(i,{...s});return l=>o.format(l)}),relativetime:r((i,s)=>{const o=new Intl.RelativeTimeFormat(i,{...s});return l=>o.format(l,s.range||"day")}),list:r((i,s)=>{const o=new Intl.ListFormat(i,{...s});return l=>o.format(l)})}}add(e,n){this.formats[e.toLowerCase().trim()]=n}addCached(e,n){this.formats[e.toLowerCase().trim()]=NU(n)}format(e,n,r,i={}){const s=n.split(this.formatSeparator);if(s.length>1&&s[0].indexOf("(")>1&&s[0].indexOf(")")<0&&s.find(l=>l.indexOf(")")>-1)){const l=s.findIndex(c=>c.indexOf(")")>-1);s[0]=[s[0],...s.splice(1,l)].join(this.formatSeparator)}return s.reduce((l,c)=>{const{formatName:u,formatOptions:d}=Nse(c);if(this.formats[u]){let h=l;try{const p=i?.formatParams?.[i.interpolationkey]||{},m=p.locale||p.lng||i.locale||i.lng||r;h=this.formats[u](l,m,{...d,...i,...p})}catch(p){this.logger.warn(p)}return h}else this.logger.warn(`there was no format function for ${u}`);return l},e)}}const Ese=(t,e)=>{t.pending[e]!==void 0&&(delete t.pending[e],t.pendingCount--)};class Tse extends pk{constructor(e,n,r,i={}){super(),this.backend=e,this.store=n,this.services=r,this.languageUtils=r.languageUtils,this.options=i,this.logger=dc.create("backendConnector"),this.waitingReads=[],this.maxParallelReads=i.maxParallelReads||10,this.readingCalls=0,this.maxRetries=i.maxRetries>=0?i.maxRetries:5,this.retryTimeout=i.retryTimeout>=1?i.retryTimeout:350,this.state={},this.queue=[],this.backend?.init?.(r,i.backend,i)}queueLoad(e,n,r,i){const s={},o={},l={},c={};return e.forEach(u=>{let d=!0;n.forEach(h=>{const p=`${u}|${h}`;!r.reload&&this.store.hasResourceBundle(u,h)?this.state[p]=2:this.state[p]<0||(this.state[p]===1?o[p]===void 0&&(o[p]=!0):(this.state[p]=1,d=!1,o[p]===void 0&&(o[p]=!0),s[p]===void 0&&(s[p]=!0),c[h]===void 0&&(c[h]=!0)))}),d||(l[u]=!0)}),(Object.keys(s).length||Object.keys(o).length)&&this.queue.push({pending:o,pendingCount:Object.keys(o).length,loaded:{},errors:[],callback:i}),{toLoad:Object.keys(s),pending:Object.keys(o),toLoadLanguages:Object.keys(l),toLoadNamespaces:Object.keys(c)}}loaded(e,n,r){const i=e.split("|"),s=i[0],o=i[1];n&&this.emit("failedLoading",s,o,n),!n&&r&&this.store.addResourceBundle(s,o,r,void 0,void 0,{skipCopy:!0}),this.state[e]=n?-1:2,n&&r&&(this.state[e]=0);const l={};this.queue.forEach(c=>{hse(c.loaded,[s],o),Ese(c,e),n&&c.errors.push(n),c.pendingCount===0&&!c.done&&(Object.keys(c.loaded).forEach(u=>{l[u]||(l[u]={});const d=c.loaded[u];d.length&&d.forEach(h=>{l[u][h]===void 0&&(l[u][h]=!0)})}),c.done=!0,c.errors.length?c.callback(c.errors):c.callback())}),this.emit("loaded",l),this.queue=this.queue.filter(c=>!c.done)}read(e,n,r,i=0,s=this.retryTimeout,o){if(!e.length)return o(null,{});if(this.readingCalls>=this.maxParallelReads){this.waitingReads.push({lng:e,ns:n,fcName:r,tried:i,wait:s,callback:o});return}this.readingCalls++;const l=(u,d)=>{if(this.readingCalls--,this.waitingReads.length>0){const h=this.waitingReads.shift();this.read(h.lng,h.ns,h.fcName,h.tried,h.wait,h.callback)}if(u&&d&&i s?.[0]===h[0]&&!r.context?(this.logger.warn(`It seems you are nesting recursively key: ${h[0]} in key: ${n[0]}`),null):this.translate(...h,n),r)),r.interpolation&&this.interpolator.reset()}const o=r.postProcess||this.options.postProcess,l=yn(o)?[o]:o;return e!=null&&l?.length&&r.applyPostProcessor!==!1&&(e=V9.handle(l,e,n,this.options&&this.options.postProcessPassResolved?{i18nResolved:{...i,usedParams:this.getUsedParamsDetails(r)},...r}:r,this)),e}resolve(e,n={}){let r,i,s,o,l;return yn(e)&&(e=[e]),e.forEach(c=>{if(this.isValidLookup(r))return;const u=this.extractFromKey(c,n),d=u.key;i=d;let h=u.namespaces;this.options.fallbackNS&&(h=h.concat(this.options.fallbackNS));const p=n.count!==void 0&&!yn(n.count),m=p&&!n.ordinal&&n.count===0,b=n.context!==void 0&&(yn(n.context)||typeof n.context=="number")&&n.context!=="",y=n.lngs?n.lngs:this.languageUtils.toResolveHierarchy(n.lng||this.language,n.fallbackLng);h.forEach(x=>{this.isValidLookup(r)||(l=x,!yU[`${y[0]}-${x}`]&&this.utils?.hasLoadedNamespace&&!this.utils?.hasLoadedNamespace(l)&&(yU[`${y[0]}-${x}`]=!0,this.logger.warn(`key "${i}" for languages "${y.join(", ")}" won't get resolved as namespace "${l}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),y.forEach(v=>{if(this.isValidLookup(r))return;o=v;const S=[d];if(this.i18nFormat?.addLookupKeys)this.i18nFormat.addLookupKeys(S,d,v,x,n);else{let T;p&&(T=this.pluralResolver.getSuffix(v,n.count,n));const M=`${this.options.pluralSeparator}zero`,A=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(p&&(n.ordinal&&T.indexOf(A)===0&&S.push(d+T.replace(A,this.options.pluralSeparator)),S.push(d+T),m&&S.push(d+M)),b){const k=`${d}${this.options.contextSeparator||"_"}${n.context}`;S.push(k),p&&(n.ordinal&&T.indexOf(A)===0&&S.push(k+T.replace(A,this.options.pluralSeparator)),S.push(k+T),m&&S.push(k+M))}}let N;for(;N=S.pop();)this.isValidLookup(r)||(s=N,r=this.getResource(v,x,N,n))}))})}),{res:r,usedKey:i,exactUsedKey:s,usedLng:o,usedNS:l}}isValidLookup(e){return e!==void 0&&!(!this.options.returnNull&&e===null)&&!(!this.options.returnEmptyString&&e==="")}getResource(e,n,r,i={}){return this.i18nFormat?.getResource?this.i18nFormat.getResource(e,n,r,i):this.resourceStore.getResource(e,n,r,i)}getUsedParamsDetails(e={}){const n=["defaultValue","ordinal","context","replace","lng","lngs","fallbackLng","ns","keySeparator","nsSeparator","returnObjects","returnDetails","joinArrays","postProcess","interpolation"],r=e.replace&&!yn(e.replace);let i=r?e.replace:e;if(r&&typeof e.count<"u"&&(i.count=e.count),this.options.interpolation.defaultVariables&&(i={...this.options.interpolation.defaultVariables,...i}),!r){i={...i};for(const s of n)delete i[s]}return i}static hasDefaultValue(e){const n="defaultValue";for(const r in e)if(Object.prototype.hasOwnProperty.call(e,r)&&n===r.substring(0,n.length)&&e[r]!==void 0)return!0;return!1}}class vU{constructor(e){this.options=e,this.supportedLngs=this.options.supportedLngs||!1,this.logger=dc.create("languageUtils")}getScriptPartFromCode(e){if(e=q0(e),!e||e.indexOf("-")<0)return null;const n=e.split("-");return n.length===2||(n.pop(),n[n.length-1].toLowerCase()==="x")?null:this.formatLanguageCode(n.join("-"))}getLanguagePartFromCode(e){if(e=q0(e),!e||e.indexOf("-")<0)return e;const n=e.split("-");return this.formatLanguageCode(n[0])}formatLanguageCode(e){if(yn(e)&&e.indexOf("-")>-1){let n;try{n=Intl.getCanonicalLocales(e)[0]}catch{}return n&&this.options.lowerCaseLng&&(n=n.toLowerCase()),n||(this.options.lowerCaseLng?e.toLowerCase():e)}return this.options.cleanCode||this.options.lowerCaseLng?e.toLowerCase():e}isSupportedCode(e){return(this.options.load==="languageOnly"||this.options.nonExplicitSupportedLngs)&&(e=this.getLanguagePartFromCode(e)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(e)>-1}getBestMatchFromCodes(e){if(!e)return null;let n;return e.forEach(r=>{if(n)return;const i=this.formatLanguageCode(r);(!this.options.supportedLngs||this.isSupportedCode(i))&&(n=i)}),!n&&this.options.supportedLngs&&e.forEach(r=>{if(n)return;const i=this.getScriptPartFromCode(r);if(this.isSupportedCode(i))return n=i;const s=this.getLanguagePartFromCode(r);if(this.isSupportedCode(s))return n=s;n=this.options.supportedLngs.find(o=>{if(o===s)return o;if(!(o.indexOf("-")<0&&s.indexOf("-")<0)&&(o.indexOf("-")>0&&s.indexOf("-")<0&&o.substring(0,o.indexOf("-"))===s||o.indexOf(s)===0&&s.length>1))return o})}),n||(n=this.getFallbackCodes(this.options.fallbackLng)[0]),n}getFallbackCodes(e,n){if(!e)return[];if(typeof e=="function"&&(e=e(n)),yn(e)&&(e=[e]),Array.isArray(e))return e;if(!n)return e.default||[];let r=e[n];return r||(r=e[this.getScriptPartFromCode(n)]),r||(r=e[this.formatLanguageCode(n)]),r||(r=e[this.getLanguagePartFromCode(n)]),r||(r=e.default),r||[]}toResolveHierarchy(e,n){const r=this.getFallbackCodes((n===!1?[]:n)||this.options.fallbackLng||[],e),i=[],s=o=>{o&&(this.isSupportedCode(o)?i.push(o):this.logger.warn(`rejecting language code not found in supportedLngs: ${o}`))};return yn(e)&&(e.indexOf("-")>-1||e.indexOf("_")>-1)?(this.options.load!=="languageOnly"&&s(this.formatLanguageCode(e)),this.options.load!=="languageOnly"&&this.options.load!=="currentOnly"&&s(this.getScriptPartFromCode(e)),this.options.load!=="currentOnly"&&s(this.getLanguagePartFromCode(e))):yn(e)&&s(this.formatLanguageCode(e)),r.forEach(o=>{i.indexOf(o)<0&&s(this.formatLanguageCode(o))}),i}}const wU={zero:0,one:1,two:2,few:3,many:4,other:5},_U={select:t=>t===1?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class _se{constructor(e,n={}){this.languageUtils=e,this.options=n,this.logger=dc.create("pluralResolver"),this.pluralRulesCache={}}addRule(e,n){this.rules[e]=n}clearCache(){this.pluralRulesCache={}}getRule(e,n={}){const r=q0(e==="dev"?"en":e),i=n.ordinal?"ordinal":"cardinal",s=JSON.stringify({cleanedCode:r,type:i});if(s in this.pluralRulesCache)return this.pluralRulesCache[s];let o;try{o=new Intl.PluralRules(r,{type:i})}catch{if(!Intl)return this.logger.error("No Intl support, please use an Intl polyfill!"),_U;if(!e.match(/-|_/))return _U;const c=this.languageUtils.getLanguagePartFromCode(e);o=this.getRule(c,n)}return this.pluralRulesCache[s]=o,o}needsPlural(e,n={}){let r=this.getRule(e,n);return r||(r=this.getRule("dev",n)),r?.resolvedOptions().pluralCategories.length>1}getPluralFormsOfKey(e,n,r={}){return this.getSuffixes(e,r).map(i=>`${n}${i}`)}getSuffixes(e,n={}){let r=this.getRule(e,n);return r||(r=this.getRule("dev",n)),r?r.resolvedOptions().pluralCategories.sort((i,s)=>wU[i]-wU[s]).map(i=>`${this.options.prepend}${n.ordinal?`ordinal${this.options.prepend}`:""}${i}`):[]}getSuffix(e,n,r={}){const i=this.getRule(e,r);return i?`${this.options.prepend}${r.ordinal?`ordinal${this.options.prepend}`:""}${i.select(n)}`:(this.logger.warn(`no plural rule found for: ${e}`),this.getSuffix("dev",n,r))}}const SU=(t,e,n,r=".",i=!0)=>{let s=fse(t,e,n);return!s&&i&&yn(n)&&(s=fj(t,n,r),s===void 0&&(s=fj(e,n,r))),s},uT=t=>t.replace(/\$/g,"$$$$");class Sse{constructor(e={}){this.logger=dc.create("interpolator"),this.options=e,this.format=e?.interpolation?.format||(n=>n),this.init(e)}init(e={}){e.interpolation||(e.interpolation={escapeValue:!0});const{escape:n,escapeValue:r,useRawValueToEscape:i,prefix:s,prefixEscaped:o,suffix:l,suffixEscaped:c,formatSeparator:u,unescapeSuffix:d,unescapePrefix:h,nestingPrefix:p,nestingPrefixEscaped:m,nestingSuffix:b,nestingSuffixEscaped:y,nestingOptionsSeparator:x,maxReplaces:v,alwaysFormat:S}=e.interpolation;this.escape=n!==void 0?n:pse,this.escapeValue=r!==void 0?r:!0,this.useRawValueToEscape=i!==void 0?i:!1,this.prefix=s?_p(s):o||"{{",this.suffix=l?_p(l):c||"}}",this.formatSeparator=u||",",this.unescapePrefix=d?"":h||"-",this.unescapeSuffix=this.unescapePrefix?"":d||"",this.nestingPrefix=p?_p(p):m||_p("$t("),this.nestingSuffix=b?_p(b):y||_p(")"),this.nestingOptionsSeparator=x||",",this.maxReplaces=v||1e3,this.alwaysFormat=S!==void 0?S:!1,this.resetRegExp()}reset(){this.options&&this.init(this.options)}resetRegExp(){const e=(n,r)=>n?.source===r?(n.lastIndex=0,n):new RegExp(r,"g");this.regexp=e(this.regexp,`${this.prefix}(.+?)${this.suffix}`),this.regexpUnescape=e(this.regexpUnescape,`${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`),this.nestingRegexp=e(this.nestingRegexp,`${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`)}interpolate(e,n,r,i){let s,o,l;const c=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{},u=m=>{if(m.indexOf(this.formatSeparator)<0){const v=SU(n,c,m,this.options.keySeparator,this.options.ignoreJSONStructure);return this.alwaysFormat?this.format(v,void 0,r,{...i,...n,interpolationkey:m}):v}const b=m.split(this.formatSeparator),y=b.shift().trim(),x=b.join(this.formatSeparator).trim();return this.format(SU(n,c,y,this.options.keySeparator,this.options.ignoreJSONStructure),x,r,{...i,...n,interpolationkey:y})};this.resetRegExp();const d=i?.missingInterpolationHandler||this.options.missingInterpolationHandler,h=i?.interpolation?.skipOnVariables!==void 0?i.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:m=>uT(m)},{regex:this.regexp,safeValue:m=>this.escapeValue?uT(this.escape(m)):uT(m)}].forEach(m=>{for(l=0;s=m.regex.exec(e);){const b=s[1].trim();if(o=u(b),o===void 0)if(typeof d=="function"){const x=d(e,s,i);o=yn(x)?x:""}else if(i&&Object.prototype.hasOwnProperty.call(i,b))o="";else if(h){o=s[0];continue}else this.logger.warn(`missed to pass in variable ${b} for interpolating ${e}`),o="";else!yn(o)&&!this.useRawValueToEscape&&(o=mU(o));const y=m.safeValue(o);if(e=e.replace(s[0],y),h?(m.regex.lastIndex+=o.length,m.regex.lastIndex-=s[0].length):m.regex.lastIndex=0,l++,l>=this.maxReplaces)break}}),e}nest(e,n,r={}){let i,s,o;const l=(c,u)=>{const d=this.nestingOptionsSeparator;if(c.indexOf(d)<0)return c;const h=c.split(new RegExp(`${d}[ ]*{`));let p=`{${h[1]}`;c=h[0],p=this.interpolate(p,o);const m=p.match(/'/g),b=p.match(/"/g);((m?.length??0)%2===0&&!b||b.length%2!==0)&&(p=p.replace(/'/g,'"'));try{o=JSON.parse(p),u&&(o={...u,...o})}catch(y){return this.logger.warn(`failed parsing options string in nesting for key ${c}`,y),`${c}${d}${p}`}return o.defaultValue&&o.defaultValue.indexOf(this.prefix)>-1&&delete o.defaultValue,c};for(;i=this.nestingRegexp.exec(e);){let c=[];o={...r},o=o.replace&&!yn(o.replace)?o.replace:o,o.applyPostProcessor=!1,delete o.defaultValue;const u=/{.*}/.test(i[1])?i[1].lastIndexOf("}")+1:i[1].indexOf(this.formatSeparator);if(u!==-1&&(c=i[1].slice(u).split(this.formatSeparator).map(d=>d.trim()).filter(Boolean),i[1]=i[1].slice(0,u)),s=n(l.call(this,i[1].trim(),o),o),s&&i[0]===e&&!yn(s))return s;yn(s)||(s=mU(s)),s||(this.logger.warn(`missed to resolve ${i[1]} for nesting ${e}`),s=""),c.length&&(s=c.reduce((d,h)=>this.format(d,h,r.lng,{...r,interpolationkey:i[1].trim()}),s.trim())),e=e.replace(i[0],s),this.regexp.lastIndex=0}return e}}const Nse=t=>{let e=t.toLowerCase().trim();const n={};if(t.indexOf("(")>-1){const r=t.split("(");e=r[0].toLowerCase().trim();const i=r[1].substring(0,r[1].length-1);e==="currency"&&i.indexOf(":")<0?n.currency||(n.currency=i.trim()):e==="relativetime"&&i.indexOf(":")<0?n.range||(n.range=i.trim()):i.split(";").forEach(o=>{if(o){const[l,...c]=o.split(":"),u=c.join(":").trim().replace(/^'+|'+$/g,""),d=l.trim();n[d]||(n[d]=u),u==="false"&&(n[d]=!1),u==="true"&&(n[d]=!0),isNaN(u)||(n[d]=parseInt(u,10))}})}return{formatName:e,formatOptions:n}},NU=t=>{const e={};return(n,r,i)=>{let s=i;i&&i.interpolationkey&&i.formatParams&&i.formatParams[i.interpolationkey]&&i[i.interpolationkey]&&(s={...s,[i.interpolationkey]:void 0});const o=r+JSON.stringify(s);let l=e[o];return l||(l=t(q0(r),i),e[o]=l),l(n)}},kse=t=>(e,n,r)=>t(q0(n),r)(e);class Mse{constructor(e={}){this.logger=dc.create("formatter"),this.options=e,this.init(e)}init(e,n={interpolation:{}}){this.formatSeparator=n.interpolation.formatSeparator||",";const r=n.cacheInBuiltFormats?NU:kse;this.formats={number:r((i,s)=>{const o=new Intl.NumberFormat(i,{...s});return l=>o.format(l)}),currency:r((i,s)=>{const o=new Intl.NumberFormat(i,{...s,style:"currency"});return l=>o.format(l)}),datetime:r((i,s)=>{const o=new Intl.DateTimeFormat(i,{...s});return l=>o.format(l)}),relativetime:r((i,s)=>{const o=new Intl.RelativeTimeFormat(i,{...s});return l=>o.format(l,s.range||"day")}),list:r((i,s)=>{const o=new Intl.ListFormat(i,{...s});return l=>o.format(l)})}}add(e,n){this.formats[e.toLowerCase().trim()]=n}addCached(e,n){this.formats[e.toLowerCase().trim()]=NU(n)}format(e,n,r,i={}){const s=n.split(this.formatSeparator);if(s.length>1&&s[0].indexOf("(")>1&&s[0].indexOf(")")<0&&s.find(l=>l.indexOf(")")>-1)){const l=s.findIndex(c=>c.indexOf(")")>-1);s[0]=[s[0],...s.splice(1,l)].join(this.formatSeparator)}return s.reduce((l,c)=>{const{formatName:u,formatOptions:d}=Nse(c);if(this.formats[u]){let h=l;try{const p=i?.formatParams?.[i.interpolationkey]||{},m=p.locale||p.lng||i.locale||i.lng||r;h=this.formats[u](l,m,{...d,...i,...p})}catch(p){this.logger.warn(p)}return h}else this.logger.warn(`there was no format function for ${u}`);return l},e)}}const Ese=(t,e)=>{t.pending[e]!==void 0&&(delete t.pending[e],t.pendingCount--)};class Tse extends gk{constructor(e,n,r,i={}){super(),this.backend=e,this.store=n,this.services=r,this.languageUtils=r.languageUtils,this.options=i,this.logger=dc.create("backendConnector"),this.waitingReads=[],this.maxParallelReads=i.maxParallelReads||10,this.readingCalls=0,this.maxRetries=i.maxRetries>=0?i.maxRetries:5,this.retryTimeout=i.retryTimeout>=1?i.retryTimeout:350,this.state={},this.queue=[],this.backend?.init?.(r,i.backend,i)}queueLoad(e,n,r,i){const s={},o={},l={},c={};return e.forEach(u=>{let d=!0;n.forEach(h=>{const p=`${u}|${h}`;!r.reload&&this.store.hasResourceBundle(u,h)?this.state[p]=2:this.state[p]<0||(this.state[p]===1?o[p]===void 0&&(o[p]=!0):(this.state[p]=1,d=!1,o[p]===void 0&&(o[p]=!0),s[p]===void 0&&(s[p]=!0),c[h]===void 0&&(c[h]=!0)))}),d||(l[u]=!0)}),(Object.keys(s).length||Object.keys(o).length)&&this.queue.push({pending:o,pendingCount:Object.keys(o).length,loaded:{},errors:[],callback:i}),{toLoad:Object.keys(s),pending:Object.keys(o),toLoadLanguages:Object.keys(l),toLoadNamespaces:Object.keys(c)}}loaded(e,n,r){const i=e.split("|"),s=i[0],o=i[1];n&&this.emit("failedLoading",s,o,n),!n&&r&&this.store.addResourceBundle(s,o,r,void 0,void 0,{skipCopy:!0}),this.state[e]=n?-1:2,n&&r&&(this.state[e]=0);const l={};this.queue.forEach(c=>{hse(c.loaded,[s],o),Ese(c,e),n&&c.errors.push(n),c.pendingCount===0&&!c.done&&(Object.keys(c.loaded).forEach(u=>{l[u]||(l[u]={});const d=c.loaded[u];d.length&&d.forEach(h=>{l[u][h]===void 0&&(l[u][h]=!0)})}),c.done=!0,c.errors.length?c.callback(c.errors):c.callback())}),this.emit("loaded",l),this.queue=this.queue.filter(c=>!c.done)}read(e,n,r,i=0,s=this.retryTimeout,o){if(!e.length)return o(null,{});if(this.readingCalls>=this.maxParallelReads){this.waitingReads.push({lng:e,ns:n,fcName:r,tried:i,wait:s,callback:o});return}this.readingCalls++;const l=(u,d)=>{if(this.readingCalls--,this.waitingReads.length>0){const h=this.waitingReads.shift();this.read(h.lng,h.ns,h.fcName,h.tried,h.wait,h.callback)}if(u&&d&&i{for(let o=0;o-1&&this.options.ns.splice(n,1)}getResource(e,n,r,i={}){const s=i.keySeparator!==void 0?i.keySeparator:this.options.keySeparator,o=i.ignoreJSONStructure!==void 0?i.ignoreJSONStructure:this.options.ignoreJSONStructure;let l;e.indexOf(".")>-1?l=e.split("."):(l=[e,n],r&&(Array.isArray(r)?l.push(...r):yn(r)&&s?l.push(...r.split(s)):l.push(r)));const c=eS(this.data,l);return!c&&!n&&!r&&e.indexOf(".")>-1&&(e=l[0],n=l[1],r=l.slice(2).join(".")),c||!o||!yn(r)?c:fj(this.data?.[e]?.[n],r,s)}addResource(e,n,r,i,s={silent:!1}){const o=s.keySeparator!==void 0?s.keySeparator:this.options.keySeparator;let l=[e,n];r&&(l=l.concat(o?r.split(o):r)),e.indexOf(".")>-1&&(l=e.split("."),i=n,n=l[1]),this.addNamespaces(n),bU(this.data,l,i),s.silent||this.emit("added",e,n,r,i)}addResources(e,n,r,i={silent:!1}){for(const s in r)(yn(r[s])||Array.isArray(r[s]))&&this.addResource(e,n,s,r[s],{silent:!0});i.silent||this.emit("added",e,n,r)}addResourceBundle(e,n,r,i,s,o={silent:!1,skipCopy:!1}){let l=[e,n];e.indexOf(".")>-1&&(l=e.split("."),i=r,r=n,n=l[1]),this.addNamespaces(n);let c=eS(this.data,l)||{};o.skipCopy||(r=JSON.parse(JSON.stringify(r))),i?$9(c,r,s):c={...c,...r},bU(this.data,l,c),o.silent||this.emit("added",e,n,r)}removeResourceBundle(e,n){this.hasResourceBundle(e,n)&&delete this.data[e][n],this.removeNamespaces(n),this.emit("removed",e,n)}hasResourceBundle(e,n){return this.getResource(e,n)!==void 0}getResourceBundle(e,n){return n||(n=this.options.defaultNS),this.getResource(e,n)}getDataByLanguage(e){return this.data[e]}hasLanguageSomeTranslations(e){const n=this.getDataByLanguage(e);return!!(n&&Object.keys(n)||[]).find(i=>n[i]&&Object.keys(n[i]).length>0)}toJSON(){return this.data}}var V9={processors:{},addPostProcessor(t){this.processors[t.name]=t},handle(t,e,n,r,i){return t.forEach(s=>{e=this.processors[s]?.process(e,n,r,i)??e}),e}};const G9=Symbol("i18next/PATH_KEY");function wse(){const t=[],e=Object.create(null);let n;return e.get=(r,i)=>(n?.revoke?.(),i===G9?t:(t.push(i),n=Proxy.revocable(r,e),n.proxy)),Proxy.revocable(Object.create(null),e).proxy}function mj(t,e){const{[G9]:n}=t(wse());return n.join(e?.keySeparator??".")}const yU={},cT=t=>!yn(t)&&typeof t!="boolean"&&typeof t!="number";class nS extends gk{constructor(e,n={}){super(),use(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],e,this),this.options=n,this.options.keySeparator===void 0&&(this.options.keySeparator="."),this.logger=dc.create("translator")}changeLanguage(e){e&&(this.language=e)}exists(e,n={interpolation:{}}){const r={...n};if(e==null)return!1;const i=this.resolve(e,r);if(i?.res===void 0)return!1;const s=cT(i.res);return!(r.returnObjects===!1&&s)}extractFromKey(e,n){let r=n.nsSeparator!==void 0?n.nsSeparator:this.options.nsSeparator;r===void 0&&(r=":");const i=n.keySeparator!==void 0?n.keySeparator:this.options.keySeparator;let s=n.ns||this.options.defaultNS||[];const o=r&&e.indexOf(r)>-1,l=!this.options.userDefinedKeySeparator&&!n.keySeparator&&!this.options.userDefinedNsSeparator&&!n.nsSeparator&&!yse(e,r,i);if(o&&!l){const c=e.match(this.interpolator.nestingRegexp);if(c&&c.length>0)return{key:e,namespaces:yn(s)?[s]:s};const u=e.split(r);(r!==i||r===i&&this.options.ns.indexOf(u[0])>-1)&&(s=u.shift()),e=u.join(i)}return{key:e,namespaces:yn(s)?[s]:s}}translate(e,n,r){let i=typeof n=="object"?{...n}:n;if(typeof i!="object"&&this.options.overloadTranslationOptionHandler&&(i=this.options.overloadTranslationOptionHandler(arguments)),typeof i=="object"&&(i={...i}),i||(i={}),e==null)return"";typeof e=="function"&&(e=mj(e,{...this.options,...i})),Array.isArray(e)||(e=[String(e)]);const s=i.returnDetails!==void 0?i.returnDetails:this.options.returnDetails,o=i.keySeparator!==void 0?i.keySeparator:this.options.keySeparator,{key:l,namespaces:c}=this.extractFromKey(e[e.length-1],i),u=c[c.length-1];let d=i.nsSeparator!==void 0?i.nsSeparator:this.options.nsSeparator;d===void 0&&(d=":");const h=i.lng||this.language,p=i.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(h?.toLowerCase()==="cimode")return p?s?{res:`${u}${d}${l}`,usedKey:l,exactUsedKey:l,usedLng:h,usedNS:u,usedParams:this.getUsedParamsDetails(i)}:`${u}${d}${l}`:s?{res:l,usedKey:l,exactUsedKey:l,usedLng:h,usedNS:u,usedParams:this.getUsedParamsDetails(i)}:l;const m=this.resolve(e,i);let b=m?.res;const y=m?.usedKey||l,x=m?.exactUsedKey||l,v=["[object Number]","[object Function]","[object RegExp]"],S=i.joinArrays!==void 0?i.joinArrays:this.options.joinArrays,N=!this.i18nFormat||this.i18nFormat.handleAsObject,T=i.count!==void 0&&!yn(i.count),M=nS.hasDefaultValue(i),A=T?this.pluralResolver.getSuffix(h,i.count,i):"",k=i.ordinal&&T?this.pluralResolver.getSuffix(h,i.count,{ordinal:!1}):"",R=T&&!i.ordinal&&i.count===0,C=R&&i[`defaultValue${this.options.pluralSeparator}zero`]||i[`defaultValue${A}`]||i[`defaultValue${k}`]||i.defaultValue;let P=b;N&&!b&&M&&(P=C);const U=cT(P),F=Object.prototype.toString.apply(P);if(N&&P&&U&&v.indexOf(F)<0&&!(yn(S)&&Array.isArray(P))){if(!i.returnObjects&&!this.options.returnObjects){this.options.returnedObjectHandler||this.logger.warn("accessing an object - but returnObjects options is not enabled!");const B=this.options.returnedObjectHandler?this.options.returnedObjectHandler(y,P,{...i,ns:c}):`key '${l} (${this.language})' returned an object instead of string.`;return s?(m.res=B,m.usedParams=this.getUsedParamsDetails(i),m):B}if(o){const B=Array.isArray(P),D=B?[]:{},$=B?x:y;for(const G in P)if(Object.prototype.hasOwnProperty.call(P,G)){const I=`${$}${o}${G}`;M&&!b?D[G]=this.translate(I,{...i,defaultValue:cT(C)?C[G]:void 0,joinArrays:!1,ns:c}):D[G]=this.translate(I,{...i,joinArrays:!1,ns:c}),D[G]===I&&(D[G]=P[G])}b=D}}else if(N&&yn(S)&&Array.isArray(b))b=b.join(S),b&&(b=this.extendTranslation(b,e,i,r));else{let B=!1,D=!1;!this.isValidLookup(b)&&M&&(B=!0,b=C),this.isValidLookup(b)||(D=!0,b=l);const G=(i.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey)&&D?void 0:b,I=M&&C!==b&&this.options.updateMissing;if(D||B||I){if(this.logger.log(I?"updateKey":"missingKey",h,u,l,I?C:b),o){const H=this.resolve(l,{...i,keySeparator:!1});H&&H.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}let K=[];const Q=this.languageUtils.getFallbackCodes(this.options.fallbackLng,i.lng||this.language);if(this.options.saveMissingTo==="fallback"&&Q&&Q[0])for(let H=0;H