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:
@@ -862,10 +862,202 @@ export const catalog: Catalog = {
|
||||
"Saved {when}": "Enregistré {when}",
|
||||
"Unsaved": "Non enregistré",
|
||||
"Agenda from {date}": "Agenda à partir du {date}",
|
||||
|
||||
// ── Toasts, dialogs and computed labels (second extraction pass) ───
|
||||
"(no subject)": "(sans objet)",
|
||||
"(skipped: {actions})": "(ignoré : {actions})",
|
||||
"Activate": "Activer",
|
||||
"Add a contact or import a vCard file.": "Ajoutez un contact ou importez un fichier vCard.",
|
||||
"Added to your calendar": "Ajouté à votre agenda",
|
||||
"All events in this calendar will be deleted.": "Tous les événements de cet agenda seront supprimés.",
|
||||
"All occurrences": "Toutes les occurrences",
|
||||
"An occurrence cannot be moved to another calendar on its own": "Une occurrence ne peut pas être déplacée seule vers un autre agenda",
|
||||
"Anything signed in with this password stops working immediately.": "Tout ce qui est connecté avec ce mot de passe cesse de fonctionner immédiatement.",
|
||||
"App password revoked": "Mot de passe d’application révoqué",
|
||||
"Applies to every date in the series.": "S’applique à toutes les dates de la série.",
|
||||
"Applies to this date only.": "S’applique uniquement à cette date.",
|
||||
"Apply to series": "Appliquer à la série",
|
||||
"Applying filter to existing messages…": "Application du filtre aux messages existants…",
|
||||
"Attachments are still uploading": "Les pièces jointes sont encore en cours d’envoi",
|
||||
"Calendar saved": "Agenda enregistré",
|
||||
"Categorised as {name}": "Catégorie « {name} » attribuée",
|
||||
"Category cleared": "Catégorie retirée",
|
||||
"Change this event?": "Modifier cet événement ?",
|
||||
"Choose a calendar": "Choisissez un agenda",
|
||||
"Choose an address book": "Choisissez un carnet d’adresses",
|
||||
"Colour updated": "Couleur modifiée",
|
||||
"Contact created": "Contact créé",
|
||||
"Contact deleted": "Contact supprimé",
|
||||
"Contact saved": "Contact enregistré",
|
||||
"Copied": "Copié",
|
||||
"Copy of {title}": "Copie de {title}",
|
||||
"Could not cancel: {error}": "Impossible d’annuler : {error}",
|
||||
"Could not copy": "Impossible de copier",
|
||||
"Could not copy the address": "Impossible de copier l’adresse",
|
||||
"Could not empty folder: {error}": "Impossible de vider le dossier : {error}",
|
||||
"Could not load source: {error}": "Impossible de charger la source : {error}",
|
||||
"Could not mark as read: {error}": "Impossible de marquer comme lu : {error}",
|
||||
"Could not save draft: {error}": "Impossible d’enregistrer le brouillon : {error}",
|
||||
"Could not save filter: {error}": "Impossible d’enregistrer le filtre : {error}",
|
||||
"Could not save filters: {error}": "Impossible d’enregistrer les filtres : {error}",
|
||||
"Could not send the receipt: {error}": "Impossible d’envoyer l’accusé de lecture : {error}",
|
||||
"Could not sign in.": "Connexion impossible.",
|
||||
"Could not store image: {error}": "Impossible d’enregistrer l’image : {error}",
|
||||
"Could not update labels: {error}": "Impossible de mettre à jour les libellés : {error}",
|
||||
"Could not update the Scheduled folder: {error}": "Impossible de mettre à jour le dossier Programmés : {error}",
|
||||
"Could not update: {error}": "Impossible de mettre à jour : {error}",
|
||||
"Create": "Créer",
|
||||
"Create an address book first": "Créez d’abord un carnet d’adresses",
|
||||
"Custom colour removed": "Couleur personnalisée retirée",
|
||||
"Deactivate": "Désactiver",
|
||||
"Delete failed: {error}": "Échec de la suppression : {error}",
|
||||
"Delete forever": "Supprimer définitivement",
|
||||
"Delete script “{name}”?": "Supprimer le script « {name} » ?",
|
||||
"Delete this event?": "Supprimer cet événement ?",
|
||||
"Delete this identity?": "Supprimer cette identité ?",
|
||||
"Delete {name}?": "Supprimer {name} ?",
|
||||
"Deleted": "Supprimé",
|
||||
"Deletes the whole series. This cannot be undone.": "Supprime toute la série. Cette action est irréversible.",
|
||||
"Did you forget the attachment?": "Avez-vous oublié la pièce jointe ?",
|
||||
"Discard": "Abandonner",
|
||||
"Discard this draft?": "Abandonner ce brouillon ?",
|
||||
"Edit calendar": "Modifier l’agenda",
|
||||
"Edit event": "Modifier l’événement",
|
||||
"Edit identity": "Modifier l’identité",
|
||||
"Edit template": "Modifier le modèle",
|
||||
"End must be after start": "La fin doit être postérieure au début",
|
||||
"Event duplicated": "Événement dupliqué",
|
||||
"Everything inside it goes too.": "Tout ce qu’il contient sera également supprimé.",
|
||||
"Filter created": "Filtre créé",
|
||||
"Filter created — it will run on new mail": "Filtre créé : il s’appliquera au courrier entrant",
|
||||
"Filter saved": "Filtre enregistré",
|
||||
"Filter saved — it will run on new mail": "Filtre enregistré : il s’appliquera au courrier entrant",
|
||||
"Filter saved, but applying it failed: {error}": "Filtre enregistré, mais son application a échoué : {error}",
|
||||
"Filters saved": "Filtres enregistrés",
|
||||
"Folder changed, but its filter rules could not be updated: {error}": "Dossier modifié, mais ses règles de filtrage n’ont pas pu être mises à jour : {error}",
|
||||
"Folder created": "Dossier créé",
|
||||
"Folder deleted": "Dossier supprimé",
|
||||
"Folder name": "Nom du dossier",
|
||||
"Folder name (use / for a subfolder, e.g. Work/Invoices)": "Nom du dossier (utilisez / pour un sous-dossier, par ex. Travail/Factures)",
|
||||
"Folder name (use / for subfolders, e.g. Work/Invoices)": "Nom du dossier (utilisez / pour des sous-dossiers, par ex. Travail/Factures)",
|
||||
"Folder “{name}” created": "Dossier « {name} » créé",
|
||||
"Forwarded": "Transféré",
|
||||
"Hand to the server, held until {when} (Ctrl+Enter)": "Confié au serveur, retenu jusqu’à {when} (Ctrl+Entrée)",
|
||||
"Hide": "Masquer",
|
||||
"Hide quoted text": "Masquer le texte cité",
|
||||
"Hide this from the compose picker": "Ne pas proposer à la rédaction",
|
||||
"Identity saved": "Identité enregistrée",
|
||||
"Images in signatures need the Files feature, which this account doesn't have.": "Les images dans les signatures nécessitent la fonction Fichiers, dont ce compte ne dispose pas.",
|
||||
"Invalid address: {address}": "Adresse invalide : {address}",
|
||||
"Invalid username or password.": "Nom d’utilisateur ou mot de passe incorrect.",
|
||||
"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?": "Elle relève d’une modification appliquée à cette occurrence et à toutes les suivantes, que le serveur ne modifie que d’un bloc. L’appliquer à toute la série ?",
|
||||
"Label name": "Nom du libellé",
|
||||
"Larger than {size} MB limit": "Dépasse la limite de {size} Mo",
|
||||
"Message sent": "Message envoyé",
|
||||
"Move failed: {error}": "Échec du déplacement : {error}",
|
||||
"Move “{name}”": "Déplacer « {name} »",
|
||||
"Moved": "Déplacé",
|
||||
"Network error. Please check your connection.": "Erreur réseau. Vérifiez votre connexion.",
|
||||
"New all-day event on {date}": "Nouvel événement sur la journée du {date}",
|
||||
"New event at {time}": "Nouvel événement à {time}",
|
||||
"New identity": "Nouvelle identité",
|
||||
"No Archive folder found. Create one named “Archive” first.": "Aucun dossier d’archive trouvé. Créez-en d’abord un nommé « Archive ».",
|
||||
"No contacts yet": "Pas encore de contacts",
|
||||
"No longer shared": "N’est plus partagé",
|
||||
"No matches": "Aucune correspondance",
|
||||
"No new mail in your inbox.": "Aucun nouveau message dans votre boîte de réception.",
|
||||
"No results": "Aucun résultat",
|
||||
"Nothing here": "Rien ici",
|
||||
"Only Deleted Items and Junk Mail can be emptied.": "Seuls la corbeille et les indésirables peuvent être vidés.",
|
||||
"Please add at least one recipient": "Ajoutez au moins un destinataire",
|
||||
"Please use an image under 512 KB for signatures.": "Utilisez une image de moins de 512 Ko pour les signatures.",
|
||||
"Preview": "Aperçu",
|
||||
"Read receipt sent": "Accusé de lecture envoyé",
|
||||
"Remove attachments that failed to upload first": "Retirez d’abord les pièces jointes dont l’envoi a échoué",
|
||||
"Removed from calendar": "Retiré de l’agenda",
|
||||
"Removes this date and leaves the rest of the series.": "Retire cette date et laisse le reste de la série.",
|
||||
"Rename address book": "Renommer le carnet d’adresses",
|
||||
"Rename category": "Renommer la catégorie",
|
||||
"Replied": "Répondu",
|
||||
"Report spam (!)": "Signaler comme spam (!)",
|
||||
"Response sent": "Réponse envoyée",
|
||||
"Revoke “{name}”?": "Révoquer « {name} » ?",
|
||||
"Script has errors": "Le script contient des erreurs",
|
||||
"Script is valid": "Le script est valide",
|
||||
"Script name is required": "Un nom de script est requis",
|
||||
"Script saved": "Script enregistré",
|
||||
"Send (Ctrl+Enter)": "Envoyer (Ctrl+Entrée)",
|
||||
"Send anyway": "Envoyer quand même",
|
||||
"Send cancelled — the message is back in Drafts": "Envoi annulé : le message est de retour dans les brouillons",
|
||||
"Send failed: {error}": "Échec de l’envoi : {error}",
|
||||
"Send invites": "Envoyer les invitations",
|
||||
"Send scheduled for {when}": "Envoi programmé pour {when}",
|
||||
"Send without a subject?": "Envoyer sans objet ?",
|
||||
"Share “{name}”": "Partager « {name} »",
|
||||
"Sharing updated": "Partage mis à jour",
|
||||
"Show": "Afficher",
|
||||
"Show quoted text": "Afficher le texte cité",
|
||||
"Show this in the compose picker": "Proposer à la rédaction",
|
||||
"Sign out other sessions?": "Déconnecter les autres sessions ?",
|
||||
"Sign out others": "Déconnecter les autres",
|
||||
"Stop sharing “{name}”?": "Arrêter le partage de « {name} » ?",
|
||||
"Switch to {theme}": "Passer à {theme}",
|
||||
"Template": "Modèle",
|
||||
"Template name": "Nom du modèle",
|
||||
"The contacts in it go too.": "Les contacts qu’il contient seront également supprimés.",
|
||||
"The default identity is always offered when composing": "L’identité par défaut est toujours proposée à la rédaction",
|
||||
"The new passwords don't match": "Les nouveaux mots de passe ne correspondent pas",
|
||||
"The server scheduled this for {when}, not the time requested.": "Le serveur l’a programmé pour {when}, et non pour l’heure demandée.",
|
||||
"This date cannot be changed on its own": "Cette date ne peut pas être modifiée seule",
|
||||
"This occurrence": "Cette occurrence",
|
||||
"Too many attempts. Please wait a few minutes and try again.": "Trop de tentatives. Attendez quelques minutes et réessayez.",
|
||||
"Try another search.": "Essayez une autre recherche.",
|
||||
"Try different keywords or filters.": "Essayez d’autres mots-clés ou filtres.",
|
||||
"Two-factor authentication is off": "L’authentification à deux facteurs est désactivée",
|
||||
"Undo window: {seconds}s": "Délai d’annulation : {seconds} s",
|
||||
"You're all caught up": "Vous êtes à jour",
|
||||
"Your browser refused the request: {error}": "Votre navigateur a refusé la demande : {error}",
|
||||
"Your browser will ask whether to open mail links in ihasmail": "Votre navigateur vous demandera s’il faut ouvrir les liens de courrier dans ihasmail",
|
||||
"Your message mentions an attachment, but nothing is attached.": "Votre message mentionne une pièce jointe, mais rien n’est joint.",
|
||||
"event": "événement",
|
||||
"Hide password": "Masquer le mot de passe",
|
||||
"Show password": "Afficher le mot de passe",
|
||||
"Settings reset to defaults": "Paramètres réinitialisés",
|
||||
"Removed. Mail links will open in whatever your browser falls back to.": "Retiré. Les liens de courrier s’ouvriront dans ce que votre navigateur utilise par défaut.",
|
||||
"Edit rule": "Modifier la règle",
|
||||
"Address copied": "Adresse copiée",
|
||||
"Search or create label": "Rechercher ou créer un libellé",
|
||||
"New label name": "Nom du nouveau libellé",
|
||||
"That folder no longer exists. Showing your inbox instead.": "Ce dossier n’existe plus. Votre boîte de réception est affichée à la place.",
|
||||
"Labels updated": "Libellés mis à jour",
|
||||
"Unsubscribe message prepared — just hit Send": "Message de désabonnement préparé : il ne reste qu’à envoyer",
|
||||
"Maximize": "Agrandir",
|
||||
"Full screen": "Plein écran",
|
||||
"Resize panes": "Redimensionner les volets",
|
||||
"QR code": "QR code",
|
||||
"Draft discarded": "Brouillon abandonné",
|
||||
"Draft saved": "Brouillon enregistré",
|
||||
"Emptying folder…": "Vidage du dossier…",
|
||||
"Nothing unread here": "Rien de non lu ici",
|
||||
},
|
||||
plurals: {
|
||||
"{n} messages": { one: "{n} message", other: "{n} messages" },
|
||||
"{n} selected": { one: "{n} sélectionné", other: "{n} sélectionnés" },
|
||||
"{n} conversations": { one: "{n} conversation", other: "{n} conversations" },
|
||||
"{n} messages will be permanently deleted.": { one: "{n} message sera définitivement supprimé.", other: "{n} messages seront définitivement supprimés." },
|
||||
"Signed out {n} other sessions": { one: "{n} autre session déconnectée", other: "{n} autres sessions déconnectées" },
|
||||
"applied to {matched} of {n} messages": { one: "appliqué à {matched} message sur {n}", other: "appliqué à {matched} messages sur {n}" },
|
||||
"Move {n} messages to…": { one: "Déplacer {n} message vers…", other: "Déplacer {n} messages vers…" },
|
||||
"This permanently deletes the folder and its {n} messages.": { one: "Cela supprime définitivement le dossier et son {n} message.", other: "Cela supprime définitivement le dossier et ses {n} messages." },
|
||||
"Added {n} contacts": { one: "{n} contact ajouté", other: "{n} contacts ajoutés" },
|
||||
"{n} people will lose access. The contacts in it are not affected.": { one: "{n} personne perdra l’accès. Les contacts qu’il contient ne sont pas affectés.", other: "{n} personnes perdront l’accès. Les contacts qu’il contient ne sont pas affectés." },
|
||||
"Imported {n} contacts": { one: "{n} contact importé", other: "{n} contacts importés" },
|
||||
"Saved for this date. {names} apply to the whole series and were left unchanged.": { one: "Enregistré pour cette date. {names} s’applique à toute la série et n’a pas été modifié.", other: "Enregistré pour cette date. {names} s’appliquent à toute la série et n’ont pas été modifiés." },
|
||||
"{n} people will lose access. Events in it are not affected.": { one: "{n} personne perdra l’accès. Les événements qu’il contient ne sont pas affectés.", other: "{n} personnes perdront l’accès. Les événements qu’il contient ne sont pas affectés." },
|
||||
"{n} messages could not be deleted": { one: "{n} message n’a pas pu être supprimé", other: "{n} messages n’ont pas pu être supprimés" },
|
||||
"({n} deleted first)": { one: "({n} supprimé auparavant)", other: "({n} supprimés auparavant)" },
|
||||
"Marked {n} messages as read": { one: "{n} message marqué comme lu", other: "{n} messages marqués comme lus" },
|
||||
"in {n} folders": { one: "dans {n} dossier", other: "dans {n} dossiers" },
|
||||
"Deleted {n} messages": { one: "{n} message supprimé", other: "{n} messages supprimés" },
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user