Files
chanora/apps/chanora_flutter/lib/l10n/app_en.arb
T
EdisonJwa b8e9c8549e feat(ui,ios,android): audio output route picker + collapse voice controls into single modal
Two user-reported issues addressed:

  1. 'on supported devices such as iphone or android user should be
     able to select audio device such as speaker or airpods or phone'
  2. 'the bottom folder is duplicated with the app bar settings'

Issue 1 \u2014 audio output route picker:

Added the audio_router 1.1.1 plugin (MIT, supports iOS + Android)
which renders the platform-native picker:

  * iOS: Apple AVRoutePickerView system sheet \u2014 the same UI as
    Control Center's audio chooser. Lists Speaker / iPhone receiver /
    AirPods / connected Bluetooth devices / AirPlay / CarPlay.
    System manages the device list; we don't have to track route
    changes manually.

  * Android (post-rc.8 when we wire the platform): Material Design 3
    dialog backed by AudioManager.setCommunicationDevice() with
    SCO Bluetooth + USB headsets filtered for VoIP.

The picker prerequisite documented by the plugin (audio session
must be playAndRecord/voiceChat before the picker fires) is already
satisfied by our AppDelegate.swift configuration from commit 0466000.

The new _AudioOutputTile widget in voice_compact.dart subscribes to
AudioRouter.currentDeviceStream so the row label + icon auto-update
when the user plugs in headphones, connects AirPods, etc. \u2014 no
manual KVO observation needed.

Tile is mobile-only (Platform.isIOS || Platform.isAndroid). Desktop
hosts continue to use the system mixer; the tile is hidden.

Plugin caveat: the published enum AudioSourceType has no .usb
variant (despite README mentioning USB support). We map only the
seven real enum cases: builtinSpeaker / builtinReceiver / bluetooth /
wiredHeadset / carAudio / airplay / unknown.

Issue 2 \u2014 collapse voice controls into the single modal sheet:

The AppBar gear icon (Icons.tune) that opened VoiceSettingsDialog
was removed. It duplicated the configuration entry point that the
status chip \u2192 modal-sheet path already provides, and the user found
that duplication confusing on a phone-narrow screen where AppBar
real estate is precious.

The voice modal sheet (showVoiceDetailsSheet) is now the **single**
voice-controls surface on mobile, with layout (top to bottom):

  1. Audio output route picker tile (iOS / Android only) \u2014 new.
  2. Mode + bind / release-tail recap (display only).
  3. 'Adjust mode & release tail' OutlinedButton that closes the
     sheet and opens the same VoiceSettingsDialog the gear icon
     used to open. One config form, not two.
  4. Mic level meter.
  5. TX / RX frame counts + mic state.
  6. PTT capability badge (desktop only).

Sheet title renamed from 'Voice settings' (which collided with the
gear-icon tooltip) to 'Voice'. New l10n keys: voiceSheetTitle,
voiceAdjustSettings, audioOutputLabel, audioRoute{Speaker,Receiver,
Bluetooth,WiredHeadset,CarAudio,Airplay,Unknown}. en + zh translated.

Build: flutter build ios --release --no-codesign clean, 50.5 s,
Runner.app 30.2 MB (+200 KB from audio_router). flutter analyze
clean (6 pre-existing Radio.groupValue deprecation infos in
voice_settings.dart, unchanged).
2026-05-16 18:18:43 +08:00

156 lines
6.8 KiB
Plaintext

