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([]);
});
});