From a28e730d861a48d71a315e2467d4d431b4a3da36 Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 08:47:51 +0100 Subject: [PATCH] Translate 350 missing French strings --- lib/l10n/app_fr.arb | 352 ++++++++++++++++++++++++++++++++- test/french_coverage_test.dart | 54 +++++ 2 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 test/french_coverage_test.dart diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1d3f1e350..71c054c18 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2877,5 +2877,355 @@ "strengthDelete": "Supprimer", "strengthSaveFailed": "Échec de l'enregistrement des modifications", "reportContactLabel": "Contact (facultatif)", - "reportContactHint": "E-mail ou Discord, seulement si vous voulez une réponse" + "reportContactHint": "E-mail ou Discord, seulement si vous voulez une réponse", + "alarmNextLabel": "PROCHAINE", + "alarmScheduleGroup": "Programme hebdomadaire", + "alarmWakeTimeRowTitle": "Heure de réveil", + "devicesWhichSourceWins": "Quelle source l’emporte", + "devicesWhichSourceWinsSub": "Quand deux de vos appareils mesurent la même chose", + "devicesResetToDefault": "Revenir à l’ordre par défaut", + "devicesCouldNotReachCorosWatch": "Impossible de la joindre. Elle doit être à proximité et non connectée à une autre application.", + "deviceFilterAllDevices": "Tous les appareils", + "devicesSyncNowSubListen": "Écouter ce qu’il envoie en ce moment", + "devicePickerTitle": "Connectez vos appareils", + "devicePickerSubtitle": "Apportez ce que vous utilisez. Vous pourrez en ajouter à tout moment.", + "devicePickerBrowseByCategory": "Parcourir par catégorie", + "devicePickerPrivacyNote": "Tout reste sur ce téléphone. Rien n’est envoyé nulle part, sauf si vous choisissez de l’exporter.", + "devicePickerSearchLabel": "Rechercher des appareils ou des types", + "devicePickerSearchHint": "Rechercher des appareils ou des types", + "devicePickerNothingNearbyTitle": "Rien trouvé pour l’instant", + "devicePickerNothingNearbyBody": "Un appareil ne répond que lorsqu’il est réveillé, porté et pas déjà connecté à un autre téléphone ou à une autre application.", + "devicePickerNearby": "À proximité", + "devicePickerScanning": "Recherche d’appareils à proximité…", + "devicePickerBlurbBand": "Le bracelet autour duquel cette application est conçue. WHOOP 4 ou 5.", + "devicePickerBlurbRing": "Lit la bague directement — sans compte ni abonnement Oura.", + "devicePickerBlurbPolarPmd": "Un Polar Verity Sense ou OH1. Rythme des battements mesuré optiquement, transmis pendant une séance, comme une ceinture thoracique.", + "devicePickerBlurbSensor": "Une ceinture thoracique ou un brassard, pour le rythme des battements pendant une séance.", + "devicePickerBlurbCoros": "Une montre de sport. Lit la batterie et la fréquence cardiaque en direct — les activités enregistrées restent sur la montre.", + "devicePickerBlurbColmi": "Une bague Colmi. Synchronise la fréquence cardiaque, les phases de sommeil, les pas, la SpO2, la VFC et le stress ; les valeurs que la bague calcule elle-même sont indiquées comme celles de Colmi. Expérimental.", + "devicePickerCannotPairFromHere": "Cet appareil ne peut pas être jumelé depuis cet écran.", + "welcomeLabResultsWritten": "{n, plural, one{1 résultat d’analyse enregistré} other{{n} résultats d’analyse enregistrés}}", + "readinessDetailBreakdownNoScoreNote": "Une vue plus souple des quatre mêmes indicateurs. Ils ne donnent pas le score du jour, qui manque pour la raison indiquée ci-dessus.", + "readinessDetailNightsOfTarget": "{have} nuits sur {need}", + "readinessDetailOutsideRange": "non, hors de votre plage habituelle", + "readinessDetailLiftingScore": "fait monter votre score", + "readinessDetailLoweringScore": "fait baisser votre score", + "readinessDetailAskHrv": "Votre VFC est-elle dans sa plage habituelle ?", + "readinessDetailAskRhr": "Votre fréquence cardiaque au repos est-elle dans sa plage habituelle ?", + "readinessDetailAskResp": "Votre fréquence respiratoire est-elle dans sa plage habituelle ?", + "readinessDetailAskTemp": "Votre température cutanée est-elle dans sa plage habituelle ?", + "dayTimelineDeviceBounded": "Le détail par appareil n’est conservé que pour les jours récents. Avant le {oldest}, on sait quel appareil enregistrait, mais pas ce qu’il a mesuré.", + "metricDetailUsingForX": "{device} est utilisé pour {metric}.", + "metricDetailPreferX": "Préférer {device}", + "metricDetailHistoryKeepsSource": "Les jours déjà terminés conservent la source avec laquelle ils ont été calculés.", + "metricDetailDimmedCaption": "Ce sont les jours où {device} a contribué. La ligne représente votre mesure combinée.", + "metricDetailSlotNoValueRecording": "{day}, aucune valeur · {who}", + "metricDetailSlotNothingRecording": "{day}, rien n’enregistrait", + "metricDetailRankToday": "Aujourd’hui est plus élevé que {pct} % de vos propres jours.", + "metricDetailRankTodayBand": "Aujourd’hui est plus élevé que {pct} % de vos propres jours — {band}.", + "metricDetailRankFrom": "Votre mesure du {date} est plus élevée que {pct} % de vos propres jours.", + "metricDetailRankFromBand": "Votre mesure du {date} est plus élevée que {pct} % de vos propres jours — {band}.", + "homeSyncedThrough": "Synchronisé jusqu’à {when}", + "homeSyncedNever": "Aucune donnée du bracelet pour l’instant", + "homeSyncingTitle": "Synchronisation avec votre bracelet", + "homeSyncingBody": "Récupération des données en cours — cela peut prendre quelques minutes s’il y a beaucoup de retard.", + "homeAnalyzingTitle": "Calcul des chiffres de la nuit dernière", + "homeAnalyzingBody": "Les données sont là — le sommeil, la récupération et l’effort arrivent.", + "homeConnectingTitle": "Connexion à votre bracelet", + "homeConnectingBody": "Patientez — cela prend généralement quelques secondes.", + "homeBriefingTitle": "Bilan", + "homeBriefingSubtitleEmpty": "Touchez pour rédiger le résumé du jour", + "sleepDetailNotSleep": "Pas du sommeil", + "sleepDetailEditNaps": "Siestes", + "sleepDetailRejectedTitle": "Marqué comme pas du sommeil", + "sleepDetailRejectedBody": "Vous avez indiqué que cette période n’était pas du sommeil, elle n’a donc aucun score.", + "sleepDetailUndoRejection": "Annuler — revenir au mode automatique", + "sleepDetailIWasAsleep": "Je dormais", + "sleepDetailAskEnough": "Avez-vous assez dormi ?", + "sleepDetailAskDeep": "Avez-vous eu votre sommeil profond habituel ?", + "sleepDetailAskSleptThrough": "Avez-vous dormi pendant tout votre temps au lit ?", + "sleepDetailAskOnTime": "Vous êtes-vous endormi à votre heure habituelle ?", + "metricHeartEffortTitle": "Effort cardiaque", + "activitySetupWorkLabel": "Effort", + "activitySetupRestLabel": "Repos", + "activitySetupRoundsLabel": "Tours", + "activitySetupNoMetEstimate": "Pas d’estimation à l’avance : cette activité n’a pas de niveau d’effort publié. Les calories viennent de la fréquence cardiaque une fois votre profil et votre plage de fréquence cardiaque connus.", + "activitySummaryCalorieNoMet": "Estimé à partir de votre fréquence cardiaque et de votre poids. Cette activité n’a pas de niveau d’effort publié, aucun n’est donc utilisé.", + "activityZonesAnchorManual": "Définis par vous, pas calculés — ces cinq seuils remplacent ceux que votre âge ou un maximum mesuré auraient donnés.", + "activityZonesSetYourOwnLink": "Définir vos propres zones", + "activityZonesEditYourOwnLink": "Modifier vos propres zones", + "activityZonesEditYourOwnTitle": "Définir vos propres zones", + "activityZonesEditYourOwnBody": "Cinq seuils en bpm, du plus bas au plus haut — là où chaque zone commence. Effacez les cinq pour revenir aux zones que nous calculons pour vous.", + "activityZonesNeedAllFive": "Les cinq seuils sont nécessaires, du plus bas au plus haut. Rien n’a été enregistré.", + "activityZonesMustAscend": "Chaque zone doit commencer plus haut que la précédente. Rien n’a été enregistré.", + "activityZonesCurrentEdgesNote": "Les plages sont vos limites de zones actuelles ; des séances plus anciennes ont pu être comptées avec d’autres.", + "activitySummaryShareToStrava": "Partager sur Strava", + "activitySummaryLapFastest": "{time}, le plus rapide", + "profileCommunityGroup": "Communauté", + "profileGithubTitle": "GitHub", + "profileGithubSub": "Mettez une étoile pour montrer votre soutien — cela aide le projet à grandir", + "profileRedditTitle": "Reddit", + "profileRedditSub": "Rejoignez r/OpenStrap — partagez vos réussites, vos questions, tout ce que vous voulez", + "profileDiscordTitle": "Discord", + "profileDiscordSub": "Échangez avec d’autres utilisateurs et les personnes qui le développent", + "profileSponsorTitle": "Sponsoriser", + "profileSponsorSub": "C’est un projet libre et gratuit — le sponsoring le fait vivre", + "nudgeDiscordTitle": "Venez dire bonjour", + "nudgeDiscordBody": "D’autres utilisateurs d’OpenStrap se retrouvent sur Discord — bugs, idées, et des personnes qui utilisent le même bracelet que vous.", + "nudgeDiscordCta": "Rejoindre Discord", + "nudgeDonateTitle": "OpenStrap vous plaît ?", + "nudgeDonateBody": "C’est un projet libre et gratuit, sans abonnement. Le sponsoring permet de le maintenir.", + "nudgeDonateCta": "Soutenir le projet", + "nudgeNotNow": "Pas maintenant", + "nudgeDontShowAgain": "Ne plus afficher", + "signalRrIntervals": "Intervalles entre battements", + "signalHr1Hz": "Fréquence cardiaque continue", + "signalHrSparse": "Fréquence cardiaque", + "signalAccel1Hz": "Mouvement", + "signalAccelHighRate": "Mouvement à haute fréquence", + "signalPpgGreen": "PPG verte", + "signalPpgRedIr": "PPG rouge/infrarouge", + "signalSkinTempRaw": "Température cutanée", + "signalVendorScalars": "Les propres chiffres de l’appareil", + "missingSignalRrIntervals": "pas d’intervalles entre battements", + "missingSignalHr1Hz": "pas de fréquence cardiaque continue", + "missingSignalHrSparse": "pas de fréquence cardiaque", + "missingSignalAccel1Hz": "pas d’accéléromètre", + "missingSignalAccelHighRate": "pas d’accéléromètre à haute fréquence", + "missingSignalPpgGreen": "pas de PPG verte", + "missingSignalPpgRedIr": "pas de PPG rouge/infrarouge", + "missingSignalSkinTempRaw": "pas de capteur de température", + "missingSignalVendorScalars": "ne fournit aucune valeur propre", + "missingSignalUnknown": "ne peut pas fournir cela", + "devicesSensorLimit": "{count, plural, =1{Ce téléphone peut jumeler au maximum un capteur en plus de votre bracelet. Retirez-le pour en ajouter un autre.} other{Ce téléphone peut jumeler au maximum {count} capteurs en plus de votre bracelet. Retirez-en un pour en ajouter un autre.}}", + "devicesOrderNotSaved": "Cet ordre n’a pas pu être enregistré. Rien n’a changé.", + "devicesRequestNotSaved": "Cette demande n’a pas pu être enregistrée. Veuillez réessayer.", + "activityReviewTitle": "Activités détectées", + "activityPossibleNap": "Sieste possible", + "activityPossibleWorkout": "Séance possible", + "activityConfirm": "Confirmer", + "activityEdit": "Modifier", + "activityDiscard": "Ignorer", + "activityChangeSport": "Changer de sport", + "activityNotAWorkout": "Ce n’est pas une séance", + "activitySaveConfirm": "Enregistrer et confirmer", + "activityNothing": "Rien à vérifier", + "activityEmptyBody": "Les siestes et séances possibles apparaîtront ici.", + "activityLoadFailed": "Impossible de charger les activités. Réessayez.", + "activityReviewProblem": "Impossible de terminer", + "activityReview": "Vérifier", + "activityLoading": "Chargement…", + "activityPending": "{count} à vérifier", + "activityLengthMinutes": "{minutes} min", + "activityStart": "Début", + "activityEnd": "Fin", + "activityCheckTimes": "Vérifiez les heures", + "activityNapWindow": "Choisissez une période passée entre 5 minutes et 3 heures.", + "activityEditNap": "Modifier la sieste", + "activityEditWorkout": "Modifier la séance", + "activityNapAlerts": "Siestes détectées", + "activityPushHint": "Notifications push uniquement. Les suggestions restent dans Activités détectées.", + "activityCatalogueName": "{key, select, kcardio{Cardio} kstrength{Force} ksports{Sports} kathletics{Athlétisme} koutdoor{Plein air} kmindbody{Corps et esprit} keveryday{Quotidien} kother{Autre} krunning{Course à pied} ktrailrunning{Trail} kwalking{Marche} khiking{Randonnée} kcycling{Vélo} kindoorbike{Vélo d’intérieur} krowing{Aviron} kswimming{Natation} kelliptical{Vélo elliptique} kstairclimber{Simulateur d’escaliers} ktreadmill{Tapis de course} kjumprope{Corde à sauter} kweighttraining{Musculation} kpowerlifting{Force athlétique} kbodyweight{Poids du corps} kcalisthenics{Callisthénie} kkettlebell{Kettlebell} kcrossfit{CrossFit} khiit{HIIT} kcircuittraining{Circuit training} kfunctional{Entraînement fonctionnel} kfootball{Football} kbasketball{Basketball} kcricket{Cricket} ktennis{Tennis} kbadminton{Badminton} ktabletennis{Tennis de table} ksquash{Squash} kpadel{Padel} kvolleyball{Volley-ball} khockey{Hockey} kbaseball{Baseball} krugby{Rugby} kgolf{Golf} kbowling{Bowling} kboxing{Boxe} kmartialarts{Arts martiaux} kwrestling{Lutte} kclimbing{Escalade} ksprinting{Sprint} ktrackintervals{Fractionné sur piste} kcrosscountry{Cross-country} khurdles{Haies} klongjump{Saut en longueur} kshotput{Lancer du poids} kjavelin{Lancer du javelot} kpolevault{Saut à la perche} kmountainbiking{VTT} kkayaking{Kayak} ksurfing{Surf} kpaddleboard{Stand-up paddle} kskiing{Ski} ksnowboarding{Snowboard} kskating{Patinage} kskateboarding{Skateboard} khorseriding{Équitation} kyoga{Yoga} kpilates{Pilates} kstretching{Étirements} kmobility{Mobilité} ktaichi{Tai-chi} kbreathwork{Exercices de respiration} kmeditation{Méditation} khousework{Tâches ménagères} kgardening{Jardinage} kdogwalking{Promenade du chien} kchildcare{Garde d’enfants} kdiy{Bricolage} kshopping{Courses} kstairs{Escaliers} kgeneralworkout{Entraînement général} kdancing{Danse} kintimacy{Intimité} kphysiotherapy{Kinésithérapie} ksauna{Sauna} kcoldplunge{Bain froid} other{{name}}}", + "exerciseCatalogueName": "{key, select, other{{name}}}", + "presentationBluetoothHeartRateSensor": "Capteur de fréquence cardiaque Bluetooth", + "presentationGarminWatch": "Montre Garmin", + "presentationPolarSensor": "Capteur Polar", + "presentationCorosWatch": "Montre Coros", + "presentationCOROSWatch": "Montre COROS", + "presentationSmartRingR11MR10M": "Bague connectée (R11M/R10M)", + "presentationDaFitMOYOUNGWatch": "Montre DaFit / MOYOUNG", + "presentationWearFitBand": "Bracelet WearFit", + "presentationSmartRingBandLefunProtocol": "Bague/bracelet connecté (protocole Lefun)", + "presentationHPlusHRBand": "Bracelet cardio HPlus", + "presentationFossilSkagenHybridSmartwatch": "Montre connectée hybride Fossil/Skagen", + "presentationColmiRing": "Bague Colmi", + "presentationNoSleepWasScoredForThisDayRestingHR": "aucun sommeil n’a été évalué pour ce jour — la FC au repos n’est mesurée que sur une période de sommeil, jamais pendant les heures d’éveil.", + "presentationBaevskyStressIndex0100RestingAutonomicTensionPRV": "Indice de stress de Baevsky → 0–100 ; tension autonome au repos (VPP).", + "presentationNoReadinessInputsPresentNeverImputed": "aucune donnée de disponibilité présente — « — » (jamais imputé).", + "presentationMETValueYourWeightRefinedByHeartRateOr": "Valeur MET × votre poids, affinée par la fréquence cardiaque — ou la fréquence cardiaque seule, pour une activité sans MET.", + "presentationMood": "Humeur", + "presentationSleepQuality": "Qualité du sommeil", + "presentationS": "s", + "presentationV": "µV", + "presentationReadsTheRingDirectlyNoAccountNoKeyExchange": "Lit la bague directement — sans compte ni échange de clés.", + "presentationPebble2OrPebble2SEOnlyPairsOnly": "Pebble 2 ou Pebble 2 SE uniquement. Jumelage seulement pour l’instant — rien n’est encore lu ni enregistré.", + "presentationTheOriginalFossilSkagenHybridSmartwatchNotTheNewer": "La montre connectée hybride Fossil/Skagen d’origine, pas la plus récente Hybrid HR. Se jumelle et se connecte ; rien n’en est encore dérivé.", + "presentationABudgetActivityBandPairsAndBanksItsRaw": "Un bracelet d’activité d’entrée de gamme. Se jumelle et conserve ses données brutes, mais rien n’en est encore dérivé.", + "presentationPairsAndBanksItsRawDataNothingIsDecoded": "Se jumelle et conserve ses données brutes — rien n’est encore décodé.", + "presentationAGenericRingOrBandSoldUnderManyStorefront": "Une bague ou un bracelet générique vendu sous de nombreux noms. Se jumelle et se connecte ; ne fournit encore rien.", + "presentationAGenericHPlusFamilyHRBandPairsAndBanks": "Un bracelet cardio générique de la famille HPlus. Se jumelle et conserve son historique ; rien n’est encore décodé en chiffre.", + "presentationPairsAndBanksItsRawDataInTheBackground": "Se jumelle et conserve ses données brutes en arrière-plan, mais n’en dérive encore rien.", + "presentationAnUnbrandedDaFitMOYOUNGStyleWatchPairsAndBanks": "Une montre sans marque de type DaFit/MOYOUNG. Se jumelle et conserve ses propres données ; rien n’en est encore dérivé.", + "presentationAnUnbrandedMakibesHR3BoardPairsAndBanksIts": "Une carte Makibes HR3 sans marque. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationAnUnbrandedID115BoardPairsAndBanksItsRaw": "Une carte ID115 sans marque. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationAnUnbrandedXWatchBoardPairsAndBanksItsRaw": "Une carte XWatch sans marque. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationAnUnbrandedWatch9BoardPairsAndBanksItsRaw": "Une carte Watch9 sans marque. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationAnSMAQ2OSSSmartwatchPairsAndBanksIts": "Une montre connectée SMA-Q2-OSS. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationATLW64OrNO1F1FitnessBandPairsAnd": "Un bracelet de fitness TLW64 ou NO1 F1. Se jumelle et conserve ses données brutes ; rien n’en est encore dérivé.", + "presentationCaffeine": "Caféine", + "presentationAlcohol": "Alcool", + "presentationMeasurementsOf": "{have} mesures sur {total}", + "presentationAveragePulse": "{value} bpm en moy.", + "presentationSoreness": "Courbatures", + "presentationScreensBeforeBed": "Écrans avant le coucher", + "presentationIn": "in", + "presentationLb": "lb", + "presentationMl": "ml", + "presentationMg": "mg", + "presentationUnits": "unités", + "presentationWeightRepsLogged": "{weight} {unit} × {reps} enregistrés", + "presentationVolumeUnit": "volume en {unit}", + "journalTagCaffeine": "caféine", + "journalTagAlcohol": "alcool", + "journalTagLateMeal": "repas tardif", + "journalTagStress": "stress", + "journalTagPoorSleep": "mauvais sommeil", + "journalTagTravel": "voyage", + "journalTagScreensLate": "écrans tard", + "journalTagMeds": "médicaments", + "journalTagSick": "malade", + "journalTagSauna": "sauna", + "journalTagColdPlunge": "bain froid", + "journalTagSocial": "sortie sociale", + "journalTagWorkout": "séance", + "journalTagRestDay": "jour de repos", + "liveHrShowingDeviceSemantics": "Affichage de {label}. Touchez pour changer d’appareil.", + "devicesResetSourceSemantics": "Remettre {signal} dans l’ordre par défaut", + "metricDetailStepsGoalValue": "{count, plural, other{{value} pas}}", + "scrubberNothingSelected": "Rien de sélectionné", + "activityZonesNameWarmUp": "Échauffement", + "activityZonesNameEasy": "Facile", + "activityZonesNameAerobic": "Aérobie", + "activityZonesNameThreshold": "Seuil", + "activityZonesNameMaxEffort": "Effort maximal", + "findingIllnessTitle": "La fréquence cardiaque au repos a augmenté", + "findingIllnessDetail": "Les dernières nuits placent votre fréquence cardiaque au repos au-dessus de l’habitude, à cause d’une nuit très élevée ou de quelques nuits légèrement plus hautes. Un seul signal : une tendance, pas une cause.", + "findingLowReadinessTitle": "Récupération faible", + "findingLowReadinessDetail": "La récupération était dans sa tranche la plus basse. Pris ensemble, les signaux de cette nuit étaient nettement en dessous de l’habitude.", + "findingLowReadinessScoredDetail": "La récupération a obtenu {score}, sa tranche la plus basse. Pris ensemble, les signaux de cette nuit étaient nettement en dessous de l’habitude.", + "missingSignalSteps": "pas de comptage de pas", + "missingSignalSkinTempC": "pas de température cutanée", + "missingSignalActivityLevel": "pas d’enregistrement d’activité", + "missingSignalDeviceHrv": "pas de VFC propre", + "missingSignalDeviceStages": "pas de phases de sommeil propres", + "missingSignalDeviceResp": "pas de fréquence respiratoire propre", + "missingSignalDeviceStress": "pas de stress propre", + "missingSignalDeviceSpo2": "pas de SpO2 propre", + "signalSteps": "Pas", + "signalSkinTempC": "Température cutanée (°C)", + "signalActivityLevel": "Activité", + "signalDeviceHrv": "La VFC propre de l’appareil", + "signalDeviceStages": "Les phases de sommeil propres de l’appareil", + "signalDeviceResp": "La fréquence respiratoire propre de l’appareil", + "signalDeviceStress": "Le stress propre de l’appareil", + "signalDeviceSpo2": "La SpO2 propre de l’appareil", + "devicePickerBlurbThermometer": "Enregistre chaque mesure comme température corporelle.", + "devicePickerBlurbMiscaleBc": "Enregistre chaque poids stabilisé et la bio-impédance, y compris les mesures en mémoire.", + "devicePickerBlurbMiscale2": "Enregistre chaque poids stabilisé, y compris les mesures en mémoire.", + "dayTimelineZoomNoHr": "Pas de fréquence cardiaque sur cette période", + "dayTimelineZoomPerMinute": "Moyennes par minute. Faites glisser les poignées pour zoomer ; deux heures ou moins affichent chaque seconde tant qu’elle est conservée.", + "dayTimelineZoomPerMinuteOnly": "Moyennes par minute. Les mesures seconde par seconde ne sont plus conservées pour cette période.", + "dayTimelineZoomPerSecond": "Chaque seconde enregistrée par le bracelet. Les mesures seconde par seconde ne sont conservées que pour les jours récents.", + "dataExportHeartRate": "Exporter la fréquence cardiaque, minute par minute", + "dataExportHeartRateSub": "Un fichier CSV pour les jours choisis, avec une moyenne par minute pour chaque minute enregistrée. Les données seconde par seconde sont dans l’export de la base de données", + "dataHeartRateRangeHelp": "La fréquence cardiaque est conservée du {first} au {last}", + "trendNormalForYou": "Normal pour vous", + "trendAboveUsual": "Au-dessus de l’habitude", + "trendBelowUsual": "En dessous de l’habitude", + "trendVsUsual": "{diff} par rapport à votre habitude de {usual}", + "trendUsualRange": "Plage habituelle {lo}–{hi}{unit}", + "trendNeedsDays": "Votre plage habituelle apparaît après {need} jours. {have} pour l’instant.", + "monthGridLegendBetter": "Mieux que d’habitude", + "monthGridLegendUsual": "Habituel pour vous", + "monthGridLegendWorse": "Moins bien que d’habitude", + "monthGridLegendOutside": "Hors de votre plage habituelle", + "monthGridRangeFootnote": "Une case par jour. Une barre en haut signifie au-dessus de votre plage habituelle, en bas en dessous. Une case seulement encadrée n’a pas de valeur.", + "monthGridNoRangeTitle": "{title} : pas encore de plage habituelle", + "monthGridFlatBody": "Vos journées ont toutes été à peu près identiques, il n’y a donc pas encore de plage habituelle à laquelle comparer un jour.", + "monthGridRowSemantics": "{title} : {have} jours sur {total} ont une valeur. {below} en dessous et {above} au-dessus de votre plage habituelle.", + "zoneRowsRange": "{lo}–{hi} bpm", + "zoneRowsRangeTop": "{lo}+ bpm", + "zoneRowsMinutes": "{minutes} min", + "advancedTitle": "Graphiques avancés", + "advancedIntro": "Des vues de recherche sur les mêmes données. Rien ici ne modifie vos scores.", + "advancedBeatsTitle": "Battement par battement", + "advancedBeatsSub": "Chaque battement face au précédent, la VFC au fil de la nuit, la capacité de décélération", + "advancedRhythmTitle": "Horaires de sommeil et rythme quotidien", + "advancedRhythmSub": "Le sommeil heure par heure, la variabilité au repos, la forme prévue d’aujourd’hui", + "healthAdvancedLinkTitle": "Graphiques avancés", + "healthAdvancedLinkSub": "Détail battement par battement et horaires de sommeil, pour les curieux", + "profileBandBattery": "Bracelet {pct} %", + "profileExportRow": "Export et sauvegarde", + "profileExportRowSub": "Tableurs, une copie complète et l’import d’historique", + "profileSettingsRow": "Réglages", + "profileSettingsRowSub": "Bracelet, application, données, confidentialité et avancé", + "sleepTabPlanTitle": "Votre plan de sommeil", + "sleepTabMoreTitle": "Réveil et horloge biologique", + "healthWellbeingTitle": "Esprit, habitudes et médicaments", + "healthWellbeingMindSub": "Respiration, humeur et journal", + "healthWellbeingHabitsSub": "Habitudes quotidiennes, comptées en jours tenus", + "healthWellbeingMedsSub": "Les prises du jour et la semaine passée", + "healthWellbeingCycleSub": "Notez vos règles et voyez où vous en êtes dans le cycle", + "activityStepsFoodTitle": "Pas et alimentation", + "activityStepsSub": "Aujourd’hui et chaque jour précédent", + "activityNutritionSub": "Notez vos repas et voyez les protéines du jour", + "homeBackToToday": "Retour à aujourd’hui", + "homeSomeDataMissingTitle": "Une partie des données du jour manque", + "homeFirstWeekTitle": "Vos premières semaines", + "homeFirstWeekWear": "Portez le bracelet cette nuit", + "homeFirstWeekSleep": "Sommeil : le matin après votre première nuit", + "homeFirstWeekStrain": "Effort : à partir du jour {day}", + "homeFirstWeekRecovery": "Récupération : à partir de la nuit {night}, une fois votre normale connue", + "homeFirstWeekSoFar": "{count, plural, =1{1 jour enregistré jusqu’ici} other{{count} jours enregistrés jusqu’ici}}", + "homeFirstWeekDone": "Terminé : {step}", + "homeCoachSetUpChip": "Posez des questions sur vos données : configurez le coach", + "homeRecoverySleeping": "Sommeil en cours…", + "homeRecoveryAfterWake": "La récupération est évaluée après votre réveil.", + "homeRecoveryFinishingUp": "Finalisation", + "homeRecoveryUpdated": "Mis à jour {time} · {from} → {to}", + "dataRecoveryChanged": "Récupération modifiée : {list}", + "homeRecoveryStartsIn": "{count, plural, =1{La récupération commence dans 1 nuit} other{La récupération commence dans {count} nuits}}", + "healthWellbeingAllSub": "Respiration, humeur, journal, habitudes et médicaments", + "healthWellbeingAllSubCycle": "Respiration, humeur, journal, habitudes, médicaments et cycle", + "settingsStepCalibration": "Calibrage des pas", + "settingsStepCalibrationSub": "Apprend dans quelle mesure votre bracelet surestime ou sous-estime votre propre marche, et le corrige sur le téléphone. Rien n’est écrit sur le bracelet et rien ne quitte l’appareil", + "stepCalTitle": "Calibrage des pas", + "stepCalIntro": "Votre bracelet compte les pas à partir de son propre capteur de mouvement, et son écart dépend de vous. Ces exercices le mesurent. Rien ne quitte votre téléphone.", + "stepCalSlowTitle": "Marche lente", + "stepCalSlowSub": "marchez lentement, téléphone en poche", + "stepCalSlowGuide": "Marchez lentement et régulièrement, bracelet bien ajusté. Comptez vous-même 100 pas.", + "stepCalBriskTitle": "Marche rapide", + "stepCalBriskSub": "marchez à votre rythme habituel", + "stepCalBriskGuide": "Marchez à votre rythme normal. Comptez vous-même 100 pas.", + "stepCalJogTitle": "Course légère", + "stepCalJogSub": "courez à allure régulière", + "stepCalJogGuide": "Courez à allure régulière. Comptez vous-même 100 pas.", + "stepCalEverydayTitle": "À la maison", + "stepCalEverydaySub": "rangez pendant trois minutes, téléphone en poche", + "stepCalEverydayGuide": "Faites des choses ordinaires à la maison — porter, soulever, ranger — avec votre téléphone en poche. Trois minutes. Ne faites pas les cent pas volontairement.", + "stepCalCalibrated": "Calibré — compté {n} fois", + "stepCalNotCalibrated": "Pas encore calibré", + "stepCalBandSeen": "{count, plural, one{pas vu par le bracelet} other{pas vus par le bracelet}}", + "stepCalCountYourself": "Comptez vos propres pas. Arrêtez-vous à {target}.", + "stepCalElapsed": "Continuez — celui-ci est chronométré.", + "stepCalHowManyCounted": "Combien de pas avez-vous comptés ?", + "stepCalFinish": "Terminé", + "stepCalCancel": "Annuler", + "stepCalKeepWalking": "Rien d’enregistré pour l’instant — commencez à marcher, bracelet bien ajusté.", + "stepCalKeepMoving": "Rien d’enregistré pour l’instant — gardez le téléphone sur vous et continuez à bouger.", + "stepCalNoSignal": "Le bracelet n’a rien enregistré pendant cet exercice.", + "stepCalNoPhoneAccess": "Cet exercice a besoin du podomètre de votre téléphone, et l’accès n’a pas été accordé. Ce podomètre sert de référence pour les mouvements à la maison.", + "stepCalPhoneNotReady": "Le podomètre de votre téléphone n’a pas répondu. Réessayez dans un instant.", + "stepCalNoPhone": "Votre téléphone n’a compté aucun pas pendant l’exercice. Gardez-le en poche la prochaine fois — il sert de référence pour les mouvements à la maison.", + "stepCalBandQuiet": "Le bracelet n’a vu presque aucun mouvement. Vérifiez qu’il est bien ajusté et qu’il transmet.", + "stepCalTooShort": "Cet exercice était trop court pour servir de mesure. Marchez jusqu’au bout du compte.", + "stepCalMismatch": "Le bracelet a compté bien plus que vous. Autre chose que la marche a été détecté.", + "stepCalWrongSource": "Cet exercice a utilisé la mauvaise référence.", + "stepCalBadData": "Cet exercice a produit des données de capteur illisibles.", + "stepCalRatioOutOfRange": "Votre compte et celui du bracelet sont trop éloignés pour être utilisés. Refaites l’exercice et marchez jusqu’au bout du compte.", + "stepCalCadenceOutOfRange": "Le bracelet n’a pas trouvé de rythme de marche dans cet exercice. Commencez à marcher dès que vous avez lancé l’exercice et gardez une allure régulière.", + "stepCalRoutineTooShort": "Cet exercice s’est arrêté avant la fin du minuteur. Continuez jusqu’à ce qu’il se termine.", + "stepCalSaveFailed": "Impossible d’enregistrer ce calibrage.", + "stepCalDoneGait": "Enregistré. Vous avez compté {count} pas et le bracelet en a vu {band}. Désormais, ce type de marche est corrigé de {pct} %.", + "stepCalDoneHousehold": "Enregistré. Votre téléphone a compté {count} pas sur les {band} pas vus par le bracelet. Les mouvements à la maison sont désormais ajustés à {gain}." } diff --git a/test/french_coverage_test.dart b/test/french_coverage_test.dart new file mode 100644 index 000000000..8601edd5d --- /dev/null +++ b/test/french_coverage_test.dart @@ -0,0 +1,54 @@ +// French was missing 350 strings outside the supplement catalogue, so whole +// screens (device picker, detected activities, step calibration, trends) fell +// back to English on an otherwise French app. +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:openstrap_edge/l10n/app_localizations.dart'; + +void main() { + final fr = lookupAppLocalizations(const Locale('fr')); + final en = lookupAppLocalizations(const Locale('en')); + + test('french has every english string outside the two known gaps', () { + Map arb(String code) => + jsonDecode(File('lib/l10n/app_$code.arb').readAsStringSync()) + as Map; + final french = arb('fr'); + final missing = [ + for (final k in arb('en').keys) + if (!k.startsWith('@') && !french.containsKey(k)) k, + ]; + // Still to do: the supplement catalogue. The WHOOP firmware warning is + // added by its own change. + missing.removeWhere((k) => + k.startsWith('supplement') || k.startsWith('whoopFirmwareWarning')); + expect(missing, isEmpty); + }); + + test('the newly covered screens read in french', () { + expect(fr.devicePickerTitle, 'Connectez vos appareils'); + expect(fr.activityReviewTitle, 'Activités détectées'); + expect(fr.stepCalTitle, 'Calibrage des pas'); + expect(fr.stepCalTitle, isNot(en.stepCalTitle)); + }); + + test('built-in activities are named in french, custom ones pass through', + () { + expect(fr.activityCatalogueName('krunning', 'Running'), 'Course à pied'); + expect(fr.activityCatalogueName('kcardio', 'Cardio'), 'Cardio'); + expect(fr.activityCatalogueName('custom', 'Mon sport'), 'Mon sport'); + }); + + test('counts agree with their noun', () { + expect(fr.stepCalBandSeen(1), 'pas vu par le bracelet'); + expect(fr.stepCalBandSeen(5), 'pas vus par le bracelet'); + expect(fr.homeFirstWeekSoFar(1), '1 jour enregistré jusqu’ici'); + expect(fr.homeFirstWeekSoFar(3), '3 jours enregistrés jusqu’ici'); + expect(fr.welcomeLabResultsWritten(1), '1 résultat d’analyse enregistré'); + expect(fr.devicesSensorLimit(2), contains('2 capteurs')); + }); +}