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:
@@ -857,10 +857,202 @@ export const catalog: Catalog = {
|
||||
"Saved {when}": "Guardado {when}",
|
||||
"Unsaved": "Sin guardar",
|
||||
"Agenda from {date}": "Agenda desde {date}",
|
||||
|
||||
// ── Toasts, dialogs and computed labels (second extraction pass) ───
|
||||
"(no subject)": "(sin asunto)",
|
||||
"(skipped: {actions})": "(omitido: {actions})",
|
||||
"Activate": "Activar",
|
||||
"Add a contact or import a vCard file.": "Añada un contacto o importe un archivo vCard.",
|
||||
"Added to your calendar": "Añadido a su calendario",
|
||||
"All events in this calendar will be deleted.": "Se eliminarán todos los eventos de este calendario.",
|
||||
"All occurrences": "Todas las repeticiones",
|
||||
"An occurrence cannot be moved to another calendar on its own": "Una repetición no puede moverse a otro calendario por sí sola",
|
||||
"Anything signed in with this password stops working immediately.": "Todo lo que haya iniciado sesión con esta contraseña dejará de funcionar de inmediato.",
|
||||
"App password revoked": "Contraseña de aplicación revocada",
|
||||
"Applies to every date in the series.": "Se aplica a todas las fechas de la serie.",
|
||||
"Applies to this date only.": "Se aplica solo a esta fecha.",
|
||||
"Apply to series": "Aplicar a la serie",
|
||||
"Applying filter to existing messages…": "Aplicando el filtro a los mensajes existentes…",
|
||||
"Attachments are still uploading": "Los archivos adjuntos aún se están subiendo",
|
||||
"Calendar saved": "Calendario guardado",
|
||||
"Categorised as {name}": "Categoría «{name}» asignada",
|
||||
"Category cleared": "Categoría quitada",
|
||||
"Change this event?": "¿Cambiar este evento?",
|
||||
"Choose a calendar": "Elija un calendario",
|
||||
"Choose an address book": "Elija una libreta de direcciones",
|
||||
"Colour updated": "Color actualizado",
|
||||
"Contact created": "Contacto creado",
|
||||
"Contact deleted": "Contacto eliminado",
|
||||
"Contact saved": "Contacto guardado",
|
||||
"Copied": "Copiado",
|
||||
"Copy of {title}": "Copia de {title}",
|
||||
"Could not cancel: {error}": "No se pudo cancelar: {error}",
|
||||
"Could not copy": "No se pudo copiar",
|
||||
"Could not copy the address": "No se pudo copiar la dirección",
|
||||
"Could not empty folder: {error}": "No se pudo vaciar la carpeta: {error}",
|
||||
"Could not load source: {error}": "No se pudo cargar el código fuente: {error}",
|
||||
"Could not mark as read: {error}": "No se pudo marcar como leído: {error}",
|
||||
"Could not save draft: {error}": "No se pudo guardar el borrador: {error}",
|
||||
"Could not save filter: {error}": "No se pudo guardar el filtro: {error}",
|
||||
"Could not save filters: {error}": "No se pudieron guardar los filtros: {error}",
|
||||
"Could not send the receipt: {error}": "No se pudo enviar la confirmación de lectura: {error}",
|
||||
"Could not sign in.": "No se pudo iniciar sesión.",
|
||||
"Could not store image: {error}": "No se pudo guardar la imagen: {error}",
|
||||
"Could not update labels: {error}": "No se pudieron actualizar las etiquetas: {error}",
|
||||
"Could not update the Scheduled folder: {error}": "No se pudo actualizar la carpeta Programados: {error}",
|
||||
"Could not update: {error}": "No se pudo actualizar: {error}",
|
||||
"Create": "Crear",
|
||||
"Create an address book first": "Cree primero una libreta de direcciones",
|
||||
"Custom colour removed": "Color personalizado quitado",
|
||||
"Deactivate": "Desactivar",
|
||||
"Delete failed: {error}": "No se pudo eliminar: {error}",
|
||||
"Delete forever": "Eliminar definitivamente",
|
||||
"Delete script “{name}”?": "¿Eliminar el script «{name}»?",
|
||||
"Delete this event?": "¿Eliminar este evento?",
|
||||
"Delete this identity?": "¿Eliminar esta identidad?",
|
||||
"Delete {name}?": "¿Eliminar {name}?",
|
||||
"Deleted": "Eliminado",
|
||||
"Deletes the whole series. This cannot be undone.": "Elimina la serie completa. No se puede deshacer.",
|
||||
"Did you forget the attachment?": "¿Ha olvidado el archivo adjunto?",
|
||||
"Discard": "Descartar",
|
||||
"Discard this draft?": "¿Descartar este borrador?",
|
||||
"Edit calendar": "Editar calendario",
|
||||
"Edit event": "Editar evento",
|
||||
"Edit identity": "Editar identidad",
|
||||
"Edit template": "Editar plantilla",
|
||||
"End must be after start": "El fin debe ser posterior al inicio",
|
||||
"Event duplicated": "Evento duplicado",
|
||||
"Everything inside it goes too.": "También se eliminará todo su contenido.",
|
||||
"Filter created": "Filtro creado",
|
||||
"Filter created — it will run on new mail": "Filtro creado: se aplicará al correo nuevo",
|
||||
"Filter saved": "Filtro guardado",
|
||||
"Filter saved — it will run on new mail": "Filtro guardado: se aplicará al correo nuevo",
|
||||
"Filter saved, but applying it failed: {error}": "Filtro guardado, pero no se pudo aplicar: {error}",
|
||||
"Filters saved": "Filtros guardados",
|
||||
"Folder changed, but its filter rules could not be updated: {error}": "Carpeta modificada, pero no se pudieron actualizar sus reglas de filtrado: {error}",
|
||||
"Folder created": "Carpeta creada",
|
||||
"Folder deleted": "Carpeta eliminada",
|
||||
"Folder name": "Nombre de la carpeta",
|
||||
"Folder name (use / for a subfolder, e.g. Work/Invoices)": "Nombre de la carpeta (use / para una subcarpeta, p. ej. Trabajo/Facturas)",
|
||||
"Folder name (use / for subfolders, e.g. Work/Invoices)": "Nombre de la carpeta (use / para subcarpetas, p. ej. Trabajo/Facturas)",
|
||||
"Folder “{name}” created": "Carpeta «{name}» creada",
|
||||
"Forwarded": "Reenviado",
|
||||
"Hand to the server, held until {when} (Ctrl+Enter)": "Se entrega al servidor y se retiene hasta {when} (Ctrl+Intro)",
|
||||
"Hide": "Ocultar",
|
||||
"Hide quoted text": "Ocultar el texto citado",
|
||||
"Hide this from the compose picker": "No ofrecer al redactar",
|
||||
"Identity saved": "Identidad guardada",
|
||||
"Images in signatures need the Files feature, which this account doesn't have.": "Las imágenes en las firmas necesitan la función Archivos, de la que esta cuenta no dispone.",
|
||||
"Invalid address: {address}": "Dirección no válida: {address}",
|
||||
"Invalid username or password.": "Usuario o contraseña incorrectos.",
|
||||
"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?": "Pertenece a un cambio aplicado a esta y a todas las repeticiones posteriores, que el servidor solo edita en conjunto. ¿Aplicarlo a la serie completa?",
|
||||
"Label name": "Nombre de la etiqueta",
|
||||
"Larger than {size} MB limit": "Supera el límite de {size} MB",
|
||||
"Message sent": "Mensaje enviado",
|
||||
"Move failed: {error}": "No se pudo mover: {error}",
|
||||
"Move “{name}”": "Mover «{name}»",
|
||||
"Moved": "Movido",
|
||||
"Network error. Please check your connection.": "Error de red. Compruebe su conexión.",
|
||||
"New all-day event on {date}": "Nuevo evento de todo el día el {date}",
|
||||
"New event at {time}": "Nuevo evento a las {time}",
|
||||
"New identity": "Nueva identidad",
|
||||
"No Archive folder found. Create one named “Archive” first.": "No se encontró una carpeta de archivo. Cree primero una llamada «Archive».",
|
||||
"No contacts yet": "Aún no hay contactos",
|
||||
"No longer shared": "Ya no está compartido",
|
||||
"No matches": "Sin coincidencias",
|
||||
"No new mail in your inbox.": "No hay correo nuevo en su bandeja de entrada.",
|
||||
"No results": "Sin resultados",
|
||||
"Nothing here": "Aquí no hay nada",
|
||||
"Only Deleted Items and Junk Mail can be emptied.": "Solo se pueden vaciar la papelera y el correo no deseado.",
|
||||
"Please add at least one recipient": "Añada al menos un destinatario",
|
||||
"Please use an image under 512 KB for signatures.": "Use una imagen de menos de 512 KB para las firmas.",
|
||||
"Preview": "Vista previa",
|
||||
"Read receipt sent": "Confirmación de lectura enviada",
|
||||
"Remove attachments that failed to upload first": "Quite primero los adjuntos que no se pudieron subir",
|
||||
"Removed from calendar": "Quitado del calendario",
|
||||
"Removes this date and leaves the rest of the series.": "Quita esta fecha y deja el resto de la serie.",
|
||||
"Rename address book": "Cambiar el nombre de la libreta",
|
||||
"Rename category": "Cambiar el nombre de la categoría",
|
||||
"Replied": "Respondido",
|
||||
"Report spam (!)": "Marcar como spam (!)",
|
||||
"Response sent": "Respuesta enviada",
|
||||
"Revoke “{name}”?": "¿Revocar «{name}»?",
|
||||
"Script has errors": "El script tiene errores",
|
||||
"Script is valid": "El script es válido",
|
||||
"Script name is required": "Se necesita un nombre de script",
|
||||
"Script saved": "Script guardado",
|
||||
"Send (Ctrl+Enter)": "Enviar (Ctrl+Intro)",
|
||||
"Send anyway": "Enviar de todos modos",
|
||||
"Send cancelled — the message is back in Drafts": "Envío cancelado: el mensaje ha vuelto a Borradores",
|
||||
"Send failed: {error}": "No se pudo enviar: {error}",
|
||||
"Send invites": "Enviar invitaciones",
|
||||
"Send scheduled for {when}": "Envío programado para {when}",
|
||||
"Send without a subject?": "¿Enviar sin asunto?",
|
||||
"Share “{name}”": "Compartir «{name}»",
|
||||
"Sharing updated": "Uso compartido actualizado",
|
||||
"Show": "Mostrar",
|
||||
"Show quoted text": "Mostrar el texto citado",
|
||||
"Show this in the compose picker": "Ofrecer al redactar",
|
||||
"Sign out other sessions?": "¿Cerrar las demás sesiones?",
|
||||
"Sign out others": "Cerrar las demás",
|
||||
"Stop sharing “{name}”?": "¿Dejar de compartir «{name}»?",
|
||||
"Switch to {theme}": "Cambiar a {theme}",
|
||||
"Template": "Plantilla",
|
||||
"Template name": "Nombre de la plantilla",
|
||||
"The contacts in it go too.": "También se eliminarán los contactos que contiene.",
|
||||
"The default identity is always offered when composing": "La identidad predeterminada siempre se ofrece al redactar",
|
||||
"The new passwords don't match": "Las contraseñas nuevas no coinciden",
|
||||
"The server scheduled this for {when}, not the time requested.": "El servidor lo programó para {when}, no para la hora solicitada.",
|
||||
"This date cannot be changed on its own": "Esta fecha no se puede cambiar por sí sola",
|
||||
"This occurrence": "Solo esta repetición",
|
||||
"Too many attempts. Please wait a few minutes and try again.": "Demasiados intentos. Espere unos minutos y vuelva a intentarlo.",
|
||||
"Try another search.": "Pruebe con otra búsqueda.",
|
||||
"Try different keywords or filters.": "Pruebe con otras palabras o filtros.",
|
||||
"Two-factor authentication is off": "La verificación en dos pasos está desactivada",
|
||||
"Undo window: {seconds}s": "Margen para deshacer: {seconds} s",
|
||||
"You're all caught up": "Está todo al día",
|
||||
"Your browser refused the request: {error}": "Su navegador rechazó la solicitud: {error}",
|
||||
"Your browser will ask whether to open mail links in ihasmail": "Su navegador le preguntará si quiere abrir los enlaces de correo en ihasmail",
|
||||
"Your message mentions an attachment, but nothing is attached.": "Su mensaje menciona un archivo adjunto, pero no hay ninguno.",
|
||||
"event": "evento",
|
||||
"Hide password": "Ocultar la contraseña",
|
||||
"Show password": "Mostrar la contraseña",
|
||||
"Settings reset to defaults": "Ajustes restablecidos a los valores predeterminados",
|
||||
"Removed. Mail links will open in whatever your browser falls back to.": "Quitado. Los enlaces de correo se abrirán en lo que use su navegador por defecto.",
|
||||
"Edit rule": "Editar regla",
|
||||
"Address copied": "Dirección copiada",
|
||||
"Search or create label": "Busque o cree una etiqueta",
|
||||
"New label name": "Nombre de la nueva etiqueta",
|
||||
"That folder no longer exists. Showing your inbox instead.": "Esa carpeta ya no existe. Se muestra su bandeja de entrada.",
|
||||
"Labels updated": "Etiquetas actualizadas",
|
||||
"Unsubscribe message prepared — just hit Send": "Mensaje de baja preparado: solo tiene que pulsar Enviar",
|
||||
"Maximize": "Maximizar",
|
||||
"Full screen": "Pantalla completa",
|
||||
"Resize panes": "Cambiar el tamaño de los paneles",
|
||||
"QR code": "Código QR",
|
||||
"Draft discarded": "Borrador descartado",
|
||||
"Draft saved": "Borrador guardado",
|
||||
"Emptying folder…": "Vaciando la carpeta…",
|
||||
"Nothing unread here": "Aquí no hay nada sin leer",
|
||||
},
|
||||
plurals: {
|
||||
"{n} messages": { one: "{n} mensaje", other: "{n} mensajes" },
|
||||
"{n} selected": { one: "{n} seleccionado", other: "{n} seleccionados" },
|
||||
"{n} conversations": { one: "{n} conversación", other: "{n} conversaciones" },
|
||||
"{n} messages will be permanently deleted.": { one: "Se eliminará {n} mensaje definitivamente.", other: "Se eliminarán {n} mensajes definitivamente." },
|
||||
"Signed out {n} other sessions": { one: "Se ha cerrado {n} sesión más", other: "Se han cerrado {n} sesiones más" },
|
||||
"applied to {matched} of {n} messages": { one: "aplicado a {matched} de {n} mensaje", other: "aplicado a {matched} de {n} mensajes" },
|
||||
"Move {n} messages to…": { one: "Mover {n} mensaje a…", other: "Mover {n} mensajes a…" },
|
||||
"This permanently deletes the folder and its {n} messages.": { one: "Esto elimina definitivamente la carpeta y su {n} mensaje.", other: "Esto elimina definitivamente la carpeta y sus {n} mensajes." },
|
||||
"Added {n} contacts": { one: "Se ha añadido {n} contacto", other: "Se han añadido {n} contactos" },
|
||||
"{n} people will lose access. The contacts in it are not affected.": { one: "{n} persona perderá el acceso. Los contactos que contiene no se ven afectados.", other: "{n} personas perderán el acceso. Los contactos que contiene no se ven afectados." },
|
||||
"Imported {n} contacts": { one: "Se ha importado {n} contacto", other: "Se han importado {n} contactos" },
|
||||
"Saved for this date. {names} apply to the whole series and were left unchanged.": { one: "Guardado para esta fecha. {names} se aplica a toda la serie y no se ha modificado.", other: "Guardado para esta fecha. {names} se aplican a toda la serie y no se han modificado." },
|
||||
"{n} people will lose access. Events in it are not affected.": { one: "{n} persona perderá el acceso. Los eventos que contiene no se ven afectados.", other: "{n} personas perderán el acceso. Los eventos que contiene no se ven afectados." },
|
||||
"{n} messages could not be deleted": { one: "No se pudo eliminar {n} mensaje", other: "No se pudieron eliminar {n} mensajes" },
|
||||
"({n} deleted first)": { one: "({n} eliminado antes)", other: "({n} eliminados antes)" },
|
||||
"Marked {n} messages as read": { one: "{n} mensaje marcado como leído", other: "{n} mensajes marcados como leídos" },
|
||||
"in {n} folders": { one: "en {n} carpeta", other: "en {n} carpetas" },
|
||||
"Deleted {n} messages": { one: "{n} mensaje eliminado", other: "{n} mensajes eliminados" },
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user