Add English localization and MIT-licensed public release documentation
Tests / unit (de, 3.10) (push) Has been cancelled
Tests / unit (de, 3.14) (push) Has been cancelled
Tests / unit (en, 3.14) (push) Has been cancelled
Tests / unit (en, 3.10) (push) Has been cancelled

This commit is contained in:
Justin
2026-09-05 21:02:14 +02:00
parent 3579667c6e
commit dd0809e877
31 changed files with 1261 additions and 377 deletions
+21 -20
View File
@@ -1,4 +1,5 @@
"""Device operations with readback and capability checks."""
from .i18n import tr
from dataclasses import asdict, dataclass
import time
@@ -31,8 +32,8 @@ class Status:
@property
def quality(self):
bits = {1: '16 Bit', 2: '24 Bit'}.get(self.resolution, '')
frequency = {1: '44,1 kHz', 2: '48 kHz', 3: '96 kHz'}.get(self.frequency, '')
bits = {1: tr('16 Bit'), 2: tr('24 Bit')}.get(self.resolution, '')
frequency = {1: tr('44,1 kHz'), 2: '48 kHz', 3: '96 kHz'}.get(self.frequency, '')
return f'{bits} / {frequency}'
@property
@@ -43,10 +44,10 @@ class Status:
def public_dict(self):
result = asdict(self)
result.update(state_name=STATES.get(self.state, f'Unbekannt ({self.state})'),
mode_name=MODES.get(self.mode, f'Unbekannt ({self.mode})'),
result.update(state_name=tr(STATES.get(self.state, tr('Unbekannt ({value})').format(value=self.state))),
mode_name=tr(MODES.get(self.mode, tr('Unbekannt ({value})').format(value=self.mode))),
codec_name=self.codec_name, audio_quality=self.quality,
transport_name=TRANSPORTS.get(self.transport, f'Unbekannt ({self.transport})'))
transport_name=tr(TRANSPORTS.get(self.transport, tr('Unbekannt ({value})').format(value=self.transport))))
return result
@@ -70,7 +71,7 @@ class Controller:
try:
self.firmware = '.'.join(map(str, q(C.GET_VERSION)[:3]))
except TimeoutError:
self.firmware = 'Unbekannt'
self.firmware = tr('Unbekannt')
# Firmware 3.11 does not respond to GET_GAMING; use events/fallback,
# matching the original application's capability logic.
event = self.transport.events.get(23, b'')
@@ -87,20 +88,20 @@ class Controller:
if matches:
return
if time.monotonic() >= deadline:
raise DeviceError('Der Dongle hat die angeforderte Einstellung nicht bestätigt. Bitte den aktuellen Status prüfen.')
raise DeviceError(tr('Der Dongle hat die angeforderte Einstellung nicht bestätigt. Bitte den aktuellen Status prüfen.'))
time.sleep(0.15)
def set_mode(self, mode: int, transport: int | None = None):
status = self.snapshot()
if mode not in MODES:
raise ValueError('Unbekannter Audiomodus.')
raise ValueError(tr('Unbekannter Audiomodus.'))
if mode == 1 and not status.gaming_allowed:
raise ValueError('Gaming ist für die aktuelle Verbindung nicht verfügbar.')
raise ValueError(tr('Gaming ist für die aktuelle Verbindung nicht verfügbar.'))
selected = status.transport if transport is None else transport
if selected not in (1, 2, 3):
raise ValueError('Unbekannter Bluetooth-Transport.')
raise ValueError(tr('Unbekannter Bluetooth-Transport.'))
if transport is not None and selected in (1, 2) and status.transports and not status.transports & selected:
raise ValueError('Die Kopfhörer unterstützen diesen Transport nicht.')
raise ValueError(tr('Die Kopfhörer unterstützen diesen Transport nicht.'))
payload = bytes((mode, selected))
self.transport.request(C.SET_MODE, payload)
self._verify(C.GET_MODE, payload, exact=False)
@@ -108,9 +109,9 @@ class Controller:
def set_codec(self, codec: int):
status = self.snapshot()
if status.mode != 0 or status.state < 2:
raise ValueError('Codec-Auswahl benötigt verbundene Kopfhörer im Standard-Modus.')
raise ValueError(tr('Codec-Auswahl benötigt verbundene Kopfhörer im Standard-Modus.'))
if codec not in CODECS or not status.codecs & codec:
raise ValueError('Dieser Codec wird für die aktuelle Verbindung nicht angeboten.')
raise ValueError(tr('Dieser Codec wird für die aktuelle Verbindung nicht angeboten.'))
payload = bytes((codec,))
self.transport.request(C.SET_CODEC, payload)
self._verify(C.GET_CODEC, payload)
@@ -123,7 +124,7 @@ class Controller:
if (state >= 2) == connected:
return
if time.monotonic() >= deadline:
raise DeviceError('Verbindung wurde angefordert, aber noch nicht bestätigt. Sind die gekoppelten Kopfhörer eingeschaltet?')
raise DeviceError(tr('Verbindung wurde angefordert, aber noch nicht bestätigt. Sind die gekoppelten Kopfhörer eingeschaltet?'))
time.sleep(0.25)
def set_broadcast(self, *, name=None, password=None, public=None, quality=None, encrypted=None):
@@ -131,9 +132,9 @@ class Controller:
name_data = validate_text(name) if name is not None else None
key_data = validate_text(password, key=True) if password is not None else None
if any(value is not None and not isinstance(value, bool) for value in (public, encrypted)):
raise ValueError('Auracast-Schalter benötigen einen booleschen Wert.')
raise ValueError(tr('Auracast-Schalter benötigen einen booleschen Wert.'))
if quality is not None and quality not in (0, 1, 2):
raise ValueError('Unbekannte Auracast-Qualität.')
raise ValueError(tr('Unbekannte Auracast-Qualität.'))
before = self.snapshot()
info = [before.broadcast_public, before.broadcast_quality, before.broadcast_encrypted]
if public is not None:
@@ -145,11 +146,11 @@ class Controller:
if encrypted is not None:
info[2] = int(encrypted)
if info[2] and key_data == b'':
raise ValueError('Verschlüsselung benötigt ein Passwort mit 416 Zeichen.')
raise ValueError(tr('Verschlüsselung benötigt ein Passwort mit 416 Zeichen.'))
if info[2] and key_data is None:
existing = self.transport.request(C.GET_KEY).rstrip(b'\0')
if len(existing) < 4:
raise ValueError('Bitte zuerst ein Passwort mit 416 Zeichen eingeben.')
raise ValueError(tr('Bitte zuerst ein Passwort mit 416 Zeichen eingeben.'))
# Preserve the original settings while changing a key, then write
# key, name, and final settings in the order used by the original app.
try:
@@ -169,9 +170,9 @@ class Controller:
except Exception as exc:
# Do not silently repeat writes or claim atomicity: settings persist
# independently on the dongle.
raise DeviceError('Auracast wurde nur teilweise oder nicht übernommen. Aktuellen Namen und Passwortschutz prüfen.') from exc
raise DeviceError(tr('Auracast wurde nur teilweise oder nicht übernommen. Aktuellen Namen und Passwortschutz prüfen.')) from exc
def factory_reset(self, *, confirmed=False):
if not confirmed:
raise ValueError('Zurücksetzen muss ausdrücklich bestätigt werden; alle Kopplungen werden gelöscht.')
raise ValueError(tr('Zurücksetzen muss ausdrücklich bestätigt werden; alle Kopplungen werden gelöscht.'))
self.transport.request(C.FACTORY_RESET)