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
@@ -226,4 +226,140 @@ export const ptBR: Partial<Dictionary> = {
'profile.activity.watching': 'Assistindo',
'profile.activity.streaming': 'Transmitindo',
'profile.activity.elapsed': '{time} decorrido',
// Configurações do canal
'channel.settings.title': 'Configurações do canal',
'channel.tab.overview': 'Visão geral',
'channel.tab.permissions': 'Permissões',
'channel.name.label': 'Nome do canal',
'channel.name.placeholder': 'novo-canal',
'channel.name.willBecome': 'Será salvo como {name}',
'channel.name.empty': 'O nome não pode ficar vazio.',
'channel.topic.label': 'Tópico',
'channel.topic.placeholder': 'Sobre o que é este canal?',
'channel.save': 'Salvar alterações',
'channel.saving': 'Salvando...',
'channel.saved': 'Salvo',
'channel.discard': 'Descartar',
'channel.saveFailed': 'Não foi possível salvar o canal.',
'channel.private.title': 'Canal privado',
'channel.private.description': 'Só os membros e cargos escolhidos vão conseguir ver este canal.',
'channel.private.note': 'Este canal fica escondido de quem não tem acesso explícito. Quem tem a permissão de Administrador e os donos do servidor sempre veem todos os canais.',
'channel.private.loadFailed': 'Não foi possível carregar as permissões do canal',
'channel.private.updateFailed': 'Não foi possível alterar a privacidade do canal',
'channel.danger': 'Zona de perigo',
'channel.delete': 'Excluir canal',
'channel.delete.title': 'Excluir #{name}?',
'channel.delete.body': 'Isto apaga o #{name} e todas as mensagens dele para sempre.',
'channel.delete.voiceNote': 'Quem estiver neste canal de voz agora será desconectado.',
'channel.delete.irreversible': 'Não dá para desfazer.',
'channel.delete.failed': 'Não foi possível excluir o canal',
// Permissões do canal — rótulos do editor de exceções
'channel.perm.viewChannel': 'Ver canal',
'channel.perm.sendMessages': 'Enviar mensagens',
'channel.perm.manageMessages': 'Gerenciar mensagens',
'channel.perm.attachFiles': 'Anexar arquivos',
'channel.perm.readHistory': 'Ler histórico de mensagens',
'channel.perm.addReactions': 'Adicionar reações',
'channel.perm.connect': 'Conectar',
'channel.perm.speak': 'Falar',
'channel.perm.stream': 'Transmitir',
'channel.perm.muteMembers': 'Silenciar membros',
'channel.perm.deafenMembers': 'Ensurdecer membros',
'channel.perm.moveMembers': 'Mover membros',
'channel.perm.disconnectMembers': 'Desconectar membros',
// Amigos — abas e listas
'friends.title': 'Amigos',
'friends.tab.online': 'Online',
'friends.tab.all': 'Todos',
'friends.tab.pending': 'Pendentes',
'friends.tab.add': 'Adicionar amigo',
'friends.tab.activity': 'Atividade',
'friends.status.online': 'Online',
'friends.status.idle': 'Ausente',
'friends.status.dnd': 'Não perturbe',
'friends.status.offline': 'Offline',
'friends.heading.online': 'Online — {count}',
'friends.heading.all': 'Todos os amigos — {count}',
'friends.heading.pending': 'Pendentes — {count}',
'friends.heading.active': 'Ativos — {count}',
'friends.heading.offline': 'Offline — {count}',
'friends.empty.online': 'Ninguém online agora.',
'friends.empty.all': 'Nenhum amigo ainda — chama alguém!',
'friends.empty.pending': 'Nenhum pedido pendente — o Nori está tirando um cochilo.',
'friends.empty.activity': 'Tudo quieto por aqui... Quando um amigo começar alguma coisa, aparece aqui!',
'friends.request.incoming': 'Pedido de amizade recebido',
'friends.request.outgoing': 'Pedido de amizade enviado',
'friends.action.message': 'Mensagem',
'friends.action.accept': 'Aceitar',
'friends.action.decline': 'Recusar',
'friends.action.cancel': 'Cancelar pedido',
'friends.action.remove': 'Remover amigo',
'friends.remove.title': 'Remover amigo',
'friends.remove.body': 'Quer mesmo remover {name} dos seus amigos? Dá para mandar um novo pedido depois.',
'friends.remove.someone': 'esta pessoa',
'friends.remove.confirm': 'Remover',
// Amigos — aba de adicionar
'friends.add.title': 'Encontrar pessoas',
'friends.add.hint': 'Busque pelo nome de usuário ou use {handle} para adicionar alguém direto.',
'friends.add.searchPlaceholder': 'Buscar ou adicionar por nome de usuário...',
'friends.add.directTo': 'Enviar pedido de amizade para',
'friends.add.send': 'Enviar pedido',
'friends.add.sending': 'Enviando...',
'friends.add.sent': 'Pedido de amizade enviado!',
'friends.add.discover': 'Descobrir pessoas',
'friends.add.empty.search': 'Nenhum usuário encontrado.',
'friends.add.empty.discover': 'Ninguém para descobrir ainda — chame o pessoal!',
'friends.add.sendRequest': 'Enviar pedido de amizade',
'friends.add.cancelling': 'Cancelando...',
'friends.add.pending': 'Pedido pendente',
'friends.add.failed': 'Não foi possível enviar o pedido',
'friends.accept.failed': 'Não foi possível aceitar o pedido',
'friends.decline.failed': 'Não foi possível recusar o pedido',
'friends.cancel.failed': 'Não foi possível cancelar o pedido',
// Lista de membros da DM em grupo
'dm.remove.title': 'Remover do grupo',
'dm.remove.body': 'Remover {name} deste grupo? A pessoa não vai mais ver as mensagens novas.',
'dm.remove.confirm': 'Remover',
'dm.remove.failed': 'Não foi possível remover o membro',
'dm.transfer.title': 'Transferir propriedade',
'dm.transfer.body': 'Transferir a propriedade para {name}? Você perde os privilégios de dono.',
'dm.transfer.confirm': 'Transferir',
'dm.transfer.failed': 'Não foi possível transferir a propriedade',
'dm.removeFriend.failed': 'Não foi possível remover o amigo',
// Modal de convite
'invite.title': 'Convidar amigos',
'invite.subtitle': 'Envie para amigos ou compartilhe um link.',
'invite.requestOnly': 'Este servidor usa pedidos de entrada — as pessoas entram pedindo aprovação a um moderador, então não há link de convite para compartilhar.',
'invite.gotIt': 'Entendi',
'invite.searchPlaceholder': 'Buscar amigos...',
'invite.remove': 'Remover {name}',
'invite.alreadyInSpace': 'Já está no servidor',
'invite.noMatch': 'Nenhum amigo encontrado',
'invite.noFriends': 'Nenhum amigo ainda',
'invite.select': 'Escolha os amigos',
'invite.send': 'Enviar {count} convite',
'invite.sendPlural': 'Enviar {count} convites',
'invite.sending': 'Enviando...',
'invite.sent': 'Enviado',
'invite.retryFailed': 'Tentar de novo',
'invite.done': 'Pronto',
'invite.orLink': 'Ou compartilhe um link',
'invite.generating': 'Gerando...',
'invite.copy': 'Copiar',
'invite.copied': 'Copiado!',
'invite.linkFailed': 'Não foi possível gerar o link de convite',
'invite.fail.invite_invalid': 'O link de convite não vale mais',
'invite.fail.not_a_friend': 'Não é seu amigo',
'invite.fail.user_not_found': 'Usuário não encontrado',
'invite.fail.already_member': 'Já é membro',
'invite.fail.upstream': 'Não deu para verificar o convite — tente de novo',
'invite.fail.cannot_invite_self': 'Você não pode se convidar',
'invite.fail.invalid_body': 'Pedido inválido',
'invite.fail.invalid_target': 'Destino inválido',
'invite.fail.network': 'Não deu para falar com a sua instância',
'invite.fail.generic': 'Não deu para enviar (erro no servidor)',
};