Second extraction pass: the strings the codemod could not see

`i18n:coverage` reported 100% while a hundred-odd strings rendered
English in every language. It was not wrong about what it measured: it
reads JSX text, and none of these were JSX text. They were toast
arguments, `confirmDialog({ title, confirmLabel })` props, `title=` and
`aria-label=` attributes, and template literals — every one built from an
expression the codemod cannot read.

176 source strings and 15 plural sets now go through t() and plural(),
translated into all nine languages. Where English put a word in a slot,
the sentence is spelled out per branch instead: `Filter ${verb}` became
"Filter saved" and "Filter created", because which word agrees with what,
and where it sits, is not a property English gets to decide for everyone.
Counts that were `${n} message${n === 1 ? "" : "s"}` are plural() calls,
so Russian and Ukrainian get three forms and Japanese and Chinese get the
one they actually have.

Two of the catalogue's own conventions were worth learning the hard way.
Plural entries are keyed on the English *other* form, not `one` — `one`
is a form English happens to have and Japanese does not. And a constant
table holding English that is translated at the render site is fine: the
literal is a key, not a leak.

Which is what the new check encodes. `scripts/i18n-literals.mjs` accepts
a string that is wrapped where it is written or is a catalogue key
somewhere, and refuses one that is neither — a string no catalogue can
translate, however many languages ship. It found twenty more than my own
sweep had, including the stale-folder toast seen in production. It runs
as part of `npm run i18n:check`.

Also fixed: the catalogue is now awaited before the first paint. The
tree is rebuilt when a catalogue lands, so components recover on their
own, but a string computed in an effect does not — a toast fired in that
gap is emitted in English and stays English. The wait costs nothing
visible, since the session bootstrap already shows a spinner and English
resolves immediately.

