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

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:
2026-09-02 11:59:25 -03:00
co-authored by Claude Opus 5
parent 293378293d
commit 860f02b2c1
14 changed files with 672 additions and 157 deletions
+136
View File
@@ -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;