diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfc82d31a..720f1dad4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ All notable changes to Bambuddy will be documented in this file.
- **H2C Firmware Update Downloads Wrong Firmware** ([#311](https://github.com/maziggy/bambuddy/issues/311)) — H2C printers were mapped to the H2D firmware API key (`h2d`), causing firmware checks to offer H2D firmware instead of H2C firmware. H2C has its own firmware track (01.01.x.x vs H2D's 01.02.x.x). Added separate `h2c` API key mapping. Also added missing H2C/H2S entries to printer model ID and 3MF model maps.
- **Sidebar Links Custom Icons Have Inverted Colors** ([#308](https://github.com/maziggy/bambuddy/issues/308)) — Custom uploaded icons in sidebar links had their colors inverted in dark mode due to a CSS `invert()` filter. The filter was intended for monochrome preset icons but was incorrectly applied to user-uploaded images (e.g., full-color logos). Removed the invert filter from custom icon rendering in the sidebar and the add/edit link modal.
- **Virtual Printer FTP Transfer Fails With Connection Reset** ([#58](https://github.com/maziggy/bambuddy/issues/58)) — Large 3MF uploads to the virtual printer intermittently failed with `[Errno 104] Connection reset by peer` while the small verify_job always succeeded. The `_handle_data_connection` callback returned immediately, allowing the asyncio server-handler task to complete while the data connection was still in active use. The passive port listener also stayed open during transfers, risking duplicate data connections. Fixed by keeping the callback alive until the transfer completes (`_transfer_done` event), closing the passive listener after accepting the connection, and rejecting duplicate data connections. Also added a 5-second drain timeout to MQTT status pushes to prevent blocking when the slicer is busy uploading.
+- **Virtual Printer IP Override for Server Mode** ([#52](https://github.com/maziggy/bambuddy/issues/52)) — The `remote_interface_ip` setting (network interface override) was only used in proxy mode, but users with multiple network interfaces (LAN + Tailscale, Docker bridges) also needed it in server modes (immediate/review/print_queue). Auto-detected IP from `_get_local_ip()` followed the OS default route, causing wrong IP in TLS certificate SAN (handshake failures) and SSDP broadcasts (slicer can't discover printer). Now the interface override applies to all modes: included in certificate SAN, passed to SSDP server as advertise IP, and triggers service restart on change. UI dropdown shown for all modes when enabled (not just proxy).
- **Camera Stop 401 When Auth Enabled** — Camera stop requests (`sendBeacon`) failed with 401 Unauthorized when authentication was enabled because `sendBeacon` cannot send auth headers. Replaced with `fetch` + `keepalive: true` which supports Authorization headers while remaining reliable during page unload.
- **Spoolman Creates Duplicate Spools on Startup** ([#295](https://github.com/maziggy/bambuddy/pull/295)) — Each AMS tray independently fetched all spools from Spoolman, causing redundant API calls and duplicate spool creation with large databases (300+ spools). Now fetches spools once and reuses cached data across all tray operations. Added retry logic (3 attempts, 500ms delay) with connection recreation for transient network errors.
- **Filament Usage Charts Inflated by Quantity Multiplier** ([#229](https://github.com/maziggy/bambuddy/issues/229)) — Daily, weekly, and filament-type charts were multiplying `filament_used_grams` by print quantity, even though the value already represents the total for the entire job. A 26-object print using 126g was counted as 3,276g. Removed the erroneous multiplier from three aggregations in `FilamentTrends.tsx`.
diff --git a/README.md b/README.md
index f5f3a2756..34824cfff 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
- Configurable printer model (X1C, P1S, A1, H2D, etc.)
- Archive mode, Review mode, Queue mode, or Proxy mode
- SSDP discovery (same LAN) or manual IP entry (VPN/remote)
+- Network interface override for multi-NIC/Docker/VPN setups
- Secure TLS/MQTT/FTP communication
### 🛠️ Maintenance & Support
diff --git a/backend/app/services/virtual_printer/manager.py b/backend/app/services/virtual_printer/manager.py
index c3d0f3a42..a04c97487 100644
--- a/backend/app/services/virtual_printer/manager.py
+++ b/backend/app/services/virtual_printer/manager.py
@@ -249,7 +249,8 @@ class VirtualPrinterManager:
needs_restart = (
model_changed
or mode_changed
- or (mode == "proxy" and (target_changed or serial_changed or remote_iface_changed))
+ or remote_iface_changed
+ or (mode == "proxy" and (target_changed or serial_changed))
)
if enabled and not self._enabled:
@@ -392,8 +393,12 @@ class VirtualPrinterManager:
self._cert_service.serial = current_serial
# Regenerate printer cert if serial changed (CA is preserved)
+ # Include remote interface IP in SAN so slicer TLS succeeds on that interface
+ additional_ips = []
+ if self._remote_interface_ip:
+ additional_ips.append(self._remote_interface_ip)
self._cert_service.delete_printer_certificate()
- cert_path, key_path = self._cert_service.generate_certificates()
+ cert_path, key_path = self._cert_service.generate_certificates(additional_ips=additional_ips or None)
logger.info("Generated certificate for serial: %s", current_serial)
# Create directories
@@ -405,6 +410,7 @@ class VirtualPrinterManager:
name=self.PRINTER_NAME,
serial=self.printer_serial,
model=self._model,
+ advertise_ip=self._remote_interface_ip,
)
self._ftp = VirtualPrinterFTPServer(
diff --git a/backend/app/services/virtual_printer/ssdp_server.py b/backend/app/services/virtual_printer/ssdp_server.py
index 4d8b6a463..38469f2c5 100644
--- a/backend/app/services/virtual_printer/ssdp_server.py
+++ b/backend/app/services/virtual_printer/ssdp_server.py
@@ -33,6 +33,7 @@ class VirtualPrinterSSDPServer:
name: str = "Bambuddy",
serial: str = "00M09A391800001", # X1C serial format for compatibility
model: str = "BL-P001", # X1C model code for best compatibility
+ advertise_ip: str = "",
):
"""Initialize the SSDP server.
@@ -40,13 +41,14 @@ class VirtualPrinterSSDPServer:
name: Display name shown in slicer discovery
serial: Unique serial number for this virtual printer (must match cert CN)
model: Model code (BL-P001=X1C, C11=P1S, O1D=H2D)
+ advertise_ip: Override IP to advertise instead of auto-detecting
"""
self.name = name
self.serial = serial
self.model = model
self._running = False
self._socket: socket.socket | None = None
- self._local_ip: str | None = None
+ self._local_ip: str | None = advertise_ip or None
def _get_local_ip(self) -> str:
"""Get the local IP address to advertise."""
diff --git a/backend/tests/unit/services/test_virtual_printer.py b/backend/tests/unit/services/test_virtual_printer.py
index c98f1323f..88d29c39c 100644
--- a/backend/tests/unit/services/test_virtual_printer.py
+++ b/backend/tests/unit/services/test_virtual_printer.py
@@ -382,6 +382,74 @@ class TestSSDPServer:
assert b"DevModel.bambu.com: BL-P001" in message
+ # ========================================================================
+ # Tests for advertise_ip parameter
+ # ========================================================================
+
+ def test_advertise_ip_sets_local_ip(self):
+ """Verify advertise_ip overrides auto-detection."""
+ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPServer
+
+ server = VirtualPrinterSSDPServer(
+ serial="TEST123",
+ name="TestPrinter",
+ model="BL-P001",
+ advertise_ip="10.0.0.50",
+ )
+
+ assert server._local_ip == "10.0.0.50"
+
+ def test_advertise_ip_empty_string_uses_auto_detect(self):
+ """Verify empty advertise_ip falls back to auto-detection."""
+ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPServer
+
+ server = VirtualPrinterSSDPServer(
+ serial="TEST123",
+ name="TestPrinter",
+ model="BL-P001",
+ advertise_ip="",
+ )
+
+ assert server._local_ip is None
+
+ def test_advertise_ip_in_notify_message(self):
+ """Verify NOTIFY message uses the advertise_ip."""
+ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPServer
+
+ server = VirtualPrinterSSDPServer(
+ serial="TEST123",
+ name="TestPrinter",
+ model="BL-P001",
+ advertise_ip="10.0.0.50",
+ )
+
+ message = server._build_notify_message()
+
+ assert b"Location: 10.0.0.50" in message
+
+ def test_advertise_ip_in_response_message(self):
+ """Verify M-SEARCH response uses the advertise_ip."""
+ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPServer
+
+ server = VirtualPrinterSSDPServer(
+ serial="TEST123",
+ name="TestPrinter",
+ model="BL-P001",
+ advertise_ip="10.0.0.50",
+ )
+
+ message = server._build_response_message()
+
+ assert b"Location: 10.0.0.50" in message
+
+ def test_default_no_advertise_ip(self):
+ """Verify default constructor has None local_ip (auto-detect)."""
+ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPServer
+
+ server = VirtualPrinterSSDPServer()
+
+ assert server._local_ip is None
+
class TestCertificateService:
"""Tests for TLS certificate generation."""
@@ -684,7 +752,7 @@ class TestVirtualPrinterManagerProxyMode:
@pytest.mark.asyncio
async def test_configure_proxy_mode_restarts_on_remote_interface_change(self, manager):
- """Verify changing remote_interface_ip restarts services."""
+ """Verify changing remote_interface_ip restarts services in proxy mode."""
# Simulate running state
manager._enabled = True
manager._mode = "proxy"
@@ -704,3 +772,215 @@ class TestVirtualPrinterManagerProxyMode:
# Should have stopped and started
manager._stop.assert_called_once()
manager._start.assert_called_once()
+
+
+class TestVirtualPrinterManagerServerModeIPOverride:
+ """Tests for remote_interface_ip in server mode (immediate/review/print_queue)."""
+
+ @pytest.fixture
+ def manager(self):
+ """Create a VirtualPrinterManager instance."""
+ from backend.app.services.virtual_printer.manager import VirtualPrinterManager
+
+ return VirtualPrinterManager()
+
+ @pytest.mark.asyncio
+ async def test_configure_immediate_mode_stores_remote_interface_ip(self, manager):
+ """Verify immediate mode stores remote_interface_ip."""
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="immediate",
+ remote_interface_ip="10.0.0.50",
+ )
+
+ assert manager._remote_interface_ip == "10.0.0.50"
+
+ @pytest.mark.asyncio
+ async def test_configure_review_mode_stores_remote_interface_ip(self, manager):
+ """Verify review mode stores remote_interface_ip."""
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="review",
+ remote_interface_ip="10.0.0.50",
+ )
+
+ assert manager._remote_interface_ip == "10.0.0.50"
+
+ @pytest.mark.asyncio
+ async def test_configure_print_queue_mode_stores_remote_interface_ip(self, manager):
+ """Verify print_queue mode stores remote_interface_ip."""
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="print_queue",
+ remote_interface_ip="10.0.0.50",
+ )
+
+ assert manager._remote_interface_ip == "10.0.0.50"
+
+ @pytest.mark.asyncio
+ async def test_remote_interface_change_restarts_immediate_mode(self, manager):
+ """Verify changing remote_interface_ip restarts services in immediate mode."""
+ manager._enabled = True
+ manager._mode = "immediate"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
+ manager._stop = AsyncMock()
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="immediate",
+ remote_interface_ip="10.0.0.99", # Changed
+ )
+
+ manager._stop.assert_called_once()
+ manager._start.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_remote_interface_change_restarts_review_mode(self, manager):
+ """Verify changing remote_interface_ip restarts services in review mode."""
+ manager._enabled = True
+ manager._mode = "review"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
+ manager._stop = AsyncMock()
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="review",
+ remote_interface_ip="10.0.0.99", # Changed
+ )
+
+ manager._stop.assert_called_once()
+ manager._start.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_remote_interface_change_restarts_print_queue_mode(self, manager):
+ """Verify changing remote_interface_ip restarts services in print_queue mode."""
+ manager._enabled = True
+ manager._mode = "print_queue"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
+ manager._stop = AsyncMock()
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="print_queue",
+ remote_interface_ip="10.0.0.99", # Changed
+ )
+
+ manager._stop.assert_called_once()
+ manager._start.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_no_restart_when_remote_interface_unchanged(self, manager):
+ """Verify no restart if remote_interface_ip hasn't changed."""
+ manager._enabled = True
+ manager._mode = "immediate"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
+ manager._stop = AsyncMock()
+ manager._start = AsyncMock()
+
+ await manager.configure(
+ enabled=True,
+ access_code="12345678",
+ mode="immediate",
+ remote_interface_ip="10.0.0.50", # Same
+ )
+
+ manager._stop.assert_not_called()
+ manager._start.assert_not_called()
+
+ @pytest.mark.asyncio
+ async def test_server_mode_passes_advertise_ip_to_ssdp(self, manager):
+ """Verify _start_server_mode passes remote_interface_ip as advertise_ip to SSDP."""
+ manager._mode = "immediate"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._model = "3DPrinter-X1-Carbon"
+
+ with (
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterSSDPServer") as mock_ssdp_cls,
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterFTPServer"),
+ patch("backend.app.services.virtual_printer.manager.SimpleMQTTServer"),
+ patch.object(manager._cert_service, "delete_printer_certificate"),
+ patch.object(
+ manager._cert_service,
+ "generate_certificates",
+ return_value=(Path("/tmp/cert.pem"), Path("/tmp/key.pem")),
+ ),
+ ):
+ mock_ssdp_cls.return_value.start = AsyncMock()
+ await manager._start_server_mode()
+
+ mock_ssdp_cls.assert_called_once_with(
+ name="Bambuddy",
+ serial=manager.printer_serial,
+ model="3DPrinter-X1-Carbon",
+ advertise_ip="10.0.0.50",
+ )
+
+ @pytest.mark.asyncio
+ async def test_server_mode_passes_additional_ips_to_certificate(self, manager):
+ """Verify _start_server_mode includes remote_interface_ip in certificate SANs."""
+ manager._mode = "immediate"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = "10.0.0.50"
+ manager._model = "3DPrinter-X1-Carbon"
+
+ with (
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterSSDPServer"),
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterFTPServer"),
+ patch("backend.app.services.virtual_printer.manager.SimpleMQTTServer"),
+ patch.object(manager._cert_service, "delete_printer_certificate"),
+ patch.object(
+ manager._cert_service,
+ "generate_certificates",
+ return_value=(Path("/tmp/cert.pem"), Path("/tmp/key.pem")),
+ ) as mock_gen_certs,
+ ):
+ await manager._start_server_mode()
+
+ mock_gen_certs.assert_called_once_with(additional_ips=["10.0.0.50"])
+
+ @pytest.mark.asyncio
+ async def test_server_mode_no_additional_ips_without_remote_interface(self, manager):
+ """Verify _start_server_mode passes None for additional_ips when no remote interface."""
+ manager._mode = "immediate"
+ manager._access_code = "12345678"
+ manager._remote_interface_ip = ""
+ manager._model = "3DPrinter-X1-Carbon"
+
+ with (
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterSSDPServer"),
+ patch("backend.app.services.virtual_printer.manager.VirtualPrinterFTPServer"),
+ patch("backend.app.services.virtual_printer.manager.SimpleMQTTServer"),
+ patch.object(manager._cert_service, "delete_printer_certificate"),
+ patch.object(
+ manager._cert_service,
+ "generate_certificates",
+ return_value=(Path("/tmp/cert.pem"), Path("/tmp/key.pem")),
+ ) as mock_gen_certs,
+ ):
+ await manager._start_server_mode()
+
+ mock_gen_certs.assert_called_once_with(additional_ips=None)
diff --git a/frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx b/frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx
index af8d0abe4..9bab5ac70 100644
--- a/frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx
+++ b/frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx
@@ -19,6 +19,8 @@ vi.mock('../../api/client', () => ({
api: {
getSettings: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn().mockResolvedValue({}),
+ getPrinters: vi.fn().mockResolvedValue([]),
+ getNetworkInterfaces: vi.fn().mockResolvedValue({ interfaces: [] }),
},
virtualPrinterApi: {
getSettings: vi.fn(),
@@ -570,4 +572,92 @@ describe('VirtualPrinterSettings', () => {
});
});
});
+
+ describe('network interface override', () => {
+ it('shows interface dropdown when enabled in immediate mode', async () => {
+ vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
+ createMockSettings({ enabled: true, mode: 'immediate' })
+ );
+
+ render(
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=Fq.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!=="",x=n.lngs?n.lngs:this.languageUtils.toResolveHierarchy(n.lng||this.language,n.fallbackLng);h.forEach(y=>{this.isValidLookup(r)||(l=y,!hU[`${x[0]}-${y}`]&&this.utils?.hasLoadedNamespace&&!this.utils?.hasLoadedNamespace(l)&&(hU[`${x[0]}-${y}`]=!0,this.logger.warn(`key "${i}" for languages "${x.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!!!")),x.forEach(v=>{if(this.isValidLookup(r))return;o=v;const S=[d];if(this.i18nFormat?.addLookupKeys)this.i18nFormat.addLookupKeys(S,d,v,y,n);else{let T;p&&(T=this.pluralResolver.getSuffix(v,n.count,n));const M=`${this.options.pluralSeparator}zero`,C=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(p&&(n.ordinal&&T.indexOf(C)===0&&S.push(d+T.replace(C,this.options.pluralSeparator)),S.push(d+T),m&&S.push(d+M)),b){const E=`${d}${this.options.contextSeparator||"_"}${n.context}`;S.push(E),p&&(n.ordinal&&T.indexOf(C)===0&&S.push(E+T.replace(C,this.options.pluralSeparator)),S.push(E+T),m&&S.push(E+M))}}let N;for(;N=S.pop();)this.isValidLookup(r)||(s=N,r=this.getResource(v,y,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 fU{constructor(e){this.options=e,this.supportedLngs=this.options.supportedLngs||!1,this.logger=uc.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 mU={zero:0,one:1,two:2,few:3,many:4,other:5},pU={select:t=>t===1?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class gse{constructor(e,n={}){this.languageUtils=e,this.options=n,this.logger=uc.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!"),pU;if(!e.match(/-|_/))return pU;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)=>mU[i]-mU[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 gU=(t,e,n,r=".",i=!0)=>{let s=ose(t,e,n);return!s&&i&&yn(n)&&(s=ij(t,n,r),s===void 0&&(s=ij(e,n,r))),s},eT=t=>t.replace(/\$/g,"$$$$");class bse{constructor(e={}){this.logger=uc.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:x,nestingOptionsSeparator:y,maxReplaces:v,alwaysFormat:S}=e.interpolation;this.escape=n!==void 0?n:cse,this.escapeValue=r!==void 0?r:!0,this.useRawValueToEscape=i!==void 0?i:!1,this.prefix=s?bp(s):o||"{{",this.suffix=l?bp(l):c||"}}",this.formatSeparator=u||",",this.unescapePrefix=d?"":h||"-",this.unescapeSuffix=this.unescapePrefix?"":d||"",this.nestingPrefix=p?bp(p):m||bp("$t("),this.nestingSuffix=b?bp(b):x||bp(")"),this.nestingOptionsSeparator=y||",",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=gU(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),x=b.shift().trim(),y=b.join(this.formatSeparator).trim();return this.format(gU(n,c,x,this.options.keySeparator,this.options.ignoreJSONStructure),y,r,{...i,...n,interpolationkey:x})};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=>eT(m)},{regex:this.regexp,safeValue:m=>this.escapeValue?eT(this.escape(m)):eT(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 y=d(e,s,i);o=yn(y)?y:""}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=oU(o));const x=m.safeValue(o);if(e=e.replace(s[0],x),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(x){return this.logger.warn(`failed parsing options string in nesting for key ${c}`,x),`${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=oU(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 xse=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}},bU=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)}},yse=t=>(e,n,r)=>t(G0(n),r)(e);class vse{constructor(e={}){this.logger=uc.create("formatter"),this.options=e,this.init(e)}init(e,n={interpolation:{}}){this.formatSeparator=n.interpolation.formatSeparator||",";const r=n.cacheInBuiltFormats?bU:yse;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()]=bU(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}=xse(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 wse=(t,e)=>{t.pending[e]!==void 0&&(delete t.pending[e],t.pendingCount--)};class _se extends oM{constructor(e,n,r,i={}){super(),this.backend=e,this.store=n,this.services=r,this.languageUtils=r.languageUtils,this.options=i,this.logger=uc.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=>{sse(c.loaded,[s],o),wse(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=F9.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!=="",x=n.lngs?n.lngs:this.languageUtils.toResolveHierarchy(n.lng||this.language,n.fallbackLng);h.forEach(y=>{this.isValidLookup(r)||(l=y,!hU[`${x[0]}-${y}`]&&this.utils?.hasLoadedNamespace&&!this.utils?.hasLoadedNamespace(l)&&(hU[`${x[0]}-${y}`]=!0,this.logger.warn(`key "${i}" for languages "${x.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!!!")),x.forEach(v=>{if(this.isValidLookup(r))return;o=v;const S=[d];if(this.i18nFormat?.addLookupKeys)this.i18nFormat.addLookupKeys(S,d,v,y,n);else{let T;p&&(T=this.pluralResolver.getSuffix(v,n.count,n));const M=`${this.options.pluralSeparator}zero`,C=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(p&&(n.ordinal&&T.indexOf(C)===0&&S.push(d+T.replace(C,this.options.pluralSeparator)),S.push(d+T),m&&S.push(d+M)),b){const E=`${d}${this.options.contextSeparator||"_"}${n.context}`;S.push(E),p&&(n.ordinal&&T.indexOf(C)===0&&S.push(E+T.replace(C,this.options.pluralSeparator)),S.push(E+T),m&&S.push(E+M))}}let N;for(;N=S.pop();)this.isValidLookup(r)||(s=N,r=this.getResource(v,y,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 fU{constructor(e){this.options=e,this.supportedLngs=this.options.supportedLngs||!1,this.logger=uc.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 mU={zero:0,one:1,two:2,few:3,many:4,other:5},pU={select:t=>t===1?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class gse{constructor(e,n={}){this.languageUtils=e,this.options=n,this.logger=uc.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!"),pU;if(!e.match(/-|_/))return pU;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)=>mU[i]-mU[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 gU=(t,e,n,r=".",i=!0)=>{let s=ose(t,e,n);return!s&&i&&yn(n)&&(s=ij(t,n,r),s===void 0&&(s=ij(e,n,r))),s},eT=t=>t.replace(/\$/g,"$$$$");class bse{constructor(e={}){this.logger=uc.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:x,nestingOptionsSeparator:y,maxReplaces:v,alwaysFormat:S}=e.interpolation;this.escape=n!==void 0?n:cse,this.escapeValue=r!==void 0?r:!0,this.useRawValueToEscape=i!==void 0?i:!1,this.prefix=s?bp(s):o||"{{",this.suffix=l?bp(l):c||"}}",this.formatSeparator=u||",",this.unescapePrefix=d?"":h||"-",this.unescapeSuffix=this.unescapePrefix?"":d||"",this.nestingPrefix=p?bp(p):m||bp("$t("),this.nestingSuffix=b?bp(b):x||bp(")"),this.nestingOptionsSeparator=y||",",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=gU(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),x=b.shift().trim(),y=b.join(this.formatSeparator).trim();return this.format(gU(n,c,x,this.options.keySeparator,this.options.ignoreJSONStructure),y,r,{...i,...n,interpolationkey:x})};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=>eT(m)},{regex:this.regexp,safeValue:m=>this.escapeValue?eT(this.escape(m)):eT(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 y=d(e,s,i);o=yn(y)?y:""}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=oU(o));const x=m.safeValue(o);if(e=e.replace(s[0],x),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(x){return this.logger.warn(`failed parsing options string in nesting for key ${c}`,x),`${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=oU(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 xse=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}},bU=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)}},yse=t=>(e,n,r)=>t(G0(n),r)(e);class vse{constructor(e={}){this.logger=uc.create("formatter"),this.options=e,this.init(e)}init(e,n={interpolation:{}}){this.formatSeparator=n.interpolation.formatSeparator||",";const r=n.cacheInBuiltFormats?bU:yse;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()]=bU(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}=xse(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 wse=(t,e)=>{t.pending[e]!==void 0&&(delete t.pending[e],t.pendingCount--)};class _se extends oM{constructor(e,n,r,i={}){super(),this.backend=e,this.store=n,this.services=r,this.languageUtils=r.languageUtils,this.options=i,this.logger=uc.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=>{sse(c.loaded,[s],o),wse(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=G1(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:ij(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),uU(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=G1(this.data,l)||{};o.skipCopy||(r=JSON.parse(JSON.stringify(r))),i?L9(c,r,s):c={...c,...r},uU(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 F9={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 I9=Symbol("i18next/PATH_KEY");function pse(){const t=[],e=Object.create(null);let n;return e.get=(r,i)=>(n?.revoke?.(),i===I9?t:(t.push(i),n=Proxy.revocable(r,e),n.proxy)),Proxy.revocable(Object.create(null),e).proxy}function aj(t,e){const{[I9]:n}=t(pse());return n.join(e?.keySeparator??".")}const hU={},JE=t=>!yn(t)&&typeof t!="boolean"&&typeof t!="number";class W1 extends oM{constructor(e,n={}){super(),ise(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],e,this),this.options=n,this.options.keySeparator===void 0&&(this.options.keySeparator="."),this.logger=uc.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=JE(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&&!fse(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=aj(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 x=m?.usedKey||l,y=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=W1.hasDefaultValue(i),C=T?this.pluralResolver.getSuffix(h,i.count,i):"",E=i.ordinal&&T?this.pluralResolver.getSuffix(h,i.count,{ordinal:!1}):"",R=T&&!i.ordinal&&i.count===0,A=R&&i[`defaultValue${this.options.pluralSeparator}zero`]||i[`defaultValue${C}`]||i[`defaultValue${E}`]||i.defaultValue;let P=b;N&&!b&&M&&(P=A);const U=JE(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 H=this.options.returnedObjectHandler?this.options.returnedObjectHandler(x,P,{...i,ns:c}):`key '${l} (${this.language})' returned an object instead of string.`;return s?(m.res=H,m.usedParams=this.getUsedParamsDetails(i),m):H}if(o){const H=Array.isArray(P),D=H?[]:{},$=H?y:x;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:JE(A)?A[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 H=!1,D=!1;!this.isValidLookup(b)&&M&&(H=!0,b=A),this.isValidLookup(b)||(D=!0,b=l);const G=(i.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey)&&D?void 0:b,I=M&&A!==b&&this.options.updateMissing;if(D||H||I){if(this.logger.log(I?"updateKey":"missingKey",h,u,l,I?A:b),o){const B=this.resolve(l,{...i,keySeparator:!1});B&&B.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 B=0;B