And the Japanese agenda title loses a space Japanese does not use:
"{date} からの予定" was written with the English habit of spacing around
a placeholder.
This commit is contained in:
2026-08-31 14:14:30 -07:00
parent a4f7d386a6
commit a6863e98cc
52 changed files with 2101 additions and 191 deletions
+192
View File
@@ -860,10 +860,202 @@ export const catalog: Catalog = {
"Saved {when}": "Salvo {when}",
"Unsaved": "Não salvo",
"Agenda from {date}": "Agenda a partir de {date}",
// ── Toasts, dialogs and computed labels (second extraction pass) ───
"(no subject)": "(sem assunto)",
"(skipped: {actions})": "(ignorado: {actions})",
"Activate": "Ativar",
"Add a contact or import a vCard file.": "Adicione um contato ou importe um arquivo vCard.",
"Added to your calendar": "Adicionado à sua agenda",
"All events in this calendar will be deleted.": "Todos os eventos desta agenda serão excluídos.",
"All occurrences": "Todas as ocorrências",
"An occurrence cannot be moved to another calendar on its own": "Uma ocorrência não pode ser movida sozinha para outra agenda",
"Anything signed in with this password stops working immediately.": "Tudo o que estiver conectado com esta senha para de funcionar imediatamente.",
"App password revoked": "Senha de aplicativo revogada",
"Applies to every date in the series.": "Vale para todas as datas da série.",
"Applies to this date only.": "Vale apenas para esta data.",
"Apply to series": "Aplicar à série",
"Applying filter to existing messages…": "Aplicando o filtro às mensagens existentes…",
"Attachments are still uploading": "Os anexos ainda estão sendo enviados",
"Calendar saved": "Agenda salva",
"Categorised as {name}": "Categoria “{name}” atribuída",
"Category cleared": "Categoria removida",
"Change this event?": "Alterar este evento?",
"Choose a calendar": "Escolha uma agenda",
"Choose an address book": "Escolha um catálogo de endereços",
"Colour updated": "Cor atualizada",
"Contact created": "Contato criado",
"Contact deleted": "Contato excluído",
"Contact saved": "Contato salvo",
"Copied": "Copiado",
"Copy of {title}": "Cópia de {title}",
"Could not cancel: {error}": "Não foi possível cancelar: {error}",
"Could not copy": "Não foi possível copiar",
"Could not copy the address": "Não foi possível copiar o endereço",
"Could not empty folder: {error}": "Não foi possível esvaziar a pasta: {error}",
"Could not load source: {error}": "Não foi possível carregar o código-fonte: {error}",
"Could not mark as read: {error}": "Não foi possível marcar como lida: {error}",
"Could not save draft: {error}": "Não foi possível salvar o rascunho: {error}",
"Could not save filter: {error}": "Não foi possível salvar o filtro: {error}",
"Could not save filters: {error}": "Não foi possível salvar os filtros: {error}",
"Could not send the receipt: {error}": "Não foi possível enviar a confirmação de leitura: {error}",
"Could not sign in.": "Não foi possível entrar.",
"Could not store image: {error}": "Não foi possível armazenar a imagem: {error}",
"Could not update labels: {error}": "Não foi possível atualizar os marcadores: {error}",
"Could not update the Scheduled folder: {error}": "Não foi possível atualizar a pasta Agendados: {error}",
"Could not update: {error}": "Não foi possível atualizar: {error}",
"Create": "Criar",
"Create an address book first": "Crie um catálogo de endereços primeiro",
"Custom colour removed": "Cor personalizada removida",
"Deactivate": "Desativar",
"Delete failed: {error}": "Falha ao excluir: {error}",
"Delete forever": "Excluir definitivamente",
"Delete script “{name}”?": "Excluir o script “{name}”?",
"Delete this event?": "Excluir este evento?",
"Delete this identity?": "Excluir esta identidade?",
"Delete {name}?": "Excluir {name}?",
"Deleted": "Excluído",
"Deletes the whole series. This cannot be undone.": "Exclui a série inteira. Isso não pode ser desfeito.",
"Did you forget the attachment?": "Você esqueceu o anexo?",
"Discard": "Descartar",
"Discard this draft?": "Descartar este rascunho?",
"Edit calendar": "Editar agenda",
"Edit event": "Editar evento",
"Edit identity": "Editar identidade",
"Edit template": "Editar modelo",
"End must be after start": "O término deve ser depois do início",
"Event duplicated": "Evento duplicado",
"Everything inside it goes too.": "Tudo o que está dentro também será excluído.",
"Filter created": "Filtro criado",
"Filter created — it will run on new mail": "Filtro criado — ele vai valer para as mensagens novas",
"Filter saved": "Filtro salvo",
"Filter saved — it will run on new mail": "Filtro salvo — ele vai valer para as mensagens novas",
"Filter saved, but applying it failed: {error}": "Filtro salvo, mas não foi possível aplicá-lo: {error}",
"Filters saved": "Filtros salvos",
"Folder changed, but its filter rules could not be updated: {error}": "Pasta alterada, mas não foi possível atualizar as regras de filtro dela: {error}",
"Folder created": "Pasta criada",
"Folder deleted": "Pasta excluída",
"Folder name": "Nome da pasta",
"Folder name (use / for a subfolder, e.g. Work/Invoices)": "Nome da pasta (use / para uma subpasta, ex.: Trabalho/Faturas)",
"Folder name (use / for subfolders, e.g. Work/Invoices)": "Nome da pasta (use / para subpastas, ex.: Trabalho/Faturas)",
"Folder “{name}” created": "Pasta “{name}” criada",
"Forwarded": "Encaminhada",
"Hand to the server, held until {when} (Ctrl+Enter)": "Entregue ao servidor e retida até {when} (Ctrl+Enter)",
"Hide": "Ocultar",
"Hide quoted text": "Ocultar o texto citado",
"Hide this from the compose picker": "Não oferecer ao escrever",
"Identity saved": "Identidade salva",
"Images in signatures need the Files feature, which this account doesn't have.": "Imagens em assinaturas exigem o recurso Arquivos, que esta conta não tem.",
"Invalid address: {address}": "Endereço inválido: {address}",
"Invalid username or password.": "Usuário ou senha incorretos.",
"It belongs to a change that was applied to this and all later occurrences, which the server will only edit as a whole. Apply to the entire series instead?": "Ela faz parte de uma alteração aplicada a esta e a todas as ocorrências seguintes, que o servidor só edita por inteiro. Aplicar à série inteira?",
"Label name": "Nome do marcador",
"Larger than {size} MB limit": "Acima do limite de {size} MB",
"Message sent": "Mensagem enviada",
"Move failed: {error}": "Falha ao mover: {error}",
"Move “{name}”": "Mover “{name}”",
"Moved": "Movido",
"Network error. Please check your connection.": "Erro de rede. Verifique sua conexão.",
"New all-day event on {date}": "Novo evento de dia inteiro em {date}",
"New event at {time}": "Novo evento às {time}",
"New identity": "Nova identidade",
"No Archive folder found. Create one named “Archive” first.": "Nenhuma pasta de arquivamento encontrada. Crie uma chamada “Archive” primeiro.",
"No contacts yet": "Ainda não há contatos",
"No longer shared": "Não está mais compartilhado",
"No matches": "Nenhuma correspondência",
"No new mail in your inbox.": "Nenhuma mensagem nova na sua caixa de entrada.",
"No results": "Nenhum resultado",
"Nothing here": "Nada aqui",
"Only Deleted Items and Junk Mail can be emptied.": "Só a lixeira e o spam podem ser esvaziados.",
"Please add at least one recipient": "Adicione pelo menos um destinatário",
"Please use an image under 512 KB for signatures.": "Use uma imagem com menos de 512 KB para assinaturas.",
"Preview": "Visualização",
"Read receipt sent": "Confirmação de leitura enviada",
"Remove attachments that failed to upload first": "Remova primeiro os anexos que falharam ao enviar",
"Removed from calendar": "Removido da agenda",
"Removes this date and leaves the rest of the series.": "Remove esta data e mantém o resto da série.",
"Rename address book": "Renomear catálogo de endereços",
"Rename category": "Renomear categoria",
"Replied": "Respondida",
"Report spam (!)": "Denunciar spam (!)",
"Response sent": "Resposta enviada",
"Revoke “{name}”?": "Revogar “{name}”?",
"Script has errors": "O script tem erros",
"Script is valid": "O script é válido",
"Script name is required": "É preciso informar um nome de script",
"Script saved": "Script salvo",
"Send (Ctrl+Enter)": "Enviar (Ctrl+Enter)",
"Send anyway": "Enviar mesmo assim",
"Send cancelled — the message is back in Drafts": "Envio cancelado — a mensagem voltou para Rascunhos",
"Send failed: {error}": "Falha ao enviar: {error}",
"Send invites": "Enviar convites",
"Send scheduled for {when}": "Envio agendado para {when}",
"Send without a subject?": "Enviar sem assunto?",
"Share “{name}”": "Compartilhar “{name}”",
"Sharing updated": "Compartilhamento atualizado",
"Show": "Mostrar",
"Show quoted text": "Mostrar o texto citado",
"Show this in the compose picker": "Oferecer ao escrever",
"Sign out other sessions?": "Encerrar as outras sessões?",
"Sign out others": "Encerrar as outras",
"Stop sharing “{name}”?": "Parar de compartilhar “{name}”?",
"Switch to {theme}": "Mudar para {theme}",
"Template": "Modelo",
"Template name": "Nome do modelo",
"The contacts in it go too.": "Os contatos dentro dele também serão excluídos.",
"The default identity is always offered when composing": "A identidade padrão é sempre oferecida ao escrever",
"The new passwords don't match": "As novas senhas não coincidem",
"The server scheduled this for {when}, not the time requested.": "O servidor agendou isto para {when}, e não para o horário solicitado.",
"This date cannot be changed on its own": "Esta data não pode ser alterada sozinha",
"This occurrence": "Esta ocorrência",
"Too many attempts. Please wait a few minutes and try again.": "Tentativas demais. Espere alguns minutos e tente de novo.",
"Try another search.": "Tente outra busca.",
"Try different keywords or filters.": "Tente outras palavras ou filtros.",
"Two-factor authentication is off": "A verificação em duas etapas está desativada",
"Undo window: {seconds}s": "Tempo para desfazer: {seconds} s",
"You're all caught up": "Você está em dia",
"Your browser refused the request: {error}": "Seu navegador recusou a solicitação: {error}",
"Your browser will ask whether to open mail links in ihasmail": "Seu navegador vai perguntar se os links de e-mail devem abrir no ihasmail",
"Your message mentions an attachment, but nothing is attached.": "Sua mensagem menciona um anexo, mas nada foi anexado.",
"event": "evento",
"Hide password": "Ocultar a senha",
"Show password": "Mostrar a senha",
"Settings reset to defaults": "Configurações restauradas para o padrão",
"Removed. Mail links will open in whatever your browser falls back to.": "Removido. Os links de e-mail vão abrir no que o seu navegador usar por padrão.",
"Edit rule": "Editar regra",
"Address copied": "Endereço copiado",
"Search or create label": "Busque ou crie um marcador",
"New label name": "Nome do novo marcador",
"That folder no longer exists. Showing your inbox instead.": "Essa pasta não existe mais. Mostrando a sua caixa de entrada.",
"Labels updated": "Marcadores atualizados",
"Unsubscribe message prepared — just hit Send": "Mensagem de cancelamento pronta — é só clicar em Enviar",
"Maximize": "Maximizar",
"Full screen": "Tela cheia",
"Resize panes": "Redimensionar os painéis",
"QR code": "Código QR",
"Draft discarded": "Rascunho descartado",
"Draft saved": "Rascunho salvo",
"Emptying folder…": "Esvaziando a pasta…",
"Nothing unread here": "Não há nada não lido aqui",
},
plurals: {
"{n} messages": { one: "{n} mensagem", other: "{n} mensagens" },
"{n} selected": { one: "{n} selecionada", other: "{n} selecionadas" },
"{n} conversations": { one: "{n} conversa", other: "{n} conversas" },
"{n} messages will be permanently deleted.": { one: "{n} mensagem será excluída definitivamente.", other: "{n} mensagens serão excluídas definitivamente." },
"Signed out {n} other sessions": { one: "{n} outra sessão encerrada", other: "{n} outras sessões encerradas" },
"applied to {matched} of {n} messages": { one: "aplicado a {matched} de {n} mensagem", other: "aplicado a {matched} de {n} mensagens" },
"Move {n} messages to…": { one: "Mover {n} mensagem para…", other: "Mover {n} mensagens para…" },
"This permanently deletes the folder and its {n} messages.": { one: "Isso exclui definitivamente a pasta e a {n} mensagem dela.", other: "Isso exclui definitivamente a pasta e as {n} mensagens dela." },
"Added {n} contacts": { one: "{n} contato adicionado", other: "{n} contatos adicionados" },
"{n} people will lose access. The contacts in it are not affected.": { one: "{n} pessoa perderá o acesso. Os contatos dentro dele não são afetados.", other: "{n} pessoas perderão o acesso. Os contatos dentro dele não são afetados." },
"Imported {n} contacts": { one: "{n} contato importado", other: "{n} contatos importados" },
"Saved for this date. {names} apply to the whole series and were left unchanged.": { one: "Salvo para esta data. {names} vale para a série inteira e não foi alterado.", other: "Salvo para esta data. {names} valem para a série inteira e não foram alterados." },
"{n} people will lose access. Events in it are not affected.": { one: "{n} pessoa perderá o acesso. Os eventos dentro dela não são afetados.", other: "{n} pessoas perderão o acesso. Os eventos dentro dela não são afetados." },
"{n} messages could not be deleted": { one: "{n} mensagem não pôde ser excluída", other: "{n} mensagens não puderam ser excluídas" },
"({n} deleted first)": { one: "({n} excluída antes)", other: "({n} excluídas antes)" },
"Marked {n} messages as read": { one: "{n} mensagem marcada como lida", other: "{n} mensagens marcadas como lidas" },
"in {n} folders": { one: "em {n} pasta", other: "em {n} pastas" },
"Deleted {n} messages": { one: "{n} mensagem excluída", other: "{n} mensagens excluídas" },
},
};