aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
commit3b2d03a919e56d14f8097844b97b844ddf526cd6 (patch)
tree31b222fedf6d608809eb47c45c5f431d7001d87a /packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
parent75065cc1ed5d9cd733871d478dc4f915663d795c (diff)
downloadmeshbay-3b2d03a919e56d14f8097844b97b844ddf526cd6.tar.gz
feat(hub): translate the web client into nine more languages
French, Spanish, Brazilian Portuguese, Simplified Chinese, Japanese, German, Italian, Dutch and Polish, all in the formal register. `hub`, `node` and `GEK` stay in English: they name the CLI, node.toml and the docs, and translating them would cut the interface off from everything an operator reads and types. Catalogues move out of i18n.js into locales/, one file per language, fetched with a dynamic import. A visitor downloads their language plus English as a fallback — about 36 KB rather than the ~180 KB that ten inlined catalogues would have cost everyone. i18n.js keeps only the loader, so the first render now waits for initLocale(). Three things the old code got wrong, none of them visible until there was a second language: - Resolution trimmed a tag to its base before matching, so a browser reporting pt-BR looked for a `pt` catalogue that does not exist and fell back to English. Matching is now exact first, then by base language. - Counted strings were single strings, so Polish could not express 1 plik / 2 pliki / 5 plików at all. t() selects through Intl.PluralRules; en.js gains the same treatment, which incidentally fixes "1 files". - Interpolation used String.replace, which reads `$&` in the replacement. A file named rap$&sody.mp3 rendered corrupted in its own delete dialog. Five strings were still hardcoded in app.js — the group name placeholder and the four visibility/join-policy descriptions — and are now keyed. test_locales.py holds the nine translations to the shape of en.js: same keys, a counted string stays counted everywhere, every plural entry covers each category Intl actually produces for that language, and the {placeholders} survive translation. Verified failing first, against a catalogue with a key removed, a placeholder dropped and the Polish `few` form deleted. The language menu also grew from one entry to ten, which overran a short viewport inside a dropdown that clipped instead of scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js403
1 files changed, 403 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
new file mode 100644
index 0000000..9d00ece
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -0,0 +1,403 @@
+/**
+ * Brazilian Portuguese — translated from locales/en.js.
+ *
+ * Register: formal. In Brazilian Portuguese that is "você" with polite phrasing,
+ * not "o senhor / a senhora" — the latter reads as stiff in software and is
+ * reserved for addressing someone markedly senior.
+ * `hub`, `node` and `GEK` are left in English: they name the CLI
+ * (`meshbay-node`), node.toml and the docs, which are English throughout.
+ */
+
+export default {
+ // Nav
+ 'nav.toggle_menu': 'Exibir o menu',
+ 'nav.light_mode': 'Tema claro',
+ 'nav.dark_mode': 'Tema escuro',
+ 'nav.logout': 'Sair',
+ 'nav.login': 'Entrar',
+
+ // Sidebar
+ 'sidebar.my_groups': 'Meus grupos',
+ 'sidebar.no_groups': 'Nenhum grupo ainda',
+ 'sidebar.discover': 'Descobrir',
+ 'sidebar.public_groups': 'Grupos públicos',
+
+ // Login
+ 'login.title': 'Entrar',
+ 'login.username': 'Nome de usuário',
+ 'login.password': 'Senha',
+ 'login.submit': 'Entrar',
+ 'login.loading': 'Entrando...',
+ 'login.no_account': 'Ainda não tem conta?',
+ 'login.register_link': 'Criar uma conta',
+
+ // Register
+ 'register.title': 'Criar uma conta',
+ 'register.username': 'Nome de usuário',
+ 'register.email': 'E-mail',
+ 'register.password': 'Frase secreta (mínimo de 12 caracteres)',
+ 'register.confirm': 'Confirmar a senha',
+ 'register.submit': 'Criar a conta',
+ 'register.loading': 'Criando a conta...',
+ 'register.has_account': 'Já tem uma conta?',
+ 'register.login_link': 'Entrar',
+ 'register.success_title': 'Conta criada',
+ 'register.success_msg': 'Agora você pode entrar com suas credenciais.',
+ 'register.go_login': 'Ir para o login',
+ 'register.err_mismatch': 'As senhas não coincidem',
+ 'register.err_min_len': {
+ one: 'Use pelo menos {n} caractere',
+ other: 'Use pelo menos {n} caracteres',
+ },
+ 'register.err_too_weak': 'Fácil demais de adivinhar. Sua frase secreta é o que '
+ + 'protege suas chaves onde elas ficam guardadas — algumas palavras sem relação '
+ + 'entre si funcionam muito bem.',
+ 'register.strength': 'Robustez: cerca de {bits} bits. Ela protege a cópia das suas '
+ + 'chaves mantida nos nodes dos quais você participa, então vale a pena caprichar.',
+
+ // Home
+ 'home.welcome': 'Bem-vindo ao MeshBay',
+ 'home.no_groups': 'Você ainda não participa de nenhum grupo.',
+ 'home.browse_prefix': 'Explore os ',
+ 'home.browse_link': 'grupos públicos',
+ 'home.browse_suffix': ' ou peça a um administrador de grupo que o convide.',
+ 'home.my_groups': 'Meus grupos',
+
+ // Explore
+ 'explore.title': 'Grupos públicos',
+ 'explore.search': 'Pesquisar grupos...',
+ 'explore.loading': 'Carregando...',
+ 'explore.empty': 'Nenhum grupo público disponível.',
+ 'explore.create_group': 'Criar um grupo',
+ 'explore.join': 'Participar',
+ 'explore.member': 'Participando',
+
+ // Group page
+ 'group.default_name': 'Grupo',
+ 'group.tab_files': 'Arquivos',
+ 'group.tab_chat': 'Conversa',
+ 'group.tab_members': 'Membros',
+ 'group.filter': 'Filtrar arquivos...',
+ 'group.col_name': 'Nome',
+ 'group.col_size': 'Tamanho',
+ 'group.col_type': 'Tipo',
+ 'group.col_date': 'Data',
+ 'group.empty_filter': 'Nenhum arquivo corresponde ao filtro',
+ 'group.empty_dir': 'Esta pasta está vazia',
+ 'group.download': 'Baixar',
+ 'group.play': 'Reproduzir',
+ 'group.offline_title': 'Nenhum node está on-line para este grupo no momento.',
+ 'group.offline_hint': 'Os arquivos aparecerão quando um node que hospeda este grupo '
+ + 'se conectar.',
+ 'group.upload': 'Enviar',
+ 'group.mkdir': 'Nova pasta',
+ 'group.mkdir_prompt': 'Nome da nova pasta:',
+ 'group.view': 'Visualizar',
+ 'group.delete': 'Excluir',
+ 'group.delete_confirm': 'Excluir {name}?',
+ 'group.delete_group': 'Excluir o grupo',
+ 'group.delete_group_confirm': 'Excluir "{name}" definitivamente e remover todos os '
+ + 'membros? Os arquivos que estão no node são preservados.',
+ 'group.err_transport': 'Módulo de transporte não carregado',
+
+ // Status
+ 'status.idle': 'Ocioso',
+ 'status.discovering': 'Procurando nodes...',
+ 'status.connecting': 'Conectando via WebRTC...',
+ 'status.fetching': 'Obtendo o índice...',
+ 'status.files': {
+ one: '{n} arquivo',
+ other: '{n} arquivos',
+ },
+ 'status.offline': 'Nenhum node on-line',
+ 'status.error': 'Falha na conexão',
+
+ // Chat
+ 'chat.empty': 'Ainda não há mensagens. Comece a conversa!',
+ 'chat.placeholder': 'Escreva uma mensagem...',
+ 'chat.send': 'Enviar',
+ 'chat.attach': 'Anexar arquivo',
+
+ // Video player
+ 'video.loading': 'Carregando {name}...',
+ 'video.buffering': 'Armazenando em buffer...',
+ 'video.close': 'Fechar (Esc)',
+ 'preview.pdf_fallback': 'Este navegador não exibirá o PDF na própria página. Baixe '
+ + 'o arquivo — de todo modo ele foi descriptografado aqui.',
+ 'group.upload_indexing': 'indexando…',
+ 'video.err_transport': 'Transporte não conectado',
+ 'video.err_mse': 'Codec sem suporte para transmissão: {codec}',
+
+ // Settings
+ 'settings.title': 'Configurações',
+ 'settings.coming_soon': 'Em breve.',
+ 'settings.downloads': 'Downloads',
+ 'settings.dl_auto': 'Salvar automaticamente',
+ 'settings.dl_auto_hint': 'Os arquivos vão direto para a pasta escolhida abaixo, sem '
+ + 'nenhuma caixa de diálogo. Baixar vinte arquivos coloca vinte arquivos lá.',
+ 'settings.dl_ask': 'Perguntar sempre',
+ 'settings.dl_ask_hint': 'Uma caixa "Salvar como" por arquivo — uma para cada '
+ + 'arquivo, inclusive quando você baixa uma seleção.',
+ 'settings.dl_folder': 'Pasta: {name}',
+ 'settings.dl_no_folder': 'Nenhuma pasta escolhida — os downloads vão para onde o '
+ + 'seu navegador os colocar',
+ 'settings.dl_choose': 'Escolher pasta',
+ 'settings.dl_change': 'Alterar',
+ 'settings.dl_forget': 'Esquecer',
+ 'settings.dl_path_note': 'Não é possível informar um caminho a uma página web, '
+ + 'portanto não há nada a digitar aqui: o seu navegador concede acesso à pasta que '
+ + 'você indicar, e o MeshBay só escreve dentro dela. Talvez seja pedida uma '
+ + 'confirmação uma vez por sessão.',
+ 'settings.dl_unsupported': 'Este navegador não consegue escrever em uma pasta de sua '
+ + 'escolha (não tem a File System Access API), então os downloads vão para a pasta '
+ + 'de downloads dele. Chrome e Edge permitem escolher uma.',
+ 'settings.profile': 'Perfil',
+ 'settings.username': 'Nome de usuário',
+ 'settings.node_pins': 'Identidades dos nodes',
+ 'settings.node_pins_hint': 'A chave de identidade de cada node é memorizada na '
+ + 'primeira conexão. Se ela mudar, a conexão é recusada — o que só é esperado '
+ + 'quando um operador reinstala um node. Confirme com ele antes de limpar.',
+ 'settings.node_pins_count': {
+ one: '{n} fixada',
+ other: '{n} fixadas',
+ },
+ 'settings.node_pins_clear': 'Limpar as identidades fixadas',
+ 'settings.danger': 'Excluir a conta',
+ 'settings.delete_hint': 'Remove sua conta, suas participações em grupos e suas '
+ + 'notificações do hub, e libera seu nome de usuário. Não alcança o que está nos '
+ + 'nodes: os arquivos que você enviou permanecem onde o operador deles os guarda, e '
+ + 'cada node mantém a identidade que fixou para você até que o operador a remova. '
+ + 'Os registros de conexão são guardados por um ano, como a lei exige.',
+ 'settings.delete_account': 'Excluir minha conta',
+ 'settings.delete_confirm': 'Digite sua frase secreta para confirmar. Esta ação não '
+ + 'pode ser desfeita.',
+ 'settings.delete_confirm_btn': 'Excluir definitivamente',
+ 'settings.cancel': 'Cancelar',
+ 'settings.appearance': 'Aparência',
+ 'settings.theme': 'Tema',
+ 'settings.theme_light': 'Claro',
+ 'settings.theme_dark': 'Escuro',
+ 'settings.theme_system': 'Sistema',
+ 'settings.language': 'Idioma',
+ 'settings.about': 'Sobre',
+ 'settings.version': 'Versão',
+ 'settings.protocol': 'Protocolo',
+ 'settings.role': 'Função',
+ 'settings.groups': 'Notificações de grupo',
+ 'settings.notifications': 'Notificações',
+ 'settings.node_key': 'Vincular um node',
+ 'settings.node_key_desc': 'Se você opera um node MeshBay, cole aqui a chave pública '
+ + 'Ed25519 dele para vinculá-lo à sua conta. Essa chave está no painel de '
+ + 'administração do seu node.',
+ 'settings.node_key_placeholder': 'Cole a chave pública Ed25519 do node (base64)',
+ 'settings.node_key_submit': 'Vincular o node',
+ 'settings.node_key_success': 'Node vinculado com sucesso',
+ 'settings.node_key_current': 'Chave do node vinculado',
+
+ // Sidebar
+ 'sidebar.create_group': 'Criar um grupo',
+ 'sidebar.settings': 'Configurações',
+ 'sidebar.admin': 'Administração',
+
+ // User menu
+ 'usermenu.language': 'Idioma',
+ 'usermenu.settings': 'Configurações',
+ 'usermenu.theme_light': 'Tema claro',
+ 'usermenu.theme_dark': 'Tema escuro',
+ 'usermenu.logout': 'Sair',
+
+ // Admin panel
+ 'admin.title': 'Administração',
+ 'admin.tab_stats': 'Estatísticas',
+ 'admin.tab_users': 'Usuários',
+ 'admin.tab_groups': 'Grupos',
+ 'admin.tab_nodes': 'Nodes',
+ 'admin.tab_logs': 'Registros',
+ 'admin.col_observed_ip': 'Visto de',
+ 'admin.col_hint': 'Endereço anunciado',
+ 'admin.col_last_seen': 'Último anúncio',
+ 'admin.node_online': 'on-line',
+ 'admin.node_offline': 'off-line',
+ 'admin.no_nodes': 'Nenhum node registrado',
+ 'admin.nodes_hint': '"Visto de" é o endereço de onde o anúncio chegou, em uma '
+ + 'requisição assinada com a chave do node — é esse que serve para responder a uma '
+ + 'requisição judicial. O endereço anunciado é o que o node acredita ser o seu, '
+ + 'aprendido de um servidor STUN e enviado a nós; serve para alcançar o node e não '
+ + 'prova coisa alguma.',
+ 'admin.tab_blocklist': 'Lista de bloqueio',
+
+ // Admin stats
+ 'admin.stat_users': 'Usuários',
+ 'admin.stat_groups': 'Grupos',
+ 'admin.stat_nodes': 'Nodes',
+ 'admin.stat_online': 'Nodes on-line',
+
+ // Admin users
+ 'admin.users_search': 'Pesquisar usuários...',
+ 'admin.col_username': 'Nome de usuário',
+ 'admin.col_role': 'Função',
+ 'admin.col_status': 'Status',
+ 'admin.col_created': 'Criação',
+ 'admin.col_actions': 'Ações',
+ 'admin.col_email': 'E-mail',
+ 'admin.col_groups': 'Grupos',
+ 'admin.no_users': 'Nenhum usuário encontrado',
+ 'admin.btn_suspend': 'Suspender',
+ 'admin.btn_unsuspend': 'Reativar',
+ 'admin.btn_details': 'Detalhes',
+ 'admin.btn_delete': 'Excluir',
+ 'admin.delete_confirm': 'Excluir a conta "{user}"? Esta ação não pode ser desfeita. '
+ + 'Os arquivos que essa pessoa enviou permanecem nos nodes que os hospedam, e cada '
+ + 'node mantém a identidade que fixou até que o operador dele a remova. Suspender é '
+ + 'a opção reversível.',
+ 'admin.user_detail': 'Detalhes do usuário',
+ 'admin.btn_close': 'Fechar',
+ 'admin.self_note': '(você)',
+
+ // Admin groups
+ 'admin.col_name': 'Nome',
+ 'admin.col_visibility': 'Visibilidade',
+ 'admin.col_members': 'Membros',
+ 'admin.no_groups': 'Nenhum grupo encontrado',
+
+ // Admin logs
+ 'admin.col_time': 'Horário',
+ 'admin.col_event': 'Evento',
+ 'admin.col_user': 'Usuário',
+ 'admin.col_ip': 'IP',
+ 'admin.col_detail': 'Detalhe',
+ 'admin.filter_all': 'Todos os eventos',
+ 'admin.no_logs': 'Nenhum registro encontrado',
+ 'admin.btn_load_more': 'Carregar mais',
+
+ // Admin blocklist
+ 'admin.col_hash': 'Hash',
+ 'admin.col_reason': 'Motivo',
+ 'admin.col_date': 'Data',
+ 'admin.col_added_by': 'Adicionado por',
+ 'admin.no_blocked': 'Nenhum hash bloqueado',
+ 'admin.add_hash': 'Adicionar um hash à lista de bloqueio',
+ 'admin.hash_placeholder': 'hash blake3 (64 caracteres hexadecimais)',
+ 'admin.reason_placeholder': 'Motivo',
+ 'admin.btn_block': 'Bloquear',
+ 'admin.btn_unblock': 'Desbloquear',
+
+ // Notifications
+ // Create group
+ 'create_group.title': 'Criar um grupo',
+ 'create_group.name': 'Nome do grupo',
+ 'create_group.name_placeholder': 'ex.: Fotos da família, Arquivos do projeto...',
+ 'create_group.visibility': 'Visibilidade',
+ 'create_group.private': 'Privado',
+ 'create_group.private_desc': 'Só os membros convidados veem os arquivos e acessam a eles',
+ 'create_group.public': 'Público',
+ 'create_group.public_desc': 'Qualquer pessoa pode descobrir e explorar este grupo',
+ 'create_group.join_policy': 'Política de entrada',
+ 'create_group.invite': 'Somente por convite',
+ 'create_group.invite_desc': 'Os membros precisam ser convidados por um administrador',
+ 'create_group.open': 'Aberto (qualquer pessoa pode entrar)',
+ 'create_group.open_desc': 'Qualquer pessoa pode entrar sem aprovação',
+ 'create_group.description': 'Descrição',
+ 'create_group.description_hint': 'Sobre o que é este grupo? (opcional)',
+ 'create_group.submit': 'Criar',
+ 'create_group.hint': 'Um grupo precisa de um node para hospedar os arquivos. Você '
+ + 'pode criar o grupo agora e conectar um node depois.',
+ 'create_group.creating': 'Criando...',
+
+ // Members
+ 'members.col_role': 'Função',
+ 'members.group_role': 'Função no grupo',
+ 'members.admin': 'Administrador',
+ 'members.group_admin': 'Administrador do grupo',
+ 'members.owner': 'Proprietário',
+ 'members.member': 'Membro',
+ 'members.invite_title': 'Convidar um membro',
+ 'members.username_placeholder': 'Nome de usuário',
+ 'members.invite_btn': 'Convidar',
+ 'members.invite_needs_pairing': 'Os convites são assinados com a chave que este node '
+ + 'fixou para o seu navegador, então faça o pareamento abaixo antes de convidar '
+ + 'alguém.',
+ 'members.invite_ask_operator': 'Só o operador do node que hospeda este grupo pode '
+ + 'convidar, a partir de um navegador pareado com ele.',
+ 'members.remove': 'Remover',
+ 'members.remove_confirm': 'Remover {user} deste grupo? A conta dessa pessoa, os '
+ + 'outros grupos dela e os arquivos que ela enviou não são afetados.',
+ 'members.remove_hint': 'Remover alguém retira a participação dessa pessoa e faz o '
+ + 'node parar de entregar a ela a chave do grupo. Não exclui a conta dela, e não '
+ + 'recupera a chave que ela já tem em mãos — rotacione a chave no node '
+ + '(meshbay-node gek-init) se isso importar.',
+ 'members.pair_title': 'Parear este navegador com o seu node',
+ 'members.pair_hint': 'O seu node só aceita ações de operador — convites, exclusão de '
+ + 'arquivos — de um navegador com o qual foi pareado. Execute '
+ + '`meshbay-node operator pair` no node e digite o código aqui. O código nunca '
+ + 'passa pelo hub.',
+ 'members.pair_btn': 'Parear',
+ 'members.pair_success': 'Este navegador agora está pareado com o node.',
+ 'members.invite_code_ready': 'Código de convite para {user} — envie a ele pelo meio '
+ + 'que vocês costumam usar. Funciona uma única vez e nunca passa pelo hub.',
+ 'members.invite_code_hint': 'A pessoa digita o código na primeira vez que abrir este '
+ + 'grupo. Você não precisa estar on-line nesse momento.',
+ 'transfers.title': 'Transferências',
+ 'transfers.clear': 'Limpar as concluídas',
+ 'transfers.cancel': 'Cancelar',
+ 'transfers.open': 'Abrir',
+ 'transfers.done': 'Concluída',
+ 'transfers.cancelled': 'Cancelada',
+ 'transfers.failed': 'Falhou',
+ 'group.select': 'Selecionar',
+ 'group.select_done': 'Concluir',
+ 'group.actions': 'Ações ({n})',
+ 'group.download_n': 'Baixar ({n})',
+ 'group.download_zip_n': 'Baixar as pastas em zip ({n})',
+ 'group.delete_n': 'Excluir ({n})',
+ 'group.delete_n_confirm': {
+ one: 'Excluir {n} item? {names}',
+ other: 'Excluir {n} itens? {names}',
+ },
+ 'group.download_zip': {
+ one: 'Baixar em zip ({n} arquivo)',
+ other: 'Baixar em zip ({n} arquivos)',
+ },
+ 'group.zip_empty': 'Essa pasta não tem nada para baixar.',
+ 'group.zip_no_stream': 'Este navegador não consegue escrever um download direto no '
+ + 'disco, então {name} ({size}) precisa ser montado antes na memória. Em um arquivo '
+ + 'grande isso pode falhar. Continuar?',
+ 'group.rmdir': 'Excluir a pasta',
+ 'group.rmdir_confirm': 'Excluir a pasta "{name}"? Ela precisa estar vazia.',
+ 'group.rmdir_not_empty': 'Essa pasta não está vazia. Exclua antes o que está dentro '
+ + 'dela — nada aqui apaga arquivos que você não vê.',
+ 'group.desc_edit': 'Editar a descrição',
+ 'group.desc_add': 'Adicionar uma descrição',
+ 'group.desc_save': 'Salvar',
+ 'group.desc_cancel': 'Cancelar',
+ 'group.desc_placeholder': 'Para que serve este grupo — os membros veem isto na '
+ + 'página inicial deles.',
+ 'group.join_code_title': 'Este node precisa reconhecer você',
+ 'group.join_code_hint': 'Peça o código de uso único a quem convidou você e digite-o '
+ + 'aqui. Depois disso este navegador fica reconhecido e a pergunta não se repete.',
+ 'group.join_code_btn': 'Participar',
+
+ 'notif.purge': 'Limpar tudo',
+ 'notif.title': 'Notificações',
+ 'notif.empty': 'Nenhuma notificação',
+ 'notif.mark_all_read': 'Marcar tudo como lido',
+
+ // Sidebar
+ 'sidebar.search': 'Pesquisar arquivos',
+
+ // Status
+
+ // Search
+ 'search.title': 'Pesquisar arquivos',
+ 'search.placeholder': 'Pesquisar em todos os grupos...',
+ 'search.no_results': 'Nenhum arquivo corresponde à sua pesquisa.',
+ 'search.synced': 'sincronizado {ago}',
+ 'search.cache_note': 'A pesquisa usa o estado de cada grupo na última vez que você o '
+ + 'abriu. Abra um grupo para atualizar os arquivos dele aqui.',
+ 'search.col_group': 'Grupo',
+ 'search.result_count': {
+ one: '{n} arquivo encontrado',
+ other: '{n} arquivos encontrados',
+ },
+ 'search.hint': 'Pesquisa nomes de arquivo em todos os índices de grupo em cache.',
+};