localization: ship fr-FR

Complete French catalog across all three namespaces (app/panel/ui),
1129 keys, machine-translated then reviewed for the two things that
go wrong automatically: technical vocabulary and plurals.

Fixed on review: a gender-agreement slip ("décrivez-la" referring to
the masculine "Prompt", should be "décrivez-le") and an incomplete
verb construction in agentRunner.keepsGoing. Also reverted "Minesweeper"
→ "Démineur" back to the untranslated proper name, matching pt-PT's
own precedent of not translating this bundled app's name.

French is the language where the 0/1/2 plural check actually bites:
Intl.PluralRules puts count=0 in the SAME category as count=1 ("one"),
unlike English/Portuguese/German/Spanish where 0 is "other". All 17
plural entries were verified against 0, 1 and 2 rather than trusting
the category names — the "one" branch is phrased as a plain singular
so it reads correctly for both 0 and 1.

Parity tests now check every non-en locale in one assertion instead
of hardcoding pt-PT; SHIPPED_LOCALES gains fr-FR so the picker offers
it. es-ES and de-DE catalogs are drafted and under review — held out
of this commit and out of SHIPPED_LOCALES until each passes its own
review, so a half-translated locale never reaches the picker.
This commit is contained in:
2026-08-23 17:17:50 +00:00
parent a6b4bc4170
commit 0457bf9ff3
7 changed files with 1453 additions and 13 deletions
@@ -0,0 +1,81 @@
{
"button": {
"cancel": "Annuler",
"confirm": "Confirmer",
"ok": "OK",
"close": "Fermer"
},
"search": {
"placeholder": "Rechercher",
"open": "Ouvrir la recherche",
"close": "Fermer la recherche",
"clear": "Effacer la recherche"
},
"select": {
"placeholder": "Sélectionner..."
},
"tabs": {
"filterPlaceholder": "Filtrer"
},
"commandBlock": {
"run": "Exécuter dans le terminal",
"copy": "Copier la commande"
},
"colorPicker": {
"placeholder": "Couleur hexadécimale"
},
"state": {
"loading": "Chargement…"
},
"dataTable": {
"empty": "Aucune donnée trouvée.",
"pagination": {
"showingPage": "Page {{current}} sur {{total}}",
"showingPageShort": "p {{current}}/{{total}}",
"rowsPerPage": "Lignes par page",
"perPage": "{{size}} / page",
"perPageShort": "{{size}}/p",
"first": "Première",
"prev": "Précédente",
"next": "Suivante",
"last": "Dernière"
}
},
"pagination": {
"label": "Pagination",
"previous": "Précédent",
"next": "Suivant",
"morePages": "Plus de pages",
"goToPrevious": "Aller à la page précédente",
"goToNext": "Aller à la page suivante"
},
"carousel": {
"previous": "Diapositive précédente",
"next": "Diapositive suivante"
},
"breadcrumb": {
"more": "Plus"
},
"theme": {
"toggle": "Basculer le mode de couleur"
},
"error": {
"forbidden": {
"title": "Accès non autorisé",
"description": "Vous n'êtes pas autorisé à effectuer cette action.",
"hint": "Si vous pensez qu'il s'agit d'une erreur, contactez votre responsable."
},
"unauthorized": {
"title": "Identifiants invalides",
"description": "Votre jeton d'accès a expiré",
"hint": "Reconnectez-vous"
},
"server": {
"title": "Erreur du serveur"
}
},
"logo": {
"icon": "Icône Officer",
"logo": "Logo Officer"
}
}
@@ -2,11 +2,12 @@ import { describe, expect, test } from 'bun:test';
import { findMissingKeys } from 'i18n';
import en from './en-GB.json';
import ptPT from './pt-PT.json';
import frFR from './fr-FR.json';
// D2: every shipped locale ships a COMPLETE catalog. Phase 0 scaffolds this catalog with no keys yet
// (ui.* strings land in phase 1) — the test still runs so it is load-bearing from day one.
// D2: every shipped locale ships a COMPLETE catalog. A key en-GB has and another locale lacks is a red
// build, not a warning — there is no regional fallback to paper over the gap.
describe('shared components (ui.*) locale catalogs', () => {
test('pt-PT has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT })).toEqual([]);
test('every shipped locale has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT, 'fr-FR': frFR })).toEqual([]);
});
});
@@ -0,0 +1,652 @@
{
"common": {
"save": "Enregistrer",
"delete": "Supprimer"
},
"minesweeper": {
"notInstalled": "Minesweeper n'est pas installé sur ce serveur.",
"needs": "Il faut wine, un préfixe 32 bits, et ce paquet à côté de la plateforme :",
"notRunning": "La session Minesweeper n'est pas en cours d'exécution.",
"error": {
"connectionFailed": "La connexion à la session Minesweeper a échoué.",
"refused": "Le serveur VNC a refusé la connexion.",
"authUnsupported": "Le serveur VNC demande une authentification que ce client ne prend pas en charge.",
"handshakeRejected": "Le serveur VNC a rejeté la prise de contact.",
"unknownMessage": "Le serveur VNC a envoyé un type de message inconnu ({{type}}).",
"unknownEncoding": "Le serveur VNC a utilisé l'encodage {{encoding}}, que ce client n'a pas demandé."
}
},
"qrTransfer": {
"chooseFile": "Choisissez un fichier à envoyer",
"startScanning": "Démarrer le scan",
"receiveAnother": "Recevoir un autre",
"stop": "Arrêter"
},
"dav": {
"empty": "Rien ici pour l'instant. Ajoutez-en un depuis un client mobile ou de bureau via CalDAV/CardDAV.",
"calendar": "Calendrier",
"contacts": "Contacts",
"readFailed": "Impossible de lire {{name}} : {{reason}}"
},
"terminal": {
"title": "Terminal",
"find": "Rechercher",
"runningShells": "Shells en cours d'exécution",
"loading": "Chargement…",
"noShells": "Aucun shell en cours d'exécution.",
"refresh": "Actualiser",
"kill": "Terminer ce shell"
},
"appStore": {
"loading": "Chargement",
"loadFailed": "Impossible de charger la boutique d'applications.",
"installed": "Installées",
"available": "Disponibles",
"needsYou": "Nécessite votre intervention",
"needsYouShort": "nécessite votre intervention",
"selectApp": "Sélectionnez une application",
"installFailed": "L'installation a échoué",
"uninstall": "Désinstaller",
"enable": "Activer",
"disable": "Désactiver",
"enabled": "activée",
"disabled": "désactivée",
"removalNote": "Désactiver arrête le sidecar et son conteneur. Désinstaller supprime aussi les conteneurs — vos données, la configuration et les tables de cette application sont conservées dans les deux cas.",
"installing": "installation en cours",
"failed": "échec",
"installedIcon": "installée"
},
"systemMonitor": {
"cpuPackage": "Boîtier CPU",
"user": "Utilisateur",
"cpuPct": "CPU%",
"memPct": "MÉM%",
"command": "Commande",
"memory": "Mémoire",
"disks": "Disques",
"temperature": "Température",
"network": "Réseau",
"power": "Alimentation",
"topProcesses": "Processus principaux",
"filterPlaceholder": "filtrer par nom ou image…",
"name": "Nom",
"status": "État",
"mem": "Mém",
"uptime": "Temps de fonctionnement",
"scopeDockers": "dockers"
},
"codeEditor": {
"closeFile": "Fermer {{name}}",
"loading": "Chargement…",
"noFiles": "Aucun fichier",
"explorer": "Explorateur",
"openAFile": "Ouvrez un fichier depuis l'explorateur pour commencer à modifier",
"refresh": "Actualiser l'arborescence et recharger les fichiers ouverts non modifiés",
"readFailed": "Échec de la lecture du fichier",
"saved": "Fichier enregistré",
"saveFailed": "Échec de l'enregistrement du fichier",
"title": "Éditeur de code"
},
"widgets": {
"resync": "Resynchroniser les widgets (rechargez pour prendre en compte les widgets récemment déployés)"
},
"preview": {
"title": "Aperçu",
"reload": "Recharger",
"changeUrl": "Changer d'URL",
"openNewTab": "Ouvrir dans un nouvel onglet",
"pointAtApp": "Pointez ce panneau vers une application en cours d'exécution",
"open": "Ouvrir",
"hint": "Tout ce que le navigateur peut atteindre. Cela reste sur ce panneau et survit à un rechargement.",
"connecting": "Connexion…"
},
"sendOff": {
"files": "Fichiers",
"where": "Où",
"loading": "Chargement…",
"landsIn": "Arrive dans leur",
"landedIn": "Arrivé dans",
"whoMaySend": "Qui peut m'envoyer des fichiers",
"allowedLandIn": "Les fichiers de qui vous autorisez arrivent dans",
"removeFile": "Supprimer {{name}}",
"sendTo": "Envoyer à {{name}}",
"turningOff": "Désactiver quelqu'un arrête les envois futurs et laisse intact ce qu'il a déjà envoyé."
},
"panelName": {
"appStore": "Boutique d'applications",
"appDetail": "Détail de l'application",
"calendar": "Calendrier",
"contacts": "Contacts",
"categories": "Catégories",
"chat": "Chat",
"chatSessions": "Sessions",
"codeEditor": "Éditeur de code",
"commander": "Commander",
"commanders": "Commanders",
"dashboardPreview": "Aperçu du Dashboard",
"dashboards": "Dashboards",
"fileBrowser": "Explorateur de fichiers",
"pluginDetail": "Détail du plugin",
"plugins": "Plugins",
"preview": "Aperçu",
"qrReceive": "Réception QR",
"qrSend": "Envoi QR",
"scope": "Portée",
"systemMonitor": "Moniteur système",
"widgets": "Widgets",
"terminal": "Terminal",
"hostTerminal": "Terminal hôte",
"runningShells": "Shells en cours d'exécution",
"pm2Processes": "processus pm2"
},
"dashboards": {
"title": "Dashboards",
"new": "Nouveau Dashboard",
"delete": "Supprimer le Dashboard",
"confirmDelete": "Voulez-vous vraiment supprimer",
"search": "Rechercher des Dashboards",
"name": "Nom",
"description": "Description",
"open": "Ouvrir le Dashboard",
"selectOne": "Sélectionnez un Dashboard ou créez-en un nouveau",
"namePlaceholder": "Mon Dashboard",
"descriptionPlaceholder": "À quoi sert ce Dashboard ?",
"template": "Modèle",
"create": "Créer",
"templates": {
"single": "Unique",
"twoColumns": "2 colonnes",
"mainSide": "Principal + latéral",
"sidebar": "Barre latérale",
"grid": "Grille 2x2",
"colsBottom": "Colonnes + bas"
}
},
"fileViewer": {
"readAloud": "Lire à voix haute",
"ocr": "Extraire le texte (OCR)",
"transcribe": "Transcrire",
"extractAudio": "Extraire l'audio",
"extract": "Extraire",
"saveNextTo": "Enregistrer à côté de l'original",
"download": "Télécharger",
"pdfViewer": "Visionneuse PDF",
"videoFailed": "Échec du chargement de la vidéo",
"imageFailed": "Échec du chargement de l'image",
"audioFailed": "Échec du chargement du fichier audio",
"audioTrack": "Piste audio",
"subtitles": "Sous-titres",
"track": "Piste {{n}}",
"saved": "Fichier enregistré",
"saveFailed": "Échec de l'enregistrement du fichier",
"scrollTop": "Remonter en haut",
"speechFailed": "Échec de la génération de l'audio vocal",
"ocrFailed": "Échec de l'extraction du texte de l'image",
"transcribeFailed": "Échec de la transcription de l'audio",
"extractAudioFailed": "Échec de l'extraction de l'audio de la vidéo",
"extractFailed": "Échec de l'extraction de l'archive",
"savedAs": "Enregistré sous “{{name}}”",
"extractedTo": "Extrait vers “{{name}}”",
"downloadInstead": "Télécharger à la place",
"archiveFile": "Fichier archive"
},
"plugins": {
"loading": "Chargement…",
"onThisMachine": "Sur cette machine",
"noPluginsIn": "Aucun plugin dans",
"available": "Disponibles",
"needsOnThisMachine": "Nécessaire sur cette machine",
"install": "Installer",
"clonesInto": "Clone le dépôt dans",
"deleteData": "Supprimer aussi toutes ses données",
"noTables": "Ce plugin ne déclare aucune table, il n'y a donc aucune donnée à conserver.",
"selectOne": "Sélectionnez un plugin.",
"disable": "Désactiver",
"enable": "Activer",
"publisher": "Éditeur",
"version": "Version",
"needsPlatform": "Nécessite la plateforme",
"wouldMount": "Serait monté",
"ships": "Inclut",
"permissions": "Permissions",
"source": "Source",
"mountsAt": "Monté sur",
"sidecar": "Sidecar",
"deleteNamed": "Supprimer {{name}}",
"updateTo": "Mettre à jour vers {{version}}"
},
"commandHq": {
"selectOne": "Sélectionnez un commander, ou démarrez-en un nouveau.",
"startConversation": "Démarrer la conversation",
"name": "Nom",
"nameHint": "Appliqué à la conversation dès son démarrage, au lieu de laisser le harness la nommer d'après votre premier message.",
"workingDirectory": "Répertoire de travail",
"cwdHint": "D'où toute commande qu'il exécute est résolue, et où son historique est conservé. Il ne peut pas être déplacé ensuite.",
"new": "Nouveau commander",
"spawned": "Lancé",
"deleteQ": "Supprimer ?",
"uncategorised": "Sans catégorie",
"loading": "Chargement…",
"platformDeploys": "Déploiements de la plateforme",
"endSession": "Terminer la session — la libère pour qu'elle puisse être reprise ailleurs",
"confirmDeleteHint": "Confirmer la suppression — supprime aussi la conversation, pas seulement l'entrée Command HQ",
"confirmDelete": "Confirmer la suppression",
"cancelDelete": "Annuler la suppression",
"deleteCommander": "Supprimer le commander",
"none": "Aucun commander ici pour l'instant.",
"startOne": "En démarrer un",
"confirmDeleteNamed": "Confirmer la suppression de {{name}}",
"deleteNamed": "Supprimer {{name}}",
"categories": "Catégories",
"noCategories": "Aucune catégorie pour l'instant",
"newCategory": "Nouvelle catégorie",
"allCommanders": "Tous les commanders",
"categoryName": "Nom de la catégorie",
"guide": {
"whatIsIt": "Ce qu'est un commander",
"starting": "En démarrer un",
"examples": "Exemples de premier message",
"whileRunning": "Pendant qu'il tourne",
"whatIsItBody1": "Un agent qui continue de travailler quand vous n'êtes pas là — il attend qu'un événement se produise, agit en conséquence, et rend compte.",
"whatIsItBody2": "C'est une conversation ordinaire. Vous commencez en lui disant, avec vos propres mots, ce qu'il doit surveiller et ce qu'il doit faire.",
"startingBody1": "Décrivez ici la tâche, comme vous le feriez à n'importe qui. Dites ce qu'il doit surveiller, où, et ce qu'il doit faire quand cela se déclenche.",
"startingBody2": "TODO : les étapes concrètes, une fois le flux de création établi. Ceci remplace “ouvrir la boîte de dialogue de tâche en arrière-plan et armer un surveillant”, que personne ne pouvait deviner.",
"examplesBody1": "TODO : deux ou trois exemples de premiers messages, une fois le flux de création établi. Ils transforment “décrivez la tâche” d'une instruction en quelque chose qu'un nouvel utilisateur peut suivre.",
"whileRunningBody1": "TODO : ce que le propriétaire voit — où son coût s'affiche, comment lire ce que font les agents qu'il a lancés, et comment l'arrêter.",
"draft": "brouillon"
},
"startHint": "Il va regarder autour de ce répertoire, puis vous demander ce qu'il doit surveiller.",
"startBlocked": "{{missing}} — ensuite ceci le démarre.",
"confirmDeletingNamed": "Confirmer la suppression de {{name}}"
},
"chatHistory": {
"newChat": "Nouveau Chat",
"tryAgain": "Réessayer",
"refresh": "Actualiser",
"refreshSessions": "Actualiser les sessions",
"newChatInDir": "Nouveau Chat dans un répertoire…",
"loadFailed": "Impossible de charger les sessions",
"reading": "Lecture des transcriptions…",
"noSessions": "Aucune session pour l'instant",
"liveFailed": "Impossible de lire les sessions en direct",
"asking": "Question posée à l'agent…",
"tabs": {
"tree": "Arborescence",
"latest": "Récentes",
"live": "En direct",
"agents": "Agents",
"commanders": "Commanders"
},
"notifyPreference": "Préférence de notification",
"notify": {
"everyTurn": "À chaque fin de tour",
"inputNeeded": "Seulement quand une réponse est nécessaire",
"none": "Aucune"
},
"notificationsLabel": "Notifications : {{choice}}",
"chooseDir": "Choisissez un répertoire de travail",
"create": "Créer",
"newFolder": "Nouveau dossier",
"selectFolder": "Sélectionner ce dossier",
"newFolderName": "Nom du nouveau dossier",
"noSubfolders": {
"one": "Aucun sous-dossier visible — {{count}} sous-dossier masqué est filtré",
"other": "Aucun sous-dossier visible — {{count}} sous-dossiers masqués sont filtrés"
},
"showHidden": "Afficher les masqués",
"showHiddenCount": "Afficher les masqués ({{count}})",
"selectSession": "Sélectionnez une session à afficher",
"endSession": "Terminer la session — arrête tout tour en cours et la libère pour qu'elle puisse être reprise ailleurs",
"noReadable": "Aucune conversation lisible ici.",
"noConversations": "Aucune conversation pour l'instant",
"newChatIn": "Nouveau Chat dans {{path}}",
"sessionTitle": "Titre de la session",
"saveTitle": "Enregistrer le titre",
"cancelRename": "Annuler le renommage",
"confirmDelete": "Confirmer la suppression",
"cancelDelete": "Annuler la suppression",
"rename": "Renommer",
"deleteSession": "Supprimer la session",
"renameFailed": "Impossible de renommer la session : {{reason}}",
"deleteFailed": "Impossible de supprimer la session : {{reason}}",
"deleteAll": "Supprimer les {{count}} ?",
"confirmDeletingNamed": "Confirmer la suppression de {{name}}",
"renameNamed": "Renommer {{name}}",
"deleteNamed": "Supprimer {{name}}",
"newChatTitle": "Nouveau Chat",
"renameHint": "Cliquez pour renommer ce Chat",
"hideHidden": "Masquer les masqués"
},
"chat": {
"newChat": "Nouveau Chat",
"rename": "Renommer",
"deleteQ": "Supprimer “{{title}}” ?",
"duplicateAgent": "Il existe déjà un agent nommé “{{name}}” sur ce Dashboard",
"composerPlaceholder": "Sur quoi voulez-vous travailler maintenant ?",
"turnFinished": "Tour terminé · pas de message de clôture",
"job": {
"title": "Tâche en arrière-plan",
"runsIn": "S'exécute dans",
"command": "Commande — exécuter exactement ceci",
"prompt": "Prompt — décrivez-le, l'agent se débrouille",
"watch": "Surveiller quelque chose",
"watchHint": "En texte libre. L'agent posera des questions avant d'armer si quelque chose n'est pas clair.",
"handBack": "Sortir et rendre la main à l'agent, puis réarmer",
"keepGoing": "Continuer — agir à chaque fois et poursuivre",
"stopAfter": "Arrêter après",
"start": "Démarrer",
"watchExample": "surveiller nos dépôts Gitea ; à chaque nouveau push, le récupérer et l'envoyer vers le miroir GitHub",
"startJob": "Démarrer une tâche en arrière-plan ou un surveillant"
},
"tasks": {
"restart": "Redémarrer",
"pinned": "Épinglée",
"clearFinished": "Effacer toutes les terminées",
"clearFinishedTasks": "Effacer toutes les tâches terminées",
"stopJob": "Arrêter cette tâche",
"dismiss": "Ignorer",
"rerun": "L'arrêter et relancer la même commande",
"collapse": "Replier",
"collapseDetail": "Replier le détail de la tâche",
"stopNamed": "Arrêter {{label}}",
"dismissNamed": "Ignorer {{label}}",
"status": "Tâche {{status}}."
},
"usage": {
"noSignIn": "Aucune connexion enregistrée — exécutez /login pour utiliser celle-ci",
"updating": "Mise à jour…",
"unreadable": "Impossible de lire l'usage sur cette machine.",
"loading": "Chargement…",
"limits": "Limites",
"byDay": "Tokens par jour",
"byModel": "Tokens par modèle",
"title": "Usage et limites",
"modelBreakdown": "Entrée {{input}} · sortie {{output}} · lecture de cache {{cacheRead}} · écriture de cache {{cacheWrite}}",
"switchTo": "Basculer vers {{account}}",
"switchToOrg": "Basculer vers {{account}} ({{org}})",
"signInExpired": "{{account}} — connexion expirée, exécutez /login"
},
"attach": {
"image": "Image",
"textFile": "Fichier texte",
"webpageUrl": "URL de page web",
"attach": "Joindre",
"readFailed": "Impossible de lire {{name}} : {{reason}}",
"unknownError": "erreur inconnue",
"webpageTitle": "Joindre une page web",
"webpageHint": "Saisissez une URL à extraire et joindre comme contexte.",
"scrapeFailed": "Échec de l'extraction de la page web",
"uploadFailed": "Échec de l'envoi de l'image",
"dropImages": "Déposez des images à joindre"
},
"signIn": {
"title": "Se connecter à Claude",
"done": "Connecté. Les nouveaux tours utilisent ces identifiants — aucun redémarrage nécessaire.",
"asking": "Demande d'un lien de connexion à la CLI…",
"openHint": "Ouvrez ce lien, autorisez, puis collez ici le code qui vous est donné.",
"authorise": "Autoriser sur claude.com",
"copyLink": "Copier le lien, pour l'ouvrir dans un autre navigateur ou sur votre téléphone",
"pasteCode": "collez le code"
},
"message": {
"copyCode": "Copier le code",
"copyMarkdown": "Copier en bloc markdown",
"showMore": "Afficher plus ({{count}} lignes de plus)",
"subagent": "Sous-agent",
"outputError": "Sortie — erreur",
"contextCleared": "Contexte effacé — rien au-dessus de ceci n'est en mémoire",
"interrupted": "Interrompu par l'utilisateur",
"agentRestarted": "Agent redémarré — tour interrompu",
"retry": "Réessayer",
"planUnchanged": "Mode plan inchangé",
"approve": "Approuver",
"frontmatter": "Frontmatter",
"systemPrompt": "Prompt système",
"reviseHint": "Que faut-il changer ? L'agent révise le plan à partir de ceci.",
"waiting": "En attente d'une réponse",
"narrationFailed": "La narration a été générée mais n'a pas pu être lue.",
"readAloudFailed": "Impossible de lire ce message à voix haute : {{reason}}",
"compacted": "Contexte compacté",
"compactedAuto": "Contexte compacté automatiquement",
"answerSent": "Réponse envoyée — en attente de l'agent",
"other": "Autre..."
},
"composer": {
"stop": "Arrêter",
"sendIntoTurn": "Envoyer ceci dans le tour déjà en cours",
"removeFromQueue": "Retirer de la file",
"send": "Envoyer",
"startHint": "Envoyez un message pour commencer",
"jumpToLatest": "Aller au message le plus récent",
"noAudio": "Aucun audio n'a été capté"
},
"agent": {
"gone": "L'agent de ce panneau n'existe plus.",
"forget": "L'oublier",
"name": "Nommer cet agent",
"peers": "Pairs : {{names}}"
},
"copy": "Copier"
},
"files": {
"folderGone": "Ce dossier a disparu — retour au début",
"searchFailed": "La recherche a échoué",
"createFolderFailed": "Échec de la création du dossier",
"createFileFailed": "Échec de la création du fichier",
"uploadFailed": "Échec de l'envoi des fichiers",
"renameFailed": "Échec du renommage",
"deleteFailed": "Échec de la suppression",
"deleteSomeFailed": "Échec de la suppression de certains éléments",
"downloadFailed": "Échec du téléchargement",
"pathCopied": "Chemin copié",
"generatingSpeech": "Génération de l'audio vocal…",
"speechFailed": "Échec de la génération de l'audio vocal",
"extracting": "Extraction de l'archive…",
"extractFailed": "Échec de l'extraction de l'archive",
"cloned": "Dépôt cloné",
"cloneFailed": "Échec du clonage du dépôt",
"cloning": "Clonage…",
"duplicateFailed": "Échec de la duplication",
"compressing": "Compression…",
"compressFailed": "Échec de la compression",
"pasteFailed": "Échec du collage",
"pasteNeedsSecure": "Coller depuis le presse-papiers nécessite un contexte sécurisé (https). Faites glisser le fichier à la place.",
"copiedItems": {
"one": "{{count}} élément copié",
"other": "{{count}} éléments copiés"
},
"cutItems": {
"one": "{{count}} élément coupé",
"other": "{{count}} éléments coupés"
},
"pastedItems": {
"one": "{{count}} élément collé",
"other": "{{count}} éléments collés"
},
"uploadedFiles": {
"one": "{{count}} fichier envoyé",
"other": "{{count}} fichiers envoyés"
},
"deleteItemsQ": {
"one": "Supprimer {{count}} élément ?",
"other": "Supprimer {{count}} éléments ?"
},
"deletedItems": {
"one": "{{count}} élément supprimé",
"other": "{{count}} éléments supprimés"
},
"createdFolder": "Dossier “{{name}}” créé",
"created": "“{{name}}” créé",
"renamedTo": "Renommé en “{{name}}”",
"deleteQ": "Supprimer “{{name}}” ?",
"deleted": "“{{name}}” supprimé",
"extractedTo": "Extrait vers “{{name}}”",
"duplicated": "“{{name}}” dupliqué",
"pastedInto": "Collé dans “{{name}}”",
"unpinned": "“{{name}}” désépinglé",
"pinned": "“{{name}}” épinglé",
"menu": {
"copyPath": "Copier le chemin",
"chatAboutThis": "En discuter dans le Chat",
"paste": "Coller",
"newFile": "Nouveau fichier",
"newFolder": "Nouveau dossier",
"uploadFiles": "Envoyer des fichiers",
"uploadFolder": "Envoyer un dossier",
"cloneRepository": "Cloner le dépôt",
"hideHiddenFiles": "Masquer les fichiers cachés",
"showHiddenFiles": "Afficher les fichiers cachés",
"createDashboardHere": "Créer un Dashboard ici",
"downloadVideo": "Télécharger la vidéo",
"dictate": "Dicter",
"dropToUpload": "Déposez des fichiers pour les envoyer",
"openFolder": "Ouvrir le dossier",
"showLocation": "Afficher l'emplacement",
"download": "Télécharger",
"noResults": "Aucun résultat trouvé",
"open": "Ouvrir",
"openInEditor": "Ouvrir dans l'éditeur",
"openInEditorTooLarge": "Ouvrir dans l'éditeur (trop volumineux)",
"pasteIntoFolder": "Coller dans le dossier",
"readAloud": "Lire à voix haute",
"extract": "Extraire",
"runTask": "Exécuter la tâche",
"runAgent": "Exécuter l'agent",
"cut": "Couper",
"copy": "Copier",
"duplicate": "Dupliquer",
"rename": "Renommer",
"compress": "Compresser",
"compressSelection": "Compresser la sélection",
"unpin": "Désépingler",
"pin": "Épingler",
"chat": "Chat...",
"delete": "Supprimer",
"gitClone": "Cloner via Git",
"searchFiles": "Rechercher des fichiers...",
"clearSelection": "Effacer la sélection",
"selected": "sélectionnés",
"refresh": "Actualiser"
},
"grid": {
"empty": "Ce dossier est vide",
"sort": "Trier :",
"name": "Nom",
"type": "Type",
"size": "Taille",
"date": "Date"
}
},
"taskRunner": {
"stop": "Arrêter",
"taskFailed": "La tâche a échoué",
"taskComplete": "Tâche terminée",
"noSubtitles": "Aucun sous-titre",
"none": "Aucun",
"noVideos": "Aucune vidéo trouvée dans ce dossier.",
"differentLayout": "Disposition différente — exécutez-les séparément :",
"skipped": "Ignorés (convertir séparément) :",
"analyzing": "Analyse des fichiers…",
"close": "Fermer",
"runningOn": "Exécuté sur",
"queue": "File d'attente",
"concurrency": "Concurrence",
"model": "Modèle",
"steps": "Étapes",
"stopPipeline": "Arrêter le pipeline",
"pipelineFailed": "Le pipeline a échoué",
"pipelineComplete": "Pipeline terminé",
"viewInJobs": "Voir dans les Jobs",
"copyOutput": "Copier la sortie",
"track": "Piste {{n}}",
"runFromStep": "Exécuter à partir de l'étape {{n}}"
},
"newEntry": {
"nameReserved": "Ce nom est réservé",
"nameSlash": "Le nom ne peut pas contenir \"/\"",
"nameExists": "\"{{name}}\" existe déjà ici",
"urlSpaces": "Une URL ne peut pas contenir d'espaces",
"cancel": "Annuler",
"file": {
"title": "Nouveau fichier",
"description": "Incluez l'extension — c'est ce que l'éditeur utilise pour choisir le langage. Il s'ouvre dès sa création.",
"label": "Nom du fichier",
"placeholder": "notes.txt",
"submit": "Créer et ouvrir"
},
"folder": {
"title": "Nouveau dossier",
"description": "Créé dans le dossier que vous consultez.",
"label": "Nom du dossier",
"placeholder": "documents",
"submit": "Créer le dossier"
},
"clone": {
"title": "Cloner le dépôt",
"description": "Cloné dans le dossier que vous consultez, sous son propre nom.",
"label": "URL du dépôt",
"placeholder": "https://github.com/owner/repo.git",
"submit": "Cloner"
}
},
"videoDownload": {
"title": "Télécharger la vidéo",
"descriptionInput": "Collez une URL de vidéo ou de playlist — les détails sont récupérés avant le téléchargement.",
"descriptionPreview": "Vérifiez et téléchargez.",
"urlPlaceholder": "https://www.youtube.com/watch?v=… ou …/playlist?list=…",
"audioOnly": "Extraire uniquement l'audio (mp3)",
"fetch": "Récupérer",
"back": "Retour",
"audioOnlyLabel": "Audio uniquement",
"video": "Vidéo",
"itemCount": {
"one": "{{count}} élément",
"other": "{{count}} éléments"
},
"subfolderPlaceholder": "Sous-dossier (facultatif) — laissez vide pour ce dossier",
"downloadAll": "Tout télécharger",
"couldNotFetch": "Impossible de récupérer",
"downloading": "Téléchargement…",
"saving": "Enregistrement…",
"saved": "Enregistré",
"retry": "Réessayer",
"download": "Télécharger",
"couldNotFetchInfo": "Impossible de récupérer les informations",
"timedOut": "Délai dépassé",
"downloadFailed": "Échec du téléchargement",
"couldNotStart": "Impossible de démarrer le téléchargement"
},
"fileBrowserWidget": {
"title": "Explorateur de fichiers",
"search": "Rechercher...",
"browse": "Parcourir",
"recent": "Récents",
"pinned": "Épinglés",
"noResults": "Aucun résultat",
"emptyDirectory": "Répertoire vide",
"noPinnedFiles": "Aucun fichier épinglé",
"noRecentFiles": "Aucun fichier récent",
"uploading": "Envoi en cours...",
"empty": "Ce dossier est vide",
"sort": "Trier :"
},
"agentRunner": {
"agentRunning": "Agent en cours d'exécution",
"keepsGoing": "Il continue à s'exécuter, que cette fenêtre reste ouverte ou non. Suivez-le dans le Chat — la session la plus récente en haut.",
"openChat": "Ouvrir le Chat",
"target": "Cible",
"starting": "Démarrage…",
"runAgent": "Exécuter l'agent",
"startFailed": "Échec du démarrage de l'agent"
},
"dictate": {
"title": "Dicter",
"description": "Enregistrement audio en cours. Appuyez sur Entrée ou cliquez sur Arrêter pour enregistrer.",
"recording": "Enregistrement...",
"startingStatus": "Démarrage...",
"stopSave": "Arrêter et enregistrer",
"noAudioCaptured": "Aucun audio n'a été capté",
"micAccessFailed": "Impossible d'accéder au microphone",
"recordingFailed": "Échec de l'enregistrement"
}
}
@@ -2,11 +2,12 @@ import { describe, expect, test } from 'bun:test';
import { findMissingKeys } from 'i18n';
import en from './en-GB.json';
import ptPT from './pt-PT.json';
import frFR from './fr-FR.json';
// D2: every shipped locale ships a COMPLETE catalog. Phase 0 scaffolds this catalog with no keys yet
// (no panel is converted in this phase) — the test still runs so it is load-bearing from day one.
// D2: every shipped locale ships a COMPLETE catalog. A key en-GB has and another locale lacks is a red
// build, not a warning — there is no regional fallback to paper over the gap.
describe('officerdev (panel.*) locale catalogs', () => {
test('pt-PT has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT })).toEqual([]);
test('every shipped locale has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT, 'fr-FR': frFR })).toEqual([]);
});
});