feat(i18n): translate the five systems users actually see
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

Ordered by what the group hits, not by string count.

  - Chat system messages ("X added Y to the group") and their sidebar
    preview twin. These are content, not chrome, so they sat in English in
    the middle of conversations.
  - Login, register and the invite landing page — the first screen anyone
    invited to the instance sees.
  - Voice — controls, connection info, the mobile join sheet, settings tab.
  - Mobile screens.
  - Settings navigation across space, user and instance, plus Discovery and
    join requests.

Three call sites build strings outside a component and so read the locale
directly rather than through the hook: the sidebar preview formatter, the
connection-info track labels, and MobileShell's screen map — which holds
plain functions that are *called*, not rendered, where a hook would run
under whichever component invoked the map. Each says so at the definition;
all three re-render often enough that a language switch shows through
promptly.

Adds a test for a mistake made three times in this series: a file with an
exported component plus a private sub-component, where only the exported one
got the hook. tsc catches it, but only after a full build and without naming
the component. The test names it, and also flags the inverse — a hook placed
below an early return, which tsc accepts and React breaks at runtime (this
project shipped that once, as error #310). Verified by mutation: removing a
hook makes it fail and print the component.

Dictionary is at 856 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 492 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 21:13:54 -03:00
co-authored by Claude Opus 5
parent 94f5da4013
commit 699855849d
20 changed files with 825 additions and 252 deletions
+215
View File
@@ -803,4 +803,219 @@ export const ptBR: Partial<Dictionary> = {
'instance.general.reset': 'Desfazer',
'instance.general.saved': 'Configurações salvas',
'instance.general.saveFailed': 'Não foi possível salvar',
// Mensagens de sistema — linha na conversa e prévia na barra lateral
'system.unknown': 'Desconhecido',
'system.someone': 'Alguém',
'system.aMember': 'Um membro',
'system.memberAdded': '{actor} adicionou {target} ao grupo',
'system.memberLeft': '{target} saiu do grupo',
'system.memberRemoved': '{actor} removeu {target} do grupo',
'system.ownerChanged': '{owner} agora é dono do grupo',
'system.nameChanged': '{actor} mudou o nome do grupo para "{name}"',
'system.nameCleared': '{actor} tirou o nome do grupo',
'system.iconChanged': '{actor} trocou o ícone do grupo',
'system.preview.inviteTo': 'Convite enviado para {space}',
'system.preview.invite': 'Convite de servidor enviado',
'system.preview.memberAdded': '{actor} adicionou {target}',
'system.preview.memberRemoved': '{actor} removeu {target}',
'system.preview.nameChanged': '{actor} mudou o nome do grupo',
'system.preview.generic': 'Mensagem do sistema',
// Login, registro e a página de convite
'auth.username': 'Nome de usuário',
'auth.password': 'Senha',
'auth.username.required': 'O nome de usuário é obrigatório',
'auth.password.required': 'A senha é obrigatória',
'auth.login.title': 'Que bom te ver!',
'auth.login.subtitle': 'A gente estava com saudade!',
'auth.login.submit': 'Entrar',
'auth.login.loggingIn': 'Entrando...',
'auth.login.failed': 'Não foi possível entrar',
'auth.login.rateLimited': 'Tentativas de login demais',
'auth.retryIn': 'Tente de novo em {seconds}s',
'auth.needAccount': 'Ainda não tem conta?',
'auth.register': 'Criar conta',
'auth.haveAccount': 'Já tem conta?',
'auth.register.title': 'Criar uma conta',
'auth.register.confirmPassword': 'Confirmar a senha',
'auth.register.username.length': 'O nome de usuário precisa ter de 3 a 32 caracteres',
'auth.register.username.charset': 'O nome de usuário só aceita letras minúsculas, números e sublinhado',
'auth.register.username.checking': 'Verificando se está livre...',
'auth.register.username.available': 'Nome de usuário disponível',
'auth.register.username.taken': 'Este nome de usuário já está em uso',
'auth.register.password.length': 'A senha precisa ter pelo menos 6 caracteres',
'auth.register.password.mismatch': 'As senhas não batem',
'auth.register.failed': 'Não foi possível criar a conta',
'auth.register.rateLimited': 'Tentativas demais',
'auth.register.continue': 'Continuar',
'auth.register.invite': 'Código ou link de convite',
'auth.register.invite.checking': 'Verificando...',
'auth.register.invite.validating': 'Validando o convite...',
'auth.register.invite.expired': 'Este link de convite expirou. Peça um novo ao administrador.',
'auth.register.invite.exhausted': 'Este convite atingiu o limite de usos. Peça ao administrador para aumentar.',
'auth.register.invite.invalid': 'Código de convite inválido.',
'auth.register.invite.invalidLink': 'Link de convite inválido — peça um novo',
'auth.register.profile.title': 'Deixe com a sua cara',
'auth.register.profile.subtitle': 'Personalize o perfil, ou faça isso depois',
'auth.register.displayName': 'Nome de exibição',
'auth.register.displayName.placeholder': 'Nome de exibição',
'auth.register.avatarColor': 'Cor da foto',
'auth.register.submit': 'Começar',
'auth.register.creating': 'Criando a conta...',
'auth.register.back': 'Voltar',
'auth.register.skip': 'Deixar para depois',
'auth.register.cropAvatar': 'Recortar a foto',
'join.page.loading': 'Carregando o convite...',
'join.page.invalid.title': 'Convite inválido',
'join.page.invalid.body': 'Este link de convite é inválido ou expirou.',
'join.page.invitedTo': 'Você foi convidado para',
'join.page.noCode': 'Nenhum código de convite informado',
'join.page.invalidCode': 'Código de convite inválido',
'join.page.loadFailed': 'Não foi possível carregar o convite',
'join.page.joinFailed': 'Não foi possível entrar no servidor',
'join.page.connectFailed': 'Não foi possível conectar',
'join.page.loginFailed': 'Não foi possível entrar',
'join.page.instance.placeholder': 'ex.: minha-instancia.com',
'join.page.alreadyMember': 'Você já está em {space}!',
'join.page.goTo': 'Ir para {space}',
'join.page.redirecting': 'Redirecionando...',
// Voz — controles, informações de conexão e a folha de entrada no celular
'voice.title': 'Voz e Vídeo',
'voice.dmCall': 'Chamada de DM',
'voice.channel': 'Canal de voz',
'voice.connected': 'Voz conectada',
'voice.connecting': 'Conectando...',
'voice.failed': 'Falha na conexão',
'voice.goToCall': 'Ir para a call',
'voice.disconnect': 'Desconectar',
'voice.camera.on': 'Ligar a câmera',
'voice.camera.off': 'Desligar a câmera',
'voice.share.start': 'Compartilhar tela',
'voice.share.stop': 'Parar de compartilhar',
'voice.soundboard': 'Soundboard',
'voice.videoQuality': 'Qualidade do vídeo',
'voice.noise.enable': 'Ligar a supressão de ruído por IA',
'voice.noise.disable': 'Desligar a supressão de ruído por IA',
'voice.info.title': 'Informações da conexão',
'voice.info.notConnected': 'Desconectado',
'voice.info.gathering': 'Coletando dados...',
'voice.info.network': 'Rede',
'voice.info.ping': 'Ping',
'voice.info.packetLoss': 'Perda de pacotes',
'voice.info.jitter': 'Jitter',
'voice.info.server': 'Servidor',
'voice.info.protocol': 'Protocolo',
'voice.info.audio': 'Áudio',
'voice.info.video': 'Vídeo',
'voice.info.microphone': 'Microfone',
'voice.info.camera': 'Câmera',
'voice.info.screen': 'Tela',
'voice.info.screenAudio': 'Áudio da tela',
'voice.info.unknown': 'Desconhecido',
'voice.info.remote': 'Remoto',
'voice.join.empty': 'Ninguém neste canal ainda.',
'voice.join.beFirst': 'Seja o primeiro a entrar!',
'voice.join.mute': 'Desligar o microfone',
'voice.join.unmute': 'Ligar o microfone',
'voice.join.switch': 'Trocar de canal',
'voice.join.join': 'Entrar na voz',
'voice.join.close': 'Fechar',
'voice.join.selectedCamera': 'Câmera escolhida',
'voice.join.enablePreview': 'Ligar a prévia da câmera',
'voice.join.checkingCamera': 'Verificando a câmera…',
'voice.join.tapToPreview': 'Toque para ver a câmera',
'voice.join.permissionDenied': 'Permissão de câmera negada.',
'voice.settings.volume': 'Volume',
'voice.settings.sfxVolume': 'Volume dos efeitos sonoros',
'voice.settings.messageSound': 'Tocar som em toda mensagem',
'voice.settings.messageSound.description': 'Desligado (padrão): só DMs e mensagens que mencionam você. Ligado: todos os canais.',
'voice.settings.processing': 'Processamento de voz',
'voice.settings.noise': 'Supressão de ruído por IA',
'voice.settings.noise.description': 'Remoção de ruído por aprendizado de máquina (RNNoise) — filtra teclado, ventilador e barulho de fundo',
'voice.settings.echo': 'Cancelamento de eco',
'voice.settings.echo.description': 'Cancela o eco do seu som voltando pelo microfone. Sempre ligado em canais de voz e chamadas.',
'voice.settings.agc': 'Controle automático de ganho',
'voice.settings.agc.description': 'Ajusta o volume do microfone sozinho — pode abaixar a sua voz durante transmissões',
// Navegação das configurações (servidor, usuário e instância)
'nav.general': 'Geral',
'nav.management': 'Gerenciamento',
'nav.overview': 'Visão geral',
'nav.discovery': 'Descoberta',
'nav.members': 'Membros',
'nav.roles': 'Cargos',
'nav.bans': 'Banimentos',
'nav.userSettings': 'Configurações do usuário',
'nav.appSettings': 'Configurações do app',
'nav.administration': 'Administração',
'nav.instance': 'Instância',
'nav.registration': 'Registro',
'nav.federation': 'Federação',
'nav.streaming': 'Transmissão',
'nav.storage': 'Armazenamento',
'nav.users': 'Usuários',
'nav.back': 'Voltar ao menu de configurações do servidor',
'space.discovery.title': 'Descoberta',
'space.discovery.visibility': 'Visibilidade',
'space.discovery.visibility.description': 'Escolha quem pode descobrir e entrar neste servidor.',
'space.discovery.description': 'Descrição',
'space.discovery.description.hint': 'Um resumo curto que aparece na página do Explorar.',
'space.discovery.description.placeholder': 'Uma descrição curta para o Explorar...',
'space.requests.loading': 'Carregando...',
'space.requests.empty': 'Nenhum pedido de entrada pendente',
'space.requests.accept': 'Aceitar',
'space.requests.decline': 'Recusar',
'space.requests.failed': 'A ação falhou',
// Telas do celular
'mobile.messages': 'Mensagens',
'mobile.noConversations': 'Nenhuma conversa ainda.',
'mobile.loadingChannels': 'Carregando canais',
'mobile.noChannels': 'Nenhum canal ainda.',
'mobile.groupInfo': 'Informações do grupo',
'mobile.info': 'Informações',
'mobile.cancelEdit': 'Cancelar a edição',
'mobile.removeGroupIcon': 'Remover o ícone do grupo',
'mobile.federatedGroup': 'Grupo federado',
'mobile.invitePeople': 'Convidar pessoas',
'mobile.inviteCopied': 'Link de convite copiado',
'mobile.inviteFailed': 'Não foi possível gerar o convite',
'mobile.createFolder': 'Criar pasta',
'mobile.folderCreated': 'Pasta criada',
'mobile.moveToFolder': 'Mover para pasta',
'mobile.movedToFolder': 'Movido para a pasta',
'mobile.removeFromFolder': 'Tirar da pasta',
'mobile.removedFromFolder': 'Tirado da pasta',
'mobile.folderUngrouped': 'Pasta desfeita',
'mobile.unnamedFolder': 'Pasta sem nome',
'mobile.leaveSpace': 'Sair do servidor',
'mobile.leave': 'Sair',
'mobile.leftSpace': 'Você saiu do servidor',
'mobile.leaveSpaceFailed': 'Não foi possível sair do servidor',
'mobile.muteUser': 'Silenciar usuário',
'mobile.channelSettings': 'Configurações do canal',
'mobile.deleteChannel': 'Excluir canal',
'mobile.channelDeleted': 'Canal excluído',
'mobile.categorySettings': 'Configurações da categoria',
'mobile.deleteCategory': 'Excluir categoria',
'mobile.categoryDeleted': 'Categoria excluída',
'mobile.delete': 'Excluir',
'mobile.createSpace': 'Criar servidor',
'mobile.joinSpace': 'Entrar num servidor',
'mobile.exploreSpaces': 'Explorar servidores',
'mobile.leaveSpace.confirm': 'Quer mesmo sair deste servidor? Você vai precisar de um novo convite para voltar.',
'mobile.deleteChannel.confirm': 'Tem certeza? Isto apaga o canal e todas as mensagens dele para sempre.',
'mobile.deleteCategory.confirm': 'Tem certeza? Os canais desta categoria vão para o nível de cima.',
'mobile.deleteChannelFailed': 'Não foi possível excluir o canal',
'mobile.voice.title': 'Chamada de voz',
'mobile.voice.collapse': 'Minimizar a chamada',
'mobile.voice.members': 'Ver membros',
'mobile.voice.unmute': 'Ligar o microfone',
'mobile.voice.deafen': 'Desligar o som',
'mobile.voice.undeafen': 'Ligar o som',
'mobile.voice.cameraOn': 'Ligar a câmera',
'mobile.voice.cameraOff': 'Desligar a câmera',
'mobile.voice.switchCamera': 'Trocar de câmera',
'mobile.voice.selectCamera': 'Escolher a câmera',
'mobile.voice.shareScreen': 'Compartilhar a tela',
'mobile.voice.stopSharing': 'Parar de compartilhar',
'mobile.voice.disconnect': 'Sair da chamada',
};