{
"@@locale": "en",
"@@x-source-of-truth": "DEC-015 (register v0.9.5). Template ARB for English. Server-provided content is NOT translated (ADR-008 / DEC-015).",
"appTitle": "Chanora",
"homeNotProductionReadyBanner": "Beta build — voice in/out wired; not production ready.",
"fieldServerHost": "Server address",
"fieldNickname": "Nickname",
"fieldServerPassword": "Server password",
"fieldServerPasswordHelp": "Leave blank if the server does not require one.",
"fieldPassword": "Password",
"fieldDisplayName": "Display name",
"connectAction": "Connect",
"disconnectAction": "Disconnect",
"refreshAction": "Refresh",
"diagnosticsAction": "Diagnostics",
"aboutAction": "About",
"aboutVersion": "Version {version}",
"@aboutVersion": {
"placeholders": { "version": { "type": "String" } }
},
"aboutNonAffiliation": "Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.",
"aboutLicenseHeading": "License",
"aboutLicenseBody": "Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.",
"aboutThirdPartyHeading": "Third-party software",
"aboutThirdPartyBody": "Chanora is built on tsclientlib, flutter_rust_bridge, cpal, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.",
"copyAction": "Copy",
"closeAction": "Close",
"startAudioAction": "Start audio",
"pttHoldToTalk": "Hold to talk",
"pttTransmitting": "Transmitting…",
"pttHoldToTalkSemanticsHint": "Press and hold to transmit voice; release to stop.",
"pttCapabilityBadge": "PTT: {level} ({backend})",
"@pttCapabilityBadge": {
"placeholders": {
"level": { "type": "String" },
"backend": { "type": "String" }
}
},
"pttCapabilityBoundKey": "Key: {key}",
"@pttCapabilityBoundKey": {
"placeholders": {
"key": { "type": "String" }
}
},
"pttCapabilityExplainTitle": "Push-to-Talk capability",
"pttCapabilityExplainFocusedHeading": "Focused PTT",
"pttCapabilityExplainFocusedBody": "Chanora is currently using Focused Push-to-Talk: the binding only fires while the Chanora window is focused. This is the universal fallback used on every platform when a global capture path is not available.",
"pttCapabilityExplainGoGlobalWindows": "On Windows, Global PTT is engaged automatically once you bind a key. No additional permission is required.",
"pttCapabilityExplainGoGlobalMacos": "On macOS, Global PTT requires Input Monitoring permission. Open System Settings → Privacy & Security → Input Monitoring, allow Chanora, then re-bind the key.",
"pttCapabilityExplainGoGlobalLinux": "On Linux, Global PTT requires a GNOME-Wayland desktop with the GlobalShortcuts portal. Re-bind the key and accept the desktop's shortcut dialog when it appears.",
"pttCapabilityExplainGoGlobalGeneric": "Global PTT is not available in this environment. Focused PTT will keep working while the Chanora window has focus.",
"pttConfigureAction": "Configure",
"pttConfigureTitle": "Configure Push-to-Talk binding",
"pttConfigurePrompt": "Press the key or mouse side button you want to use for Push-to-Talk.",
"pttConfigureWaiting": "(waiting for input…)",
"pttConfigureCaptured": "Captured",
"pttConfigurePrivacyNote": "Chanora never logs the actual key value. Only the input class (keyboard / mouse-side-button) and a platform-neutral label leave this dialog.",
"pttConfigureSaveAction": "Save",
"pttConfigurePortalRedirect": "Your desktop environment will open its own shortcut dialog. Pick the key you want to use for Push-to-Talk.",
"inputMuteAction": "Mute mic",
"inputUnmuteAction": "Unmute mic",
"outputMuteAction": "Mute speaker",
"outputUnmuteAction": "Unmute speaker",
"joinChannelAction": "Join channel",
"channelPasswordTitle": "Channel password",
"bookmarksHeading": "Bookmarks",
"bookmarksEmpty": "No bookmarks yet. Enter a server above and tap \"Save bookmark\".",
"bookmarkAddAction": "Save bookmark",
"bookmarkAddTitle": "Save bookmark",
"bookmarkDeleteAction": "Delete bookmark",
"statusIdle": "Not connected",
"statusConnecting": "Connecting…",
"statusConnected": "Connected to {server}",
"@statusConnected": {
"placeholders": { "server": { "type": "String" } }
},
"statusError": "Error: {message}",
"@statusError": {
"placeholders": { "message": { "type": "String" } }
},
"statusReconnecting": "Reconnecting… attempt {attempt}, in {delay}s",
"@statusReconnecting": {
"placeholders": {
"attempt": { "type": "int" },
"delay": { "type": "int" }
}
},
"statusConnectionLost": "Connection lost: {reason}",
"@statusConnectionLost": {
"placeholders": { "reason": { "type": "String" } }
},
"audioStatsLine": "TX {sent} frames • RX {received} frames • Mic {ptt}",
"@audioStatsLine": {
"placeholders": {
"sent": { "type": "int" },
"received": { "type": "int" },
"ptt": { "type": "String" }
}
},
"channelsHeading": "Channels",
"clientsHeading": "Online clients",
"countChannelsAndClients": "{channels} channels • {clients} online",
"@countChannelsAndClients": {
"placeholders": {
"channels": { "type": "int" },
"clients": { "type": "int" }
}
},
"voiceModePtt": "PTT",
"voiceModeContinuous": "Continuous",
"voiceModeVoiceActivity": "Voice activity",
"voiceModeComingSoon": "Coming soon",
"voiceModeLabel": "Transmit mode",
"voiceReleaseTailLabel": "Release tail",
"voiceReleaseTailHint": "ms",
"voiceLeaveAction": "Leave voice",
"voiceHardMuteLabel": "Mute microphone",
"voiceOutputMuteLabel": "Mute speakers",
"voiceSettingsTitle": "Voice settings",
"voiceBindKeyAction": "Bind PTT key",
"voicePttHoldHint": "Hold the button",
"voiceMicOn": "on",
"voiceMicOff": "off",
"voiceSheetTitle": "Voice",
"voiceAdjustSettings": "Adjust mode & release tail",
"audioOutputLabel": "Audio output",
"audioRouteSpeaker": "Speaker",
"audioRouteReceiver": "iPhone receiver",
"audioRouteBluetooth": "Bluetooth",
"audioRouteWiredHeadset": "Wired headset",
"audioRouteCarAudio": "Car audio",
"audioRouteAirplay": "AirPlay",
"audioRouteUnknown": "Unknown",
"channelJoinFailedPermission": "Insufficient permission to join this channel.",
"channelJoinFailedPassword": "Wrong channel password.",
"channelJoinFailedFull": "Channel is full.",
"channelJoinFailedFamilyFull": "Channel family limit reached.",
"channelJoinFailedPrivate": "This channel is private.",
"channelJoinFailedTimeout": "Could not join channel: the server didn't respond in time.",
"channelJoinFailedGeneric": "Could not join channel: {message}",
"@channelJoinFailedGeneric": {
"placeholders": {
"message": { "type": "String" }
}
}
}