feat: add private relay for end-to-end-encrypted remote access (#2087)

Adds OpenChamber Relay — an opt-in way to reach an instance from a phone,
browser, or another desktop from anywhere, with no open inbound ports, no
tunnel, and no shared LAN. The instance dials outbound to a relay; all app
traffic (HTTP, the event stream, terminal, dictation) is multiplexed and
encrypted through a single connection per client, so the relay only ever
forwards opaque ciphertext.

Transport
- End-to-end-encrypted channel over WebCrypto (ECDH P-256 -> HKDF ->
  AES-256-GCM) with a capability-negotiated handshake and a small
  HTTP/SSE/WebSocket multiplexing protocol. A byte-compatible JS host mirror
  is cross-checked by tests.
- Host: outbound connection manager, per-client tunnel dispatcher to the local
  server over loopback, reuse of the existing instance identity key, and
  management routes. Disabled by default; explicit opt-in.
- Client: plugs into the existing runtime layer (runtime-fetch/-url/-switch/
  -auth, event pipeline, terminal, dictation) so features work over the relay
  unchanged; direct-URL and Electron realtime-proxy paths are untouched.

Pairing & UX
- Relay section in Settings -> Remote Instances (live status, QR/link pairing,
  revocation via the existing client-token list) and the mobile connect flow.
- Frame batching and idle-gated keepalive keep tunnel message volume low
  without affecting streaming smoothness.

Security
- The tunnel is transport only; the server authenticates every tunneled
  request exactly as for a direct remote client.
  fragments only. The relay stores no keys, tokens, or payloads.

Operability
- The endpoint can be pinned to a self-hosted rel
  paired clients inherit it from the offer automatically.
- Relay module DOCUMENTATION.md and a relay-trans
  invariants that future WebSocket/streaming changes must follow.

The relay transport is complete and tested; the UI for enabling and pairing
is gated behind openchamber_relay_gate and stays
This commit is contained in:
Bohdan Triapitsyn
2026-07-08 03:44:02 +03:00
committed by GitHub
parent 42e470cefa
commit 859b4529da
74 changed files with 7768 additions and 99 deletions
@@ -287,6 +287,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': 'This device',
'settings.remoteInstances.clientAuth.lastUsed': 'Last used {date}',
'settings.remoteInstances.clientAuth.neverUsed': 'Never used',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': 'Let your other devices connect from anywhere without opening ports. Traffic is end-to-end encrypted — the relay cannot read it.',
'settings.remoteInstances.relay.enableHint': 'Nothing is shared until you enable the relay on this server.',
'settings.remoteInstances.relay.actions.enable': 'Enable Relay',
'settings.remoteInstances.relay.actions.disable': 'Disable',
'settings.remoteInstances.relay.confirm.disable': 'Disable the relay? Devices connected through it will be disconnected immediately.',
'settings.remoteInstances.relay.state.loading': 'Checking relay status...',
'settings.remoteInstances.relay.state.disabled': 'Disabled',
'settings.remoteInstances.relay.state.connecting': 'Connecting',
'settings.remoteInstances.relay.state.connected': 'Connected',
'settings.remoteInstances.relay.state.reconnecting': 'Reconnecting',
'settings.remoteInstances.relay.state.error': 'Error',
'settings.remoteInstances.relay.status.clientsOne': '{count} device connected',
'settings.remoteInstances.relay.status.clientsMany': '{count} devices connected',
'settings.remoteInstances.relay.pair.title': 'Pair a device',
'settings.remoteInstances.relay.pair.labelPlaceholder': 'Device name (optional)',
'settings.remoteInstances.relay.pair.includeToken': 'Include access token (one-scan pairing)',
'settings.remoteInstances.relay.pair.noTokenHint': 'Without a token, the device signs in with this server’s UI password after connecting.',
'settings.remoteInstances.relay.pair.generate': 'Create pairing link',
'settings.remoteInstances.relay.pair.requiresConnected': 'Pairing becomes available once the relay is connected.',
'settings.remoteInstances.relay.pair.linkLabel': 'Pairing link',
'settings.remoteInstances.relay.pair.warning': 'This link grants access to this server. Do not share it.',
'settings.remoteInstances.relay.pair.qrAlt': 'Relay pairing QR code',
'settings.remoteInstances.relay.pair.showQr': 'Show QR code',
'settings.remoteInstances.relay.pair.qrDialogTitle': 'Scan to pair',
'settings.remoteInstances.relay.pair.qrDialogDescription': 'Scan this QR code with the OpenChamber app on your other device.',
'settings.remoteInstances.relay.pair.manageHint': 'Manage or revoke paired devices in the “Connect to this server” list above.',
'settings.remoteInstances.relay.toast.enableFailed': 'Failed to enable relay',
'settings.remoteInstances.relay.toast.disableFailed': 'Failed to disable relay',
'settings.remoteInstances.relay.toast.offerFailed': 'Failed to create pairing link',
'settings.remoteInstances.relay.toast.linkCopied': 'Pairing link copied',
'settings.remoteInstances.sidebar.phase.ready': 'Ready',
'settings.remoteInstances.sidebar.phase.error': 'Error',
'settings.remoteInstances.sidebar.phase.reconnect': 'Reconnect',
+1
View File
@@ -58,6 +58,7 @@ export const dict = {
'mobile.connect.scan.unsupported': 'QR scanning is only available in the installed mobile app.',
'mobile.connect.saved.title': 'Saved connections',
'mobile.connect.saved.empty': 'No saved connections yet.',
'mobile.connect.relay.badge': 'via OpenChamber Relay',
'mobile.connect.error.urlRequired': 'Enter a server URL.',
'mobile.connect.error.invalidUrl': 'That server URL is not valid.',
'mobile.connect.error.unreachable': 'Could not reach that OpenChamber server.',
@@ -254,6 +254,37 @@ export const settingsDict = {
"settings.remoteInstances.clientAuth.state.thisDevice": "Este dispositivo",
"settings.remoteInstances.clientAuth.lastUsed": "Último uso {date}",
"settings.remoteInstances.clientAuth.neverUsed": "Nunca usado",
"settings.remoteInstances.relay.title": "OpenChamber Relay",
"settings.remoteInstances.relay.description": "Permite que tus otros dispositivos se conecten desde cualquier lugar sin abrir puertos. El tráfico está cifrado de extremo a extremo: el relay no puede leerlo.",
"settings.remoteInstances.relay.enableHint": "No se comparte nada hasta que actives el relay en este servidor.",
"settings.remoteInstances.relay.actions.enable": "Activar Relay",
"settings.remoteInstances.relay.actions.disable": "Desactivar",
"settings.remoteInstances.relay.confirm.disable": "¿Desactivar el relay? Los dispositivos conectados a través de él se desconectarán de inmediato.",
"settings.remoteInstances.relay.state.loading": "Comprobando el estado del relay...",
"settings.remoteInstances.relay.state.disabled": "Desactivado",
"settings.remoteInstances.relay.state.connecting": "Conectando",
"settings.remoteInstances.relay.state.connected": "Conectado",
"settings.remoteInstances.relay.state.reconnecting": "Reconectando",
"settings.remoteInstances.relay.state.error": "Error",
"settings.remoteInstances.relay.status.clientsOne": "{count} dispositivo conectado",
"settings.remoteInstances.relay.status.clientsMany": "{count} dispositivos conectados",
"settings.remoteInstances.relay.pair.title": "Emparejar un dispositivo",
"settings.remoteInstances.relay.pair.labelPlaceholder": "Nombre del dispositivo (opcional)",
"settings.remoteInstances.relay.pair.includeToken": "Incluir token de acceso (emparejamiento con un solo escaneo)",
"settings.remoteInstances.relay.pair.noTokenHint": "Sin token, el dispositivo inicia sesión con la contraseña de la interfaz de este servidor tras conectarse.",
"settings.remoteInstances.relay.pair.generate": "Crear enlace de emparejamiento",
"settings.remoteInstances.relay.pair.requiresConnected": "El emparejamiento estará disponible cuando el relay esté conectado.",
"settings.remoteInstances.relay.pair.linkLabel": "Enlace de emparejamiento",
"settings.remoteInstances.relay.pair.warning": "Este enlace concede acceso a este servidor. No lo compartas.",
"settings.remoteInstances.relay.pair.qrAlt": "Código QR de emparejamiento del relay",
"settings.remoteInstances.relay.pair.showQr": "Mostrar código QR",
"settings.remoteInstances.relay.pair.qrDialogTitle": "Escanear para emparejar",
"settings.remoteInstances.relay.pair.qrDialogDescription": "Escanea este código QR con la app de OpenChamber en tu otro dispositivo.",
"settings.remoteInstances.relay.pair.manageHint": "Gestiona o revoca los dispositivos emparejados en la lista «Conexión a este servidor» de arriba.",
"settings.remoteInstances.relay.toast.enableFailed": "No se pudo activar el relay",
"settings.remoteInstances.relay.toast.disableFailed": "No se pudo desactivar el relay",
"settings.remoteInstances.relay.toast.offerFailed": "No se pudo crear el enlace de emparejamiento",
"settings.remoteInstances.relay.toast.linkCopied": "Enlace de emparejamiento copiado",
"settings.remoteInstances.sidebar.phase.ready": "Listo",
"settings.remoteInstances.sidebar.phase.error": "Error",
"settings.remoteInstances.sidebar.phase.reconnect": "Reconectar",
+1
View File
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
"mobile.connect.scan.unsupported": "El escaneo de QR solo está disponible en la app móvil instalada.",
"mobile.connect.saved.title": "Conexiones guardadas",
"mobile.connect.saved.empty": "Aún no hay conexiones guardadas.",
"mobile.connect.relay.badge": "a través de OpenChamber Relay",
"mobile.connect.error.urlRequired": "Introduce una URL de servidor.",
"mobile.connect.error.invalidUrl": "Esa URL de servidor no es válida.",
"mobile.connect.error.unreachable": "No se pudo conectar con ese servidor de OpenChamber.",
@@ -1789,6 +1789,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': 'Cet appareil',
'settings.remoteInstances.clientAuth.lastUsed': 'Dernière utilisation le {date}',
'settings.remoteInstances.clientAuth.neverUsed': 'Jamais utilisé',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': 'Permettez à vos autres appareils de se connecter depuis n’importe où sans ouvrir de ports. Le trafic est chiffré de bout en bout — le relais ne peut pas le lire.',
'settings.remoteInstances.relay.enableHint': 'Rien n’est partagé tant que vous n’activez pas le relais sur ce serveur.',
'settings.remoteInstances.relay.actions.enable': 'Activer le relais',
'settings.remoteInstances.relay.actions.disable': 'Désactiver',
'settings.remoteInstances.relay.confirm.disable': 'Désactiver le relais ? Les appareils connectés via celui-ci seront déconnectés immédiatement.',
'settings.remoteInstances.relay.state.loading': 'Vérification de l’état du relais...',
'settings.remoteInstances.relay.state.disabled': 'Désactivé',
'settings.remoteInstances.relay.state.connecting': 'Connexion',
'settings.remoteInstances.relay.state.connected': 'Connecté',
'settings.remoteInstances.relay.state.reconnecting': 'Reconnexion',
'settings.remoteInstances.relay.state.error': 'Erreur',
'settings.remoteInstances.relay.status.clientsOne': '{count} appareil connecté',
'settings.remoteInstances.relay.status.clientsMany': '{count} appareils connectés',
'settings.remoteInstances.relay.pair.title': 'Associer un appareil',
'settings.remoteInstances.relay.pair.labelPlaceholder': 'Nom de l’appareil (facultatif)',
'settings.remoteInstances.relay.pair.includeToken': 'Inclure le jeton d’accès (association en un seul scan)',
'settings.remoteInstances.relay.pair.noTokenHint': 'Sans jeton, l’appareil se connecte avec le mot de passe de l’interface de ce serveur.',
'settings.remoteInstances.relay.pair.generate': 'Créer un lien d’association',
'settings.remoteInstances.relay.pair.requiresConnected': 'L’association devient disponible une fois le relais connecté.',
'settings.remoteInstances.relay.pair.linkLabel': 'Lien d’association',
'settings.remoteInstances.relay.pair.warning': 'Ce lien donne accès à ce serveur. Ne le partagez pas.',
'settings.remoteInstances.relay.pair.qrAlt': 'Code QR d’association du relais',
'settings.remoteInstances.relay.pair.showQr': 'Afficher le code QR',
'settings.remoteInstances.relay.pair.qrDialogTitle': 'Scanner pour associer',
'settings.remoteInstances.relay.pair.qrDialogDescription': 'Scannez ce code QR avec l’application OpenChamber sur votre autre appareil.',
'settings.remoteInstances.relay.pair.manageHint': 'Gérez ou révoquez les appareils associés dans la liste « Connexion à ce serveur » ci-dessus.',
'settings.remoteInstances.relay.toast.enableFailed': 'Échec de l’activation du relais',
'settings.remoteInstances.relay.toast.disableFailed': 'Échec de la désactivation du relais',
'settings.remoteInstances.relay.toast.offerFailed': 'Échec de la création du lien d’association',
'settings.remoteInstances.relay.toast.linkCopied': 'Lien d’association copié',
'settings.openchamber.about.field.openCodeVersion': 'Version d’OpenCode',
'settings.openchamber.about.state.unknown': 'inconnue',
'settings.voice.page.field.ttsInputMode': 'Mode d’entrée TTS',
+1
View File
@@ -2503,6 +2503,7 @@ export const dict = {
'mobile.connect.scan.unsupported': 'Le scan QR est disponible uniquement dans l\'app mobile installée.',
'mobile.connect.saved.title': 'Connexions enregistrées',
'mobile.connect.saved.empty': 'Aucune connexion enregistrée pour le moment.',
'mobile.connect.relay.badge': 'via OpenChamber Relay',
'mobile.connect.error.urlRequired': 'Saisissez une URL de serveur.',
'mobile.connect.error.invalidUrl': 'Cette URL de serveur n\'est pas valide.',
'mobile.connect.error.unreachable': 'Impossible de joindre ce serveur OpenChamber.',
@@ -287,6 +287,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': 'このデバイス',
'settings.remoteInstances.clientAuth.lastUsed': '最終使用 {date}',
'settings.remoteInstances.clientAuth.neverUsed': '未使用',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': 'ポートを開放せずに、他のデバイスからどこからでも接続できます。通信はエンドツーエンドで暗号化され、リレーは内容を読めません。',
'settings.remoteInstances.relay.enableHint': 'このサーバーでリレーを有効にするまで、何も共有されません。',
'settings.remoteInstances.relay.actions.enable': 'リレーを有効にする',
'settings.remoteInstances.relay.actions.disable': '無効にする',
'settings.remoteInstances.relay.confirm.disable': 'リレーを無効にしますか?リレー経由で接続中のデバイスは即座に切断されます。',
'settings.remoteInstances.relay.state.loading': 'リレーの状態を確認中...',
'settings.remoteInstances.relay.state.disabled': '無効',
'settings.remoteInstances.relay.state.connecting': '接続中',
'settings.remoteInstances.relay.state.connected': '接続済み',
'settings.remoteInstances.relay.state.reconnecting': '再接続中',
'settings.remoteInstances.relay.state.error': 'エラー',
'settings.remoteInstances.relay.status.clientsOne': '{count} 台のデバイスが接続中',
'settings.remoteInstances.relay.status.clientsMany': '{count} 台のデバイスが接続中',
'settings.remoteInstances.relay.pair.title': 'デバイスをペアリング',
'settings.remoteInstances.relay.pair.labelPlaceholder': 'デバイス名(任意)',
'settings.remoteInstances.relay.pair.includeToken': 'アクセストークンを含める(1回のスキャンでペアリング)',
'settings.remoteInstances.relay.pair.noTokenHint': 'トークンなしの場合、デバイスは接続後にこのサーバーのUIパスワードでサインインします。',
'settings.remoteInstances.relay.pair.generate': 'ペアリングリンクを作成',
'settings.remoteInstances.relay.pair.requiresConnected': 'ペアリングはリレーの接続後に利用できます。',
'settings.remoteInstances.relay.pair.linkLabel': 'ペアリングリンク',
'settings.remoteInstances.relay.pair.warning': 'このリンクはこのサーバーへのアクセスを許可します。共有しないでください。',
'settings.remoteInstances.relay.pair.qrAlt': 'リレーペアリング用QRコード',
'settings.remoteInstances.relay.pair.showQr': 'QRコードを表示',
'settings.remoteInstances.relay.pair.qrDialogTitle': 'スキャンしてペアリング',
'settings.remoteInstances.relay.pair.qrDialogDescription': '他のデバイスのOpenChamberアプリでこのQRコードをスキャンします。',
'settings.remoteInstances.relay.pair.manageHint': 'ペアリング済みデバイスの管理や取り消しは、上の「このサーバーへの接続」一覧で行えます。',
'settings.remoteInstances.relay.toast.enableFailed': 'リレーを有効にできませんでした',
'settings.remoteInstances.relay.toast.disableFailed': 'リレーを無効にできませんでした',
'settings.remoteInstances.relay.toast.offerFailed': 'ペアリングリンクを作成できませんでした',
'settings.remoteInstances.relay.toast.linkCopied': 'ペアリングリンクをコピーしました',
'settings.remoteInstances.sidebar.phase.ready': '準備完了',
'settings.remoteInstances.sidebar.phase.error': 'エラー',
'settings.remoteInstances.sidebar.phase.reconnect': '再接続',
+1
View File
@@ -58,6 +58,7 @@ export const dict: Record<I18nKey, string> = {
'mobile.connect.cancelPassword': '別のサーバーを使用',
'mobile.connect.saved.title': '保存された接続',
'mobile.connect.saved.empty': '保存された接続はまだありません。',
'mobile.connect.relay.badge': 'OpenChamber Relay 経由',
'mobile.connect.error.urlRequired': 'サーバー URL を入力してください。',
'mobile.connect.error.invalidUrl': 'そのサーバー URL は無効です。',
'mobile.connect.error.unreachable': 'その OpenChamber サーバーに接続できませんでした。',
@@ -254,6 +254,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': '이 기기',
'settings.remoteInstances.clientAuth.lastUsed': '마지막 사용 {date}',
'settings.remoteInstances.clientAuth.neverUsed': '사용한 적 없음',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': '포트를 열지 않고도 다른 기기가 어디서든 연결할 수 있습니다. 트래픽은 종단 간 암호화되어 릴레이는 내용을 읽을 수 없습니다.',
'settings.remoteInstances.relay.enableHint': '이 서버에서 릴레이를 켜기 전까지는 아무것도 공유되지 않습니다.',
'settings.remoteInstances.relay.actions.enable': '릴레이 켜기',
'settings.remoteInstances.relay.actions.disable': '끄기',
'settings.remoteInstances.relay.confirm.disable': '릴레이를 끄시겠습니까? 릴레이를 통해 연결된 기기는 즉시 연결이 끊어집니다.',
'settings.remoteInstances.relay.state.loading': '릴레이 상태 확인 중...',
'settings.remoteInstances.relay.state.disabled': '꺼짐',
'settings.remoteInstances.relay.state.connecting': '연결 중',
'settings.remoteInstances.relay.state.connected': '연결됨',
'settings.remoteInstances.relay.state.reconnecting': '재연결 중',
'settings.remoteInstances.relay.state.error': '오류',
'settings.remoteInstances.relay.status.clientsOne': '기기 {count}대 연결됨',
'settings.remoteInstances.relay.status.clientsMany': '기기 {count}대 연결됨',
'settings.remoteInstances.relay.pair.title': '기기 페어링',
'settings.remoteInstances.relay.pair.labelPlaceholder': '기기 이름 (선택 사항)',
'settings.remoteInstances.relay.pair.includeToken': '액세스 토큰 포함 (한 번 스캔으로 페어링)',
'settings.remoteInstances.relay.pair.noTokenHint': '토큰이 없으면 기기는 연결 후 이 서버의 UI 비밀번호로 로그인합니다.',
'settings.remoteInstances.relay.pair.generate': '페어링 링크 만들기',
'settings.remoteInstances.relay.pair.requiresConnected': '페어링은 릴레이가 연결된 후 사용할 수 있습니다.',
'settings.remoteInstances.relay.pair.linkLabel': '페어링 링크',
'settings.remoteInstances.relay.pair.warning': '이 링크는 이 서버에 대한 접근 권한을 부여합니다. 공유하지 마세요.',
'settings.remoteInstances.relay.pair.qrAlt': '릴레이 페어링 QR 코드',
'settings.remoteInstances.relay.pair.showQr': 'QR 코드 표시',
'settings.remoteInstances.relay.pair.qrDialogTitle': '스캔하여 페어링',
'settings.remoteInstances.relay.pair.qrDialogDescription': '다른 기기의 OpenChamber 앱으로 이 QR 코드를 스캔하세요.',
'settings.remoteInstances.relay.pair.manageHint': '페어링된 기기는 위의 “이 서버에 연결” 목록에서 관리하거나 철회할 수 있습니다.',
'settings.remoteInstances.relay.toast.enableFailed': '릴레이를 켤 수 없습니다',
'settings.remoteInstances.relay.toast.disableFailed': '릴레이를 끔 수 없습니다',
'settings.remoteInstances.relay.toast.offerFailed': '페어링 링크를 만들지 못했습니다',
'settings.remoteInstances.relay.toast.linkCopied': '페어링 링크를 복사했습니다',
'settings.remoteInstances.sidebar.phase.ready': '준비됨',
'settings.remoteInstances.sidebar.phase.error': '오류',
'settings.remoteInstances.sidebar.phase.reconnect': '재연결',
+1
View File
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
'mobile.connect.scan.unsupported': 'QR 스캔은 설치된 모바일 앱에서만 사용할 수 있습니다.',
'mobile.connect.saved.title': '저장된 연결',
'mobile.connect.saved.empty': '아직 저장된 연결이 없습니다.',
'mobile.connect.relay.badge': 'OpenChamber Relay 경유',
'mobile.connect.error.urlRequired': '서버 URL을 입력하세요.',
'mobile.connect.error.invalidUrl': '유효하지 않은 서버 URL입니다.',
'mobile.connect.error.unreachable': '해당 OpenChamber 서버에 연결할 수 없습니다.',
@@ -1477,6 +1477,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': 'To urządzenie',
'settings.remoteInstances.clientAuth.lastUsed': 'Ostatnio użyto {date}',
'settings.remoteInstances.clientAuth.neverUsed': 'Nigdy nie użyto',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': 'Pozwól swoim innym urządzeniom łączyć się z dowolnego miejsca bez otwierania portów. Ruch jest szyfrowany od końca do końca — relay nie może go odczytać.',
'settings.remoteInstances.relay.enableHint': 'Nic nie jest udostępniane, dopóki nie włączysz relay na tym serwerze.',
'settings.remoteInstances.relay.actions.enable': 'Włącz Relay',
'settings.remoteInstances.relay.actions.disable': 'Wyłącz',
'settings.remoteInstances.relay.confirm.disable': 'Wyłączyć relay? Urządzenia połączone przez niego zostaną natychmiast rozłączone.',
'settings.remoteInstances.relay.state.loading': 'Sprawdzanie stanu relay...',
'settings.remoteInstances.relay.state.disabled': 'Wyłączony',
'settings.remoteInstances.relay.state.connecting': 'Łączenie',
'settings.remoteInstances.relay.state.connected': 'Połączono',
'settings.remoteInstances.relay.state.reconnecting': 'Ponowne łączenie',
'settings.remoteInstances.relay.state.error': 'Błąd',
'settings.remoteInstances.relay.status.clientsOne': '{count} urządzenie połączone',
'settings.remoteInstances.relay.status.clientsMany': 'Połączone urządzenia: {count}',
'settings.remoteInstances.relay.pair.title': 'Sparuj urządzenie',
'settings.remoteInstances.relay.pair.labelPlaceholder': 'Nazwa urządzenia (opcjonalnie)',
'settings.remoteInstances.relay.pair.includeToken': 'Dołącz token dostępu (parowanie jednym skanem)',
'settings.remoteInstances.relay.pair.noTokenHint': 'Bez tokenu urządzenie zaloguje się po połączeniu hasłem interfejsu tego serwera.',
'settings.remoteInstances.relay.pair.generate': 'Utwórz link parowania',
'settings.remoteInstances.relay.pair.requiresConnected': 'Parowanie będzie dostępne, gdy relay zostanie połączony.',
'settings.remoteInstances.relay.pair.linkLabel': 'Link parowania',
'settings.remoteInstances.relay.pair.warning': 'Ten link daje dostęp do tego serwera. Nie udostępniaj go.',
'settings.remoteInstances.relay.pair.qrAlt': 'Kod QR parowania relay',
'settings.remoteInstances.relay.pair.showQr': 'Pokaż kod QR',
'settings.remoteInstances.relay.pair.qrDialogTitle': 'Zeskanuj, aby sparować',
'settings.remoteInstances.relay.pair.qrDialogDescription': 'Zeskanuj ten kod QR aplikacją OpenChamber na drugim urządzeniu.',
'settings.remoteInstances.relay.pair.manageHint': 'Zarządzaj sparowanymi urządzeniami lub odbieraj im dostęp na liście „Połączenie z tym serwerem” powyżej.',
'settings.remoteInstances.relay.toast.enableFailed': 'Nie udało się włączyć relay',
'settings.remoteInstances.relay.toast.disableFailed': 'Nie udało się wyłączyć relay',
'settings.remoteInstances.relay.toast.offerFailed': 'Nie udało się utworzyć linku parowania',
'settings.remoteInstances.relay.toast.linkCopied': 'Skopiowano link parowania',
'settings.remoteInstances.sidebar.toast.retriedWithRandomPort': 'Ponowiono próbę z losowym lokalnym portem',
'settings.remoteInstances.sidebar.toast.retryFailed': 'Nie udało się ponowić połączenia',
'settings.remoteInstances.sidebar.total': 'Suma: {count}',
+1
View File
@@ -60,6 +60,7 @@ export const dict: Record<I18nKey, string> = {
'mobile.connect.scan.unsupported': 'Skanowanie QR jest dostępne tylko w zainstalowanej aplikacji mobilnej.',
'mobile.connect.saved.title': 'Zapisane połączenia',
'mobile.connect.saved.empty': 'Brak zapisanych połączeń.',
'mobile.connect.relay.badge': 'przez OpenChamber Relay',
'mobile.connect.error.urlRequired': 'Podaj adres URL serwera.',
'mobile.connect.error.invalidUrl': 'Ten adres URL serwera jest nieprawidłowy.',
'mobile.connect.error.unreachable': 'Nie udało się połączyć z tym serwerem OpenChamber.',
@@ -254,6 +254,37 @@ export const settingsDict = {
"settings.remoteInstances.clientAuth.state.thisDevice": "Este dispositivo",
"settings.remoteInstances.clientAuth.lastUsed": "Último uso em {date}",
"settings.remoteInstances.clientAuth.neverUsed": "Nunca usado",
"settings.remoteInstances.relay.title": "OpenChamber Relay",
"settings.remoteInstances.relay.description": "Permita que seus outros dispositivos se conectem de qualquer lugar sem abrir portas. O tráfego é criptografado de ponta a ponta — o relay não consegue lê-lo.",
"settings.remoteInstances.relay.enableHint": "Nada é compartilhado até você ativar o relay neste servidor.",
"settings.remoteInstances.relay.actions.enable": "Ativar Relay",
"settings.remoteInstances.relay.actions.disable": "Desativar",
"settings.remoteInstances.relay.confirm.disable": "Desativar o relay? Os dispositivos conectados por ele serão desconectados imediatamente.",
"settings.remoteInstances.relay.state.loading": "Verificando o status do relay...",
"settings.remoteInstances.relay.state.disabled": "Desativado",
"settings.remoteInstances.relay.state.connecting": "Conectando",
"settings.remoteInstances.relay.state.connected": "Conectado",
"settings.remoteInstances.relay.state.reconnecting": "Reconectando",
"settings.remoteInstances.relay.state.error": "Erro",
"settings.remoteInstances.relay.status.clientsOne": "{count} dispositivo conectado",
"settings.remoteInstances.relay.status.clientsMany": "{count} dispositivos conectados",
"settings.remoteInstances.relay.pair.title": "Parear um dispositivo",
"settings.remoteInstances.relay.pair.labelPlaceholder": "Nome do dispositivo (opcional)",
"settings.remoteInstances.relay.pair.includeToken": "Incluir token de acesso (pareamento com um único escaneamento)",
"settings.remoteInstances.relay.pair.noTokenHint": "Sem token, o dispositivo faz login com a senha da interface deste servidor após conectar.",
"settings.remoteInstances.relay.pair.generate": "Criar link de pareamento",
"settings.remoteInstances.relay.pair.requiresConnected": "O pareamento fica disponível quando o relay estiver conectado.",
"settings.remoteInstances.relay.pair.linkLabel": "Link de pareamento",
"settings.remoteInstances.relay.pair.warning": "Este link concede acesso a este servidor. Não o compartilhe.",
"settings.remoteInstances.relay.pair.qrAlt": "Código QR de pareamento do relay",
"settings.remoteInstances.relay.pair.showQr": "Mostrar código QR",
"settings.remoteInstances.relay.pair.qrDialogTitle": "Escanear para parear",
"settings.remoteInstances.relay.pair.qrDialogDescription": "Escaneie este código QR com o app OpenChamber no seu outro dispositivo.",
"settings.remoteInstances.relay.pair.manageHint": "Gerencie ou revogue dispositivos pareados na lista “Conectar a este servidor” acima.",
"settings.remoteInstances.relay.toast.enableFailed": "Falha ao ativar o relay",
"settings.remoteInstances.relay.toast.disableFailed": "Falha ao desativar o relay",
"settings.remoteInstances.relay.toast.offerFailed": "Falha ao criar o link de pareamento",
"settings.remoteInstances.relay.toast.linkCopied": "Link de pareamento copiado",
"settings.remoteInstances.sidebar.phase.ready": "Pronto",
"settings.remoteInstances.sidebar.phase.error": "Erro",
"settings.remoteInstances.sidebar.phase.reconnect": "Reconectar",
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
"mobile.connect.scan.unsupported": "A leitura de QR só está disponível no app móvel instalado.",
"mobile.connect.saved.title": "Conexões salvas",
"mobile.connect.saved.empty": "Nenhuma conexão salva ainda.",
"mobile.connect.relay.badge": "via OpenChamber Relay",
"mobile.connect.error.urlRequired": "Informe a URL de um servidor.",
"mobile.connect.error.invalidUrl": "Essa URL de servidor não é válida.",
"mobile.connect.error.unreachable": "Não foi possível acessar esse servidor OpenChamber.",
@@ -254,6 +254,37 @@ export const settingsDict = {
"settings.remoteInstances.clientAuth.state.thisDevice": "Цей пристрій",
"settings.remoteInstances.clientAuth.lastUsed": "Останнє використання {date}",
"settings.remoteInstances.clientAuth.neverUsed": "Ще не використовувався",
"settings.remoteInstances.relay.title": "OpenChamber Relay",
"settings.remoteInstances.relay.description": "Дозволяє вашим іншим пристроям підключатися звідки завгодно без відкриття портів. Трафік шифрується наскрізно — релей не може його прочитати.",
"settings.remoteInstances.relay.enableHint": "Нічого не передається, доки ви не увімкнете релей на цьому сервері.",
"settings.remoteInstances.relay.actions.enable": "Увімкнути Relay",
"settings.remoteInstances.relay.actions.disable": "Вимкнути",
"settings.remoteInstances.relay.confirm.disable": "Вимкнути релей? Пристрої, підключені через нього, будуть негайно відключені.",
"settings.remoteInstances.relay.state.loading": "Перевірка стану релею...",
"settings.remoteInstances.relay.state.disabled": "Вимкнено",
"settings.remoteInstances.relay.state.connecting": "Підключення",
"settings.remoteInstances.relay.state.connected": "Підключено",
"settings.remoteInstances.relay.state.reconnecting": "Повторне підключення",
"settings.remoteInstances.relay.state.error": "Помилка",
"settings.remoteInstances.relay.status.clientsOne": "Підключено пристроїв: {count}",
"settings.remoteInstances.relay.status.clientsMany": "Підключено пристроїв: {count}",
"settings.remoteInstances.relay.pair.title": "Спарувати пристрій",
"settings.remoteInstances.relay.pair.labelPlaceholder": "Назва пристрою (необов'язково)",
"settings.remoteInstances.relay.pair.includeToken": "Додати токен доступу (спарювання одним скануванням)",
"settings.remoteInstances.relay.pair.noTokenHint": "Без токена пристрій після підключення входить за паролем інтерфейсу цього сервера.",
"settings.remoteInstances.relay.pair.generate": "Створити посилання для спарювання",
"settings.remoteInstances.relay.pair.requiresConnected": "Спарювання стане доступним після підключення релею.",
"settings.remoteInstances.relay.pair.linkLabel": "Посилання для спарювання",
"settings.remoteInstances.relay.pair.warning": "Це посилання надає доступ до цього сервера. Не діліться ним.",
"settings.remoteInstances.relay.pair.qrAlt": "QR-код спарювання релею",
"settings.remoteInstances.relay.pair.showQr": "Показати QR-код",
"settings.remoteInstances.relay.pair.qrDialogTitle": "Скануйте для спарювання",
"settings.remoteInstances.relay.pair.qrDialogDescription": "Відскануйте цей QR-код застосунком OpenChamber на іншому пристрої.",
"settings.remoteInstances.relay.pair.manageHint": "Керуйте спареними пристроями або відкликайте їх у списку «Підключення до цього сервера» вище.",
"settings.remoteInstances.relay.toast.enableFailed": "Не вдалося увімкнути релей",
"settings.remoteInstances.relay.toast.disableFailed": "Не вдалося вимкнути релей",
"settings.remoteInstances.relay.toast.offerFailed": "Не вдалося створити посилання для спарювання",
"settings.remoteInstances.relay.toast.linkCopied": "Посилання для спарювання скопійовано",
"settings.remoteInstances.sidebar.phase.ready": "Готово",
"settings.remoteInstances.sidebar.phase.error": "Помилка",
"settings.remoteInstances.sidebar.phase.reconnect": "Повторне підключення",
+1
View File
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
"mobile.connect.scan.unsupported": "Сканування QR доступне лише у встановленій мобільній апці.",
"mobile.connect.saved.title": "Збережені підключення",
"mobile.connect.saved.empty": "Збережених підключень ще немає.",
"mobile.connect.relay.badge": "через OpenChamber Relay",
"mobile.connect.error.urlRequired": "Введи адресу сервера.",
"mobile.connect.error.invalidUrl": "Ця адреса сервера некоректна.",
"mobile.connect.error.unreachable": "Не вдалося достукатись до цього OpenChamber сервера.",
@@ -254,6 +254,37 @@ export const settingsDict = {
'settings.remoteInstances.clientAuth.state.thisDevice': '此设备',
'settings.remoteInstances.clientAuth.lastUsed': '上次使用 {date}',
'settings.remoteInstances.clientAuth.neverUsed': '从未使用',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': '无需开放端口,即可让你的其他设备从任何地方连接。流量端到端加密,中继无法读取内容。',
'settings.remoteInstances.relay.enableHint': '在此服务器上启用中继之前,不会共享任何内容。',
'settings.remoteInstances.relay.actions.enable': '启用中继',
'settings.remoteInstances.relay.actions.disable': '停用',
'settings.remoteInstances.relay.confirm.disable': '停用中继?通过它连接的设备将立即断开。',
'settings.remoteInstances.relay.state.loading': '正在检查中继状态...',
'settings.remoteInstances.relay.state.disabled': '已停用',
'settings.remoteInstances.relay.state.connecting': '连接中',
'settings.remoteInstances.relay.state.connected': '已连接',
'settings.remoteInstances.relay.state.reconnecting': '重新连接中',
'settings.remoteInstances.relay.state.error': '错误',
'settings.remoteInstances.relay.status.clientsOne': '已连接 {count} 台设备',
'settings.remoteInstances.relay.status.clientsMany': '已连接 {count} 台设备',
'settings.remoteInstances.relay.pair.title': '配对设备',
'settings.remoteInstances.relay.pair.labelPlaceholder': '设备名称(可选)',
'settings.remoteInstances.relay.pair.includeToken': '包含访问令牌(扫一次即完成配对)',
'settings.remoteInstances.relay.pair.noTokenHint': '不包含令牌时,设备连接后需使用此服务器的界面密码登录。',
'settings.remoteInstances.relay.pair.generate': '创建配对链接',
'settings.remoteInstances.relay.pair.requiresConnected': '中继连接后即可配对。',
'settings.remoteInstances.relay.pair.linkLabel': '配对链接',
'settings.remoteInstances.relay.pair.warning': '此链接可访问此服务器,请勿分享。',
'settings.remoteInstances.relay.pair.qrAlt': '中继配对二维码',
'settings.remoteInstances.relay.pair.showQr': '显示二维码',
'settings.remoteInstances.relay.pair.qrDialogTitle': '扫描配对',
'settings.remoteInstances.relay.pair.qrDialogDescription': '用其他设备上的 OpenChamber 应用扫描此二维码。',
'settings.remoteInstances.relay.pair.manageHint': '可在上方“连接到此服务器”列表中管理或吊销已配对的设备。',
'settings.remoteInstances.relay.toast.enableFailed': '无法启用中继',
'settings.remoteInstances.relay.toast.disableFailed': '无法停用中继',
'settings.remoteInstances.relay.toast.offerFailed': '无法创建配对链接',
'settings.remoteInstances.relay.toast.linkCopied': '已复制配对链接',
'settings.remoteInstances.sidebar.phase.ready': '就绪',
'settings.remoteInstances.sidebar.phase.error': '错误',
'settings.remoteInstances.sidebar.phase.reconnect': '重连',
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
'mobile.connect.scan.unsupported': '二维码扫描仅在已安装的移动应用中可用。',
'mobile.connect.saved.title': '已保存的连接',
'mobile.connect.saved.empty': '暂无已保存的连接。',
'mobile.connect.relay.badge': '通过 OpenChamber Relay 连接',
'mobile.connect.error.urlRequired': '请输入服务器 URL。',
'mobile.connect.error.invalidUrl': '该服务器 URL 无效。',
'mobile.connect.error.unreachable': '无法连接到该 OpenChamber 服务器。',
@@ -260,6 +260,37 @@
'settings.remoteInstances.clientAuth.state.thisDevice': '此裝置',
'settings.remoteInstances.clientAuth.lastUsed': '上次使用:{date}',
'settings.remoteInstances.clientAuth.neverUsed': '從未使用',
'settings.remoteInstances.relay.title': 'OpenChamber Relay',
'settings.remoteInstances.relay.description': '無需開放連接埠,即可讓你的其他裝置從任何地方連線。流量端對端加密,中繼無法讀取內容。',
'settings.remoteInstances.relay.enableHint': '在此伺服器上啟用中繼之前,不會共享任何內容。',
'settings.remoteInstances.relay.actions.enable': '啟用中繼',
'settings.remoteInstances.relay.actions.disable': '停用',
'settings.remoteInstances.relay.confirm.disable': '停用中繼?透過它連線的裝置將立即中斷連線。',
'settings.remoteInstances.relay.state.loading': '正在檢查中繼狀態...',
'settings.remoteInstances.relay.state.disabled': '已停用',
'settings.remoteInstances.relay.state.connecting': '連線中',
'settings.remoteInstances.relay.state.connected': '已連線',
'settings.remoteInstances.relay.state.reconnecting': '重新連線中',
'settings.remoteInstances.relay.state.error': '錯誤',
'settings.remoteInstances.relay.status.clientsOne': '已連線 {count} 台裝置',
'settings.remoteInstances.relay.status.clientsMany': '已連線 {count} 台裝置',
'settings.remoteInstances.relay.pair.title': '配對裝置',
'settings.remoteInstances.relay.pair.labelPlaceholder': '裝置名稱(選填)',
'settings.remoteInstances.relay.pair.includeToken': '包含存取權杖(掃一次即完成配對)',
'settings.remoteInstances.relay.pair.noTokenHint': '不包含權杖時,裝置連線後需使用此伺服器的介面密碼登入。',
'settings.remoteInstances.relay.pair.generate': '建立配對連結',
'settings.remoteInstances.relay.pair.requiresConnected': '中繼連線後即可配對。',
'settings.remoteInstances.relay.pair.linkLabel': '配對連結',
'settings.remoteInstances.relay.pair.warning': '此連結可存取此伺服器,請勿分享。',
'settings.remoteInstances.relay.pair.qrAlt': '中繼配對 QR 碼',
'settings.remoteInstances.relay.pair.showQr': '顯示 QR 碼',
'settings.remoteInstances.relay.pair.qrDialogTitle': '掃描配對',
'settings.remoteInstances.relay.pair.qrDialogDescription': '用另一台裝置上的 OpenChamber 應用程式掃描此 QR 碼。',
'settings.remoteInstances.relay.pair.manageHint': '可在上方「連線到此伺服器」清單中管理或撤銷已配對的裝置。',
'settings.remoteInstances.relay.toast.enableFailed': '無法啟用中繼',
'settings.remoteInstances.relay.toast.disableFailed': '無法停用中繼',
'settings.remoteInstances.relay.toast.offerFailed': '無法建立配對連結',
'settings.remoteInstances.relay.toast.linkCopied': '已複製配對連結',
'settings.remoteInstances.page.section.instance': '執行個體',
'settings.remoteInstances.page.section.instanceDescription': '核心 SSH 設定。',
'settings.remoteInstances.page.field.mode': '模式',
@@ -59,6 +59,7 @@ export const dict: Record<I18nKey, string> = {
'mobile.connect.scan.unsupported': 'QR code 掃描僅在已安裝的行動應用程式中可用。',
'mobile.connect.saved.title': '已儲存的連線',
'mobile.connect.saved.empty': '尚未儲存任何連線。',
'mobile.connect.relay.badge': '透過 OpenChamber Relay 連線',
'mobile.connect.error.urlRequired': '請輸入伺服器網址。',
'mobile.connect.error.invalidUrl': '該伺服器網址無效。',
'mobile.connect.error.unreachable': '無法連線至該 OpenChamber 伺服器。',