feat(channels): rename channels, and quieter defaults for sound and streams
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Four owner requests in one pass. Rename: the PATCH /api/channels/:id route already accepted a name and a topic, and the API client already exposed it — the only thing missing was a way to reach it. The channel settings Overview tab now edits both, gated on MANAGE_CHANNELS like the server is, with Save/Discard appearing only when the form is dirty. The server's lowercase-and-hyphenate rule moved to @backspace/shared so the field can show what a name will actually become instead of letting it change shape on save; one copy of the rule, used by both sides. Stream volume: someone else's screen share now starts at 50 instead of 100 (DEFAULT_STREAM_VOLUME). The publisher boosts system audio before sending, so the old default arrived far too loud for a viewer who never touched the slider. Only the default moves — the slider still covers the full range and a value the user set is still persisted. Sound effects: SFX_BASE_VOLUME 0.8 -> 0.4, halving every cue. The .ogg files were left alone; they already peak below full scale, so the harshness was level, not recorded distortion. Every SFX playback path goes through getSfxVolume(), so this is the single knob. Translations: channel settings (including the override editor's permission labels), the friends page end to end — tabs, lists, requests, the add tab and discovery cards — the invite modal including its per-invite failure reasons, and the group-DM member actions. Failure reasons are carried as translation keys rather than finished text, so results already on screen re-label when the language changes. Verified: web build (tsc + vite), server typecheck, 490 web tests, 804 server tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -227,6 +227,142 @@ export const en = {
|
||||
'profile.activity.watching': 'Watching',
|
||||
'profile.activity.streaming': 'Streaming',
|
||||
'profile.activity.elapsed': '{time} elapsed',
|
||||
// Channel settings
|
||||
'channel.settings.title': 'Channel Settings',
|
||||
'channel.tab.overview': 'Overview',
|
||||
'channel.tab.permissions': 'Permissions',
|
||||
'channel.name.label': 'Channel Name',
|
||||
'channel.name.placeholder': 'new-channel',
|
||||
'channel.name.willBecome': 'Will be saved as {name}',
|
||||
'channel.name.empty': 'The name cannot be empty.',
|
||||
'channel.topic.label': 'Topic',
|
||||
'channel.topic.placeholder': "What's this channel about?",
|
||||
'channel.save': 'Save Changes',
|
||||
'channel.saving': 'Saving...',
|
||||
'channel.saved': 'Saved',
|
||||
'channel.discard': 'Discard',
|
||||
'channel.saveFailed': 'Could not save the channel.',
|
||||
'channel.private.title': 'Private Channel',
|
||||
'channel.private.description': 'Only selected members and roles will be able to view this channel.',
|
||||
'channel.private.note': 'This channel is hidden from members without explicit access. Users with the Administrator permission or space owners can always see all channels.',
|
||||
'channel.private.loadFailed': 'Failed to load channel overrides',
|
||||
'channel.private.updateFailed': 'Failed to update channel privacy',
|
||||
'channel.danger': 'Danger Zone',
|
||||
'channel.delete': 'Delete Channel',
|
||||
'channel.delete.title': 'Delete #{name}?',
|
||||
'channel.delete.body': 'This will permanently delete #{name} and all of its messages.',
|
||||
'channel.delete.voiceNote': 'Any users currently in this voice channel will be disconnected.',
|
||||
'channel.delete.irreversible': 'This action cannot be undone.',
|
||||
'channel.delete.failed': 'Failed to delete channel',
|
||||
|
||||
// Channel permissions — override editor labels
|
||||
'channel.perm.viewChannel': 'View Channel',
|
||||
'channel.perm.sendMessages': 'Send Messages',
|
||||
'channel.perm.manageMessages': 'Manage Messages',
|
||||
'channel.perm.attachFiles': 'Attach Files',
|
||||
'channel.perm.readHistory': 'Read Message History',
|
||||
'channel.perm.addReactions': 'Add Reactions',
|
||||
'channel.perm.connect': 'Connect',
|
||||
'channel.perm.speak': 'Speak',
|
||||
'channel.perm.stream': 'Stream',
|
||||
'channel.perm.muteMembers': 'Mute Members',
|
||||
'channel.perm.deafenMembers': 'Deafen Members',
|
||||
'channel.perm.moveMembers': 'Move Members',
|
||||
'channel.perm.disconnectMembers': 'Disconnect Members',
|
||||
// Friends — tabs and lists
|
||||
'friends.title': 'Friends',
|
||||
'friends.tab.online': 'Online',
|
||||
'friends.tab.all': 'All',
|
||||
'friends.tab.pending': 'Pending',
|
||||
'friends.tab.add': 'Add Friend',
|
||||
'friends.tab.activity': 'Activity',
|
||||
'friends.status.online': 'Online',
|
||||
'friends.status.idle': 'Idle',
|
||||
'friends.status.dnd': 'Do Not Disturb',
|
||||
'friends.status.offline': 'Offline',
|
||||
'friends.heading.online': 'Online — {count}',
|
||||
'friends.heading.all': 'All Friends — {count}',
|
||||
'friends.heading.pending': 'Pending — {count}',
|
||||
'friends.heading.active': 'Active — {count}',
|
||||
'friends.heading.offline': 'Offline — {count}',
|
||||
'friends.empty.online': "No one's online right now.",
|
||||
'friends.empty.all': 'No friends yet — add someone!',
|
||||
'friends.empty.pending': 'No pending requests — Nori is napping.',
|
||||
'friends.empty.activity': "It's quiet for now... When friends start an activity, we'll show it here!",
|
||||
'friends.request.incoming': 'Incoming Friend Request',
|
||||
'friends.request.outgoing': 'Outgoing Friend Request',
|
||||
'friends.action.message': 'Message',
|
||||
'friends.action.accept': 'Accept',
|
||||
'friends.action.decline': 'Decline',
|
||||
'friends.action.cancel': 'Cancel Request',
|
||||
'friends.action.remove': 'Remove Friend',
|
||||
'friends.remove.title': 'Remove Friend',
|
||||
'friends.remove.body': 'Are you sure you want to remove {name} as a friend? You can always send them a new friend request later.',
|
||||
'friends.remove.someone': 'this user',
|
||||
'friends.remove.confirm': 'Remove',
|
||||
|
||||
// Friends — add tab
|
||||
'friends.add.title': 'Find People',
|
||||
'friends.add.hint': 'Search by username or use {handle} to add someone directly.',
|
||||
'friends.add.searchPlaceholder': 'Search or add by username...',
|
||||
'friends.add.directTo': 'Send friend request to',
|
||||
'friends.add.send': 'Send Request',
|
||||
'friends.add.sending': 'Sending...',
|
||||
'friends.add.sent': 'Friend request sent!',
|
||||
'friends.add.discover': 'Discover People',
|
||||
'friends.add.empty.search': 'No users match your search.',
|
||||
'friends.add.empty.discover': 'No discoverable users yet — invite people to join!',
|
||||
'friends.add.sendRequest': 'Send Friend Request',
|
||||
'friends.add.cancelling': 'Cancelling...',
|
||||
'friends.add.pending': 'Request Pending',
|
||||
'friends.add.failed': 'Failed to send request',
|
||||
'friends.accept.failed': 'Failed to accept request',
|
||||
'friends.decline.failed': 'Failed to decline request',
|
||||
'friends.cancel.failed': 'Failed to cancel request',
|
||||
|
||||
// DM roster — group members
|
||||
'dm.remove.title': 'Remove from Group',
|
||||
'dm.remove.body': "Remove {name} from this group? They won't be able to see new messages.",
|
||||
'dm.remove.confirm': 'Remove',
|
||||
'dm.remove.failed': 'Failed to remove member',
|
||||
'dm.transfer.title': 'Transfer Ownership',
|
||||
'dm.transfer.body': "Transfer ownership to {name}? You'll lose owner privileges.",
|
||||
'dm.transfer.confirm': 'Transfer',
|
||||
'dm.transfer.failed': 'Failed to transfer ownership',
|
||||
'dm.removeFriend.failed': 'Failed to remove friend',
|
||||
|
||||
// Invite modal
|
||||
'invite.title': 'Invite Friends',
|
||||
'invite.subtitle': 'Send to friends, or share a link.',
|
||||
'invite.requestOnly': 'This space uses join requests — people join by requesting approval from a manager, so it has no invite link to share.',
|
||||
'invite.gotIt': 'Got it',
|
||||
'invite.searchPlaceholder': 'Search friends...',
|
||||
'invite.remove': 'Remove {name}',
|
||||
'invite.alreadyInSpace': 'Already in space',
|
||||
'invite.noMatch': 'No friends match your search',
|
||||
'invite.noFriends': 'No friends yet',
|
||||
'invite.select': 'Select Friends',
|
||||
'invite.send': 'Send {count} Invite',
|
||||
'invite.sendPlural': 'Send {count} Invites',
|
||||
'invite.sending': 'Sending...',
|
||||
'invite.sent': 'Sent',
|
||||
'invite.retryFailed': 'Retry failed',
|
||||
'invite.done': 'Done',
|
||||
'invite.orLink': 'Or share a link',
|
||||
'invite.generating': 'Generating...',
|
||||
'invite.copy': 'Copy',
|
||||
'invite.copied': 'Copied!',
|
||||
'invite.linkFailed': 'Failed to generate invite link',
|
||||
'invite.fail.invite_invalid': 'Invite link no longer valid',
|
||||
'invite.fail.not_a_friend': 'Not a friend',
|
||||
'invite.fail.user_not_found': 'User not found',
|
||||
'invite.fail.already_member': 'Already a member',
|
||||
'invite.fail.upstream': "Couldn't verify invite — try again",
|
||||
'invite.fail.cannot_invite_self': 'Cannot invite yourself',
|
||||
'invite.fail.invalid_body': 'Invalid request',
|
||||
'invite.fail.invalid_target': 'Invalid target',
|
||||
'invite.fail.network': "Couldn't reach your instance",
|
||||
'invite.fail.generic': "Couldn't send (server error)",
|
||||
} as const;
|
||||
|
||||
export type TranslationKey = keyof typeof en;
|
||||
|
||||
Reference in New Issue
Block a user