diff --git a/lib/ble/band_status_l10n.dart b/lib/ble/band_status_l10n.dart new file mode 100644 index 00000000..0d4c870d --- /dev/null +++ b/lib/ble/band_status_l10n.dart @@ -0,0 +1,99 @@ +// Localized [BandStatus] copy, kept out of `ble_state.dart` on purpose: that +// file is pure transport-layer logic (no Flutter, no BuildContext) and is +// unit-tested directly against its literal English text in +// `test/ble_state_test.dart`. This is the render-time wrapper every UI call +// site (devices.dart, pairing.dart, pair_sensor.dart) should use instead of +// reading `.title`/`.reason`/`.fix` straight off a `BandStatus` — same split +// as `sourceState`/`_localizedSourceState` in devices.dart. + +import 'package:flutter/widgets.dart' show BuildContext; + +import '../l10n/app_localizations.dart'; +import 'ble_state.dart' show BandCondition, BandStatus; + +BandStatus localizedBandStatus(BuildContext c, BandStatus s) { + final l = AppLocalizations.of(c); + switch (s.condition) { + case BandCondition.bluetoothDenied: + return BandStatus( + s.condition, + l?.bandStatusBluetoothDeniedTitle ?? s.title, + l?.bandStatusBluetoothDeniedReason ?? s.reason, + fix: l?.bandStatusBluetoothDeniedFix ?? s.fix, + ); + case BandCondition.bluetoothOff: + return BandStatus( + s.condition, + l?.bandStatusBluetoothOffTitle ?? s.title, + l?.bandStatusBluetoothOffReason ?? s.reason, + fix: l?.bandStatusBluetoothOffFix ?? s.fix, + ); + case BandCondition.bluetoothUnsupported: + return BandStatus( + s.condition, + l?.bandStatusBluetoothUnsupportedTitle ?? s.title, + l?.bandStatusBluetoothUnsupportedReason ?? s.reason, + ); + case BandCondition.reconnectPaused: + return BandStatus( + s.condition, + l?.bandStatusReconnectPausedTitle ?? s.title, + l?.bandStatusReconnectPausedReason(s.bondRefusals ?? 0) ?? s.reason, + fix: l?.bandStatusRepairFix ?? s.fix, + bondRefusals: s.bondRefusals, + ); + case BandCondition.repairNeeded: + return BandStatus( + s.condition, + l?.bandStatusRepairNeededTitle ?? s.title, + l?.bandStatusRepairNeededReason ?? s.reason, + fix: l?.bandStatusRepairFix ?? s.fix, + ); + case BandCondition.syncStuck: + return BandStatus( + s.condition, + l?.bandStatusSyncStuckTitle ?? s.title, + l?.bandStatusSyncStuckReason ?? s.reason, + fix: l?.bandStatusSyncStuckFix ?? s.fix, + ); + case BandCondition.strapUnresponsive: + return BandStatus( + s.condition, + l?.bandStatusStrapUnresponsiveTitle ?? s.title, + l?.bandStatusStrapUnresponsiveReason ?? s.reason, + fix: l?.bandStatusStrapUnresponsiveFix ?? s.fix, + ); + case BandCondition.clockLost: + return BandStatus( + s.condition, + l?.bandStatusClockLostTitle ?? s.title, + l?.bandStatusClockLostReason ?? s.reason, + fix: l?.bandStatusClockLostFix ?? s.fix, + ); + case BandCondition.connected: + return BandStatus( + s.condition, + l?.devicesConnected ?? s.title, + l?.bandStatusConnectedReason ?? s.reason, + ); + case BandCondition.connecting: + return BandStatus( + s.condition, + l?.bandStatusConnectingTitle ?? s.title, + l?.bandStatusConnectingReason ?? s.reason, + ); + case BandCondition.scanning: + return BandStatus( + s.condition, + l?.bandStatusScanningTitle ?? s.title, + l?.bandStatusScanningReason ?? s.reason, + ); + case BandCondition.disconnected: + return BandStatus( + s.condition, + l?.devicesNotConnected ?? s.title, + l?.bandStatusDisconnectedReason ?? s.reason, + fix: l?.bandStatusDisconnectedFix ?? s.fix, + ); + } +} diff --git a/lib/ble/ble_state.dart b/lib/ble/ble_state.dart index 28ea0889..3696ee32 100644 --- a/lib/ble/ble_state.dart +++ b/lib/ble/ble_state.dart @@ -203,7 +203,13 @@ class BandStatus { /// The way forward, or null when there is genuinely nothing to do. final String? fix; - const BandStatus(this.condition, this.title, this.reason, {this.fix}); + /// Set only for [BandCondition.reconnectPaused] — the count already baked + /// into [reason]'s English text, carried separately so a UI layer can + /// re-render the reason in another language without re-parsing it. + final int? bondRefusals; + + const BandStatus(this.condition, this.title, this.reason, + {this.fix, this.bondRefusals}); /// True for the states that need to be shown. The four ordinary link states /// (connected/connecting/scanning/disconnected) are the app's normal @@ -271,6 +277,7 @@ BandStatus bandStatusFor({ 'batteries on a link that will not open. Nothing is reconnecting ' 'until you act.', fix: repairFix, + bondRefusals: bondRefusals, ); } if (needsRepairGuide) { diff --git a/lib/gestures/device_action.dart b/lib/gestures/device_action.dart index 943bc0b7..09df0135 100644 --- a/lib/gestures/device_action.dart +++ b/lib/gestures/device_action.dart @@ -15,6 +15,10 @@ // product decision): answer/reject call (Android ANSWER_PHONE_CALLS; impossible on // iOS), workout lap. +import 'package:flutter/widgets.dart' show BuildContext; + +import '../l10n/app_localizations.dart'; + enum DeviceAction { none, mediaPlayPause, @@ -127,6 +131,71 @@ extension DeviceActionX on DeviceAction { } } + /// Localized [label], falling back to the English string above when there is + /// no [AppLocalizations] in scope (e.g. a widget test with no delegate + /// wired). `.label`/`.blurb` stay pure and untouched — same split as + /// `sourceTierLabel`/`sourceTierDetail` in devices.dart. + String localizedLabel(BuildContext c) { + final l = AppLocalizations.of(c); + switch (this) { + case DeviceAction.none: + return l?.deviceActionNoneLabel ?? label; + case DeviceAction.mediaPlayPause: + return l?.deviceActionMediaPlayPauseLabel ?? label; + case DeviceAction.mediaNext: + return l?.deviceActionMediaNextLabel ?? label; + case DeviceAction.mediaPrev: + return l?.deviceActionMediaPrevLabel ?? label; + case DeviceAction.volumeUp: + return l?.deviceActionVolumeUpLabel ?? label; + case DeviceAction.volumeDown: + return l?.deviceActionVolumeDownLabel ?? label; + case DeviceAction.ringPhone: + return l?.deviceActionRingPhoneLabel ?? label; + case DeviceAction.torch: + return l?.deviceActionTorchLabel ?? label; + case DeviceAction.markMoment: + return l?.deviceActionMarkMomentLabel ?? label; + case DeviceAction.workoutToggle: + return l?.deviceActionWorkoutToggleLabel ?? label; + case DeviceAction.logWater: + return l?.deviceActionLogWaterLabel ?? label; + case DeviceAction.broadcastToTasker: + return l?.deviceActionBroadcastToTaskerLabel ?? label; + } + } + + /// Localized [blurb] — see [localizedLabel]. + String localizedBlurb(BuildContext c) { + final l = AppLocalizations.of(c); + switch (this) { + case DeviceAction.none: + return l?.deviceActionNoneBlurb ?? blurb; + case DeviceAction.mediaPlayPause: + return l?.deviceActionMediaPlayPauseBlurb ?? blurb; + case DeviceAction.mediaNext: + return l?.deviceActionMediaNextBlurb ?? blurb; + case DeviceAction.mediaPrev: + return l?.deviceActionMediaPrevBlurb ?? blurb; + case DeviceAction.volumeUp: + return l?.deviceActionVolumeUpBlurb ?? blurb; + case DeviceAction.volumeDown: + return l?.deviceActionVolumeDownBlurb ?? blurb; + case DeviceAction.ringPhone: + return l?.deviceActionRingPhoneBlurb ?? blurb; + case DeviceAction.torch: + return l?.deviceActionTorchBlurb ?? blurb; + case DeviceAction.markMoment: + return l?.deviceActionMarkMomentBlurb ?? blurb; + case DeviceAction.workoutToggle: + return l?.deviceActionWorkoutToggleBlurb ?? blurb; + case DeviceAction.logWater: + return l?.deviceActionLogWaterBlurb ?? blurb; + case DeviceAction.broadcastToTasker: + return l?.deviceActionBroadcastToTaskerBlurb ?? blurb; + } + } + /// In-app actions act on our own app/backend (handled in Dart, no native call, /// available on every platform). Everything else (except `none`) is native. bool get isInApp => diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b4d72979..9370b33b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -293,5 +293,2017 @@ "welcomePartOfFileNotUsedTitle": "Ein Teil dieser Datei konnte nicht verwendet werden", "welcomeStrandedDays": "{n, plural, one{1 Tag kam außer der Reihe an und diente nur als Kontext für den folgenden Tag.} other{{n} Tage kamen außer der Reihe an und dienten nur als Kontext für den folgenden Tag.}}", "welcomeLateRows": "{n, plural, one{1 Zeile kam an, nachdem ihr Tag bereits bewertet und abgeschlossen war.} other{{n} Zeilen kamen an, nachdem ihr Tag bereits bewertet und abgeschlossen war.}}", - "welcomeExportAgainInDateOrder": "Erneut in Datumsreihenfolge exportieren" + "welcomeExportAgainInDateOrder": "Erneut in Datumsreihenfolge exportieren", + "scanBarcodeTitle": "Barcode scannen", + "scanBarcodeClose": "Schließen", + "scanBarcodeInstructions": "Halte den Barcode innerhalb des Rahmens. Es wird nichts aufgezeichnet — nur die Ziffern werden gelesen.", + "scanBarcodeNoAccessTitle": "Kein Kamerazugriff", + "scanBarcodeCameraFailedTitle": "Die Kamera konnte nicht gestartet werden", + "scanBarcodeNoAccessBody": "Zum Scannen wird die Kamera benötigt, und dieser App wurde dafür keine Berechtigung erteilt.", + "scanBarcodeCameraFailedBody": "Dieses Gerät konnte seine Kamera für den Scanner nicht öffnen.", + "scanBarcodeTypeInstead": "Stattdessen Zahlen eingeben", + "findingsLogTitle": "Beobachtungen", + "findingsLogEmptyTitle": "Nichts ist aufgefallen", + "findingsLogEmptyBody": "Die Überwachungen auf Krankheit, ungewöhnliche nächtliche Physiologie, Hauttemperatur und eine Verschiebung deiner Ruheherzfrequenz waren alle unauffällig. Das ist ein Ergebnis, kein leerer Bildschirm.", + "findingsLogDerivedNote": "Wird bei jedem Öffnen aus deinen eigenen Tagen neu berechnet, nicht zum Zeitpunkt des Ereignisses gespeichert — wird ein Tag also neu analysiert, ändert sich auch die Anzeige hier entsprechend.", + "startCardDefaultSub": "Wähle eins aus und leg los", + "monthGridCoverage": "{have} von {total} Tagen", + "monthGridSemanticsLabel": "{title}: {have} von {total} Tagen haben einen Wert. Nach deinem eigenen Bereich schattiert.", + "monthGridDaysAgo": "Vor {days} Tagen", + "monthGridToday": "Heute", + "monthGridFootnote": "Eine Zelle pro Tag. Je dunkler, desto höher liegt der Tag in DEINEM eigenen Bereich — dem 10. bis 90. Perzentil jedes gespeicherten Tages — und eine Zelle mit Umriss ist ein Tag ohne Wert, kein niedriger Wert. Mehr Anstrengung ist nicht bessere Anstrengung, und mehr Schlaf ist nicht besserer Schlaf; dies zeigt, wo ein Tag lag, nicht wie er verlief.", + "monthGridNotShadedYetTitle": "{title} ist noch nicht schattiert", + "monthGridNotShadedYetBody": "{days, plural, one{{days} Tag} other{{days} Tage}} ergeben keinen Bereich — die Schattierung zeigt, wo ein Tag innerhalb deines eigenen Bereichs liegt. Sie erscheint ab {min}.", + "whatChangedTitle": "Was sich geändert hat", + "whatChangedSub": "GEGENÜBER DEINER EIGENEN HISTORIE", + "whatChangedNoDataTitle": "Für diesen Tag liegen noch keine Daten vor", + "whatChangedNoDataBody": "Die Analyse vergleicht einen Tag mit den vorherigen, und für diesen Tag gibt es keinen Wert zum Vergleichen. Nichts ist ungewöhnlich, weil nichts bekannt ist.", + "whatChangedLearningTitle": "Lernt noch, was für dich normal ist", + "whatChangedLearningBody": "Ungewöhnlich ergibt nur im Vergleich zu einem Bereich einen Sinn, und dahinter {days, plural, one{liegt erst {days} Tag} other{liegen erst {days} Tage}} an Historie. Die Analyse beginnt ab {min}.", + "whatChangedNothingTitle": "Nichts ist aufgefallen", + "whatChangedNothingBody": "Jede Kennzahl mit ausreichender Historie lag innerhalb des Bereichs, den deine eigenen Tage vorgeben. Das ist die normale Antwort, und sie ist vollständig.", + "whatChangedMethodologyNote": "Gemessen an deinen eigenen vorangegangenen Tagen, in deinen eigenen Einheiten, mit dem angegebenen Zeitfenster — damit du es anzweifeln kannst. Nichts hier ist eine Ursache oder eine Diagnose.", + "whatChangedDayLinkTitle": "Was an diesem Tag passiert ist", + "whatChangedDayLinkSub": "Schlaf, Einheiten, Mahlzeiten und Einträge in zeitlicher Reihenfolge", + "whatChangedMonthSection": "Der Monat dahinter", + "journalFieldErrorNoName": "Gib ihm einen Namen", + "journalFieldErrorInvalidName": "Verwende mindestens einen Buchstaben oder eine Zahl", + "journalFieldErrorNoUnit": "Gib an, in welcher Einheit gemessen wird (mg, ml, Tassen…)", + "journalFieldErrorDuplicate": "Du erfasst bereits etwas unter diesem Namen", + "journalFieldTitle": "Etwas anderes erfassen", + "journalFieldNameLabel": "Was möchtest du erfassen?", + "journalFieldNameHint": "Magnesium, Bildschirmzeit, Kopfschmerzen…", + "journalFieldKindQuestion": "Was für eine Art Zahl ist das?", + "journalFieldKindRating": "Eine Bewertung von 1–5", + "journalFieldKindAmount": "Eine Menge", + "journalFieldKindMinutes": "Minuten", + "journalFieldUnitLabel": "Einheit", + "journalFieldUnitHint": "mg, ml, Tassen…", + "journalFieldStepSize": "Schrittgröße", + "journalFieldMaxPerDay": "Höchstwert, den du an einem Tag erfassen würdest", + "journalFieldAskLastTime": "Fragen, wann es zuletzt war", + "journalFieldStartTracking": "Erfassung starten", + "aiBriefingForDay": "FÜR {day}", + "aiBriefingNoModelTitle": "Es ist kein Modell eingerichtet", + "aiBriefingNoModelBody": "Ein Briefing wird von einem Modell verfasst, das du auswählst. Solange du keines ausgewählt hast, gibt es nichts zu erzeugen und es wurde nirgendwohin etwas gesendet.", + "aiBriefingChooseModel": "Modell auswählen", + "aiBriefingNothingTitle": "Für heute wurde nichts geschrieben", + "aiBriefingNothingBody": "Briefings werden nach einem Zeitplan erstellt oder hier auf Abruf.", + "aiBriefingWriting": "Wird geschrieben…", + "aiBriefingWriteNow": "Jetzt eins schreiben", + "aiBriefingWriteAgain": "Erneut schreiben", + "aiBriefingFailedTitle": "Das hat nicht funktioniert", + "aiBriefingFailedGeneric": "Fehlgeschlagen: {error}", + "aiBriefingSentSection": "Was gesendet wurde", + "aiBriefingReadSection": "Was gelesen wurde", + "aiBriefingNoneBody": "Nichts. Es gab keine Anfrage — die Notiz oben wurde auf diesem Telefon geschrieben.", + "aiBriefingLocalBody": "Diese Zahlen gingen an {host}, auf diesem Gerät. Nichts hat es verlassen.", + "aiBriefingCloudBody": "Diese Zahlen, und nichts weiter, wurden an {host} als {model} gesendet. Keine Rohaufzeichnungen, kein Name, keine Kennung.", + "aiBriefingNoneCardTitle": "Nichts ist aufgefallen, also wurde nichts angefragt", + "aiBriefingNoneCardBody": "Der Durchlauf läuft auf diesem Telefon. Er ruft ein Modell nur auf, wenn er einen Befund zu übergeben hat, und heute hatte er keinen.", + "aiBriefingEmptyCardTitle": "Es gab nichts zu senden", + "aiBriefingEmptyCardBody": "Als dies geschrieben wurde, hatte keine Kennzahl einen Wert, daher enthielt der Prompt keinen.", + "napsFellAsleepHelp": "WANN DU EINGESCHLAFEN BIST", + "napsWokeUpHelp": "WANN DU AUFGEWACHT BIST", + "napsInvalidWindow": "Ein Nickerchen dauert zwischen 5 Minuten und 6 Stunden. Alles Längere ist Schlaf und gehört in die Nacht, wo sich die Phasen ablesen lassen.", + "napsOverlap": "Das überschneidet sich mit einem an diesem Tag bereits vorhandenen Nickerchen. Entferne dieses zuerst, statt dieselbe Stunde doppelt zu zählen.", + "napsNotReanalysed": "Der Tag wurde nicht neu analysiert — es lief bereits eine andere Analyse. Deine Änderung ist gespeichert und wird beim nächsten Mal angewendet.", + "napsTitle": "Nickerchen", + "napsNoReadingTitle": "Keine Nickerchen-Daten für diesen Tag", + "napsNoReadingBody": "Nickerchen werden aus derselben 1-Hz-Aufzeichnung wie der Rest des Tages berechnet, und für diesen Tag liegt nicht genug davon vor.", + "napsEmptyTitle": "Keine Nickerchen an diesem Tag", + "napsEmptyBody": "An diesem Tag war nichts lange genug ruhig genug, mit dem Herzfrequenzabfall, der mit Durchschlafen einhergeht.", + "napsCountsToward": "{mins} Nickerchen werden auf deinen heutigen Schlafbedarf angerechnet.", + "napsNotAppliedTitle": "Das wurde nicht übernommen", + "napsWorking": "Wird verarbeitet…", + "napsLogANap": "Nickerchen eintragen", + "napsRemovedSection": "Entfernt", + "napsPutBackSemantic": "Dieses Nickerchen wiederherstellen", + "napsPutBackLabel": "Wiederherstellen", + "napsRemovalKept": "Eine Entfernung wird als Zeitfenster statt als ID gespeichert, damit sie auch dann noch gilt, wenn sich die Grenzen des Detektors verschieben.", + "napsYouLoggedThis": "Von dir eingetragen", + "napsDetected": "Erkannt", + "napsLoggedWithMins": "{mins} · von dir eingetragen", + "napsDetectedWithMins": "{mins} Schlaf · erkannt", + "napsDeleteSemantic": "Dieses Nickerchen löschen", + "napsNotANapSemantic": "Das war kein Nickerchen", + "napsDeleteLabel": "Löschen", + "napsNotANapLabel": "Kein Nickerchen", + "readinessDetailTitle": "Bereitschaft", + "readinessDetailNotScoredTitle": "Für die Bereitschaft liegt kein Wert vor", + "readinessDetailLastNightScored": "Die letzte bewertete Nacht war {day}.", + "readinessDetailWhatWasMissing": "Was gefehlt hat", + "readinessDetailWhatWentIntoIt": "Was eingeflossen ist", + "readinessDetailInputsFooter": "{used}/{total} Eingaben. Jede wird anhand deiner eigenen Historie eingeordnet — eine parallele Ansicht derselben Eingaben, keine Aufschlüsselung der Zahl oben.", + "readinessDetailNoBreakdownTitle": "Noch keine Aufschlüsselung", + "readinessDetailNoBreakdownBody": "Das Einordnen jeder Eingabe anhand deiner eigenen Historie dauert etwa zwei Wochen an Nächten.", + "readinessDetailHistoryTitle": "Verlauf", + "readinessDetailLastNDays": "{n, plural, one{Letzter {n} Tag} other{Letzte {n} Tage}}", + "readinessDetailNoHistoryTitle": "Kein Bereitschaftsverlauf", + "readinessDetailNoHistoryBody": "0 Tage bewertet.", + "readinessDetailWearOvernight": "Trage das Band über Nacht", + "readinessDetailUnit": "/100", + "readinessDetailDaysAgo": "{n, plural, one{vor {n} Tag} other{vor {n} Tagen}}", + "readinessDetailToday": "Heute", + "readinessDetailMeasured": "Gemessen", + "readinessDetailNotMeasured": "Nicht gemessen", + "readinessDetailNightsOfHistory": "{n, plural, one{{n} Nacht deiner eigenen Historie} other{{n} Nächte deiner eigenen Historie}}", + "readinessDetailNeedSuffix": "{need}. Jede Eingabe wird anhand deiner eigenen Nächte eingeordnet, daher kann der Wert erst starten, wenn genug davon vorliegen.", + "readinessDetailNoNoteFallback": "Alles oben Genannte war vorhanden, und dennoch konnte der Vergleich mit deiner eigenen Historie nicht durchgeführt werden.", + "readinessDetailNotAvailable": "nicht verfügbar", + "readinessDetailContributionNotReported": "Beitrag nicht angegeben", + "readinessDetailRelativeUncalibrated": "relativ, unkalibriert", + "readinessDetailWithinSpread": "innerhalb deiner üblichen Spanne", + "readinessDetailWeightPercent": "{pct}% Gewichtung", + "dayStepsTitle": "Schritte", + "dayStepsThroughDay": "Im Tagesverlauf", + "dayStepsToday": "heute", + "dayStepsOnDay": "am {day}", + "dayStepsNoTimesTitle": "Keine Zeiten hinter der Zählung {when}", + "dayStepsNoStepsTitle": "Keine Schritte gezählt {when}", + "dayStepsStrapCounterBody": "Die {count} Schritte, die {when} gezählt wurden, stammen vom eigenen Schrittzähler des Bands, der eine laufende Tagessumme ohne Zeiten meldet. Es gibt nichts, was sich auf einer Uhr einordnen ließe.", + "dayStepsNothingCounted": "Nichts, das Schritte zählen kann, hat {when} Daten aufgezeichnet.", + "dayStepsChartTitle": "WANN SIE GEZÄHLT WURDEN", + "dayStepsUnit": "Schritte", + "dayStepsYourPhone": "Dein Telefon", + "dayStepsYourBand": "Dein Band", + "dayStepsCounted": "Gezählt", + "dayStepsHonestyMixed": "Gezählt am Handgelenk und über dein Telefon, und beide irren sich unterschiedlich: das Handgelenk unterschätzt einen echten Spaziergang und kann rhythmische Handbewegungen als Gehen deuten, während das Telefon nur die Schritte zählt, bei denen du es bei dir trugst.", + "dayStepsHonestyStrap": "Gezählt am Handgelenk, wo ein echter Spaziergang tendenziell unterschätzt wird und rhythmische Handbewegungen als Gehen gedeutet werden können.", + "dayStepsHonestyPhone": "Gezählt von deinem Telefon, daher erscheinen hier nur die Schritte, bei denen du es bei dir trugst.", + "roughNightSignRhr": "dein Ruhepuls war höher", + "roughNightSignHrv": "deine HRV war niedriger", + "roughNightSignDip": "dein Puls sank nachts weniger ab als sonst", + "roughNightSignTemp": "deine Haut war wärmer", + "roughNightLateTraining": "Du hast bis {at} trainiert, was das oft schon allein verursacht.", + "roughNightIllness": "Die Krankheitserkennung hat diese Nacht ebenfalls markiert — ein anhaltender Anstieg gegenüber deiner eigenen Basislinie, keine Diagnose.", + "roughNightLuteal": "Du befindest dich in der Lutealphase, die allein schon Ruhepuls und Hauttemperatur erhöht.", + "roughNightWarmRoom": "Deine Haut war wärmer als sonst — ein warmes Zimmer verursacht das ebenfalls.", + "roughNightDismiss": "Verwerfen", + "roughNightDefaultHeadline": "Eine schwierigere Nacht als sonst", + "roughNightSummary": "{sentence}, im Vergleich zu deinen eigenen Nächten. Das ist eine Messung der Nacht, kein Urteil über dich.", + "roughNightTellWhatHappened": "Erzählen, was los war", + "roughNightNothingToAnswer": "Nichts zu beantworten — diese Karte berichtet nur über die Nacht.", + "roughNightWhatElse": "Was war sonst noch los?", + "roughNightAnythingElse": "Noch etwas?", + "roughNightSaving": "Wird gespeichert", + "roughNightLogIt": "Für diese Nacht eintragen", + "roughNightAddHowMuch": "Menge hinzufügen", + "roughNightDoNotAskAgain": "Nicht erneut fragen", + "roughNightSeveralMoved": "Mehrere nächtliche Messwerte haben sich gemeinsam verändert", + "driverBreakdownHigherThanUsual": "Höher als gewöhnlich", + "driverBreakdownLowerThanUsual": "Niedriger als gewöhnlich", + "driverBreakdownRightOnUsual": "{now} · genau im gewohnten Bereich", + "driverBreakdownAboveUsual": "{now} · {delta} über deinem gewöhnlichen Wert von {usual}", + "driverBreakdownBelowUsual": "{now} · {delta} unter deinem gewöhnlichen Wert von {usual}", + "driverBreakdownWeightPct": "{pct} % Gewicht", + "driverBreakdownNotAvailable": "nicht verfügbar", + "driverBreakdownContributionNotReported": "Beitrag nicht gemeldet", + "driverBreakdownRelativeUncalibrated": "relativ, unkalibriert", + "driverBreakdownWithinUsualSpread": "innerhalb deiner gewöhnlichen Schwankung", + "driverBreakdownBiggerThanNoise": "größer als das Messrauschen", + "driverBreakdownSmallerThanNoise": "außerhalb deiner gewöhnlichen Schwankung, aber klein genug, um Messrauschen zu sein", + "driverBreakdownWhatHelped": "Was geholfen hat", + "driverBreakdownWhatHeldYouBack": "Was dich zurückgehalten hat", + "driverBreakdownNeither": "Weder noch", + "driverBreakdownFooter": "Jeder Eingabewert wird an deiner eigenen Historie gemessen – eine parallele Ansicht derselben Werte, keine Ausschnitte der Punktzahl selbst. „Messrauschen“ bezeichnet, wie weit sich ein Messwert von allein bewegen kann, ohne dass sich etwas geändert hat. Muster in deinen eigenen Aufzeichnungen, keine Ursachen.", + "driverBreakdownHideHistory": "{label}, Verlauf ausblenden", + "driverBreakdownShowHistory": "{label}, Verlauf anzeigen", + "driverBreakdownDaysAgo": "{n, plural, one{vor {n} Tag} other{vor {n} Tagen}}", + "driverBreakdownToday": "Heute", + "driverBreakdownUsualRange": "Dein gewöhnlicher Bereich {lo}–{hi}{unit}", + "driverBreakdownAbsenceTitle": "Keine Aufschlüsselung verfügbar", + "driverBreakdownAbsenceAlgoVersion": "Die Berechnung der Bereitschaft hat sich mit dem letzten Update geändert und wird gerade neu erstellt.", + "driverBreakdownAbsenceStale": "Die letzte Zusammenfassung ist zu alt, um dafür einzustehen.", + "driverBreakdownAbsenceNoVersion": "Die gespeicherte Zusammenfassung trägt keinen Versionsstempel.", + "driverBreakdownSyncTheBand": "Armband synchronisieren", + "driverBreakdownAbsenceNoReason": "Nichts Aufgezeichnetes erklärt, warum die letzte Nacht keine Aufschlüsselung hat.", + "coachFiguresCouldNotBeDrawn": "Eine Abbildung konnte nicht gezeichnet werden", + "coachFiguresNoType": "Der Coach hat eine Abbildung ohne Typ gesendet.", + "coachFiguresUnsupportedType": "Der Coach hat eine Abbildung vom Typ „{type}“ angefordert, die diese App nicht zeichnen kann.", + "coachFiguresFigure": "Abbildung", + "coachFiguresSeriesN": "Serie {n}", + "coachFiguresLaneN": "Spur {n}", + "coachFiguresNoSleepSegments": "Keine Schlafsegmente", + "coachFiguresNoTimeInZone": "Keine Zeit in der Zone", + "coachFiguresMinTotal": "{n} Min. insgesamt", + "coachFiguresGauge": "Anzeige", + "coachFiguresGaugeNoValue": "Der Coach hat eine Anzeige ohne Wert gesendet.", + "coachFiguresSummary": "Zusammenfassung", + "coachFiguresEmptySummary": "Der Coach hat eine leere Zusammenfassung gesendet.", + "coachFiguresTable": "Tabelle", + "coachFiguresTableNoRows": "Der Coach hat eine Tabelle ohne Zeilen gesendet.", + "circadianDetailTitle": "Innere Uhr", + "circadianDetailNoNightsTitle": "Noch keine Nächte zum Darstellen", + "circadianDetailNoNightsBody": "0 Nächte ausgewertet.", + "circadianDetailNoNightsFix": "Band über Nacht tragen", + "circadianDetailSleepTitle": "Schlaf, Nacht für Nacht", + "circadianDetailAsleep": "Schlafend", + "circadianDetailSleepFootnote": "{count, plural, one{{count} Nacht, je eine Spalte. Je dunkler, desto mehr Schlaf in dieser Stunde.} other{{count} Nächte, je eine Spalte. Je dunkler, desto mehr Schlaf in dieser Stunde.}}", + "circadianDetailYourRhythm": "Dein Rhythmus", + "circadianDetailWhichNights": "Welche Nächte", + "circadianDetailHide": "Ausblenden", + "circadianDetailShow": "Anzeigen", + "circadianDetailTodayPredicted": "Heute, vorhergesagt", + "circadianDetailRhythmStrength": "Rhythmusstärke", + "circadianDetailWhenStill": "Wenn du still bist", + "circadianDetailNoStillTitle": "Noch keine Ruhemomente zum Auswerten", + "circadianDetailNoStillBody": "Dies liest die Herzschlagfolge nur in Sekunden, in denen du dich nicht bewegt hast, und {days, plural, one{der letzte Tag hatte} other{die letzten {days} Tage hatten}} zu wenige davon, um eine Stunde zu bilden.", + "circadianDetailStillnessTitle": "Herzschlagvariabilität in Ruhe", + "circadianDetailStillnessFootnote": "Jede Stunde ist der Medianwert aus {lo}–{hi} Fünf-Minuten-Abschnitten, in denen du wirklich still warst, über {days, plural, one{den letzten Tag} other{die letzten {days} Tage}} — nie nur heute. {drawn} von 24 Stunden hatten mindestens drei Abschnitte; der Rest bleibt leer. Kein Stresswert — Aufsitzen, ein warmer Raum oder Kaffee verändern ihn genauso stark.", + "circadianDetailForecastTitle": "Wie der heutige Tag wahrscheinlich verlaufen wird", + "circadianDetailForecastFootnote": "Keine Skala — die Form ist das ganze Ergebnis.", + "circadianDetailTroughText": "Der flachste Abschnitt liegt bei {troughLabel}, gegen {start}–{end}.", + "circadianDetailPredictionDisclaimer": "Dies ist eine Vorhersage, keine Messung. Nichts am Band misst, wie wach du bist, und es kennt nur die letzte Nacht und sonst nichts — ein Nickerchen, Kaffee oder alles, was heute passiert, erreicht es nie.", + "circadianDetailAssumedPhaseNote": "Dein eigener Uhr-Höhepunkt ist noch nicht ermittelt, daher wird ein Durchschnittswert verwendet.", + "circadianDetailNotADrivingCheck": "Das ist keine Fahrtauglichkeitsprüfung und kein Schichtsicherheitstool, und es sagt nicht, dass du beeinträchtigt bist.", + "circadianDetailChronotype": "Chronotyp", + "circadianDetailMidSleepFree": "Schlafmitte, freie Tage", + "circadianDetailMidSleepWork": "Schlafmitte, Arbeitstage", + "circadianDetailSocialJetlag": "Sozialer Jetlag", + "circadianDetailLater": "später", + "circadianDetailEarlier": "früher", + "circadianDetailNightsCompared": "Verglichene freie / Arbeitsnächte", + "circadianDetailRegularityIndex": "Regelmäßigkeitsindex", + "circadianDetailNightsLeastAlike": "Unähnlichste Nächte", + "circadianDetailSamePairScale": "Dieses Paar, gleiche Skala", + "circadianDetailRhythmNotEstablished": "Dein Rhythmus ist noch nicht ermittelt", + "circadianDetailPairFootnote": "Das Paar mit der geringsten Übereinstimmung, von {count}. Ein länger geratenes Wochenende ist ein anderer Zeitplan, keine schlechtere Nacht. Paare, bei denen von einem der Tage zu wenig aufgezeichnet wurde, werden ausgelassen.", + "circadianDetailStability": "Tag-zu-Tag-Stabilität", + "circadianDetailFragmentation": "Stunde-zu-Stunde-Fragmentierung", + "circadianDetailAmplitude": "Relative Amplitude", + "circadianDetailM10Start": "Beginn der 10 Stunden mit höchster Herzfrequenz", + "circadianDetailL5Start": "Beginn der 5 Stunden mit niedrigster Herzfrequenz", + "circadianDetailRhythmPeak": "Rhythmusspitze", + "circadianDetailPeakSwing": "Spitze-Mittelwert-Ausschlag", + "circadianDetailFitCurve": "Anpassung an eine 24-h-Kurve", + "circadianDetailStrengthNotMeasured": "Rhythmusstärke ist noch nicht gemessen", + "circadianDetailStrengthWhy": "Benötigt aufeinanderfolgende Tage mit allen 24 aufgezeichneten Stunden.", + "circadianDetailStrengthFootnoteKnown": "Aus {used, plural, one{{used} vollständig aufgezeichneten Tag} other{{used} vollständig aufgezeichneten Tagen}} der Herzfrequenz. Das sind deine höchsten und niedrigsten Herzfrequenz-Stunden, nicht deine aktivsten.", + "circadianDetailStrengthFootnoteUnknown": "Aus einer Reihe vollständig aufgezeichneter Tage der Herzfrequenz. Das sind deine höchsten und niedrigsten Herzfrequenz-Stunden, nicht deine aktivsten.", + "beatsTitle": "Herzschläge", + "beatsNoNightTitle": "Noch keine Nacht zum Darstellen", + "beatsNoNightBody": "Auf diesem Telefon wurde noch keine ausgewertete Nacht erzeugt, daher gibt es keine Schlagintervalle zum Darstellen.", + "beatsNoNightFix": "Band über Nacht tragen, dann synchronisieren", + "beatsNightOf": "Nacht vom {date}", + "beatsPoincareSection": "Jeder Schlag gegen den vorherigen", + "beatsBeatsGoneTitle": "Die Schläge dieser Nacht sind nicht mehr auf diesem Telefon", + "beatsBeatsGoneBody": "Einzelne Schlagintervalle werden ein paar Tage nach der Auswertung der Nacht aufbewahrt und dann gelöscht. Die daraus abgeleiteten Werte bleiben dauerhaft erhalten", + "beatsBeatsGoneMeasured": " — diese Nacht ergab SD1 {sd1} ms, SD2 {sd2} ms", + "beatsScatterTitle": "Jedes Intervall, gegen das vorherige aufgetragen", + "beatsScatterFootnote": "Die Diagonale ist der Punkt, an dem ein Schlag genauso lang war wie der vorherige. Streuung quer zu dieser Linie ist SD1, Schlag für Schlag; Streuung entlang der Linie ist SD2, die langsamere Drift.", + "beatsSd1Label": "SD1", + "beatsSd2Label": "SD2", + "beatsIntervalsLabel": "Intervalle", + "beatsIntervalsSurvived": "{count, plural, one{{count} Intervall hat die Korrektur überstanden} other{{count} Intervalle haben die Korrektur überstanden}}", + "beatsDroppedArtifact": " — {count, plural, one{{count} wurde als Artefakt verworfen und ist} other{{count} wurden als Artefakte verworfen und sind}} nicht in der Cloud", + "beatsPulseNotEcg": "Puls, kein EKG — echt und deins, aber nicht das Bild, das ein EKG zeichnet.", + "beatsMeasuredOn": "Gemessen mit {device}; Bänder lesen nicht dieselben Werte wie andere.", + "beatsVariabilitySection": "Variabilität im Verlauf der Nacht", + "beatsUnitNights": "Nächte", + "beatsUnitScreenedNotScreened": "erfasst / nicht erfasst", + "beatsVariabilityWhy": "Kein Halbstundenblock dieser Nacht hatte genug saubere Schläge, um einen RMSSD-Wert zu veröffentlichen.", + "beatsNoBinsStored": "Für diese Nacht wurden keine Blöcke gespeichert.", + "beatsRmssdTitle": "RMSSD in Halbstundenblöcken", + "beatsStart": "Start", + "beatsBandFootnote": "Der Balken zeigt, wie sicher wir uns beim Block sind, nicht einen Bereich, den dein Körper durchlaufen hat. Die Markierung darin ist der Wert.", + "beatsHolesFootnote": "{count, plural, one{ {count} Block hat zu wenige saubere Schläge, um einen zu veröffentlichen, und bleibt leer statt verbunden.} other{ {count} Blöcke haben zu wenige saubere Schläge, um einen zu veröffentlichen, und bleiben leer statt verbunden.}}", + "beatsFirstThird": "Erstes Drittel", + "beatsLastThird": "Letztes Drittel", + "beatsDcSection": "Dezelerationskapazität", + "beatsDcWhy": "Noch keine gespeicherte Nacht hat einen Wert ergeben.", + "beatsDcNoData": "Noch keine Nacht hat einen Wert ergeben.", + "beatsDcChartTitle": "Deine eigenen Nächte, der Reihe nach", + "beatsDaysAgo": "vor {count} Tagen", + "beatsTodayLabel": "Heute", + "beatsAnchorsLastNight": "Anker letzte Nacht", + "beatsCleanBeats": "Saubere Schläge", + "beatsDcNote": "Nur deins. Vergleiche es mit deinen eigenen anderen Nächten und mit nichts anderem — für ein Handgelenk gibt es kein Referenzband.\n\nEs mittelt die Schläge um jeden Moment, in dem sich dein Herz verlangsamt hat. Eine steigende Linie kann ein saubereres Signal bedeuten statt eines anderen Herzens, also lies sie zusammen mit der Ankerzahl und dem Anteil sauberer Schläge oben. Wenn du innerhalb dieses Zeitfensters das Band gewechselt hast, sind die beiden Hälften nicht vergleichbar.", + "beatsRhythmSection": "Rhythmus-Screening", + "beatsRhythmChartTitle": "Eine Zelle pro Tag", + "beatsScreenNotFired": "Screening hat nicht ausgelöst", + "beatsScreenFired": "Screening hat ausgelöst", + "beatsNotScreened": "Nicht gescreent", + "beatsNoDayScreened": "Kein Tag in diesem Zeitfenster wurde gescreent", + "beatsScreenNote": "Ein Screening, kein Test.\n\nEin Tag, an dem das Screening nicht ausgelöst hat, ist kein Tag, an dem du freigesprochen bist — es kann nichts ausschließen und konnte es nie. Umrandete Tage wurden überhaupt nicht gescreent: zu wenige saubere Schläge oder zu viel Bewegung.\n\nPuls am Handgelenk ist kein EKG. Wenn dich Symptome hierhergeführt haben, kann eine ärztliche Fachperson das richtig untersuchen.", + "beatsScreenedSummary": "{screened} der letzten {win} Tage wurden gescreent", + "beatsFiredSummary": "; das Screening löste an {fired} Tagen aus", + "beatsNotScreenedNote": " Letzte Nacht wurde nicht gescreent: {note}", + "logWorkoutCouldNotLog": "Konnte nicht erfasst werden — versuch es noch mal.", + "logWorkoutCouldNotDismiss": "Konnte nicht verworfen werden — versuch es noch mal.", + "logWorkoutAdjustTimes": "Zeiten anpassen", + "logWorkoutDetectedActivityTitle": "Erkannte Aktivität", + "logWorkoutYoursToConfirmSub": "VON DIR ZU BESTÄTIGEN", + "logWorkoutReadFailedTitle": "Erkannte Aktivität konnte nicht gelesen werden", + "logWorkoutReadFailedBody": "Die Datenbank hat nicht geantwortet. Es wurde nichts erfasst oder verworfen.", + "logWorkoutTryAgain": "Erneut versuchen", + "logWorkoutReadingSpotted": "Wird gelesen, was das Band erfasst hat…", + "logWorkoutNothingToReviewTitle": "Nichts zu überprüfen", + "logWorkoutNothingToReviewBody": "Diese Aktivität wurde möglicherweise schon erfasst oder verworfen.", + "logWorkoutHardMinutesTitle": "Das sind die intensiven Minuten, nicht die ganze Einheit", + "logWorkoutHardMinutesBody": "Die Erkennung meldet nur die anhaltende Belastung, die sie sehen konnte — ein Aufwärmen und die Pausen zwischen Sätzen fallen also heraus. Passe die Zeiten vor dem Erfassen an, wenn der Zeitraum zu kurz ist.", + "logWorkoutMinutesOfEffort": "{mins} Min. Belastung", + "logWorkoutAvgHr": "Ø HF", + "logWorkoutPeakHr": "Max. HF", + "logWorkoutLooksLike": "Sieht aus wie", + "logWorkoutLogIt": "Erfassen", + "logWorkoutNotAWorkout": "Kein Training", + "logWorkoutToday": "Heute", + "logWorkoutYesterday": "Gestern", + "logWorkoutDefaultTitle": "Vergangenes Training erfassen", + "logWorkoutWindowRescoredSub": "ZEITRAUM, NEU BEWERTET", + "logWorkoutYourOwnTimesSub": "DEINE EIGENEN ZEITEN", + "logWorkoutWhenGroup": "Wann", + "logWorkoutActivityLabel": "Aktivität", + "logWorkoutDateLabel": "Datum", + "logWorkoutStartedLabel": "Beginn", + "logWorkoutEndedLabel": "Ende", + "logWorkoutLengthLabel": "Dauer", + "logWorkoutNextMorningSub": "am nächsten Morgen", + "logWorkoutWindowInvalidTitle": "Dieser Zeitraum kann nicht gespeichert werden", + "logWorkoutTimesUpdatedTitle": "Zeiten aktualisiert", + "logWorkoutLoggedTitle": "Training erfasst", + "logWorkoutUnscoredSaved": "Gespeichert. In diesem Zeitraum wurde keine Herzfrequenz erfasst, daher gibt es keine Belastung und keine Kalorienangabe — nur die Zeiten sind hinterlegt.", + "logWorkoutCouldNotSave": "Konnte nicht gespeichert werden — versuch es noch mal.", + "logWorkoutScoredTitle": "Berechnet aus den Aufzeichnungen des Bands", + "logWorkoutScoredBody": "Belastung und Kalorien stammen aus der sekundengenauen Herzfrequenz in diesem Zeitraum, mit derselben Methode wie beim Tag. Nichts wird aus der Dauer geschätzt.", + "logWorkoutSaving": "Speichern…", + "logWorkoutSaveNewTimes": "Neue Zeiten speichern", + "logWorkoutSearchActivities": "Aktivitäten suchen", + "logWorkoutNoActivityByName": "Keine Aktivität mit diesem Namen", + "logFoodTitle": "Mahlzeit erfassen", + "logFoodClose": "Schließen", + "logFoodIAte": "Ich habe {meal} gegessen", + "logFoodAgain": "Erneut", + "logFoodAddNumbers": "Werte hinzufügen", + "logFoodScanBarcode": "Barcode scannen", + "logFoodScanSubOn": "Fragt openfoodfacts.org zum Barcode ab und trägt ein, wofür es einstehen kann", + "logFoodScanSubOff": "Sucht das Produkt online. Fragt vorher nach", + "logFoodLookingUpTitle": "Wird gesucht", + "logFoodLookingUpBody": "Die Felder füllen sich, sobald die Antwort da ist.", + "logFoodWhatLabel": "Was", + "logFoodWhatHint": "Hähnchen mit Reis", + "logFoodPortionLabel": "Portion", + "logFoodUnknownHint": "unbekannt", + "logFoodEnergyLabel": "Energie", + "logFoodProteinLabel": "Eiweiß", + "logFoodCarbsLabel": "Kohlenhydrate", + "logFoodFatLabel": "Fett", + "logFoodFibreLabel": "Ballaststoffe", + "logFoodBlankHint": "Eine leere Zahl bleibt leer. Nur „Was“ wird benötigt.", + "logFoodSayWhatFirst": "Gib zuerst an, was es war.", + "logFoodConsentTitle": "Barcodes online nachschlagen?", + "logFoodConsentBody1": "Ein Scan sendet den Barcode an openfoodfacts.org, eine freie, offene Lebensmitteldatenbank. Dort werden der Barcode und deine IP-Adresse gesehen. Nichts über dich, deine Mahlzeiten oder deine Gesundheit verlässt dieses Telefon, und ein bereits gescannter Barcode wird aus deiner eigenen Kopie beantwortet, ohne erneut anzufragen.", + "logFoodConsentBody2": "Die Werte werden von der Öffentlichkeit eingetragen, und einige davon sind falsch — alles, was eine Plausibilitätsprüfung nicht besteht, bleibt daher leer statt ausgefüllt zu werden. Alles, was ausgefüllt wird, kannst du vor dem Speichern noch bearbeiten.", + "logFoodConsentBody3": "Du kannst das in Einstellungen › Datenschutz wieder ausschalten. Die Werte von der Verpackung abzutippen funktioniert so oder so.", + "logFoodAllowLookups": "Abfragen erlauben", + "logFoodNotNow": "Jetzt nicht", + "logFoodBreakfast": "Frühstück", + "logFoodLunch": "Mittagessen", + "logFoodDinner": "Abendessen", + "logFoodSnack": "Snack", + "logFoodNoNumbersTitle": "Keine Werte für dieses Produkt", + "logFoodNoNumbersBody": "Open Food Facts kennt das Produkt, aber ohne verwertbare Nährwerte — oder die vorhandenen Werte haben die Plausibilitätsprüfung nicht bestanden.", + "logFoodNotFoundTitle": "Nicht bei Open Food Facts", + "logFoodNotFoundBody": "Diesen Barcode hat noch niemand hinzugefügt.", + "logFoodFlaggedTitle": "Dieser Eintrag ist als fehlerhaft markiert", + "logFoodFlaggedBody": "Open Food Facts markiert dieses Produkt als fehlerhaft, daher wurde keiner seiner Werte übernommen.", + "logFoodUnreachableTitle": "Keine Antwort von Open Food Facts", + "logFoodUnreachableBody": "openfoodfacts.org konnte nicht erreicht werden.", + "logFoodRefusedTitle": "Barcode-Abfrage ist deaktiviert", + "logFoodRefusedBody": "Es wurde nichts gesendet. Du kannst sie in Einstellungen › Datenschutz aktivieren.", + "logFoodPortionNoteBase": "Open Food Facts gibt dies pro 100 g an. Ändere die Portion, und die Werte passen sich an.", + "logFoodPortionNoteServing": "Open Food Facts gibt dies pro 100 g an. Ändere die Portion, und die Werte passen sich an. Die eigene Portionsangabe der Verpackung ist {serving}.", + "logFoodPillOpenFoodFacts": "Open Food Facts", + "logFoodPillYours": "Von dir", + "logFoodBareOccasion": "ERFASST · KEINE ENERGIEANGABE", + "logFoodOpensInBrowser": "{label}, öffnet im Browser", + "dayTimelineChargerOn": "Am Ladegerät", + "dayTimelineChargerOff": "Vom Ladegerät genommen", + "dayTimelineDoubleTap": "Du hast das Band doppelt angetippt", + "dayTimelineRestarted": "Das Band hat neu gestartet", + "dayTimelineBatteryPackAttached": "Akkupack angebracht", + "dayTimelineBatteryPackRemoved": "Akkupack entfernt", + "dayTimelineAlarmWentOff": "Alarm hat ausgelöst", + "dayTimelineAsleep": "Schlaf", + "dayTimelineNap": "Nickerchen", + "dayTimelineWorkout": "Training", + "dayTimelineBandOffWrist": "Band nicht am Handgelenk", + "dayTimelineHighestHr": "Höchste Herzfrequenz", + "dayTimelineLowestHr": "Niedrigste Herzfrequenz", + "dayTimelineBpmAt": "{bpm} bpm um {time}", + "dayTimelineTakenAt": "Eingenommen um {time}", + "dayTimelineLastAt": "zuletzt um {time}", + "dayTimelineTaggedTitle": "Markiert", + "dayTimelineTitle": "Überblick über deinen Tag", + "dayTimelineSub": "MITTERNACHT BIS MITTERNACHT", + "dayTimelineHeartRateTitle": "Herzfrequenz", + "dayTimelineMidnight": "Mitternacht", + "dayTimelineNoon": "Mittag", + "dayTimelineMoving": "In Bewegung", + "dayTimelineNotRecorded": "Nicht erfasst", + "dayTimelineNothingRecordedTitle": "An diesem Tag wurde nichts erfasst", + "dayTimelineNothingRecordedBody": "Kein Schlaf, keine Einheit, kein Eintrag und kein zeitlich erfasstes Band-Ereignis. Ein solcher Tag bedeutet meist, dass das Band nicht getragen wurde.", + "dayTimelineNoTimeTitle": "Nichts an diesem Tag hat eine Uhrzeit", + "dayTimelineNoTimeBody": "Was erfasst wurde, steht unten.", + "dayTimelineWhatHappenedSection": "Was passiert ist", + "dayTimelineAlsoLoggedSection": "Ebenfalls an diesem Tag erfasst", + "dayTimelineNoTimeNote": "Dies wurde für den Tag erfasst, aber ohne Uhrzeit, deshalb erscheint es nicht auf der Zeitachse.", + "dayTimelinePatternsNote": "Muster in deinen eigenen Daten, keine Ursachen. Dass zwei Dinge hier nahe beieinanderstehen, bedeutet nur, dass sie zeitlich nah beieinander lagen — mehr behauptet diese Seite nicht.", + "journalComposeNotReady": "Noch nicht bereit — öffne zuerst die App.", + "journalComposeSaveFailed": "Konnte nicht gespeichert werden — Speicher prüfen und erneut versuchen.", + "journalComposeWhenWasLastOne": "Wann war das letzte Mal?", + "journalComposeTitle": "Journal", + "journalComposeTodaySection": "Heute", + "journalComposeTrackSomethingElse": "Etwas anderes erfassen", + "journalComposeAnythingElseLabel": "Sonstiges", + "journalComposeAnythingElseHint": "Ein paar Worte zum Tag.", + "journalComposeSavingLabel": "Speichert", + "journalComposeHowAreYouFeeling": "Wie fühlst du dich?", + "journalComposeNotAnsweredYet": "Noch nicht beantwortet", + "journalComposeMoodOfFive": "Stimmung {value} von 5 · nochmal tippen zum Löschen", + "journalComposeMoodOfFiveSelected": "Stimmung {n} von 5, ausgewählt. Zum Löschen aktivieren.", + "journalComposeMoodOfFiveLabel": "Stimmung {n} von 5", + "journalComposeNotLogged": "Nicht erfasst", + "journalComposeWhenWasLastField": "Wann war das letzte Mal {field}", + "journalComposeAddTimeOfLastOne": "Uhrzeit des letzten Mals hinzufügen", + "journalComposeLastAt": "Zuletzt um {time}", + "journalComposeIncrease": "Erhöhen", + "journalComposeDecrease": "Verringern", + "journalComposeWeightLabel": "Gewicht", + "journalComposeNotEntered": "Nicht erfasst", + "journalComposeEnteredNotMeasured": "{value} · eingegeben, nicht gemessen", + "journalComposeEnterWeight": "Gewicht eingeben", + "journalComposeEnter": "Eingeben", + "journalComposeChange": "Ändern", + "journalComposeSeeWeightTrend": "Gewichtsverlauf ansehen", + "journalComposeSeeTheTrend": "Verlauf ansehen", + "journalComposeWeightToday": "Gewicht heute", + "journalComposeWeightKgLabel": "Gewicht (kg)", + "journalComposeWeightScaleNote": "Was du oder deine Waage anzeigen. Das Band misst dies nicht.", + "journalComposeClear": "Löschen", + "journalComposeNotEnoughEntriesTitle": "Nicht genug Einträge für einen Verlauf", + "journalComposeNotEnoughEntriesBody": "Die Linie ist ein Sieben-Tage-Durchschnitt deiner Eingaben, dafür sind mindestens zwei Tage nötig. Dazwischen wird nichts ergänzt.", + "journalComposeSevenDayTrend": "Sieben-Tage-Trend", + "journalComposeTrendFootnote": "Von dir eingegeben. Tage ohne Eintrag bleiben leer.", + "journalComposeWeightTrendExplainer": "Von dir oder deiner Waage eingegeben — das Band misst das Gewicht nicht. Dargestellt wird ein Sieben-Tage-Durchschnitt, denn eine Waage schwankt allein durch Wasser und Nahrung um ein bis zwei Kilo, und die Rohwerte würden das als Veränderung des Körpers zeigen. {count, plural, one{{count} Tag erfasst.} other{{count} Tage erfasst.}}", + "nutritionTabToday": "Heute", + "nutritionTabWeek": "Woche", + "nutritionTabGoals": "Ziele", + "nutritionLogFood": "Essen protokollieren", + "nutritionTitle": "Ernährung", + "nutritionEmptyTodayTitle": "Heute noch nichts erfasst", + "nutritionEmptyTodayBody": "Ein Tap genügt für einen vollständigen Eintrag.", + "nutritionLogOccasionFix": "Eine Mahlzeit erfassen", + "nutritionOccasionsSection": "Mahlzeiten", + "nutritionAddAction": "Hinzufügen", + "nutritionFloorTitle": "Die heutige Energie ist ein Mindestwert, kein Gesamtwert", + "nutritionFloorBody": "{unknown} von {total} Mahlzeiten wurden ohne Energiewert erfasst, daher ist die Zahl oben das Mindeste, was du gegessen hast, nicht das, was du tatsächlich gegessen hast.", + "nutritionAddNumbersFix": "Werte zu einer Mahlzeit hinzufügen", + "nutritionDaysNotCounted": "{excluded} der letzten {span} Tage konnten nicht gezählt werden", + "nutritionDayCountsRule": "Ein Tag zählt, sobald jede Mahlzeit einen Energiewert trägt.", + "nutritionDaysLoggedLabel": "Tage mit irgendeinem Eintrag", + "nutritionPartialExcluded": "{partial} erfasst, aber unvollständig, daher aus jedem Durchschnitt unten ausgeschlossen", + "nutritionEnergyByDay": "Energie, Tag für Tag", + "nutritionSevenDayAvg": "Sieben-Tage-Durchschnitt", + "nutritionNoCompleteDayTitle": "Noch kein vollständiger Tag zum Mitteln", + "nutritionNoCompleteDayBody": "Du hast keinen.", + "nutritionLabelEnergy": "Energie", + "nutritionLabelProtein": "Protein", + "nutritionLabelCarbs": "Kohlenhydrate", + "nutritionLabelFat": "Fett", + "nutritionLabelFibre": "Ballaststoffe", + "nutritionEnergyBalance": "Energiebilanz", + "nutritionLabelEaten": "GEGESSEN", + "nutritionLabelBurned": "VERBRANNT", + "nutritionLabelBalance": "BILANZ", + "nutritionEatenMeanNote": "Gegessen ist der Durchschnitt über {days} vollständige Tage. Verbrannt gilt nur für heute.", + "nutritionEnergyLoggedTitle": "Erfasste Energie", + "nutritionPartialFootnote": "{n} unvollständig, aus den Durchschnitten unten ausgeschlossen.", + "nutritionNothingLoggedYet": "Noch nichts erfasst", + "nutritionNoEnergyFiguresYet": "Noch keine Energiewerte", + "nutritionDailyEnergy": "Tägliche Energie", + "nutritionDailyProtein": "Tägliches Protein", + "nutritionEnergyWord": "Energie", + "nutritionProteinWord": "Protein", + "nutritionYourTargetsSection": "Deine Ziele", + "nutritionHintNone": "keiner", + "nutritionNoTargetsTitle": "Keine Ziele festgelegt", + "nutritionNoTargetsBody": "Ein Ziel hier ist eines, das du selbst eingibst.", + "nutritionSetTargetFix": "Ein Ziel festlegen", + "nutritionEditAction": "Bearbeiten", + "nutritionBodySpentToday": "Was dein Körper heute verbraucht hat", + "nutritionEstimatedExpenditure": "Geschätzter Verbrauch", + "nutritionNotMeasured": "Nicht gemessen", + "nutritionExpenditureSub": "HEUTE, AUS HERZFREQUENZ UND DEINEM PROFIL", + "nutritionRemoveTitle": "{label} entfernen?", + "nutritionRemoveBody": "Damit verschwindet der Tag aus jedem Durchschnitt, der ihn mitgezählt hat. Das lässt sich nicht rückgängig machen.", + "nutritionNothingToMeasure": "Noch nichts, an dem {label} gemessen werden kann", + "nutritionFloorAverageBody": "An jedem vollständigen Tag fehlte bei einer Mahlzeit der {nutrient}-Wert, daher wäre der Durchschnitt nur eine Untergrenze.", + "nutritionCountedNoFigureBody": "{count} der letzten {span} Tage zählten, aber keiner trug einen {nutrient}-Wert.", + "nutritionDayCountsRuleFull": "Ein Tag zählt, sobald jede Mahlzeit einen Wert trägt und der Eintrag bis zum Abend reicht. Keiner der letzten {span} Tage hat das getan.", + "nutritionOnTarget": "Im Ziel", + "nutritionRateAbove": "{amount} {unit}/Tag darüber", + "nutritionRateBelow": "{amount} {unit}/Tag darunter", + "nutritionMeanOfDays": "{n, plural, one{Durchschnitt über {n} vollständigen Tag} other{Durchschnitt über {n} vollständige Tage}}", + "nutritionLoggedToday": "HEUTE ERFASST", + "nutritionEatenToday": "HEUTE GEGESSEN", + "nutritionAtLeast": "Mindestens", + "nutritionOccasionsUnit": "{n, plural, one{Mahlzeit} other{Mahlzeiten}}", + "nutritionOccasionsCount": "{n, plural, one{{n} Mahlzeit} other{{n} Mahlzeiten}}", + "nutritionLabelBalanceAtLeast": "BILANZ MINDESTENS", + "nutritionNotLogged": "Nicht erfasst", + "nutritionLoggedNoEnergy": "{n} erfasst · Energie nicht verzeichnet", + "nutritionAtLeastPrefix": "mindestens ", + "nutritionMealBreakfast": "Frühstück", + "nutritionMealLunch": "Mittagessen", + "nutritionMealDinner": "Abendessen", + "nutritionMealSnacks": "Snacks", + "nutritionNotCounted": "Nicht gezählt", + "nutritionNotRecorded": "Nicht verzeichnet", + "nutritionEveryDayNoFigure": "AN JEDEM VOLLSTÄNDIGEN TAG FEHLTE BEI EINER MAHLZEIT DER {label}-WERT", + "nutritionNoDayRecorded": "KEIN VOLLSTÄNDIGER TAG HAT {label} VERZEICHNET", + "nutritionMeanOfCompleteDaysCaps": "{n, plural, one{DURCHSCHNITT ÜBER {n} VOLLSTÄNDIGEN TAG} other{DURCHSCHNITT ÜBER {n} VOLLSTÄNDIGE TAGE}}", + "nutritionLeftOutAsFloor": " · {n} ALS MINDESTWERT AUSGESCHLOSSEN", + "nutritionWaterLabel": "Wasser", + "nutritionTapToChange": "− oder + tippen zum Ändern", + "nutritionNoneYet": "Noch keins", + "nutritionAddWater": "Wasser hinzufügen", + "nutritionRemoveWater": "Wasser entfernen", + "coachApiKeyLabel": "API-Schlüssel", + "coachApiKeyLocalLabel": "API-Schlüssel (lokal nicht nötig)", + "coachAsking": "Wird abgefragt…", + "coachAskLabel": "Den Coach fragen", + "coachBaseUrlLabel": "Basis-URL", + "coachBriefingMenuSub": "Der exakte Schnappschuss, der dieses Gerät verlassen hat", + "coachBriefingMenuTitle": "Briefing, und was gesendet wurde", + "coachChooseModelFix": "Ein Modell wählen", + "coachCloudDataNote": "Deine Fragen und die Daten, die der Coach liest, werden an diesen Endpunkt gesendet. Sieh dir unter „Was gesendet wurde“ genau an, was das ist.", + "coachDeleteChat": "{title} löschen", + "coachDeleteIt": "Löschen", + "coachDestructiveWarning": "Dies entfernt Daten von diesem Gerät und kann nicht rückgängig gemacht werden.", + "coachEndpointUnreachable": "Dieser Endpunkt war nicht erreichbar: {error}", + "coachErrorTitle": "Das hat nicht funktioniert", + "coachInputHint": "Frag etwas zu deiner Gesundheit…", + "coachIntroBody": "Frag alles, was die App misst, und sie kann Essen, Wasser, Workouts, Dosen und dein Befinden protokollieren — und fragt dabei immer erst nach.", + "coachKeychainRefused": "Der Schlüsselbund hat den Schlüssel abgelehnt: {error}", + "coachKeyStillSavedBody": "Er konnte diesmal nicht aus dem Schlüsselbund gelesen werden, was passiert, wenn die App bei gesperrtem Telefon aufwacht.", + "coachKeyStillSavedTitle": "Dein Schlüssel ist noch gespeichert", + "coachListModels": "Modelle auflisten", + "coachLocalDataNote": "Deine Fragen und die Daten, die der Coach liest, bleiben auf deiner eigenen Maschine.", + "coachLocalSub": "In diesem Netzwerk. Nichts verlässt deine Maschine.", + "coachMenuSemantic": "Chats und KI-Einstellungen", + "coachModelHint": "suchen oder eine ID eingeben", + "coachModelLabel": "Modell", + "coachModelsFound": "{n, plural, one{{n} Modell. Eins antippen.} other{{n} Modelle. Eins antippen.}}", + "coachNavTitle": "Coach", + "coachNewChat": "Neuer Chat", + "coachNoChatsYet": "Noch nichts — das ist dein erstes Gespräch.", + "coachNoDataBody": "Der Coach antwortet anhand deiner eigenen abgeleiteten Tage, und davon gibt es auf diesem Gerät noch keine.", + "coachNoDataTitle": "Noch keine Daten zum Lesen", + "coachNoModelsListed": "Dieser Endpunkt hat keine Modelle aufgelistet. Gib unten eins ein.", + "coachNotSetUp": "Nicht eingerichtet", + "coachNotSetUpBody": "Er läuft mit einem Modell, das du wählst — eines auf deiner eigenen Maschine oder jeder OpenAI-kompatible Anbieter mit deinem eigenen Schlüssel. In keinem Fall läuft etwas über OpenStrap.", + "coachNotSetUpTitle": "Der Coach ist nicht eingerichtet", + "coachPastChats": "FRÜHERE CHATS", + "coachPickModelFirst": "Wähle oder gib zuerst ein Modell ein.", + "coachSafeWarning": "Es wird nichts geschrieben, bis du unten tippst.", + "coachSaveIt": "Speichern", + "coachSendLabel": "Senden", + "coachSetupNavSub": "Bring dein eigenes Modell mit", + "coachSetupNavTitle": "KI-Einstellungen", + "coachSomethingWrong": "Etwas ist schiefgelaufen: {error}", + "coachStarterAteYesterday": "Was habe ich gestern gegessen?", + "coachStarterHrvChart": "Zeig mir meine HRV im letzten Monat als Diagramm", + "coachStarterLogRun": "Ich bin heute Morgen 40 Minuten gelaufen — protokollier das", + "coachStarterLogWater": "Protokolliere 500 ml Wasser für heute", + "coachStarterRecovery": "Wie erholt bin ich heute, und warum?", + "coachStarterSleep": "Wie war mein Schlaf diese Woche?", + "coachTryAgainFix": "Erneut versuchen", + "coachTryAsking": "Frag doch mal", + "coachUntitledChat": "Chat ohne Titel", + "coachWhereModelRuns": "Wo das Modell läuft", + "coachYourDataYourModel": "DEINE DATEN, DEIN MODELL", + "investigateNerdStatsLabel": "DETAILWERTE", + "investigateProvenanceLabel": "Herkunft", + "investigateDayLabel": "Tag", + "investigateCoverageLabel": "Abdeckung", + "investigateSleepWindowLabel": "Schlaffenster", + "investigateSourceLabel": "Quelle", + "investigateSourceOnDevice": "Band-Aufzeichnungen · auf diesem Telefon berechnet", + "investigateSourceImported": "Importiert · {source}", + "investigateAlgoVersionLabel": "Algorithmusversion", + "investigateWhatHappenedTitle": "Was an diesem Tag geschah", + "investigateWhatHappenedSub": "Schlaf, Sitzungen, Mahlzeiten und Einträge in zeitlicher Reihenfolge", + "investigateWhichSensorCounted": "Welcher Sensor gezählt hat", + "investigateStrapPedometer": "Band · 100-Hz-Schrittzähler", + "investigateStrapOnChipCounter": "Band · integrierter Zähler", + "investigatePhonePedometer": "Telefon · Schrittzähler", + "investigateDayTotal": "Tagesgesamt", + "investigateStrapChipReported": "Band-Chip gemeldet", + "investigateTimeDomain": "Zeitbereich", + "investigateRmssd": "RMSSD", + "investigateSdnn": "SDNN", + "investigateSdann": "SDANN", + "investigateSdnnIndex": "SDNN-Index", + "investigatePnn50": "pNN50", + "investigateLnRmssd": "ln RMSSD", + "investigateBaselineRmssd": "Ihr Basis-RMSSD", + "investigateStabilityCv": "Stabilität (VK)", + "investigateFrequencyDomain": "Frequenzbereich", + "investigateUlfPower": "ULF-Leistung", + "investigateVlfPower": "VLF-Leistung", + "investigateLfPower": "LF-Leistung", + "investigateHfPower": "HF-Leistung", + "investigateTotalPower": "Gesamtleistung", + "investigateLfHf": "LF / HF", + "investigateLfNormalised": "LF, normalisiert", + "investigateHfNormalised": "HF, normalisiert", + "investigateHfGated": "HF gefiltert", + "investigateYes": "ja", + "investigateNo": "nein", + "investigateNoFrequencySpectrum": "Kein Frequenzspektrum für diese Nacht", + "investigateRecordingTooShort": "Die Aufzeichnung war zu kurz, um die Bänder aufzulösen.", + "investigateNonLinear": "Nichtlinear", + "investigateSd1Sleep": "SD1, Schlaf", + "investigateSd2Sleep": "SD2, Schlaf", + "investigateSd124h": "SD1, 24 Std.", + "investigateSd224h": "SD2, 24 Std.", + "investigateSd1Sd224h": "SD1 / SD2, 24 Std.", + "investigateSuccessiveIntervalsOver70ms": "Aufeinanderfolgende Intervalle über 70 ms", + "investigateIrregularRhythmFlagSleep": "Markierung für unregelmäßigen Rhythmus, Schlaf", + "investigateIrregularRhythmFlag24h": "Markierung für unregelmäßigen Rhythmus, 24 Std.", + "investigateFlagRaised": "ausgelöst", + "investigateFlagClear": "unauffällig", + "investigateDecelerationCapacity": "Dezelerationskapazität", + "investigateAccelerationCapacity": "Akzelerationskapazität", + "investigateDcAnchors": "DC-Ankerpunkte", + "investigateSignalQuality": "Signalqualität", + "investigateBeatsAnalysed": "Analysierte Schläge", + "investigateBeatsAnalysed24h": "Analysierte Schläge, 24 Std.", + "investigateNoShapeForNight": "Keine Verlaufsform für diese Nacht", + "investigateTooFewBeatsToBin": "Die Nacht hatte zu wenige saubere Schläge zum Einteilen.", + "investigateShapeOfTheNight": "Verlaufsform der Nacht", + "investigateBinRmssd": "RMSSD pro Block", + "investigateSamplingRange": "Streubereich", + "investigateShapeFootnote": "{drawn} von {total} Blöcken hatten genügend Schläge zum Auswerten; der Rest sind Lücken, keine Nullen. Das äußere Paar ist die eigene Streubreite des Schätzers, kein Bereich, in dem Sie sich befanden. Dies beschreibt die Nacht und kann sie nicht erklären — ein niedriges erstes Drittel ist ebenso vereinbar mit Alkohol, einer späten Mahlzeit, spätem Training, einem warmen Zimmer, einer beginnenden Krankheit oder gar nichts.", + "investigateNightShape": "Verlaufsform der Nacht", + "investigateBinWidth": "Blockbreite", + "investigateBinsRead": "Ausgewertete Blöcke", + "investigateFirstThird": "Erstes Drittel", + "investigateLastThird": "Letztes Drittel", + "investigateLastThirdOverFirst": "Letztes Drittel ÷ erstes", + "investigate29DaysAgo": "Vor 29 Tagen", + "investigateToday": "Heute", + "investigateDcFootnoteWithBeats": "Nur Ihre eigenen Nächte — für Pulsschläge gibt es keinen Referenzbereich. Die Signalqualität lässt diese Linie von Nacht zu Nacht schwanken, und letzte Nacht waren es {beats} Schläge.", + "investigateDcFootnote": "Nur Ihre eigenen Nächte — für Pulsschläge gibt es keinen Referenzbereich. Die Signalqualität lässt diese Linie von Nacht zu Nacht schwanken.", + "investigateIrregularRhythmScreen": "Screening auf unregelmäßigen Rhythmus", + "investigateOneSquarePerDay": "ein Quadrat pro Tag", + "investigate12WeeksAgo": "Vor 12 Wochen", + "investigateThisWeek": "Diese Woche", + "investigateScreenRan": "Screening durchgeführt", + "investigateRhythmStripFootnote": "An {ran} {ran, plural, one{Tag} other{Tagen}} durchgeführt, hat an {raised} Tagen ausgeschlagen. Ein umrandetes Quadrat ist ein Tag, an dem es nicht lief. Ein sauberer Streifen ist kein negatives Ergebnis: Dies ist ein Screening anhand des Pulsrhythmus, das einen ektopen Schlag nicht von einem verpassten Schlag oder einer Bewegung des Bands am Handgelenk unterscheiden kann.", + "investigateNoRestingBreathingRate": "Keine Ruheatemfrequenz außerhalb des Schlafs", + "investigateNoRestingBreathingRateBody": "Dies liest die Atmung nur aus dreiminütigen Abschnitten, in denen das Band Sie fast völlig still gesehen hat, außerhalb des Schlaffensters. Die meisten Tage haben keine — ein Tag ohne ist ein Tag, an dem Sie sich bewegt haben, kein Tag, an dem etwas nicht stimmte.", + "investigateBreathingAtRestAwake": "Atmung in Ruhe, wach", + "investigateStillStretchesOutsideSleep": "Ruhige Abschnitte außerhalb des Schlafs", + "investigateLowest": "Niedrigster", + "investigateNextLowest": "Nächstniedrigster", + "investigateHighestOfThem": "Höchster davon", + "investigateFloorNotRateBody": "Ein Mindestwert, keine Tagesrate. Nur Abschnitte, in denen Sie fast völlig still waren, lassen sich überhaupt auswerten, das sind also die ruhigsten Minuten, die das Band außerhalb Ihres Schlafs gesehen hat — nichts hier beschreibt den Rest Ihres Tages, und Atmung während Bewegung lässt sich nicht aus dem Schlagrhythmus rekonstruieren.", + "investigateCycleScreenDidNotRun": "Das Zyklus-Screening lief diese Nacht nicht", + "investigateNotEnoughCleanBeats": "Nicht genügend saubere Schläge, um es auszuführen.", + "investigateHeartRateCycles": "Herzfrequenzzyklen", + "investigateCyclesCounted": "Gezählte Zyklen", + "investigateObservedHoursAnalysed": "Analysierte beobachtete Stunden", + "investigateCyclesPerObservedHour": "Zyklen pro beobachteter Stunde", + "investigateMeanCycleLength": "Mittlere Zyklusdauer", + "investigateMeanDipDepth": "Mittlere Einbruchtiefe", + "investigateCycleLengthQuartiles": "Zyklusdauer, Quartile", + "investigateDipDepthQuartiles": "Einbruchtiefe, Quartile", + "investigateNotEnoughNightsAcross": "Nicht genügend Nächte für die Übersicht über mehrere Nächte", + "investigateNeedsSeveralNights": "Dafür sind mehrere Nächte mit jeweils einigen beobachteten Stunden nötig.", + "investigateDroppedIrregular": "{count, plural, one{# Nacht ausgeschlossen, weil das Screening auf unregelmäßigen Rhythmus sie markiert hat} other{# Nächte ausgeschlossen, weil das Screening auf unregelmäßigen Rhythmus sie markiert hat}}", + "investigateDroppedThin": "{count, plural, one{# Nacht wegen zu weniger beobachteter Stunden ausgeschlossen} other{# Nächte wegen zu weniger beobachteter Stunden ausgeschlossen}}", + "investigateAcrossNOwnNights": "ÜBER {n} IHRER EIGENEN NÄCHTE", + "investigateCvhrAboveUsual": "In Ihren jüngsten Nächten lief der von diesem Screening erfasste Herzfrequenzzyklus höher als in den {n} zugrunde liegenden Nächten.", + "investigateCvhrInsideUsual": "In Ihren jüngsten Nächten blieb der von diesem Screening erfasste Herzfrequenzzyklus innerhalb der Spanne der {n} zugrunde liegenden Nächte.", + "investigateCvhrExplainer": "Es ist ein Muster in Ihrem Puls, keine Messung Ihrer Atmung, und kein Test auf irgendetwas. Derselbe Zyklus entsteht durch einen unregelmäßigen Rhythmus, durch Höhenlage und durch jede zerstückelte Nacht — und Betablocker, Diabetes und Nervenerkrankungen schwächen ihn ab, sodass eine wirklich gestörte Atmung hier oft gar keine Spur hinterlässt.", + "investigateCvhrNotNegativeResult": "Nichts hier ist also ein negatives Ergebnis, und nichts hier entlastet irgendetwas, und nichts davon sagt etwas über eine einzelne Nacht aus — der Wert einer einzigen Nacht schwankt schon für sich allein aus einem Dutzend Gründen.", + "investigateCvhrSeeClinicianIfSymptoms": "Wenn Sie schnarchen, unausgeruht aufwachen oder jemand beobachtet hat, dass Sie im Schlaf zu atmen aufgehört haben, kann eine Ärztin oder ein Arzt das richtig testen.", + "investigateStageMinutesAsCounted": "Phasenminuten, wie gezählt", + "investigateLight": "Leicht", + "investigateDeep": "Tief", + "investigateRem": "REM", + "investigateAwake": "Wach", + "investigateTotalSleep": "Gesamtschlaf", + "investigateSegmentationConfidence": "Segmentierungs-Konfidenz", + "investigateNotPublished": "nicht veröffentlicht", + "investigateNothingComputedForKey": "Für diesen Schlüssel wurde nichts berechnet", + "investigateNoStoredSeries": "Keine gespeicherte Reihe", + "investigateNothingStoredYet": "Für {metric} ist noch nichts gespeichert.", + "investigateSeries": "Reihe", + "investigateDaysDerived": "Berechnete Tage", + "investigateLatest": "Neuester", + "investigateMean": "Mittelwert", + "investigateMedian": "Median", + "investigateSd": "SA", + "investigateMin": "Min", + "investigateMax": "Max", + "investigateUnit": "Einheit", + "investigateUnitless": "einheitenlos", + "investigateStorage": "Speicherung", + "investigateOneValuePerDerivedDay": "ein Wert pro berechnetem Tag", + "investigateMethodLabel": "METHODE", + "investigateNotDocumented": "Nicht dokumentiert.", + "calmBreathingResonanceLabel": "Resonanz", + "calmBreathingResonanceDescription": "Gleichmäßiges Ein- und Ausatmen mit etwa {rate} Atemzügen pro Minute. Die einzige mit Kohärenz-Score.", + "calmBreathingCloseBreathing": "Atemübung schließen", + "calmBreathingFinishNow": "Jetzt beenden", + "calmBreathingStop": "Stopp", + "calmBreathingEndSession": "Sitzung beenden", + "calmBreathingBegin": "Beginnen", + "calmBreathingTakeABreath": "Atme durch.", + "calmBreathingRingLeads": "Der Ring gibt den Takt vor. Legen Sie das Telefon weg.", + "calmBreathingScoredPill": "Bewertet", + "calmBreathingHowLong": "Dauer", + "calmBreathingMinutesSemantic": "{m, plural, one{# Minute} other{# Minuten}}", + "calmBreathingMinutesAbbrev": "{m} Min.", + "calmBreathingYourOwnPace": "Ihr eigenes Tempo", + "calmBreathingWindowRowSemantic": "Vorher und nachher messen, fügt vier Minuten hinzu", + "calmBreathingMeasureBeforeAfter": "Vorher und nachher messen · fügt 4 Min. hinzu", + "calmBreathingNeedsBandBeatTiming": "Erfordert das getragene Band — der Vergleich basiert auf dem Schlagrhythmus.", + "calmBreathingFindYourPace": "Finden Sie das Tempo, dem Ihr Herz folgt", + "calmBreathingSweepIntro": "Sechs Minuten: {rates} Atemzüge pro Minute, je zwei Minuten. Erst wenn sich zwei Sitzungen einig sind, ändert sich etwas.", + "calmBreathingSweepAgreed": "Zwei Sitzungen haben sich auf {rate} Atemzüge pro Minute geeinigt, und Resonanz ist auf dieses Tempo eingestellt. Wiederholen Sie es zur Überprüfung.", + "calmBreathingPaceOfRate": "TEMPO {block} VON {total} · {rate} ATEMZÜGE PRO MINUTE", + "calmBreathingOfClock": "von {clock}", + "calmBreathingNoScoreForSession": "Kein Kohärenz-Score für diese Sitzung", + "calmBreathingScoringNeedsBand": "Die Bewertung erfordert den Schlagrhythmus vom Band. Nicht verbunden, daher gibt diese Sitzung nur den Takt vor, wird aber nicht gespeichert.", + "calmBreathingBeforeLabel": "VORHER", + "calmBreathingAfterLabel": "NACHHER", + "calmBreathingSitStill": "Sitzen Sie einen Moment still.", + "calmBreathingStaySitting": "Bleiben Sie sitzen.", + "calmBreathingNothingPacingScored": "Atmen Sie so, wie Sie es normalerweise tun würden. Nichts gibt den Takt vor, und nichts wird bewertet.", + "calmBreathingPatternNotScored": "{pattern} wird nicht bewertet. Resonanz ist das einzige Muster im Tempo, für das der Score entwickelt wurde.", + "calmBreathingTooFewBeatTimings": "Zu wenige saubere Schlagzeiten während der Sitzung, um sie zu bewerten.", + "calmBreathingThatIsDone": "Das war's.", + "calmBreathingCardiacCoherence": "Herzkohärenz", + "calmBreathingHowStronglyFollowedPace": "WIE STARK IHRE HERZFREQUENZ DEM TEMPO FOLGTE", + "calmBreathingStoppedThere": "Dort abgebrochen.", + "calmBreathingHowStronglyEachPace": "WIE STARK IHRE HERZFREQUENZ JEDEM TEMPO FOLGTE", + "calmBreathingBreathsAMinute": "{rate} Atemzüge pro Minute", + "calmBreathingNotReached": "nicht erreicht", + "calmBreathingTooFewCleanBeats": "zu wenige saubere Schläge", + "calmBreathingRankingExplainer": "Eine Rangfolge von drei Tempi aus einer Sitzung. Die Blöcke laufen unmittelbar nacheinander, sodass jedes Tempo gemessen wird, während Sie sich noch vom vorherigen erholen. Es zeigt, welchem Tempo Ihr Herz am stärksten folgte, und sonst nichts.", + "calmBreathingVerdictAborted": "Sie haben auf halbem Weg abgebrochen, es gab also nichts zu vergleichen. Nichts hat sich geändert.", + "calmBreathingVerdictCouldNotScore": "Mindestens ein Tempo konnte nicht bewertet werden, es gibt also nichts einzustufen. Nichts hat sich geändert.", + "calmBreathingVerdictTied": "Zwei der Tempi erzielten den gleichen Wert, diese Sitzung kann sie also nicht unterscheiden. Nichts hat sich geändert.", + "calmBreathingVerdictConfirmed": "Von den getesteten Tempi ergab {w} Ihre stärkste Reaktion — und das ist nun die zweite Sitzung in Folge. Resonanz ist auf dieses Tempo eingestellt.", + "calmBreathingVerdictFirstWin": "Von den getesteten Tempi ergab {w} Ihre stärkste Reaktion. Noch ist nichts festgelegt: Das Tempo ändert sich erst, wenn zwei Sitzungen dasselbe wählen.", + "breathPatternBoxName": "Box", + "breathPatternBoxDesc": "Vier Zählzeiten in jede Richtung, inklusive Atempausen. Beruhigend, wenn die Gedanken rasen.", + "breathPattern478Name": "4-7-8", + "breathPattern478Desc": "Eine lange Atempause und ein noch längeres Ausatmen. Wird meist zum Einschlafen genutzt.", + "breathPatternExtendedExhaleName": "Langes Ausatmen", + "breathPatternExtendedExhaleDesc": "Doppelt so lang aus wie ein. Keine Atempausen, daher leicht eine Weile durchzuhalten.", + "breathPhaseInhale": "Einatmen", + "breathPhaseHold": "Halten", + "breathPhaseExhale": "Ausatmen", + "breathPhaseWork": "Belastung", + "breathPhaseRest": "Pause", + "metricDetailToday": "Heute", + "metricDetailRange7Days": "7 Tage", + "metricDetailRange30Days": "30 Tage", + "metricDetailRange6Months": "6 Monate", + "metricDetailRangeYear": "Jahr", + "metricDetailLockedNote": "{label} benötigt {needed} Tage Verlauf. Du hast {have}.", + "metricDetailNotShownTitle": "Nicht als Trend angezeigt", + "metricDetailNothingRecordedToday": "Heute nichts aufgezeichnet", + "metricDetailNoHistoryYet": "Noch kein Verlauf für {metric}", + "metricDetailNoValueYet": "Für heute liegt noch kein Wert vor.", + "metricDetailNoValueYetWiderRanges": "Für heute liegt noch kein Wert vor. Die breiteren Zeiträume oben enthalten die Tage, für die es einen gibt.", + "metricDetailNoValueInWindow": "An keinem Tag in diesem Zeitraum wurde ein Wert erzeugt.", + "metricDetailWearBandFix": "Trage das Band über Nacht, um die Reihe zu starten", + "metricDetailBeatsLinkTitle": "Herzschläge", + "metricDetailBeatsLinkSub": "Die Intervalle, aus denen eine Nacht besteht, dargestellt", + "metricDetailBreakdownLinkTitle": "Aufschlüsselung", + "metricDetailBreakdownLinkSub": "Jeder Abschnitt des heutigen Tages und was ihn gezählt hat", + "metricDetailNerdStatsTitle": "Nerd-Statistiken", + "metricDetailNerdStatsSub": "Die Zahlen hinter dem Bild", + "metricDetailDailyAverage": "Tagesdurchschnitt · {count} von {win} Tagen", + "metricDetailLatestReading": "Letzter {value} {unit} · {asOf}", + "metricDetailAlgoBreakFootnote": "{n, plural, one{Die gepunktete Linie zeigt eine Änderung, wie diese Tage berechnet wurden. Messwerte auf beiden Seiten stammen aus unterschiedlichen Versionen.} other{Die gepunkteten Linien zeigen Änderungen, wie diese Tage berechnet wurden. Messwerte auf beiden Seiten einer Linie stammen aus unterschiedlichen Versionen.}}", + "metricDetailDaysAgoLabel": "{n, plural, one{vor {n} Tag} other{vor {n} Tagen}}", + "metricDetailWornChartTitle": "Getragen", + "metricDetailHoursADayUnit": "Std./Tag", + "metricDetailWearFootnote": "{have} von diesen {win} Tagen haben einen Tragezeit-Eintrag. Der Rest sind Lücken in beiden Diagrammen — die Linie oben wird nicht darüber hinweg gezogen.", + "metricDetailSlotNoRecord": "{day}, kein Eintrag", + "metricDetailSlotWithValue": "{day}, {value} {unit}", + "metricDetailOpenDay": "{day} öffnen", + "metricDetailNoRecordLabel": "Kein Eintrag", + "metricDetailLowest": "Niedrigster", + "metricDetailTypical": "Typisch", + "metricDetailHighest": "Höchster", + "metricDetailFromDaysCount": "Aus {n} deiner eigenen Tage.", + "metricDetailPercentileTodayNoBand": "Heute liegt beim {ordinal} Perzentil deines eigenen Verlaufs.", + "metricDetailPercentileTodayBand": "Heute liegt beim {ordinal} Perzentil deines eigenen Verlaufs — {band}.", + "metricDetailPercentileFromNoBand": "Dein Messwert vom {date} liegt beim {ordinal} Perzentil deines eigenen Verlaufs.", + "metricDetailPercentileFromBand": "Dein Messwert vom {date} liegt beim {ordinal} Perzentil deines eigenen Verlaufs — {band}.", + "metricDetailDaysWithWithout": "{withCount} Tage mit · {withoutCount} ohne", + "metricDetailPatternsNotCauses": "Muster in deinen eigenen Einträgen, keine Ursachen.", + "metricDetailChooseDayHelp": "Tag auswählen", + "metricDetailPreviousDay": "Vorheriger Tag", + "metricDetailNextDay": "Nächster Tag", + "metricDetailChooseDayShowing": "Tag auswählen. Zeigt {day}", + "metricDetailNormalRangeSection": "Dein normaler Bereich", + "metricDetailWhatMovesItSection": "Was ihn beeinflusst", + "cycleRemoveLogTitle": "{date} entfernen?", + "cycleRemoveLogBody": "Zyklustag, Phase und das vorhergesagte nächste Datum werden alle aus den Tagen berechnet, die du erfasst. Nur der heutige Tag kann erfasst werden, daher lässt sich dieser Eintrag nicht wiederherstellen.", + "cycleWhatAppliesToYou": "Was auf dich zutrifft", + "cyclePreferNotToSay": "Möchte ich nicht angeben", + "cyclePreferNotToSayWhy": "Die App lässt die Phase deaktiviert.", + "cycleReproCyclingLabel": "Ich habe einen natürlichen Zyklus", + "cycleReproCyclingWhy": "Berechnet eine Phase aus deinen erfassten Zyklusbeginnen.", + "cycleReproContraceptionLabel": "Hormonelle Verhütung", + "cycleReproContraceptionWhy": "Kein Eisprung zum Zählen, also keine Phase. Blutungen werden weiterhin erfasst.", + "cycleReproNoneLabel": "Schwanger, im Wochenbett oder ohne Zyklus", + "cycleReproNoneWhy": "Keine Phase und keine Vorhersage. Deine biometrischen Werte werden weiterhin angezeigt.", + "cycleReproNotSet": "Nicht festgelegt", + "cycleTrackingOffTitle": "Zyklus-Tracking ist deaktiviert", + "cycleTrackingOffBody": "Es bleibt auf diesem Telefon.", + "cycleTurnOnTracking": "Zyklus-Tracking aktivieren", + "cycleNoPeriodTitle": "Noch keine Periode erfasst", + "cycleNoPeriodBody": "Berechnet aus den Tagen, die du erfasst.", + "cycleLogPeriodButton": "Periodenbeginn heute erfassen", + "cycleLogKindStart": "BEGINN", + "cycleLogKindEnd": "ENDE", + "cycleAcrossCyclesTitle": "Über deine Zyklen hinweg", + "cycleUnitCompleteCycle": "vollständiger Zyklus", + "cycleUnitCompleteCycles": "vollständige Zyklen", + "cycleOpenAction": "Öffnen", + "cycleWhatYouNoticedToday": "Was dir heute aufgefallen ist", + "cycleLoggedDays": "Erfasste Tage", + "cycleReproOptionalHint": "Optional. Solange du es nicht angibst, lässt die App die Phase deaktiviert.", + "cycleReproPrivateHint": "Nur du und dieses Telefon. Wird nie exportiert.", + "cycleTurnOffTracking": "Zyklus-Tracking deaktivieren", + "cycleDayInThisCycle": "TAG IN DIESEM ZYKLUS", + "cycleCountedFromLastStart": "gezählt ab deinem letzten erfassten Zyklusbeginn", + "cycleOfAboutDays": "von etwa {days}", + "cyclePhaseMenstrual": "Menstruation", + "cyclePhaseFollicular": "Follikelphase", + "cyclePhaseOvulation": "Eisprungfenster", + "cyclePhaseLuteal": "Lutealphase", + "cycleNextPeriodBetween": "NÄCHSTE PERIODE, ERWARTET ZWISCHEN", + "cycleNextPeriodAround": "NÄCHSTE PERIODE, ERWARTET UM", + "cycleFromOneMeasuredGap": "basierend auf deinem einzigen gemessenen Intervall, das nicht zeigen kann, wie stark dein Zyklus schwankt", + "cyclePastEndOfIt": "{days} Tage nach dessen Ende · ", + "cycleInsideItNow": "du befindest dich gerade darin · ", + "cycleInDaysRange": "in {lo}–{hi} Tagen · ", + "cycleHalfOfMeasuredGaps": "die Hälfte deiner {n} gemessenen Intervalle lag innerhalb einer so breiten Spanne", + "cycleLeadDaysLate": "{days} Tage überfällig · ", + "cycleLeadToday": "heute · ", + "cycleLeadInDays": "in {days} Tagen · ", + "cycleWhatYouUsuallyNotice": "Was dir normalerweise auffällt", + "cycleSymptomShapeSummary": "Vier Zahlen, eine pro Zykluswoche, zurückgerechnet ab deinen eigenen erfassten Zyklusbeginnen. Du hast an {daysByWeek} Tagen jeder Woche über {cycles} Zyklen hinweg etwas erfasst — das sind die einzigen Tage, die hier zählen.", + "cycleRemoveLoggedDay": "{date} entfernen", + "cycleSymptomCramps": "Krämpfe", + "cycleSymptomHeadache": "Kopfschmerzen", + "cycleSymptomBloating": "Blähungen", + "cycleSymptomFatigue": "Erschöpfung", + "cycleSymptomLowMood": "gedrückte Stimmung", + "cycleSymptomAcne": "Akne", + "cycleSymptomTenderBreasts": "empfindliche Brüste", + "cycleSymptomNausea": "Übelkeit", + "cycleThisCycle": "Dieser Zyklus", + "cycleByDayOfYourCycle": "Nach Tag deines Zyklus", + "cycleHowLongCyclesBeen": "Wie lang deine Zyklen waren", + "cycleRestingHeartRate": "Ruheherzfrequenz", + "cycleUnitBpm": "bpm", + "cycleHrvRmssdTitle": "HRV (RMSSD)", + "cycleUnitMs": "ms", + "cycleNotEnoughDescribeDayTitle": "Noch nicht genug Zyklen, um einen Zyklustag zu beschreiben", + "cycleNotEnoughDescribeDayBody": "Jeder Punkt hier ist der Median desselben Tages über zwei oder mehr deiner eigenen Zyklen. Noch keiner hat zwei dahinter.", + "cycleOwnPastCyclesDescribed": "Deine eigenen vergangenen Zyklen, beschrieben. Tage, die nur ein Zyklus erreicht hat, bleiben leer, statt gezeichnet zu werden — eine Nacht ist kein Median. Es beschreibt, was passiert ist, nicht, was passieren wird.", + "cycleDayOneLabel": "Tag 1", + "cycleDayNLabel": "Tag {n}", + "cycleMiddleOfNCycles": "Median aus {n} Zyklen an jedem Tag.", + "cycleMiddleOfRangeCycles": "Median aus {lo} bis {hi} Zyklen an jedem Tag.", + "cycleNotEnoughCompareTitle": "Noch nicht genug Zyklen, um einen Tag mit sich selbst zu vergleichen", + "cycleCompareBodyGeneric": "Das stellt den heutigen Tag neben denselben Tag deiner vorherigen Zyklen. Es braucht drei, die so weit gekommen sind.", + "cycleCompareBodyWithDay": "Das stellt den heutigen Tag neben denselben Tag deiner vorherigen Zyklen. Es braucht drei, die Tag {day} erreicht haben.", + "cycleNightOfLabel": "NACHT VOM {date}", + "cycleComparisonNotCorrection": "Ein Vergleich, keine Korrektur. Nichts an deiner Readiness wurde dadurch neu skaliert, und nichts hier ist eine Trainingsanweisung.", + "cycleCompareHrvLabel": "HRV", + "cycleCompareLine": "{label} {z1} im Vergleich zu deinen letzten 3 Wochen, {z2} im Vergleich zu deinen letzten {n} Tagen {cycleDay} deines Zyklus.", + "cycleLengthsTitle": "Deine Zykluslängen im Vergleich zu einer veröffentlichten Spanne", + "cycleLengthsBody": "Deaktiviert, sofern du es nicht anforderst. Zeigt die Tage zwischen deinen eigenen erfassten Zyklusbeginnen neben der veröffentlichten Spanne für einen erwachsenen Zyklus, ohne sonst etwas darüber auszusagen.", + "cycleShowIt": "Anzeigen", + "cycleNotEnoughLoggedTitle": "Noch nicht genug erfasste Zyklen", + "cycleNotEnoughLoggedBody": "Das braucht eine lange Datenreihe: bisher {n} von {total} Intervallen, was etwa einem Jahr entspricht, in dem jeder Beginn erfasst wurde.", + "cycleGapTitle": "Es gibt eine Lücke in deinen erfassten Zyklusbeginnen", + "cycleGapBody": "Einer davon liegt mehr als {days} Tage nach dem vorherigen. Ein nie erfasster Beginn und ein Zyklus, der tatsächlich so lange dauerte, sehen von hier aus gleich aus, daher wird nichts gezeichnet.", + "cycleDaysBetweenStarts": "Tage zwischen deinen erfassten Zyklusbeginnen", + "cycleUnitDays": "Tage", + "cycleLegendYourCycles": "Deine Zyklen", + "cycleLegendPublishedRange": "Veröffentlichte Spanne", + "cycleTwoLinesFootnote": "Die beiden Linien liegen bei {low} und {high} Tagen.", + "cycleLengthChangesReasons": "Die Zykluslänge ändert sich aus vielen Gründen — Schilddrüse, Stress, Gewichtsveränderung, Verhütung, PCOS und andere. Das sind deine eigenen erfassten Daten neben einer veröffentlichten Spanne. Es ist ein Grund, eine Ärztin oder einen Arzt zu fragen, keine Antwort von einer.", + "cycleHideLengths": "Zykluslängen ausblenden", + "cycleDescriptiveOnly": "Nur beschreibend.", + "cycleNotEnoughDerivedNights": "Noch nicht genug abgeleitete Nächte in diesem Zyklus", + "cycleMdcNoteInsideSpread": " Jeder hier gezeigte Tag liegt innerhalb deiner eigenen Nacht-zu-Nacht-Streuung: der größte Unterschied zwischen zweien davon beträgt {s}, und {n} ist die kleinste Veränderung, die sich hiermit vom Rauschen unterscheiden lässt. Eine Form, keine Verschiebung.", + "cycleMdcNoteVaries": " Deine Nächte schwanken bereits von sich aus um {n}, daher werden Tage, die enger beieinanderliegen als das, nicht unterschieden. Der größte Unterschied hier beträgt {s}.", + "healthTabOverview": "Übersicht", + "healthTabExplore": "Erkunden", + "healthTabTrends": "Trends", + "healthTabVitals": "Vitalwerte", + "healthTabLabs": "Labor", + "healthTitle": "Gesundheit", + "healthCouldNotRead": "Deine {what} konnten nicht gelesen werden", + "healthReadFailedBody": "Die gespeicherten Zeilen konnten nicht geladen werden. Es wurde nichts gelöscht — dies ist ein fehlgeschlagener Lesevorgang.", + "healthTryAgain": "Erneut versuchen", + "healthWhatVitals": "Vitalwerte", + "healthWhatLabResults": "Laborergebnisse", + "healthMeasuresUnit": "Messwerte", + "healthRowRestingHr": "Ruheherzfrequenz", + "healthRowHrv": "HRV", + "healthRowSleep": "Schlaf", + "healthRowStress": "Stress", + "healthRowRespRate": "Atemfrequenz", + "healthSubOvernight": "Über Nacht", + "healthSubRmssdAsleep": "RMSSD, im Schlaf", + "healthSubLastNight": "Letzte Nacht", + "healthSubAsleep": "Im Schlaf", + "healthNoMetric": "Keine {name}", + "healthWhyReadFromSleep": "Wird aus dem Schlaf abgeleitet, und es wurde keine Nacht bewertet.", + "healthWhyReadOnlyFromSleep": "Wird nur aus dem Schlaf abgeleitet, und es wurde keine Nacht bewertet.", + "healthWhySleepNotLongEnough": "Es wurde keine Schlafphase aufgezeichnet, die lang genug für eine Bewertung war.", + "healthWhyReadFromNight": "Wird aus der Nacht abgeleitet, und es wurde keine Nacht bewertet.", + "healthWhyNoReadingLastNight": "Keine Messung von letzter Nacht.", + "healthIllnessRedTitle": "Mehrere Nächte in Folge weichen von deinem Normalwert ab", + "healthIllnessLastNightTitle": "Letzte Nacht lag außerhalb deines normalen Bereichs", + "healthIllnessDayTitle": "{day} lag außerhalb deines normalen Bereichs", + "healthIllnessBodyNoZ": "Deine nächtliche Ruheherzfrequenz liegt seit einiger Zeit über deinem eigenen Ausgangswert. Dies beobachtet nur ein Signal. Es benennt ein Muster, keine Ursache.", + "healthIllnessBodyWithZ": "Deine nächtliche Ruheherzfrequenz liegt seit einiger Zeit über deinem eigenen Ausgangswert; in jener Nacht lag sie {z} Standardabweichungen {direction}. Dies beobachtet nur ein Signal. Es benennt ein Muster, keine Ursache.", + "healthDirectionAbove": "darüber", + "healthDirectionBelow": "darunter", + "healthIllnessAdvice": "Beachtenswert, wenn es länger als ein paar Tage anhält.", + "healthObservationsTitle": "Beobachtungen", + "healthSeeAll": "Alle anzeigen", + "healthNapsTitle": "Nickerchen", + "healthNoNapReading": "Kein Nickerchen erfasst", + "healthNoNapReadingFor": "Kein Nickerchen erfasst für {day}", + "healthNapsBody": "Nickerchen stammen aus derselben sekundengenauen Aufzeichnung wie der Rest des Tages, und für diesen Tag liegt davon nicht genug vor.", + "healthDaytimeSleep": "Schlaf am Tag", + "healthValueNone": "Keine", + "healthNoneDetectedOn": "Keine erkannt · {day}", + "healthNapCountLabel": "{n, plural, one{{n} Nickerchen} other{{n} Nickerchen}}", + "healthAddOrCorrect": "Hinzufügen oder korrigieren", + "healthNoTrendYet": "Noch kein Trend für {label}", + "healthZeroDaysStored": "0 Tage gespeichert.", + "healthVsDayAverage": "im Vergleich zu deinem {days}-Tage-Durchschnitt", + "healthAsOf": " · Stand {date}", + "healthNoBaseline": "kein Ausgangswert", + "healthFirstReadings": "erste Messwerte", + "healthTimeAsleep": "Schlafzeit", + "healthVsNeed": "im Vergleich zu deinem Bedarf von {need}", + "healthBodyClockTitle": "Innere Uhr", + "healthChronotypeJetlagRegularity": "Chronotyp, Jetlag und Regelmäßigkeit", + "healthChronotypeLabel": "CHRONOTYP", + "healthSocialJetlagLabel": "SOZIALER JETLAG", + "healthRegularityLabel": "REGELMÄSSIGKEIT", + "healthConsistencyTitle": "Beständigkeit", + "healthDaysWithRecord": "Tage mit einem berechneten Eintrag in den letzten 30 Tagen", + "healthToday": "Heute", + "healthRowHeartRate": "Herzfrequenz", + "healthRowSkinTemp": "Hauttemperatur", + "healthVsOwnNights": "im Vergleich zu deinen eigenen Nächten", + "healthVsOwnNightsOn": "im Vergleich zu deinen eigenen Nächten · {day}", + "healthRowWearTime": "Tragezeit", + "healthTheDay": "den Tag", + "healthCoverageOf": "{pct} % von {day}", + "healthNothingMeasuredDay": "Für diesen Tag wurde nichts gemessen", + "healthNoBandRecordings": "Für diesen Tag sind keine Aufzeichnungen vom Band eingegangen.", + "healthSyncTheBand": "Band synchronisieren", + "healthDeepDivesTitle": "Tiefenanalysen", + "healthHeartRateVariability": "Herzfrequenzvariabilität", + "healthTimeFrequencyNonLinear": "Zeit, Frequenz und nichtlinear", + "healthRmssdOfLastNights": "RMSSD, {have} von den letzten {days} Nächten", + "healthNightsAgo": "vor {n} Nächten", + "healthOneNightNotTrend": "Eine Nacht ist noch kein Trend", + "healthMeasuresWithHistory": "Messwerte mit gespeichertem Verlauf auf diesem Gerät", + "healthEachOneOpens": "Jeder öffnet sein Diagramm, deinen eigenen Bereich und die Berechnungsweise.", + "healthCatHeartRhythm": "Herz & Rhythmus", + "healthCatBreathing": "Atmung", + "healthCatMovementLoad": "Bewegung & Belastung", + "healthCatBodyWear": "Körper & Tragedauer", + "healthBlurbRestingHr": "Die niedrigste anhaltende Rate der Nacht", + "healthBlurbHrv": "RMSSD über das sauberste Schlaffenster", + "healthBlurbHrvCv": "Wie stark das von Nacht zu Nacht schwankt", + "healthBlurbLfHf": "Wo sich die Leistung des Schlagabstands über die Frequenzen verteilt", + "healthBlurbDip": "Wie stark deine Herzfrequenz im Schlaf sinkt", + "healthBlurbHrr": "Wie schnell sie in der Minute nach einer Belastung sinkt", + "healthBlurbSleep": "Schlafzeit, ermittelt aus Bewegung und Herzschlagzeit", + "healthBlurbEfficiency": "Schlaf als Anteil der im Bett verbrachten Zeit", + "healthBlurbDeep": "Herzfrequenz-Stabilität innerhalb des NREM-Schlafs", + "healthBlurbRem": "Ermittelt aus Herzschlagvariabilität und Bewegung", + "healthBlurbNapMin": "Schlaf, der außerhalb der Hauptnacht erkannt wurde", + "healthBlurbRespRate": "Atemzüge pro Minute, aus der Herzschlagzeit ermittelt", + "healthBlurbBrv": "Wie stark diese Rate über die Nacht hinweg schwankt", + "healthBlurbSteps": "Von einem Schrittzähler gezählt, niemals modelliert", + "healthBlurbActiveMin": "Minuten an Bewegungsvolumen, nicht an Fortbewegung", + "healthBlurbCalories": "Aktive Energie aus Herzfrequenz und deinem Profil", + "healthBlurbStrain": "Kardiovaskuläre Belastung über den Tag, auf einer Skala von 0–21", + "healthBlurbTrimp": "Zeit in jeder Zone, gewichtet nach ihrer Belastung", + "healthBlurbSkinTemp": "Abstand zu deinen eigenen jüngsten Nächten", + "healthBlurbWear": "Minuten mit vorhandener Bandaufzeichnung", + "healthNothingMeasuredHere": "Hier wurde noch nichts gemessen", + "healthNotMeasuredYet": "Noch nicht gemessen", + "healthNoDayProduced": "Kein Tag auf diesem Gerät hat bisher einen erzeugt.", + "healthNoLabResults": "Keine Laborergebnisse", + "healthNoLabResultsBody": "Nichts erfasst. Alles, was du hier hinzufügst, bleibt auf diesem Gerät, und alles, was du entfernst, verschwindet von ihm.", + "healthLastPanel": "Letztes Panel {date} · manuell erfasst", + "healthMarkersYouNamed": "Von dir benannte Marker", + "healthAddAResult": "Ergebnis hinzufügen", + "healthRangesDifferByLab": "Die Referenzbereiche unterscheiden sich je nach Labor. Verwende den aus deinem Bericht.", + "healthRemoveMarkerFrom": "{marker} vom {date} entfernen", + "healthNoReferenceInterval": "Kein Referenzbereich · {date}", + "healthTypicalRange": "Üblich {low}–{high} · {date}", + "healthRemoveLabelFrom": "{label} vom {date} entfernen?", + "healthRemoveLabBody": "Die {value} {unit}, die du für diese Entnahme erfasst hast. Sie verlassen dieses Gerät, und es gibt kein Rückgängigmachen.", + "healthRemoveLabOlderNote": " Deine Entnahme vom {date} bleibt erhalten und wird stattdessen hier angezeigt.", + "healthRemovedNoneLeft": "{label} vom {date} entfernt. Es sind keine {label}-Ergebnisse mehr vorhanden.", + "healthRemovedShowingOlder": "{label} vom {date} entfernt. Jetzt wird deine Entnahme vom {older} angezeigt.", + "healthRemoveTheMarker": "Marker {label} entfernen", + "healthNothingLoggedUnderIt": "Nichts darunter erfasst", + "healthResultsCount": "{n, plural, one{{n} Ergebnis · {unit}} other{{n} Ergebnisse · {unit}}}", + "healthStillHoldsResults": "{count, plural, one{{label} enthält noch {count} Ergebnis. Entferne dieses zuerst — der Marker ist das, was es kennzeichnet.} other{{label} enthält noch {count} Ergebnisse. Entferne diese zuerst — der Marker ist das, was sie kennzeichnet.}}", + "healthRemoveMarkerQ": "{label} entfernen?", + "healthRemoveMarkerBody": "Er verschwindet aus der Markerliste, sodass du ihn nicht mehr erfassen kannst. Es geht nichts Gemessenes verloren — du hast keine Ergebnisse darunter.", + "healthMarkerLabel": "Marker", + "healthValueUnit": "Wert ({unit})", + "healthDateDrawn": "Entnahmedatum (JJJJ-MM-TT)", + "healthValueMustBeNumber": "Der Wert muss eine reine Zahl sein, ohne Einheit. Es wurde nichts gespeichert.", + "healthDateFormatError": "Das Datum muss im Format JJJJ-MM-TT sein. Es wurde nichts gespeichert.", + "healthCouldNotSaveIt": "Konnte nicht gespeichert werden: {error}", + "homeStepSensorStrapPhone": "Band + Telefon", + "homeStepSensorStrap": "Band", + "homeStepSensorPhone": "Telefon", + "homeOvernightBuilding": "Die Daten der letzten Nacht werden noch verarbeitet.", + "homeOvernightNothingYet": "Von letzter Nacht ist noch nichts in der App angekommen.", + "homeMonthJanuary": "Januar", + "homeMonthFebruary": "Februar", + "homeMonthMarch": "März", + "homeMonthApril": "April", + "homeMonthMay": "Mai", + "homeMonthJune": "Juni", + "homeMonthJuly": "Juli", + "homeMonthAugust": "August", + "homeMonthSeptember": "September", + "homeMonthOctober": "Oktober", + "homeMonthNovember": "November", + "homeMonthDecember": "Dezember", + "homeMonthJanuaryShort": "Jan.", + "homeMonthFebruaryShort": "Feb.", + "homeMonthMarchShort": "März", + "homeMonthAprilShort": "Apr.", + "homeMonthMayShort": "Mai", + "homeMonthJuneShort": "Juni", + "homeMonthJulyShort": "Juli", + "homeMonthAugustShort": "Aug.", + "homeMonthSeptemberShort": "Sep.", + "homeMonthOctoberShort": "Okt.", + "homeMonthNovemberShort": "Nov.", + "homeMonthDecemberShort": "Dez.", + "homeWeekdayMonday": "Montag", + "homeWeekdayTuesday": "Dienstag", + "homeWeekdayWednesday": "Mittwoch", + "homeWeekdayThursday": "Donnerstag", + "homeWeekdayFriday": "Freitag", + "homeWeekdaySaturday": "Samstag", + "homeWeekdaySunday": "Sonntag", + "homeReadinessNotScored": "Nicht bewertet", + "homeReadinessGoodToGo": "Startklar", + "homeReadinessSteady": "Stabil", + "homeReadinessTakeItEasy": "Ruhig angehen", + "homeReadinessRestToday": "Heute ausruhen", + "homeDriverHrv": "HRV", + "homeDriverRhr": "Ruheherzfrequenz", + "homeDriverResp": "Atemfrequenz", + "homeDriverTemp": "Hauttemperatur", + "homeDbRebuiltTitle": "Deine Datenbank wurde neu aufgebaut, um die App zu starten", + "homeDbRebuiltNothingRecovered": "Es konnte nichts wiederhergestellt werden.", + "homeDbRebuiltRecovered": "Wiederhergestellt: {list}.", + "homeDbRebuiltEmpty": "Leer: {list}.", + "homeDbRebuiltKept": "Die Originaldatei bleibt unter {path} erhalten – es wurde nichts gelöscht.", + "homeWorkoutHoldTitle": "Ein Workout läuft noch", + "homeWorkoutHoldBody": "Der heutige Tag pausiert, solange ein Workout läuft: Das Band zeichnet weiter auf, aber die Werte werden erst nach Ende der Session berechnet. Beende das Workout über die Leiste unten, dann füllt sich der Tag – Synchronisieren allein ändert das nicht.", + "homeInsightsRebuildingTitle": "Deine tageübergreifenden Auswertungen werden gerade neu aufgebaut", + "homeInsightsRebuildingAlgoVersion": "Die Berechnung hat sich mit dem letzten Update geändert.", + "homeInsightsStaleOverWeek": "Die letzte Auswertung wurde vor über einer Woche erstellt – zu alt, um sich darauf zu verlassen.", + "homeInsightsStaleOnDay": "Die letzte Auswertung wurde am {day} erstellt – zu alt, um sich darauf zu verlassen.", + "homeInsightsNoVersionStamp": "Die gespeicherte Auswertung trägt keine Versionsangabe.", + "homeSyncBand": "Band synchronisieren", + "homeWhyLabel": "Warum?", + "homeCalibrating": "Wird kalibriert", + "homeCalibratingNights": "{have} von {need} Nächten", + "homeCalibratingDays": "{have} von {need} Tagen", + "homeGapNoReason": "Es wurde nichts festgehalten, was erklärt, warum dies fehlt.", + "homeRingRecovery": "Erholung", + "homeRingStrain": "Belastung", + "homeRingSleep": "Schlaf", + "homeRingNoStrain": "Keine Belastung", + "homeRingNoSleep": "Kein Schlaf", + "homeSleepGapFallback": "Es wurde keine ausreichend lange Nacht zur Auswertung aufgezeichnet.", + "homeStrainOf21": "von 21", + "homeSleepNoTarget": "Noch kein Zielwert", + "homeOfSpan": "von {duration}", + "homeLoadFailedTitle": "Der heutige Tag konnte nicht gelesen werden", + "homeLoadFailedBody": "Der gespeicherte Tag konnte nicht geladen werden. Es wurde nichts gelöscht – das ist ein Lesefehler, kein Datenverlust.", + "homeTryAgain": "Erneut versuchen", + "homeNothingDerivedTitle": "Noch nichts berechnet", + "homeNothingDerivedBody": "Es wurden noch keine Aufzeichnungen des Bands verarbeitet.", + "homeAskCoach": "Coach fragen", + "homeProfileSettings": "Profil und Einstellungen", + "homeNothingTodayTitle": "Für heute nichts aufgezeichnet", + "homeNothingTodayBody": "Die letzte von der App ausgewertete Nacht war {day}. Seitdem ist nichts angekommen.", + "homeReadinessNotScoredTitle": "Die Erholung ist heute nicht bewertet", + "homeReadinessNeedBody": "{need}, um zu wissen, was für dich normal ist.", + "homeReadinessNoReason": "Es wurde nichts festgehalten, was das erklärt.", + "homeSeeWhatWasMissing": "Sehen, was gefehlt hat", + "homeAtAGlance": "Auf einen Blick", + "homeTodaysPlan": "Plan für heute", + "homeBreakdownTitle": "Aufschlüsselung deines Tages", + "homeBreakdownSubtitle": "Stunde für Stunde", + "homeIllnessRedTitle": "Mehrere Nächte in Folge weichen von deinem Normalwert ab", + "homeIllnessAmberSameNight": "Letzte Nacht lag außerhalb deines normalen Bereichs", + "homeIllnessAmberOtherNight": "{day} lag außerhalb deines normalen Bereichs", + "homeIllnessBodyNoZ": "Deine nächtliche Ruheherzfrequenz lag zuletzt über deinem eigenen Ausgangswert. Dies bildet nur ein einzelnes Signal ab. Es zeigt ein Muster, aber keine Ursache.", + "homeIllnessBodyAbove": "Deine nächtliche Ruheherzfrequenz lag zuletzt über deinem eigenen Ausgangswert; in dieser Nacht lag sie {z} Standardabweichungen darüber. Dies bildet nur ein einzelnes Signal ab. Es zeigt ein Muster, aber keine Ursache.", + "homeIllnessBodyBelow": "Deine nächtliche Ruheherzfrequenz lag zuletzt über deinem eigenen Ausgangswert; in dieser Nacht lag sie {z} Standardabweichungen darunter. Dies bildet nur ein einzelnes Signal ab. Es zeigt ein Muster, aber keine Ursache.", + "homeIllnessAdvice": "Beobachtenswert, falls es mehrere Tage anhält.", + "homeHeartRate": "Herzfrequenz", + "homeRestingSub": "Ruhe", + "homeNoRestingHr": "Keine Ruheherzfrequenz", + "homeNoRestingHrWhy": "Die Ruheherzfrequenz wird aus dem Schlaf ermittelt, und es wurde kein Schlaf aufgezeichnet.", + "homeSteps": "Schritte", + "homeStepsNone": "Keine", + "homeStepsNotRecorded": "NICHT ERFASST", + "homeStepsPercentGoal": "{pct} % des Ziels", + "homeActiveEnergy": "Aktive Energie", + "homeCaloriesEstimated": "Geschätzt", + "homeCaloriesTotal": "{total} gesamt", + "homeNoEnergyEstimate": "Keine Energieschätzung", + "homeStepsLeft": "{left} Schritte übrig", + "homeMovement": "Bewegung", + "homeGoalSteps": "Ziel {goal}", + "homeStepGoalMet": "Schrittziel erreicht", + "homeStrainTargetMet": "Belastungsziel erreicht", + "homeAimForStrain": "Ziel: Belastung von {aim}", + "homeTraining": "Training", + "homeSleepNeedRow": "{duration} Schlaf", + "homeTonight": "Heute Nacht", + "homeNeed": "Noch offen", + "homeBedTime": "Zubettgehen {time}", + "homeNoPlanTitle": "Noch kein Plan für heute", + "homeNoPlanWhyStale": "Die tageübergreifenden Auswertungen, aus denen er stammt, werden gerade neu aufgebaut.", + "homeNoPlanWhyNone": "Es ist noch keiner festgelegt.", + "homeGreetingStillUp": "Noch wach", + "homeGreetingMorning": "Guten Morgen", + "homeGreetingAfternoon": "Guten Tag", + "homeGreetingEvening": "Guten Abend", + "wellnessTitle": "Wohlbefinden", + "wellnessTabMind": "Geist", + "wellnessTabRecovery": "Erholung", + "wellnessTabHabits": "Gewohnheiten", + "wellnessTabMedication": "Medikamente", + "wellnessTabCycle": "Zyklus", + "wellnessStartASitting": "SITZUNG STARTEN", + "wellnessExercisesNoun": "Übungen", + "wellnessPickOneAndGo": "Eine auswählen und loslegen", + "wellnessLastMinutes": "Zuletzt: {count} Min.", + "wellnessWriteTheDayDown": "Den Tag festhalten", + "wellnessOpen": "Öffnen", + "wellnessStressLastNight": "Stress letzte Nacht", + "wellnessNoStressTitle": "Keine Stressmessung für letzte Nacht", + "wellnessNoStressBody": "Stress wird anhand der Herzschlagfolge während deiner Nachtruhe gemessen, und letzte Nacht lag keine Messung vor.", + "wellnessAutonomicTension": "Autonome Anspannung", + "wellnessStressLevelLow": "niedrig", + "wellnessStressLevelNormal": "normal", + "wellnessStressLevelElevated": "erhöht", + "wellnessStressLevelHigh": "hoch", + "wellnessJournalDefaultSubtitle": "Alles, was du dir über heute merken möchtest", + "wellnessJournalSubtitleShort": "{fields} und eine Notiz", + "wellnessJournalSubtitleLong": "{fields} und {more} weitere, plus eine Notiz", + "wellnessTurnInBy": "Geh bis {time} schlafen", + "wellnessDebtBody": "Du liegst {debt} unter deinem eigenen Bedarf, und heute Nacht sind es {need}.", + "wellnessSeeWhatLastNightCost": "Sieh, was dich letzte Nacht gekostet hat", + "wellnessWhatChargedAndDrained": "Was dich aufgeladen und ausgelaugt hat", + "wellnessNoDriversTitle": "Noch keine Bereitschaftsfaktoren", + "wellnessNoDriversBody": "Es braucht genug Nächte, um zu wissen, was für dich normal ist.", + "wellnessSleepNeedTonight": "Schlafbedarf heute Nacht", + "wellnessNoSleepNeedTitle": "Noch kein Schlafbedarf", + "wellnessNoSleepNeedBody": "Es liegt kein Grund vor, warum für heute Nacht kein Schlafbedarf ermittelt wurde.", + "wellnessTonightsNeed": "Bedarf heute Nacht", + "wellnessSleepDebt": "Schlafdefizit", + "wellnessAddedForStrain": "Zusätzlich für Belastung", + "wellnessCreditedFromNaps": "Gutgeschrieben durch Nickerchen", + "wellnessTargetBedtime": "Ziel-Schlafenszeit", + "wellnessTargetWake": "Ziel-Aufwachzeit", + "wellnessRemoveHabitSemantic": "{label} entfernen", + "wellnessDaysYouDidIt": "Tage, an denen du es getan hast", + "wellnessAddAHabit": "Gewohnheit hinzufügen", + "wellnessWhatYouLogTitle": "Was du protokollierst, im Vergleich zu deinen Werten", + "wellnessWhatYouLogSubtitle": "Dosis, Gewohnheitsunterschied und Wochentag", + "wellnessRemoveHabitConfirmTitle": "{label} entfernen?", + "wellnessRemoveHabitConfirmBody": "Sie wird nicht mehr abgefragt. Die bereits erfassten Tage bleiben erhalten.", + "wellnessHabitHint": "Nach dem Mittagessen spazieren gehen", + "wellnessAlreadyTrack": "Du erfasst „{name}“ bereits.", + "wellnessNothingScheduledTitle": "Nichts geplant", + "wellnessNothingScheduledBody": "Füge hinzu, was du nimmst und wann.", + "wellnessAddAMedication": "Medikament hinzufügen", + "wellnessNothingDueTodayTitle": "Heute nichts fällig", + "wellnessNothingDueTodayBody": "Was du nimmst, ist für andere Tage oder Zeiten geplant.", + "wellnessAdherence": "Therapietreue", + "wellnessNothingToScoreTitle": "Noch nichts zu bewerten", + "wellnessNothingToScoreBody": "Es ist noch keine geplante Dosis fällig geworden.", + "wellnessTakenOfScheduled": "Eingenommen, von den in den letzten sieben Tagen geplanten.", + "wellnessDosesUnit": "Dosen", + "wellnessUndoSkipped": "Übersprungen rückgängig machen", + "wellnessSkippedOnPurpose": "Absichtlich übersprungen", + "wellnessBackToNotTaken": "Zurück zu nicht eingenommen.", + "wellnessRecordedAsDecision": "Als bewusste Entscheidung erfasst, nicht als Fehler.", + "wellnessWhichDaysDue": "An welchen Tagen es fällig ist", + "wellnessRemoveMedTitle": "{label} entfernen", + "wellnessRemoveMedBody": "Es wird nicht mehr geplant. Markierte Dosen bleiben erhalten.", + "wellnessRemoveMedConfirmTitle": "{label} entfernen?", + "wellnessRemoveMedConfirmBody": "Es wird nicht mehr geplant und zählt nicht mehr zur Therapietreue. Bereits markierte Dosen bleiben erhalten.", + "wellnessMedHint": "Vitamin D", + "wellnessNameLabel": "Name", + "wellnessAdd": "Hinzufügen", + "wellnessEveryDay": "Jeden Tag", + "wellnessWeekdays": "Wochentage", + "wellnessWeekends": "Wochenenden", + "wellnessMon": "Mo", + "wellnessTue": "Di", + "wellnessWed": "Mi", + "wellnessThu": "Do", + "wellnessFri": "Fr", + "wellnessSat": "Sa", + "wellnessSun": "So", + "wellnessWhenYouTakeIt": "Wann du es nimmst", + "wellnessChangeTheTime": "Uhrzeit ändern", + "wellnessWhichDays": "AN WELCHEN TAGEN", + "wellnessPickAtLeastOneDay": "Wähle mindestens einen Tag.", + "wellnessDueDays": "Fällig {days}.", + "wellnessMoreForMed": "Weitere Optionen für {label}", + "wellnessMedAtTime": "{label} um {time}", + "wellnessStateTaken": "eingenommen", + "wellnessStateSkipped": "übersprungen", + "wellnessStateNotTaken": "nicht eingenommen", + "wellnessStateDueLater": "später fällig", + "wellnessMarkDone": "Als erledigt markieren", + "wellnessWhatYouLogScreenTitle": "Was du protokollierst", + "wellnessNothingSeparatedTitle": "Noch nichts hat sich abgehoben", + "wellnessNothingSeparatedBody": "Alles, was du protokollierst, wird gegen deine Erholung, HRV, Ruheherzfrequenz und Schlafeffizienz getestet. Bisher hat nichts die Schwelle überschritten.", + "wellnessTheDaysYouDidIt": "Die Tage, an denen du es getan hast", + "wellnessHowMuchAndWhatFollowed": "Wie viel, und was folgte", + "wellnessLinkNeverCause": "Ein Zusammenhang an deinen eigenen Tagen, niemals eine Ursache. Die Tage, an denen du etwas tust, sind ohnehin schon solche Tage.", + "wellnessWhichDayOfWeek": "Welcher Wochentag", + "wellnessHigher": "höher", + "wellnessLower": "niedriger", + "wellnessHeadlineBinary": "An den {n} Tagen, an denen du {field} erfasst hast, war {outcome} {amount} {direction}", + "wellnessHeadlineNoSlope": "An den {n} Tagen, an denen du {field} erfasst hast, ging mehr davon mit einem {direction}en {outcome} einher", + "wellnessHeadlineSlope": "An den {n} Tagen, an denen du {field} erfasst hast, war {outcome} pro {step} um {amount} {direction}", + "wellnessMatchedSameDay": "Mit den Werten desselben Tages abgeglichen.", + "wellnessMatchedNightFollowed": "Mit der folgenden Nacht abgeglichen.", + "wellnessMatchedNightEnded": "Mit der Nacht abgeglichen, die an diesem Morgen endete.", + "wellnessAgainstDaysYouDidNot": "Im Vergleich zu den {n} Tagen, an denen du es nicht getan hast", + "wellnessRangeTo": "{lo} bis {hi}", + "wellnessRankCorrelation": "Rangkorrelation {rho}{ci}. ", + "wellnessCaffeineCaveat": "Dies ist nur dein letzter Koffeinkonsum des Tages: Zwei Tassen und fünf sehen hier gleich aus, sodass „später“ eigentlich „mehr“ bedeuten kann. Ein langer, stressiger Tag führt sowohl zum späten Kaffee als auch zur schlechten Nacht.", + "wellnessHourLater": "Stunde später", + "wellnessPointUnit": "Punkt", + "wellnessNotEnoughWeeksTitle": "Noch nicht genug Wochen", + "wellnessNotEnoughWeeksBody": "Der Vergleich der sieben Wochentage braucht mindestens acht Wochen an Daten, mit fünf Vorkommen jedes Wochentags darin.", + "wellnessNoDayStandsOutTitle": "Kein Wochentag sticht heraus", + "wellnessNoDayStandsOutBody": "Kein Tag hebt sich von den anderen sechs ab, wenn man berücksichtigt, dass alle sieben geprüft wurden.", + "wellnessWeekdayHeadline": "{weekday}: Die Bereitschaft liegt {delta} {direction} als dein Gesamtmedian", + "wellnessWeekdayDetail": "Aus {n} davon. Ein Wochentag ist keine Ursache, sondern ein Rahmen für das, was du an ihm tust. Nichts hier ist eine Empfehlung.", + "wellnessPluralMonday": "montags", + "wellnessPluralTuesday": "dienstags", + "wellnessPluralWednesday": "mittwochs", + "wellnessPluralThursday": "donnerstags", + "wellnessPluralFriday": "freitags", + "wellnessPluralSaturday": "samstags", + "wellnessPluralSunday": "sonntags", + "sleepDetailNavTitle": "Schlaf", + "sleepDetailNoNightTitle": "Keine Nacht zum Anzeigen", + "sleepDetailNoNightBody": "Keine Aufzeichnungsspanne des Bands lang genug für eine Bewertung.", + "sleepDetailNoNightFix": "Trage das Band über Nacht und synchronisiere am Morgen", + "sleepDetailStagesSection": "Phasen", + "sleepDetailVersusUsualSection": "Im Vergleich zu deinem Üblichen", + "sleepDetailUnusualLastNight": "Auffälliges letzte Nacht", + "sleepDetailUnusualOnDay": "Auffälliges am {day}", + "sleepDetailOvernightSection": "Nächtliche Signale", + "sleepDetailTonightSection": "Heute Nacht", + "sleepDetailTotalSleep": "Gesamtschlaf", + "sleepDetailInBed": "IM BETT", + "sleepDetailWatched": "BEOBACHTET", + "sleepDetailAsleepOfThat": "DAVON GESCHLAFEN", + "sleepDetailAsleep": "GESCHLAFEN", + "sleepDetailWatchedExplain": "Wir haben {watched} von {inBed} im Bett beobachtet; der Rest ist keine Messung. Schlaf sowie die Phasenanteile unten beziehen sich auf die beobachtete Zeit.", + "sleepDetailWindowMine": "Du hast dieses Zeitfenster festgelegt", + "sleepDetailWindowFallback": "Dieses Zeitfenster wurde aus der Herzfrequenz abgeleitet", + "sleepDetailWindowAuto": "Dieses Zeitfenster wurde aus den Signalen ermittelt", + "sleepDetailWindowFallbackBody": "Die Auswertung konnte die Übergänge nicht finden, daher sind die Zeiten eine Schätzung.", + "sleepDetailWindowSol": "Vom Beginn deines Zeitfensters bis zum Einschlafen: {band}.", + "sleepDetailConfirmTimes": "Diese Zeiten stimmen", + "sleepDetailChangeTimes": "Zeiten ändern", + "sleepDetailSetTimesMyself": "Zeiten selbst festlegen", + "sleepDetailBackToAutomatic": "Zurück zu automatisch", + "sleepDetailReanalysing": "Nacht wird neu analysiert…", + "sleepDetailCorrectionFailedTitle": "Diese Korrektur wurde nicht angewendet", + "sleepDetailBedTimeHelp": "WANN DU INS BETT GEGANGEN BIST", + "sleepDetailWakeTimeHelp": "WANN DU AUFGESTANDEN BIST", + "sleepDetailReanalyseFailed": "Die Nacht wurde nicht neu analysiert — entweder lief bereits eine andere Neuanalyse, oder sie ist fehlgeschlagen. Die von dir festgelegten Zeiten sind gespeichert; „Alles neu analysieren“ unter Deine Daten wendet sie an.", + "sleepDetailNoHypnogramTitle": "Kein Hypnogramm für diese Nacht", + "sleepDetailNoHypnogramBody": "Die Phasenanalyse benötigt Bewegung und Herzschlag-Timing. Eines davon fehlte.", + "sleepDetailThroughTheNight": "Im Verlauf der Nacht", + "sleepDetailUnitStage": "Phase", + "sleepDetailTapDragCycles": "{n, plural, one{Tippe oder ziehe im Diagramm für jeden Moment. {n} Zyklus.} other{Tippe oder ziehe im Diagramm für jeden Moment. {n} Zyklen.}}", + "sleepDetailTapDragCyclesAvg": "{n, plural, one{Tippe oder ziehe im Diagramm für jeden Moment. {n} Zyklus, im Schnitt {avg}.} other{Tippe oder ziehe im Diagramm für jeden Moment. {n} Zyklen, im Schnitt {avg}.}}", + "sleepDetailTapDragNone": "Tippe oder ziehe im Diagramm für jeden Moment der Nacht.", + "sleepDetailNoWakeups": "Keine Aufwachphasen von 5 Minuten oder mehr; kürzere sind für ein Handgelenk unsichtbar.", + "sleepDetailAtLeastWakeups": "{n, plural, one{Mindestens {n} Aufwachphase von 5 Minuten oder mehr; kürzere sind für ein Handgelenk unsichtbar.} other{Mindestens {n} Aufwachphasen von 5 Minuten oder mehr; kürzere sind für ein Handgelenk unsichtbar.}}", + "sleepDetailLongestStretch": "Längster ununterbrochener Abschnitt {longest}.", + "sleepDetailHypnogramLabel": "Hypnogramm", + "sleepDetailPercentThroughNight": "{pct} % der Nacht", + "sleepDetailNotMeasured": "nicht gemessen", + "sleepDetailScrubAt": "{at}, {stage}", + "sleepDetailHeartRate": "Herzfrequenz", + "sleepDetailHrv": "HRV", + "sleepDetailBreathing": "Atmung", + "sleepDetailTemp": "Temp.", + "sleepDetailNotMeasuredCap": "Nicht gemessen", + "sleepDetailNoSignalAtMoment": "Zu diesem Zeitpunkt wurde kein Signal aufgezeichnet.", + "sleepDetailStageAwake": "Wach", + "sleepDetailStageRem": "REM", + "sleepDetailStageLight": "Leichtschlaf", + "sleepDetailStageDeep": "Tiefschlaf", + "sleepDetailDeep": "Tief", + "sleepDetailLight": "Leicht", + "sleepDetailNoStageSplitTitle": "Keine Phasenaufteilung für diese Nacht", + "sleepDetailNoStageSplitBody": "Kein Herzschlag-Timing über das gesamte Zeitfenster.", + "sleepDetailStageRangeExplain": "Jede Phase ist ein Bereich, keine Zählung — je besser die Nacht erfasst wurde, desto enger ist er. Tiefschlaf ist am breitesten. Wachphasen bleiben eine einzelne Zahl. Detailstatistik enthält die genauen Zahlen.", + "sleepDetailTimeAsleep": "Schlafzeit", + "sleepDetailShorterThanUsual": "kürzer als üblich", + "sleepDetailLongerThanUsual": "länger als üblich", + "sleepDetailLessThanUsual": "weniger als üblich", + "sleepDetailMoreThanUsual": "mehr als üblich", + "sleepDetailAsleepWhileInBed": "Geschlafen während der Bettzeit", + "sleepDetailLowerThanUsual": "niedriger als üblich", + "sleepDetailHigherThanUsual": "höher als üblich", + "sleepDetailFellAsleep": "Eingeschlafen", + "sleepDetailEarlierThanUsual": "früher als üblich", + "sleepDetailLaterThanUsual": "später als üblich", + "sleepDetailNotEnoughNightsTitle": "Nicht genug Nächte für einen Vergleich", + "sleepDetailNightsSoFar": "{have} von {min} Nächten bisher", + "sleepDetailBarExplain": "Der Balken ist die mittlere Hälfte deiner eigenen Nächte.", + "sleepDetailLessThanAny": "{noun} {value} — weniger als in jeder deiner letzten {count} Nächte, die niedrigste davon war {lowest}.", + "sleepDetailMoreThanAny": "{noun} {value} — mehr als in jeder deiner letzten {count} Nächte, die höchste davon war {highest}.", + "sleepDetailYouSlept": "Du hast geschlafen", + "sleepDetailShortestNightLately": "Deine kürzeste Nacht in letzter Zeit", + "sleepDetailLongestNightLately": "Deine längste Nacht in letzter Zeit", + "sleepDetailSleepingHrHighTitle": "Herzfrequenz im Schlaf war erhöht", + "sleepDetailSleepingHrHighBody": "{bpm} bpm über deiner eigenen Baseline. Häufig nach Alkohol, einer späten Mahlzeit, einer harten Einheit oder einer beginnenden Infektion — dies ist eine Messung, keine Diagnose.", + "sleepDetailNothingStoodOut": "Nichts Auffälliges.", + "sleepDetailSleepingHr": "HERZFREQUENZ SCHLAF", + "sleepDetailLowest": "NIEDRIGSTE", + "sleepDetailBreathingCaps": "ATMUNG", + "sleepDetailSkinTemp": "Hauttemp.", + "sleepDetailSleepNeedNotEstablished": "Schlafbedarf noch nicht ermittelt", + "sleepDetailYourNeedIs": "Dein Bedarf beträgt {need}", + "sleepDetailYouAreDown": "du liegst {debt} im Minus", + "sleepDetailLightsOut": "Licht aus", + "sleepDetailToAimFor": "als Ziel", + "sleepDetailNoPersonalRangeYet": "Noch kein persönlicher Bereich — {count} von {min} Nächten.", + "sleepDetailNotFarEnoughToCall": "Nicht weit genug vom Üblichen entfernt, um es zu sagen", + "sleepDetailTypicalForYou": "Typisch für dich", + "sleepDetailVerdictSummary": "{verdict} · üblich {lo}–{hi} über {n} Nächte", + "sleepDetailNoOvernightTitle": "Keine nächtlichen Signallinien", + "sleepDetailNoOvernightBody": "An diesem Tag sind keine nächtlichen Aufzeichnungen eingegangen.", + "sleepDetailSolUnder15": "unter 15 Minuten", + "sleepDetailSolOverHour": "über eine Stunde", + "sleepDetailSolRange": "{lo}–{hi} Minuten", + "workoutTabForYou": "Für dich", + "workoutTabActivities": "Aktivitäten", + "workoutTabHistory": "Verlauf", + "workoutScreenTitle": "Training", + "workoutStartSessionLabel": "SESSION STARTEN", + "workoutActivitiesNoun": "Aktivitäten", + "workoutThisWeek": "Diese Woche", + "workoutTrainingLoad": "Trainingsbelastung", + "workoutTodaysStrainAction": "Heutige Belastung", + "workoutMechanicalLoadTitle": "MECHANISCHE BELASTUNG", + "workoutKgLiftedUnit": "kg gehoben", + "workoutTonnageFootnoteIntro": "Wiederholungen × Last bei den mit Gewicht protokollierten Sätzen. ", + "workoutTonnageFootnotePartial": "Sätze ohne Gewichtsangabe sind nicht enthalten, daher ist dies eher eine Untergrenze als eine Gesamtsumme. ", + "workoutTonnageFootnoteOutro": "Exakt für das, was du eingegeben hast, und über Übungen hinweg nicht vergleichbar — deshalb bleibt es bei Belastung und Erholung außen vor.", + "workoutOverreachHeadline": "Deine letzten 7 Tage Belastung liegen beim {ratio}-Fachen deiner üblichen sechs Wochen, und dein Ruhepuls war in {nightsElevated} von {nightsConsidered} Nächten über dem Üblichen.", + "workoutOverreachBody": "Zwei Messwerte, die zufällig in dieselbe Richtung zeigen. Krankheit, Reisen, Höhe, Alkohol und mehrere Nächte mit schlechtem Schlaf erzeugen alle dasselbe Muster, und nichts hier kann sie unterscheiden.", + "workoutNoLoadTitle": "Noch keine Trainingsbelastung", + "workoutNoLoadBody": "Fitness und Ermüdung sind 42-Tage- bzw. 7-Tage-Durchschnitte. Dafür sind etwa zwei Wochen an Sessions nötig.", + "workoutFitnessLabel": "Fitness", + "workoutDailyLoadTitle": "TÄGLICHE BELASTUNG", + "workoutTrimpUnit": "TRIMP", + "workoutDailyLoadFootnoteIntro": "Banister-Trainingsimpuls — Minuten gewichtet nach Herzfrequenzreserve. ", + "workoutDailyLoadAllDays": "Die letzten sieben Tage.", + "workoutDailyLoadPartialDays": "{days} der letzten sieben Tage ergaben einen Wert.", + "workoutFatigueLabel": "Ermüdung", + "workoutFormLabel": "Form", + "workoutNotYet": "Noch nicht", + "workoutFormFresh": "Frisch", + "workoutFormSteady": "Stabil", + "workoutFormBuilding": "Im Aufbau", + "workoutFormOverreaching": "Übertraining", + "workoutSearchActivitiesLabel": "Aktivitäten suchen", + "workoutSearchActivitiesCount": "{count, plural, one{{count} Aktivität suchen} other{{count} Aktivitäten suchen}}", + "workoutQuickStartHeader": "SCHNELLSTART", + "workoutCalorieNeedWeightTitle": "Kalorienschätzungen benötigen dein Gewicht", + "workoutAddWeightFix": "Gewicht im Profil hinzufügen", + "workoutCalorieEstimatesTitle": "Kalorienangaben sind Schätzungen", + "workoutSuggestionsTitle": "{n, plural, one{{n} erkannte, aber nicht protokollierte Anstrengung} other{{n} erkannte, aber nicht protokollierte Anstrengungen}}", + "workoutSuggestionsBody": "Das Band hat anhaltende Belastung erkannt, aber nichts wurde gestartet. Nichts wird protokolliert, bis du es bestätigst.", + "workoutReviewFix": "{n, plural, one{Prüfen} other{Alle prüfen}}", + "workoutLogPastTitle": "Etwas gemacht, das das Band verpasst hat?", + "workoutLogPastBody": "Trage die Zeiten selbst ein — die Bewertung erfolgt anhand der in diesem Zeitraum aufgezeichneten Herzfrequenz, wie bei jeder anderen Session.", + "workoutLogPastFix": "Vergangenes Training protokollieren", + "workoutNoSessionsTitle": "Noch keine Sessions aufgezeichnet", + "workoutNoSessionsBody": "Sessions erscheinen hier, sobald du eine startest.", + "workoutStartWorkoutFix": "Training starten", + "workoutTrackedLabel": "Erfasst", + "workoutWeeklyLoadLabel": "Wochenbelastung", + "workoutNoneLabel": "Keine", + "workoutImportedThisWeekNote": "{count} der Sessions dieser Woche stammen von {storeName}. Sie zählen hier mit, fließen aber nicht in die Wochenbelastung ein — ein importiertes Training kommt ohne Herzfrequenzverlauf an, und eine Belastungszahl ohne diesen wäre erfunden.", + "workoutAutoImportOnLabel": "Automatischer Import an. Zum Deaktivieren tippen.", + "workoutAutoImportOffLabel": "Automatischer Import aus. Zum Aktivieren tippen.", + "workoutImportFromStore": "Von {storeName} importieren", + "workoutFetchNowLabel": "Trainings jetzt abrufen", + "workoutImportDenied": "{storeName} hat den Zugriff auf Trainings nicht erlaubt. Es wurde nichts gelesen.", + "workoutImportEmpty": "Nichts kam zurück. {storeName} enthält keine Trainings im geteilten Zeitraum.", + "workoutImportNoRoutes": " {storeName} teilt keine Routen, daher hat keines Koordinaten.", + "workoutImportNoneWithRoute": " Keines davon hatte eine aufgezeichnete Route.", + "workoutImportSomeWithRoute": " {count} kamen mit einer Route.", + "workoutImportBroughtIn": "{count, plural, one{{count} Training importiert.} other{{count} Trainings importiert.}}", + "workoutImportFailed": "Fehlgeschlagen: {error}", + "workoutMorningAfterTitle": "Der Morgen danach", + "workoutMorningAfterBody": "Deine eigene Historie, keine Regel über die Aktivität — diese Morgen hatten auch den jeweiligen Abend davor. Nichts hier ist ein Grund, eine Session auszulassen.", + "workoutAfterActivity": "Nach {name}", + "workoutUnchangedLabel": "Unverändert", + "workoutRestingHeartRateLabel": "Ruhepuls", + "workoutHrvLabel": "HRV", + "workoutMorningCount": "{n, plural, one{{n} Morgen} other{{n} Morgen}}", + "workoutInsideRangeSuffix": " · innerhalb deiner üblichen Nacht-zu-Nacht-Spanne", + "workoutDeleteSessionLabel": "Diese Session löschen", + "workoutStrainLabel": "Belastung", + "workoutTimeInZonesTitle": "ZEIT IN ZONEN", + "workoutMinutesUnit": "Minuten", + "workoutFixTimesOnSessionLabel": "Zeiten dieser Session korrigieren", + "workoutFixTimes": "Zeiten korrigieren", + "workoutTimeStatLabel": "Zeit", + "workoutDistanceStatLabel": "Distanz", + "workoutCaloriesStatLabel": "Kalorien", + "workoutNotCostedValue": "Nicht berechnet", + "workoutMaxHrStatLabel": "Max. Puls", + "workoutNoReadingValue": "Keine Messung", + "workoutConfirmDeleteTitle": "Diese {activity}-Session löschen?", + "workoutDeleteBodyOwn": "Sie verschwindet aus OpenStrap. Eine Kopie in {storeName}, falls vorhanden, bleibt dort erhalten.", + "workoutDeleteBodyImported": "Sie verschwindet aus OpenStrap und wird nicht erneut importiert. Das Original in {storeName} bleibt erhalten.", + "workoutWhenToday": "Heute, {time}", + "workoutWhenYesterday": "Gestern, {time}", + "workoutWeekdayLetterMon": "M", + "workoutWeekdayLetterTue": "D", + "workoutWeekdayLetterWed": "M", + "workoutWeekdayLetterThu": "D", + "workoutWeekdayLetterFri": "F", + "workoutWeekdayLetterSat": "S", + "workoutWeekdayLetterSun": "S", + "workoutWeekdayAbbrMon": "Mo", + "workoutWeekdayAbbrTue": "Di", + "workoutWeekdayAbbrWed": "Mi", + "workoutWeekdayAbbrThu": "Do", + "workoutWeekdayAbbrFri": "Fr", + "workoutWeekdayAbbrSat": "Sa", + "workoutWeekdayAbbrSun": "So", + "activitySetupRouteLabel": "Route", + "activitySetupRouteDetail": "Wird aufgezeichnet, falls der Standort verfügbar ist, und bleibt auf diesem Telefon", + "activitySetupHeartRateLabel": "Herzfrequenz", + "activitySetupBandConnected": "Band verbunden", + "activitySetupNoBandConnected": "Kein Band verbunden", + "activitySetupPrivateLabel": "Private Einheit", + "activitySetupPrivateDetail": "Ausgeblendet in Zusammenfassungen und Exporten", + "activitySetupCaloriesNeedWeight": "Für Kalorien wird dein Gewicht benötigt.", + "activitySetupCalorieEstimate": "Etwa {est} kcal pro {minutes} Min, basierend auf {met} MET und deinem Gewicht.", + "activitySetupTrackSets": "Sätze, Wiederholungen und Gewicht — von dir erfasst", + "activitySetupTrackDistanceGps": "Distanz, Tempo und Herzfrequenz", + "activitySetupTrackTime": "Zeit und Herzfrequenz", + "activitySetupTrackInterval": "Runden und Herzfrequenz", + "activitySetupTrackStillness": "Zeit, Atmung und Ruhe", + "activitySetupSessionRunningTitle": "Es läuft bereits eine Einheit", + "activitySetupSessionRunningBody": "Es kann immer nur eine Einheit gleichzeitig laufen.", + "activitySetupOpenRunningSession": "Laufende Einheit öffnen", + "activitySetupStart": "Start", + "activityPickerTitle": "Aktivität wählen", + "activityPickerSearchLabel": "Aktivitäten durchsuchen", + "activityPickerSearchHint": "{count} Aktivitäten durchsuchen", + "activityPickerNoMatchTitle": "Keine passende Aktivität gefunden", + "activityPickerNoMatchBody": "Der Katalog umfasst etwa siebzig Aktivitäten mit veröffentlichtem Energieverbrauch. Wähle die passendste.", + "activityPickerQuickStart": "SCHNELLSTART", + "activityPickerRecent": "ZULETZT", + "activityPickerCalorieEstimatesTitle": "Kalorienangaben sind Schätzungen", + "activityPickerMetValue": "{met} MET", + "activityPickerKcalPer30": "{kcal} kcal / 30 Min", + "dayStrainToday": "HEUTE", + "dayStrainTitle": "Tagesbelastung", + "dayStrainNoTraceTitle": "Keine Belastungskurve für diesen Tag", + "dayStrainNoMinuteTraceTitle": "Keine minutengenaue Kurve für diesen Tag", + "dayStrainNoReasonBody": "Nichts Aufgezeichnetes erklärt, warum an diesem Tag keine Belastung entstand.", + "dayStrainScoredNoTraceBody": "Die Tagesbelastung beträgt {strain}. Die Wachminuten, aus denen sie berechnet wurde, sind für diesen Tag nicht gespeichert.", + "dayStrainWearBandFix": "Trage das Band den ganzen Tag über", + "dayStrainChartTitle": "BELASTUNG IM TAGESVERLAUF", + "dayStrainChartFootnote": "Kumulativ, daher steigt sie nur — die STEILEN Abschnitte zeigen, wo die Belastung war. Berechnet aus {drawn} aufgezeichneten Wachminuten.", + "dayStrainPeakHr": "Max. Herzfrequenz", + "dayStrainWorn": "Getragen", + "dayStrainLowCoverageTitle": "Das Band hat {pct} % dieses Tages erfasst", + "dayStrainLowCoverageBody": "Die Belastung ist eine Summe über die aufgezeichneten Minuten. Ein nur teilweise getragener Tag zeigt daher einen niedrigeren Wert als ein vollständiger und die beiden sind nicht vergleichbar.", + "dayStrainTimeInZonesSection": "Zeit in Zonen", + "dayStrainZonesChartTitle": "ZEIT IN ZONEN", + "dayStrainZoneFootnoteKarvonen": "Die Zonengrenzen überspannen die Lücke zwischen deiner gemessenen Ruheherzfrequenz und der höchsten gemessenen Herzfrequenz ({maxHr} bpm). Beide wurden an dir gemessen.", + "dayStrainZoneFootnoteObserved": "Die Zonengrenzen sind Prozentsätze der höchsten gemessenen Herzfrequenz ({maxHr} bpm) — gemessen, nicht geschätzt.", + "dayStrainHowSet": "Wie diese festgelegt werden", + "dayStrainInputsSection": "Woraus dies besteht", + "dayStrainInputsBase": "Banister-TRIMP über deine Herzfrequenz im Wachzustand, skaliert auf 0–21.", + "dayStrainInputsMaxHr": "Sie wurde gegen ein angenommenes Maximum von {maxHr} bpm berechnet — geschätzt anhand deines Alters und deines Bands, nicht gemessen.", + "dayStrainInputsMeasuredCeilingNote": "Der Zonenbalken oben verwendet stattdessen die gemessene Obergrenze; die Belastung wurde nicht darauf umgestellt, da dies jeden bisher gesehenen Belastungswert neu schreiben würde.", + "dayStrainInputsRhrAnchor": "Der andere Ankerpunkt ist deine Ruheherzfrequenz der vorherigen Nacht, sodass eine vom Band verpasste Nacht den ganzen Tag verschiebt.", + "activityZonesTitle": "Herzfrequenzzonen", + "activityZonesYourZonesSection": "Deine Zonen", + "activityZonesIntensitySection": "Wohin deine Intensität ging", + "activityZonesNoCeilingTitle": "Noch keine gemessene Obergrenze", + "activityZonesNoCeilingTanakaTail": " Bis eine gemessen wird, basieren die Zonen unten auf deinem Alter.", + "activityZonesNoCeilingDefaultBody": "Wir zählen nur eine hohe Messung, die das Band 15 Sekunden lang während einer Bewegung gehalten hat. Ein einsekündiger Ausschlag ist keine Herzfrequenz.", + "activityZonesWearBandFix": "Trage das Band bei deinen üblichen harten Einheiten", + "activityZonesHighestSeenLabel": "HÖCHSTER GEMESSENER WERT", + "activityZonesBpmUnit": "bpm", + "activityZonesCeilingOnDate": "am {date}", + "activityZonesCeilingDuringSession": "während {session}", + "activityZonesHighestSeenFootnote": "Das ist der höchste von uns gemessene Wert, keine Grenze — er steigt allmählich, sobald das Band härtere Anstrengungen sieht. Teste ihn nicht absichtlich aus.", + "activityZonesNoZonesTitle": "Noch keine Zonen", + "catalogueZonesWhy": "Die Zonengrenzen sind Prozentsätze einer maximalen Herzfrequenz, geschätzt aus deinem Alter — nicht an dir gemessen.", + "activityZonesNoAgeBody": "Die Zonengrenzen sind Prozentsätze einer maximalen Herzfrequenz, und ohne dein Alter gibt es nichts, wovon ein Prozentsatz berechnet werden könnte.", + "activityZonesNoZonesDefaultBody": "Nichts Aufgezeichnetes erklärt, warum es noch keine Zonengrenzen gibt.", + "activityZonesAddAgeFix": "Füge dein Alter im Profil hinzu", + "activityZonesAnchorKarvonen": "Berechnet aus zwei Werten, die das Band an dir gemessen hat: deine Ruheherzfrequenz ({restingHr}, der Median deiner letzten {restingDays} Nächte) und die höchste gemessene Herzfrequenz ({maxHr}). Eine niedrige Ruheherzfrequenz macht Zone 1 breit. Dies sind die üblichen Bänder, nicht deine eigenen gemessenen Schwellenwerte.", + "activityZonesAnchorObserved": "Berechnet aus der höchsten gemessenen Herzfrequenz ({maxHr}). Nach {restingMinDays} Nächten mit Ruheherzfrequenz (du hast {restingDays}) kommt deine Ruheherzfrequenz hinzu, was besser zu dir passt. Dies sind die üblichen Bänder, nicht deine eigenen gemessenen Schwellenwerte.", + "activityZonesAnchorTanaka": "Berechnet aus {maxHr} bpm, geschätzt anhand deines Alters statt an dir gemessen — die Abweichung kann in beide Richtungen bis zu 20 bpm betragen. Die Grenzen wechseln zu einer gemessenen Obergrenze, sobald das Band eine ausreichend harte Einheit sieht.", + "activityZonesAnchorDefault": "Die Zonengrenzen sind Prozentsätze einer maximalen Herzfrequenz.", + "activityZonesNotShownTitle": "Noch nicht angezeigt", + "activityZonesNeedsMonthBody": "Benötigt etwa einen Monat aufgezeichneter Einheiten, jeweils mit minutengenauer Herzfrequenz.", + "activityZonesAgeEstimateBody": "Die Balken würden nur die Altersschätzung widerspiegeln, nicht dein Training. Sie erscheinen, sobald die Zonengrenzen oben gemessen sind.", + "activityZonesSessionMinutesChartTitle": "SITZUNGSMINUTEN, LETZTE 28 TAGE", + "activityZonesShapePyramidal": "Die meisten deiner Minuten sind locker, weniger im mittleren Bereich, am wenigsten hart — eine Pyramide.", + "activityZonesShapePolarised": "Die meisten deiner Minuten sind locker und der Rest ist hart, mit wenig dazwischen.", + "activityZonesShapeMiddleHeavy": "Die meisten deiner Minuten liegen im mittleren Bereich statt locker oder hart zu sein.", + "activityZonesShapeSummary": "{easy} Min. locker, {moderate} moderat, {hard} hart, über {sessions} aufgezeichnete Einheiten. Eine Beschreibung, kein Ziel.", + "activityShareTitle": "Teilen", + "activityShareOpenFailed": "Der Teilen-Dialog konnte nicht geöffnet werden.", + "activitySharePhotoHeader": "DEIN FOTO", + "activityShareAddPhoto": "Foto hinzufügen", + "activityShareChangePhoto": "Foto ändern", + "activitySharePhotoHint": "Von diesem Telefon. Es wird nichts hochgeladen", + "activityShareRemovePhoto": "Foto entfernen", + "activityShareBasemapHeader": "KARTENGRUND", + "activityShareDrawMap": "Echte Karte anzeigen", + "activityShareMapHint": "Fragt openstreetmap.org nach den Kacheln für diese Route. Ausgeschaltet zeichnet sich die Route selbst", + "activityShareFetchingMapTitle": "Karte wird geladen", + "activityShareFetchingMapBody": "Die Grafik wird gezeichnet, sobald jede Kachel da ist.", + "activityShareNoMapTitle": "Keine Karte für diese Grafik", + "activityShareNoMapBody": "Die Kartenkacheln konnten nicht geladen werden, daher zeichnet sich die Route selbst. Der Rest der Grafik bleibt unverändert.", + "activityShareStatusPrivateTitle": "Diese Einheit ist privat", + "activityShareStatusPrivateBody": "In Zusammenfassungen und Exporten ausgeblendet.", + "activityPosterFormatPost": "Beitrag", + "activityPosterFormatStory": "Story", + "activitySummaryRpeHeadline": "WIE ANSTRENGEND WAR ES?", + "activitySummaryRpeBody": "Deine eigene Einschätzung der Anstrengung. Es ist ein Gefühl, keine Messung — genau das ist der Sinn, denn sie kann von den Zahlen oben abweichen.", + "activitySummaryRateEffort": "Diese Anstrengung mit {n} von 10 bewerten", + "activitySummaryRpeVeryEasy": "1 · sehr leicht", + "activitySummaryRpeMaximal": "10 · maximal", + "activitySummaryNotNow": "Jetzt nicht", + "activitySummaryShareThis": "Diese {name}-Einheit teilen", + "activitySummaryChangeType": "Aktivitätstyp ändern", + "activitySummaryUnsavedTitle": "Diese Einheit ist noch nicht gespeichert", + "activitySummaryUnsavedBody": "Das Schreiben auf dieses Telefon ist fehlgeschlagen.", + "activitySummarySaving": "Wird gespeichert", + "activitySummaryTryAgain": "Erneut versuchen", + "activitySummaryPrivate": "Privat", + "activitySummaryStepsBasis": "Die Schritte stammen vom eigenen Bewegungssensor des Bands, der sie nur beim Gehen zählt.", + "activitySummaryCaloriesNeedWeight": "Für Kalorien wird dein Gewicht benötigt.", + "activitySummaryNoCalorieNoStrain": "Für diese Einheit liegt kein Kalorienwert vor. Eine Energieschätzung aus der Herzfrequenz benötigt deine maximale und deine Ruheherzfrequenz, und eine davon ist nicht festgelegt.", + "activitySummaryNoCalorieWithStrain": "Für diese Einheit liegt kein Kalorienwert vor — eine Energieschätzung aus der Herzfrequenz benötigt deine maximale und deine Ruheherzfrequenz, und eine davon ist nicht festgelegt. Die Belastung oben ist der Aufwand, der tatsächlich gemessen wurde, auf seiner eigenen Skala von 0–21.", + "activitySummaryCalorieNoHr": "Geschätzt aus {met} MET und deinem Gewicht. Während dieser Einheit wurde keine Herzfrequenz erfasst, sie fließt also nicht in den Wert ein.", + "activitySummaryCalorieWithHr": "Geschätzt aus {met} MET, deinem Gewicht und der Herzfrequenz.", + "activitySummaryNothingLoggedWithLoad": "Es wurde nichts mit Gewicht erfasst", + "activitySummarySetUnit": "{n, plural, one{Satz} other{Sätze}}", + "activitySummaryVolumeLoadedSets": "Volumen der belasteten Sätze", + "activitySummaryTotalVolume": "Gesamtvolumen", + "activitySummaryElapsedTime": "Verstrichene Zeit", + "activitySummaryClimbed": "+{m} m Anstieg", + "activitySummaryLapsCaption": "{n, plural, one{{n} Bahn} other{{n} Bahnen}}", + "activitySummaryNoRouteTitle": "Keine Route für diese Einheit", + "activitySummaryNoRouteBody": "Der Standort war deaktiviert, oder diese Aktivität wurde nicht mit GPS aufgezeichnet.", + "activitySummaryRouteTitle": "ROUTE", + "activitySummarySlower": "Langsamer", + "activitySummaryFaster": "Schneller", + "activitySummaryStartFinishPinned": "Start und Ziel sind markiert.", + "activitySummaryRouteFootnote": "{distance} {unit}, Start und Ziel markiert.", + "activitySummaryNoSetsTitle": "Keine Sätze erfasst", + "activitySummaryNoSetsBody": "Für diese Einheit wurde nichts eingetragen, daher gibt es weder Gewicht noch Gesamtvolumen.", + "activitySummaryNoRoundsTitle": "Keine Runden erfasst", + "activitySummaryNoRoundsBody": "0 Runden erfasst.", + "activitySummaryIntervalLadderTitle": "INTERVALLLEITER", + "activitySummaryWork": "Belastung", + "activitySummaryRest": "Pause", + "activitySummaryRoundLabel": "Runde {n}", + "activitySummaryLongestBlock": "Längster Block {time}.", + "activitySummaryPosesCount": "{n, plural, one{{n} Haltung} other{{n} Haltungen}}", + "activitySummaryNoLapsTitle": "Keine Bahnen gezählt", + "activitySummaryNoLapsBody": "0 Bahnen getippt.", + "activitySummaryLapsTitle": "BAHNEN", + "activitySummarySecondsPerLap": "Sekunden pro Bahn", + "activitySummaryLapLabel": "Bahn {n}", + "activitySummaryPoolLength": "{m} m Becken", + "activitySummaryFastest": "schnellste {time}", + "activitySummarySlowest": "langsamste {time}", + "activitySummaryNoElevationTitle": "Kein Höhenprofil", + "activitySummaryNoElevationBody": "Keine Route, oder die Route enthielt keine Höhendaten.", + "activitySummaryElevationTitle": "HÖHE", + "activitySummaryStart": "Start", + "activitySummaryFinish": "Ziel", + "activitySummaryGain": "Anstieg", + "activitySummaryLoss": "Abstieg", + "activitySummaryPeak": "Höchster Punkt", + "activitySummaryColdPlungeWhy": "Kälte verengt die Blutgefäße, die der Sensor ausliest. Hier nichts zu finden ist erwartet, kein Fehler.", + "activitySummaryHeatWhy": "Hitze, Schweiß und ein Band, das sich beim Aufwärmen lockert, verhindern alle, dass der Sensor einen Puls erkennt. Hier nichts zu finden ist normal, kein Fehler.", + "activitySummaryNoPulseTitle": "Keine Pulsmessung für diese {activity}-Einheit", + "activitySummaryOneMinutePulse": "Eine Minute Puls, und nicht mehr", + "activitySummaryPulseGapNote": "Das Band hat in {have} von {total} Minuten einen Puls gefunden. Die Lücken sind zu erwarten, gezeichnet ist also der Teil, den es sehen konnte.", + "activitySummaryTooShortTitle": "Zu kurz zum Darstellen", + "activitySummaryTooShortBody": "Eine Minute Herzfrequenz ist ein Punkt, keine Linie.", + "activitySummaryNoHrTitle": "Keine Herzfrequenz für diese Einheit", + "activitySummaryNoHrBody": "Das Band hat während dieser Einheit nichts gemeldet.", + "activitySummaryCheckBandConnection": "Bandverbindung prüfen", + "activitySummaryPartialTrace": "Teilweise Aufzeichnung — das Band hat {pct}% dieser Minuten übermittelt.", + "activitySummaryHeartRateTitle": "HERZFREQUENZ", + "activitySummaryHardMinutesNote": "{min} Min. über 80% deines Maximums.", + "activitySummaryTimeInZonesTitle": "ZEIT IN ZONEN", + "activitySummaryTopSet": "Bester Satz", + "activitySummaryOneRepMax": "1RM geschätzt {kg} kg", + "activitySummarySomeSetsNoLoadTitle": "Einige Sätze hatten kein Gewicht", + "activitySummarySomeSetsNoLoadBody": "In Sätzen und Wiederholungen gezählt, aber vom Volumen ausgeschlossen.", + "activitySummaryScore": "Spielstand", + "activitySummaryGameSetLabel": "Satz {n}", + "activitySummaryNoSplitsTitle": "Keine Zwischenzeiten für diese Einheit", + "activitySummaryNoSplitsBody": "Zwischenzeiten benötigen eine erfasste Distanz.", + "activitySummaryKm": "KM", + "activitySummaryPace": "TEMPO", + "activitySummaryHr": "HF", + "activitySummarySetsLoggedZero": "0 Sätze erfasst.", + "activitySummaryRoundHeader": "R", + "activitySummaryWorkHeader": "BELASTUNG", + "activitySummaryRestHeader": "PAUSE", + "activitySummaryAvgBpm": "Ø HF", + "activitySummaryLapHeader": "BAHN", + "activitySummaryTimeHeader": "ZEIT", + "activitySummarySpeedVsFastest": "TEMPO vs SCHNELLSTE", + "activitySummaryBodyweightReps": "{n, plural, one{{n} Wdh. · Körpergewicht} other{{n} Wdh. · Körpergewicht}}", + "activitySummaryRpeValue": "RPE {v}", + "activitySummaryNothingToPlot": "Nichts darzustellen für diese {activity}-Einheit", + "activitySummaryNoSeriesTitle": "Keine Reihen darzustellen", + "activitySummaryNoSeriesBody": "Diese Einheit hat keine Minutenwerte aufgezeichnet.", + "activitySummaryHeartRateZones": "Herzfrequenzzonen", + "activitySummaryTabOverview": "Übersicht", + "activitySummaryTabSplits": "Zwischenzeiten", + "activitySummaryTabGraphs": "Grafiken", + "activityLiveAddALap": "Bahn hinzufügen", + "activityLiveAddExerciseTitle": "Übung hinzufügen", + "activityLiveAllowLocation": "Standort erlauben", + "activityLiveBestLabel": "Bestwert", + "activityLiveBodyweightExcludedNote": "Körpergewicht — nicht im Volumen enthalten", + "activityLiveBodyweightOnly": "nur Körpergewicht", + "activityLiveBpmUnit": "bpm", + "activityLiveBwAbbrev": "KG", + "activityLiveChangeStroke": "Schwimmstil wechseln", + "activityLiveDecrease": "{label} verringern", + "activityLiveDeniedForeverBody": "Der Standortzugriff ist für diese App verweigert; das lässt sich nur in den Einstellungen ändern.", + "activityLiveDurationHeader": "DAUER", + "activityLiveEffortRpeHeader": "ANSTRENGUNG (RPE)", + "activityLiveEndSet": "Satz beenden", + "activityLiveExerciseOf": "ÜBUNG {index} VON {total}", + "activityLiveFinishSessionLabel": "Einheit beenden", + "activityLiveHoldTime": "Halten · {time}", + "activityLiveIncrease": "{label} erhöhen", + "activityLiveIntervalSubtitle": "{workSec} S BELASTUNG · {restSec} S PAUSE", + "activityLiveKcalEstUnit": "kcal · geschätzt", + "activityLiveKgVolumeUnit": "kg Volumen", + "activityLiveLapButtonLabel": "BAHN", + "activityLiveLapsChartTitle": "BAHNEN", + "activityLiveLapsCount": "{count, plural, one{{count} Bahn} other{{count} Bahnen}} · {stroke}", + "activityLiveLapsFootnote": "Schnellste {time} · die Balkenlänge zeigt die Geschwindigkeit dazu.", + "activityLiveLapXLabel": "Bahn {n}", + "activityLiveLogAsBodyweight": "Als Körpergewicht erfassen", + "activityLiveMatchSetSubtitle": "SATZ {n}", + "activityLiveMetrePoolLabel": "{len}-Meter-Becken", + "activityLiveMinimiseLabel": "Minimieren", + "activityLiveNextExercise": "Nächste Übung", + "activityLiveNextLabel": "ALS NÄCHSTES", + "activityLiveNextPose": "Nächste Haltung", + "activityLiveNextRest": "Pause · {time}", + "activityLiveNextWork": "Belastung · {time}", + "activityLiveNoHrBody": "Das Band ist nicht verbunden, es kommen also keine Werte für diese Einheit an.", + "activityLiveNoHrTitle": "Keine Herzfrequenz", + "activityLiveNoHrYetBody": "Das Band ist verbunden, hat aber noch keinen Schlag gemeldet — es muss eng sitzen, eine Fingerbreite über dem Handgelenkknochen.", + "activityLiveNoHrYetTitle": "Noch keine Herzfrequenz", + "activityLiveNoneYet": "Noch keiner", + "activityLiveNoRouteFailedBody": "Das Telefon hat bei der Standortanfrage einen Fehler zurückgegeben.", + "activityLiveNoRouteFailedTitle": "Keine Route: Standortfehler", + "activityLiveNoRouteNotAllowedTitle": "Keine Route: Standort nicht erlaubt", + "activityLiveNoRouteOffBody": "Die Standortdienste sind auf diesem Telefon deaktiviert, es kommen also keine Ortungen an.", + "activityLiveNoRouteOffTitle": "Keine Route: Standort deaktiviert", + "activityLiveOneLapFewer": "Eine Bahn weniger", + "activityLiveOpenSettings": "Einstellungen öffnen", + "activityLiveOpponentLabel": "GEGNER", + "activityLivePauseLabel": "Pausieren", + "activityLivePerLapUnit": "pro Bahn", + "activityLivePointLabel": "Punkt für {side}", + "activityLivePoolSubtitle": "{len}M-BECKEN · {stroke}", + "activityLivePoseBridge": "Brücke", + "activityLivePoseChair": "Stuhl", + "activityLivePoseChildsPose": "Kindeshaltung", + "activityLivePoseForwardFold": "Vorbeuge", + "activityLivePoseMountain": "Berg", + "activityLivePoseOf": "HALTUNG {index} VON {total}", + "activityLivePosePigeon": "Taube", + "activityLivePosePlank": "Unterarmstütz", + "activityLivePoseSavasana": "Savasana", + "activityLivePoseTriangle": "Dreieck", + "activityLivePoseWarriorTwo": "Krieger II", + "activityLivePreviousExercise": "Vorherige Übung", + "activityLivePreviousLabel": "Vorherige", + "activityLivePrivateSession": "Private Einheit", + "activityLiveRecordingRoute": "Route wird aufgezeichnet", + "activityLiveRepsBodyweightRow": "{n, plural, one{{n} Wiederholung · Körpergewicht} other{{n} Wiederholungen · Körpergewicht}}", + "activityLiveRepsLabel": "WIEDERHOLUNGEN", + "activityLiveRepsLoggedBodyweight": "{n, plural, one{{n} Wiederholung erfasst} other{{n} Wiederholungen erfasst}}", + "activityLiveRepsOnly": "{n, plural, one{{n} Wiederholung} other{{n} Wiederholungen}}", + "activityLiveRepsUnit": "Wiederholungen", + "activityLiveRestingHeader": "PAUSE", + "activityLiveRestWord": "Pause", + "activityLiveResumeLabel": "Fortsetzen", + "activityLiveRoundLabel": "RUNDE {n}", + "activityLiveRouteFootnoteNoDistance": "Startpunkt gesetzt; die Distanz erscheint, sobald sich die Ortungen einpendeln.", + "activityLiveRouteFootnoteWithDistance": "{distance} nach den bisher aufgezeichneten Ortungen.", + "activityLiveRouteSoFarTitle": "ROUTE BISHER", + "activityLiveSetNumber": "Satz {n}", + "activityLiveSetsCountSubtitle": "{n, plural, one{{n} SATZ} other{{n} SÄTZE}}", + "activityLiveSetsListHeader": "SÄTZE", + "activityLiveSetsUnit": "Sätze", + "activityLiveStepsUnit": "Schritte", + "activityLiveStrainUnit": "Belastung", + "activityLiveStrokeBack": "Rücken", + "activityLiveStrokeBreast": "Brust", + "activityLiveStrokeFly": "Schmetterling", + "activityLiveStrokeFree": "Kraul", + "activityLiveThisExerciseLabel": "DIESE ÜBUNG", + "activityLiveTimeInZonesTitle": "ZEIT IN ZONEN", + "activityLiveTimeUnit": "Zeit", + "activityLiveTryAgain": "Erneut versuchen", + "activityLiveTurnOnLocation": "Standort aktivieren", + "activityLiveVolumeSetsSubtitle": "{kg} KG · {n, plural, one{{n} SATZ} other{{n} SÄTZE}}", + "activityLiveWeightLabel": "GEWICHT", + "activityLiveWeightRepsLogged": "{kg} kg × {n} erfasst", + "activityLiveWorkWord": "Belastung", + "activityLiveYouLabel": "DU", + "activityLiveZoneLabel": "Zone {z}", + "activityLiveLogSet": "Satz erfassen", + "activityLiveRestOverAnnounce": "Pause vorbei", + "activityLiveSkipRest": "Pause überspringen", + "gesturesNavTitle": "Doppeltippen", + "gesturesSectionTitle": "Zweimal auf das Band tippen", + "gesturesSectionBody": "Nur solange die App verbunden und aktiv ist. Ein Tippen, das das Band gespeichert hat, während dein Telefon nicht erreichbar war, kommt später mit einem alten Zeitstempel an und wird ignoriert, statt Stunden später ausgelöst zu werden.", + "gesturesItDoesTitle": "Es tut", + "gesturesNoPhoneActionsTitle": "Nichts auf dem Telefon?", + "gesturesNoPhoneActionsBody": "Telefon klingeln lassen und die Taschenlampe fehlen, weil die App das System nicht fragen konnte, was dieses Gerät erlaubt. Öffne die App erneut und komm zurück; die App-internen Aktionen oben funktionieren so oder so.", + "settingsBarcodeSaveFailed": "Das konnte nicht gespeichert werden — es könnte beim nächsten Öffnen der App wieder zurückkommen.", + "settingsIconRowTitle": "Symbol", + "settingsIconRowConfirmHint": "iPhone fragt zur Bestätigung nach", + "settingsIconChoiceLabel": "Symbol {label}.", + "settingsSelectedSuffix": " Ausgewählt.", + "settingsHealthSyncOff": "Aus. Es wird nichts in {store} geschrieben", + "settingsHealthSyncReady": "Schreibt Schlaf, Ruheherzfrequenz, HRV, Atemfrequenz, Energie und Workouts jedes Tages nach {store}, sobald sie final sind", + "settingsHealthSyncNeedsPermission": "{store} hat keinen Schreibzugriff gewährt. Zum Öffnen tippen", + "settingsHealthSyncNotInstalled": "Health Connect ist nicht installiert. Zum Herunterladen tippen", + "settingsHealthSyncNeedsUpdate": "Health Connect ist zu alt zum Schreiben. Zum Aktualisieren tippen", + "settingsHealthSyncUnsupported": "Dieses Gerät hat keinen Gesundheitsspeicher zum Schreiben", + "settingsHealthSyncChecking": "{store} wird geprüft …", + "settingsWriteToHealthStoreRowTitle": "In {store} schreiben", + "settingsHealthShareOffTitle": "Beitrag deaktiviert", + "settingsHealthShareOffNeverUploaded": "Es wurde nie etwas hochgeladen. Es wird auch nichts hochgeladen.", + "settingsHealthShareOffDetail": "Es wird nichts weiter hochgeladen.\n\nEine Kopie deiner Datenbank wurde am {date} hochgeladen. Der Server behält nur die aktuellste Kopie pro Gerät. Wir haben versucht, ihm mitzuteilen, dass deine Einwilligung zurückgezogen wurde — diese Nachricht wird einmal gesendet und nicht erneut versucht; ist dieses Telefon offline, kam sie also nicht an, und wir können dir auch nicht zeigen, dass die Kopie gelöscht ist.", + "settingsOk": "OK", + "settingsHealthShareOnTitle": "Deine Gesundheitsdaten beitragen?", + "settingsHealthShareOnBody": "Einmal täglich, im WLAN und während des Ladens, wird eine komprimierte Kopie deiner GESAMTEN Datenbank hochgeladen — jeder abgeleitete Tag und jede rohe Sensorzeile, die das Band gesendet hat. Sie dient der Verbesserung der Algorithmen.\n\nSie ist in keinem sinnvollen Sinn anonym: Es ist deine gesamte Gesundheitsgeschichte. Du kannst dies jederzeit ausschalten, und ab diesem Moment wird nichts weiter gesendet.", + "settingsNo": "Nein", + "settingsContribute": "Beitragen", + "settingsResetTitle": "Alles löschen?", + "settingsResetBody": "Dies löscht dauerhaft und ohne Kopie an anderer Stelle:\n\n· jeden gemessenen Tag, Schlaf, Workout und jede Route\n· jedes Laborergebnis, jede Mahlzeit, Medikamentendosis, Gewohnheit, Atemübung und jeden protokollierten Satz\n· dein Tagebuch, Zyklusprotokoll und gleitende Baselines\n· dein Profil, jede Einstellung und jeden gespeicherten KI-Schlüssel\n· das Homescreen-Widget und jede geplante Erinnerung\n\nDas Band wird entkoppelt und kann den bereits übertragenen Verlauf nicht erneut senden. Exportiere zuerst über Deine Daten, wenn du eine Kopie möchtest.", + "settingsResetKeepData": "Meine Daten behalten", + "settingsResetDeleteEverything": "Alles löschen", + "settingsNavTitle": "Einstellungen", + "settingsGroupTheBand": "Das Band", + "settingsAlarmRowTitle": "Alarm", + "settingsAlarmRowSub": "Vibriert am Handgelenk, nach der eigenen Uhr des Bands", + "settingsGroupThisPhone": "Dieses Telefon", + "settingsStepsRowTitle": "Schritte", + "settingsStepsRowSub": "Der eigene Schrittzähler dieses Telefons, für die Stunden, die das Band nicht abdeckt. Nichts verlässt das Gerät", + "settingsGroupNotifications": "Benachrichtigungen", + "settingsManageNotificationsRowTitle": "Benachrichtigungen verwalten", + "settingsManageNotificationsRowSub": "Was dich unterbrechen darf, Ruhezeiten und Aus-Schalter für alle davon", + "settingsGroupPreferences": "Einstellungen", + "settingsUnitsRowTitle": "Einheiten", + "settingsAppearanceRowTitle": "Erscheinungsbild", + "settingsCycleTrackingRowTitle": "Zyklus-Tracking", + "settingsCycleTrackingRowSub": "Fügt den Tab Zyklus zu Wellness hinzu. Aus blendet ihn aus und behält alles bereits Erfasste", + "settingsGroupYourData": "Deine Daten", + "settingsExportBackupImportRowTitle": "Exportieren, sichern, importieren", + "settingsExportBackupImportRowSub": "Tabellenkalkulationen, eine vollständige Kopie und Verlauf importieren", + "settingsGroupAutomation": "Automatisierung", + "settingsDoubleTapRowTitle": "Doppeltippen", + "settingsDoubleTapRowSub": "Was ein Doppeltippen auf das Band bewirkt", + "settingsTaskerShortcutsRowTitle": "Tasker und Kurzbefehle", + "settingsTaskerShortcutsRowSub": "Nur Android für ausgehende Ereignisse. iOS kann das Band vibrieren lassen, kann aber nicht dadurch ausgelöst werden", + "settingsGroupPrivacy": "Datenschutz", + "settingsCrashReportsRowTitle": "Absturzberichte", + "settingsCrashReportsRowSub": "Es wird nichts gesendet, bis du zustimmst", + "settingsBarcodeLookupRowTitle": "Barcodes online nachschlagen", + "settingsBarcodeLookupRowSub": "Sendet einen gescannten Barcode an openfoodfacts.org. Nichts über dich wird mitgeschickt", + "settingsContributeHealthDataRowTitle": "Meine Gesundheitsdaten beitragen", + "settingsContributeHealthDataRowSub": "Lädt einmal täglich, im WLAN und während des Ladens, deine gesamte Datenbank hoch, um die Algorithmen zu verbessern", + "settingsCheckForUpdatesRowTitle": "Nach Updates suchen", + "settingsUpdateBelowMinimum": "Dieser Build liegt unter dem minimal unterstützten Build. Installiere die neuere Version von GitHub", + "settingsUpdateAvailable": "Ein neuerer Build ist auf GitHub veröffentlicht", + "settingsUpdateCheckSub": "Fragt beim Start den Release-Server ab. Er sieht deine IP-Adresse und wann du die App öffnest", + "settingsGroupAbout": "Über", + "settingsVersionRowTitle": "Version", + "settingsNoticesLicencesRowTitle": "Hinweise und Lizenzen", + "settingsNoticesLicencesRowSub": "Wer diese App nicht ist, und wessen Daten sie verwendet", + "settingsGroupDeveloper": "Entwickler", + "settingsComponentGalleryRowTitle": "Komponentengalerie", + "settingsComponentGalleryRowSub": "Jede Komponente, bei jeder Textgröße, in beiden Themes", + "settingsDeveloperModeRowTitle": "Entwicklermodus", + "settingsResetAllDataRowTitle": "Alle Daten zurücksetzen", + "settingsNotificationsNavTitle": "Benachrichtigungen", + "settingsNotificationsNavSub": "WAS DICH UNTERBRECHEN DARF", + "settingsNotificationsOffSystemTitle": "Benachrichtigungen sind auf Systemebene deaktiviert", + "settingsNotificationsOffSystemBody": "Nichts davon kann dich erreichen, bis das Betriebssystem es zulässt.", + "settingsTurnThemOn": "Aktivieren", + "settingsGroupManageNotifications": "Benachrichtigungen verwalten", + "settingsHealthExceptionsRowTitle": "Gesundheitsausnahmen", + "settingsHealthExceptionsRowSub": "Höchstens eine pro Tag, und nur wenn sich etwas in deiner eigenen Baseline verändert hat", + "settingsBandAlertsRowTitle": "Band-Benachrichtigungen", + "settingsBandAlertsRowSub": "Akku leer, am Ladegerät, keine Rückmeldung mehr", + "settingsAlertMeAtRowTitle": "Benachrichtige mich bei", + "settingsAlertMeAtRowSub": "Warnt, wenn das Band unter diesen Ladestand fällt", + "settingsRecoveryReadyRowTitle": "Erholung bereit", + "settingsRecoveryReadyRowSub": "Ein Hinweis, sobald dein morgendlicher Erholungswert vorliegt", + "settingsWeeklyLookbackRowTitle": "Wochenrückblick", + "settingsWeeklyLookbackRowSub": "Sonntagabend, aber nur für eine Woche, in der tatsächlich etwas gefunden wurde. Die meisten Wochen bleiben still", + "settingsDetectedWorkoutsRowTitle": "Erkannte Workouts", + "settingsDetectedWorkoutsRowSub": "Fragt nach Anstrengungen, die das Band erkannt hat, die du nicht selbst gestartet hast. Aus blendet die Aufforderung und die Übersichtskarten aus; das Band misst trotzdem weiter", + "settingsMovementNudgeRowTitle": "Bewegungserinnerung", + "settingsMovementNudgeRowSub": "Erinnert dich nach einer ruhigen Phase — zwei Stunden ganz ohne Bewegung oder 90 Minuten in Schreibtischhaltung. Telefon-Benachrichtigung plus Vibration am Band, solange es verbunden ist", + "settingsWindDownRowTitle": "Zur-Ruhe-Kommen", + "settingsWindDownRowSub": "Ein Hinweis etwa 45 Minuten vor der aus deinen eigenen Nächten gelernten Schlafenszeit, außerhalb deiner Ruhezeiten. Erscheint nach etwa einer Woche Tragezeit", + "settingsStepGoalAlertsRowTitle": "Schrittziel-Benachrichtigungen", + "settingsStepGoalAlertsRowSub": "Sagt dir einmal Bescheid, wenn du dein heutiges Schrittziel erreichst", + "settingsMedicationRemindersRowTitle": "Medikamentenerinnerungen", + "settingsMedicationRemindersRowSub": "Eine Benachrichtigung pro geplanter Dosis, zu den von dir eingegebenen Zeiten — mit Vibration am Band, falls verbunden. Für eine bereits als eingenommen oder übersprungen markierte Dosis wird nichts gesendet", + "settingsDailyCheckInRowTitle": "Täglicher Check-in", + "settingsDailyCheckInRowSub": "Ein Hinweis am Abend, um den Tag festzuhalten — Stimmung, Energie, Stress. Wird übersprungen, wenn der Tag bereits eine Bewertung hat", + "settingsWaterReminderRowTitle": "Wassererinnerung", + "settingsWaterReminderRowSub": "Eine Vibration am Band und eine Benachrichtigung auf deinem Telefon während deiner Wachstunden, um dich an das Protokollieren eines Getränks zu erinnern. Es wird so oder so nichts gemessen", + "settingsRemindMeEveryRowTitle": "Erinnere mich alle", + "settingsGroupTheStrap": "Das Band", + "settingsBuzzOnAppNotificationsRowTitle": "Bei App-Benachrichtigungen vibrieren", + "settingsBuzzOnAppNotificationsRowSub": "Wähle, welche Telefon-Apps das Band vibrieren lassen", + "settingsGroupQuietHours": "Ruhezeiten", + "settingsQuietHoursRowTitle": "Ruhezeiten", + "settingsQuietHoursRowSub": "In diesem Zeitfenster vibriert nichts", + "settingsQuietHoursStartsRowTitle": "Beginnt", + "settingsQuietHoursEndsRowTitle": "Endet", + "settingsHealthExceptionsBreakThroughRowTitle": "Gesundheitsausnahmen durchbrechen die Ruhe", + "settingsAlarmNotOnListTitle": "Der Alarm steht nicht auf dieser Liste", + "settingsAlarmNotOnListBody": "Storniere ihn stattdessen auf dem Alarm-Bildschirm.", + "settingsImportNoPermission": "{store} hat diese Felder nicht freigegeben. Es wurde nichts gelesen.", + "settingsImportEmptyWithBirthday": "Es kam nichts zurück. {store} enthält weder Größe, Gewicht, Geburtstag noch Geschlecht für dich — trage sie hier stattdessen ein.", + "settingsImportEmpty": "Es kam nichts zurück. {store} enthält weder Größe, Gewicht noch Geschlecht für dich — trage sie hier stattdessen ein.", + "settingsImportNoChange": "{fields} gelesen. Dein Profil sagt bereits dasselbe, also hat sich nichts geändert.", + "settingsImportUpdated": "{fields} von {store} aktualisiert.", + "settingsImportFailed": "Fehlgeschlagen: {error}", + "settingsAgeFieldLabel": "Alter", + "settingsEditProfileNavTitle": "Profil bearbeiten", + "settingsNameFieldLabel": "NAME", + "settingsSexFieldLabel": "GESCHLECHT", + "settingsSexMale": "Männlich", + "settingsSexFemale": "Weiblich", + "settingsSexPreferNotToSay": "Keine Angabe", + "settingsAgeYearsFieldLabel": "ALTER (JAHRE)", + "settingsFourFieldsTitle": "Diese vier ändern deine Werte", + "settingsFourFieldsBody": "Sie speisen Herzfrequenzzonen, Kalorienschätzungen und Trainingsbelastung. Lösche eines, und nur die Metriken, die es brauchen, bleiben nicht verfügbar.", + "settingsImportBlockAppleHealth": "Größe, Gewicht, Geburtstag und Geschlecht, direkt aus {store}. Größe und Gewicht werden jedes Mal übernommen; dein Alter und Geschlecht füllen nur eine Lücke, da sich beides nicht ändert und ein bereits vorhandener Wert deine eigene Wahl war.", + "settingsImportBlockOther": "Größe und Gewicht, direkt aus {store}. Es gibt weder einen Geburtstag noch ein Geschlecht zu lesen — das kann keine App — also trage diese beiden oben selbst ein.", + "settingsNotSetHint": "Nicht festgelegt", + "settingsAutomationNavTitle": "Automatisierung", + "settingsSyncFinishesSectionTitle": "Wenn eine Synchronisierung abgeschlossen ist", + "settingsSyncFinishesAndroidBody": "Die App sendet einen Intent, auf den deine Automatisierungs-App ein Profil starten kann. Filtere nach der Aktion unten; sie enthält, wie viele Datensätze wann angekommen sind, höchstens einmal pro Minute.", + "settingsSyncFinishesIosBody": "iOS kann das nicht. Eine persönliche Kurzbefehle-Automatisierung kann nur durch Apples eigene feste Liste von Ereignissen ausgelöst werden, und keine App kann eines hinzufügen — also kann hier nichts einen Kurzbefehl für dich starten. Android hat es; das ist eine Plattformgrenze, keine Einstellung.", + "settingsSyncFinishesExtras": "Extras: records (int), at (Unix-Sekunden)", + "settingsNeverSendSectionTitle": "Was niemals gesendet wird", + "settingsNeverSendBody": "Keine Readiness, keine Strain, kein Schlafwert — auf keiner der beiden Plattformen. Eine Zahl, die diese App als fehlend mit einem Grund angezeigt hätte, wird in dem Moment, in dem sie hinausgeht, zu einer bloßen Null. Fakten über die Synchronisierung gehen hinaus; Messwerte nicht.", + "settingsBuzzFromShortcutSectionTitle": "Das Band per Kurzbefehl vibrieren lassen", + "settingsBuzzFromShortcutAndroidBody": "Sende wtf.openstrap.openstrap_edge.BUZZ_STRAP mit diesem Token als String-Extra „token“. Ohne ihn könnte jede App auf dem Telefon dein Band vibrieren lassen.", + "settingsBuzzFromShortcutIosBody": "Diese Richtung funktioniert unter iOS: Ein Kurzbefehl, den du selbst ausführst, kann die App erreichen. Was er nicht kann, ist sich selbst auszuführen, wenn das Band synchronisiert.", + "settingsNoTokenYet": "Noch kein Token — diesen Bildschirm erneut öffnen.", + "settingsCopied": "Kopiert", + "settingsCopyTheToken": "Token kopieren", + "bandStatusBluetoothDeniedTitle": "Bluetooth ist für diese App deaktiviert", + "bandStatusBluetoothDeniedReason": "Das Telefon verweigert OpenStrap den Zugriff auf die Bluetooth-Funkeinheit, daher kann nichts gescannt oder verbunden werden. Das liegt nicht am Band — näher herangehen hilft nicht.", + "bandStatusBluetoothDeniedFix": "Einstellungen → OpenStrap öffnen und Bluetooth erlauben", + "bandStatusBluetoothOffTitle": "Bluetooth ist ausgeschaltet", + "bandStatusBluetoothOffReason": "Die Funkeinheit des Telefons ist aus, daher kann keine App das Band erreichen. Das Band zeichnet währenddessen weiter auf; nichts geht verloren.", + "bandStatusBluetoothOffFix": "Bluetooth einschalten", + "bandStatusBluetoothUnsupportedTitle": "Dieses Telefon hat keine Bluetooth-Low-Energy-Funkeinheit", + "bandStatusBluetoothUnsupportedReason": "Das Band ist nur über Bluetooth Low Energy erreichbar. Importierte Daten funktionieren weiterhin; eine Live-Verbindung nicht.", + "bandStatusReconnectPausedTitle": "Die Wiederverbindung wurde pausiert", + "bandStatusReconnectPausedReason": "{n, plural, one{Das Band hat den Kopplungsschlüssel {n} Mal in Folge abgelehnt, daher hat die App das erneute Versuchen eingestellt, statt die Funkeinheit zu blockieren und beide Akkus an einer Verbindung zu verbrauchen, die sich nicht öffnen wird. Nichts verbindet sich neu, bis du handelst.} other{Das Band hat den Kopplungsschlüssel {n} Mal in Folge abgelehnt, daher hat die App das erneute Versuchen eingestellt, statt die Funkeinheit zu blockieren und beide Akkus an einer Verbindung zu verbrauchen, die sich nicht öffnen wird. Nichts verbindet sich neu, bis du handelst.}}", + "bandStatusRepairNeededTitle": "Das Band muss erneut gekoppelt werden", + "bandStatusRepairNeededReason": "Die Verbindung kommt zustande, aber das Band lehnt den Verschlüsselungsschlüssel des Telefons ab, sodass jeder Befehl verworfen wird und keine Daten übertragen werden. Deine Aufzeichnungen sind auf dem Band sicher.", + "bandStatusRepairFix": "Das Band in den Bluetooth-Einstellungen des Telefons vergessen und dann hier erneut koppeln", + "bandStatusSyncStuckTitle": "Ein Datensatz-Paket wird nicht fertig übertragen", + "bandStatusSyncStuckReason": "Das Band sendet dasselbe Paket immer wieder, weil die App ihm die Bestätigung nicht zustellen kann. Alles darin ist hier bereits gespeichert — nichts geht verloren —, aber das Band kann erst weitermachen, wenn die Bestätigung ankommt.", + "bandStatusSyncStuckFix": "Band neu verbinden; wenn sich das morgen wiederholt, erneut koppeln", + "bandStatusStrapUnresponsiveTitle": "Das Band gibt seine Aufzeichnungen nicht mehr heraus", + "bandStatusStrapUnresponsiveReason": "Das Band meldet neuere Aufzeichnungen, die es nicht sendet. Diese Aufzeichnungen bleiben sicher auf dem Band; sie werden nur nicht übertragen.", + "bandStatusStrapUnresponsiveFix": "Das Band eine Minute lang aufladen, dann erneut verbinden", + "bandStatusClockLostTitle": "Synchronisierungen enden ohne jegliche Daten", + "bandStatusClockLostReason": "Das Band schließt jede Synchronisierung ab, ohne auch nur einen Sensorwert zu übergeben — das bedeutet fast immer, dass seine interne Uhr die Synchronisation verloren hat. Die App stellt sie bei jeder Verbindung neu.", + "bandStatusClockLostFix": "Das Band einige Minuten verbunden lassen; kommt bis morgen nichts an, erneut koppeln", + "bandStatusConnectedReason": "Das Band ist verbunden und gibt seine Aufzeichnungen weiter.", + "bandStatusConnectingTitle": "Verbindung wird aufgebaut", + "bandStatusConnectingReason": "Verbindung zum Band wird geöffnet.", + "bandStatusScanningTitle": "Suche nach dem Band", + "bandStatusScanningReason": "Wartet darauf, dass sich das Band bemerkbar macht.", + "bandStatusDisconnectedReason": "Das Band ist außer Reichweite, liegt auf dem Ladegerät oder ist mit einer anderen App verbunden. Es zeichnet trotzdem weiter auf.", + "bandStatusDisconnectedFix": "Das Band näher ans Telefon bringen und jede andere damit verbundene App schließen", + "devicesTierBeatToBeatLabel": "Schlag-zu-Schlag-Intervalle", + "devicesTierBeatToBeatDetail": "Elektrische R-Zacken-Erkennung.", + "devicesTierWristOpticalLabel": "Optischer Puls am Handgelenk", + "devicesTierWristOpticalDetail": "Durchgehender Puls rund um die Uhr, Schlaf und Temperatur. Der Schlagrhythmus wird aus einer Pulswelle abgeleitet, daher ist die HRV hier eigentlich PRV.", + "devicesTierPhoneLabel": "Nur Schritte", + "devicesTierPhoneDetail": "Der eigene Bewegungs-Coprozessor des Telefons. Schritte und sonst nichts.", + "deviceActionNoneLabel": "Nichts tun", + "deviceActionNoneBlurb": "Doppeltippen bewirkt nichts.", + "deviceActionMediaPlayPauseLabel": "Musik wiedergeben / pausieren", + "deviceActionMediaPlayPauseBlurb": "Wiedergabe umschalten.", + "deviceActionMediaNextLabel": "Nächster Titel", + "deviceActionMediaNextBlurb": "Zum nächsten Titel springen.", + "deviceActionMediaPrevLabel": "Vorheriger Titel", + "deviceActionMediaPrevBlurb": "Zum vorherigen Titel zurück.", + "deviceActionVolumeUpLabel": "Lauter", + "deviceActionVolumeUpBlurb": "Medienlautstärke um eine Stufe erhöhen.", + "deviceActionVolumeDownLabel": "Leiser", + "deviceActionVolumeDownBlurb": "Medienlautstärke um eine Stufe senken.", + "deviceActionRingPhoneLabel": "Mein Telefon klingeln lassen", + "deviceActionRingPhoneBlurb": "Einen lauten Ton abspielen, um dein Telefon zu finden.", + "deviceActionTorchLabel": "Taschenlampe", + "deviceActionTorchBlurb": "Taschenlampe des Telefons ein- oder ausschalten.", + "deviceActionMarkMomentLabel": "Moment markieren", + "deviceActionMarkMomentBlurb": "Den aktuellen Moment in deinem Journal markieren.", + "deviceActionWorkoutToggleLabel": "Training starten / beenden", + "deviceActionWorkoutToggleBlurb": "Ein Training vom Handgelenk aus starten oder beenden.", + "deviceActionLogWaterLabel": "Wasser protokollieren", + "deviceActionLogWaterBlurb": "Ein Glas zum heutigen Wasser hinzufügen — derselbe Schritt wie das + auf dem Ernährungsbildschirm.", + "deviceActionBroadcastToTaskerLabel": "An Tasker senden", + "deviceActionBroadcastToTaskerBlurb": "Einen Broadcast-Intent auslösen, damit Tasker jede beliebige Automatisierung starten kann." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d08cf842..4ea74915 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1438,5 +1438,10155 @@ "welcomeExportAgainInDateOrder": "Export again in date order", "@welcomeExportAgainInDateOrder": { "description": "Fix button: partial data loss." + }, + "scanBarcodeTitle": "Scan the barcode", + "@scanBarcodeTitle": { + "description": "Title at the top of the barcode-scanner sheet" + }, + "scanBarcodeClose": "Close", + "@scanBarcodeClose": { + "description": "Semantic label for the close (X) button on the barcode-scanner sheet" + }, + "scanBarcodeInstructions": "Hold the barcode inside the frame. Nothing is recorded — the digits are all this reads.", + "@scanBarcodeInstructions": { + "description": "Instructional caption under the camera preview in the barcode scanner" + }, + "scanBarcodeNoAccessTitle": "No camera access", + "@scanBarcodeNoAccessTitle": { + "description": "Title shown when camera permission was denied for barcode scanning" + }, + "scanBarcodeCameraFailedTitle": "The camera did not start", + "@scanBarcodeCameraFailedTitle": { + "description": "Title shown when the camera failed to start for barcode scanning" + }, + "scanBarcodeNoAccessBody": "Scanning needs the camera, and this app has not been given it.", + "@scanBarcodeNoAccessBody": { + "description": "Body text explaining camera permission was refused, in the barcode scanner" + }, + "scanBarcodeCameraFailedBody": "This device would not open its camera for the scanner.", + "@scanBarcodeCameraFailedBody": { + "description": "Body text explaining the device could not open its camera for the barcode scanner" + }, + "scanBarcodeTypeInstead": "Type the numbers instead", + "@scanBarcodeTypeInstead": { + "description": "Fallback action label offering manual entry instead of camera scanning" + }, + "findingsLogTitle": "Observations", + "@findingsLogTitle": { + "description": "Page title of the Observations (findings log) screen" + }, + "findingsLogEmptyTitle": "Nothing has stood out", + "@findingsLogEmptyTitle": { + "description": "Title of the empty-state card on the Observations screen when no findings exist" + }, + "findingsLogEmptyBody": "The watches for illness, unusual overnight physiology, skin temperature and a shift in your resting heart rate have all been quiet. That is an outcome, not an empty screen.", + "@findingsLogEmptyBody": { + "description": "Body text of the empty-state card on the Observations screen listing the four quiet watches" + }, + "findingsLogDerivedNote": "Worked out from your own days each time this opens, not written down when it happened — so if a day is re-analysed, what it says here changes with it.", + "@findingsLogDerivedNote": { + "description": "Footnote on the Observations screen explaining findings are recomputed each time the screen opens" + }, + "startCardDefaultSub": "Pick one and go", + "@startCardDefaultSub": { + "description": "Default subline shown under the count on the Workout/Wellness start card, e.g. 'Pick one and go'" + }, + "monthGridCoverage": "{have} of {total} days", + "@monthGridCoverage": { + "description": "Month grid: coverage count above a metric strip, e.g. '23 of 30 days'.", + "placeholders": { + "have": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "monthGridSemanticsLabel": "{title}: {have} of {total} days have a value. Shaded against your own range.", + "@monthGridSemanticsLabel": { + "description": "Month grid: accessibility label read for one metric's heat-map strip.", + "placeholders": { + "title": { + "type": "String" + }, + "have": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "monthGridDaysAgo": "{days} days ago", + "@monthGridDaysAgo": { + "description": "Month grid: left-edge axis label, e.g. '30 days ago'.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "monthGridToday": "Today", + "@monthGridToday": { + "description": "Month grid: right-edge axis label for the most recent day." + }, + "monthGridFootnote": "One cell per day. Darker is further up YOUR own range — the 10th to 90th percentile of every day you have stored — and an outlined cell is a day with no value, not a low one. More strain is not better strain and longer sleep is not healthier sleep; this says where a day sat, not how it went.", + "@monthGridFootnote": { + "description": "Month grid: explanatory footnote under the strips about what shading means." + }, + "monthGridNotShadedYetTitle": "{title} is not shaded yet", + "@monthGridNotShadedYetTitle": { + "description": "Month grid: status card title when a metric has too little history to shade, e.g. 'Sleep is not shaded yet'.", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "monthGridNotShadedYetBody": "{days, plural, one{{days} day} other{{days} days}} is not a range — a shade is where a day sits in your own range. It appears at {min}.", + "@monthGridNotShadedYetBody": { + "description": "Month grid: status card body explaining how many days of history exist and how many are needed before shading starts.", + "placeholders": { + "days": { + "type": "int" + }, + "min": { + "type": "int" + } + } + }, + "whatChangedTitle": "What changed", + "@whatChangedTitle": { + "description": "What Changed screen: nav bar title." + }, + "whatChangedSub": "AGAINST YOUR OWN HISTORY", + "@whatChangedSub": { + "description": "What Changed screen: nav bar subtitle, all caps." + }, + "whatChangedNoDataTitle": "Nothing has landed for this day yet", + "@whatChangedNoDataTitle": { + "description": "What Changed screen: status card title when the selected day has no data at all." + }, + "whatChangedNoDataBody": "The sweep compares a day against the ones before it, and this day has no value to compare. Nothing about it is unusual because nothing about it is known.", + "@whatChangedNoDataBody": { + "description": "What Changed screen: status card body explaining why a dataless day has no findings." + }, + "whatChangedLearningTitle": "Still learning your usual", + "@whatChangedLearningTitle": { + "description": "What Changed screen: status card title shown while there isn't yet enough history for the sweep to judge anything." + }, + "whatChangedLearningBody": "Unusual only means anything against a range, and there {days, plural, one{is {days} day} other{are {days} days}} of history behind this one. The sweep starts at {min}.", + "@whatChangedLearningBody": { + "description": "What Changed screen: status card body stating how many days of history exist and the minimum needed for the sweep to start.", + "placeholders": { + "days": { + "type": "int" + }, + "min": { + "type": "int" + } + } + }, + "whatChangedNothingTitle": "Nothing stood out", + "@whatChangedNothingTitle": { + "description": "What Changed screen: status card title when the sweep ran but found nothing unusual." + }, + "whatChangedNothingBody": "Every metric with enough history sat inside the range your own days have set. That is the normal answer, and it is a complete one.", + "@whatChangedNothingBody": { + "description": "What Changed screen: status card body explaining that every metric sat within its normal range." + }, + "whatChangedMethodologyNote": "Measured against your own trailing days, in your own units, with the window attached — so you can disbelieve it. Nothing here is a cause and nothing here is a diagnosis.", + "@whatChangedMethodologyNote": { + "description": "What Changed screen: footnote under the findings list explaining the methodology and its limits." + }, + "whatChangedDayLinkTitle": "What happened that day", + "@whatChangedDayLinkTitle": { + "description": "What Changed screen: link row title to the day's timeline." + }, + "whatChangedDayLinkSub": "Sleep, sessions, meals and logs in time order", + "@whatChangedDayLinkSub": { + "description": "What Changed screen: link row subtitle describing what the timeline shows." + }, + "whatChangedMonthSection": "The month behind it", + "@whatChangedMonthSection": { + "description": "What Changed screen: section heading above the embedded month grid." + }, + "journalFieldErrorNoName": "Give it a name", + "@journalFieldErrorNoName": { + "description": "Custom journal field sheet: validation error when the name field is left empty." + }, + "journalFieldErrorInvalidName": "Use at least one letter or number", + "@journalFieldErrorInvalidName": { + "description": "Custom journal field sheet: validation error when the name has no letters or digits to build a key from." + }, + "journalFieldErrorNoUnit": "Say what it is counted in (mg, ml, cups…)", + "@journalFieldErrorNoUnit": { + "description": "Custom journal field sheet: validation error when an amount-kind field has no unit entered." + }, + "journalFieldErrorDuplicate": "You already track something by that name", + "@journalFieldErrorDuplicate": { + "description": "Custom journal field sheet: validation error when the entered name collides with an existing field." + }, + "journalFieldTitle": "Track something else", + "@journalFieldTitle": { + "description": "Custom journal field sheet: sheet title." + }, + "journalFieldNameLabel": "What do you want to track?", + "@journalFieldNameLabel": { + "description": "Custom journal field sheet: text field label asking what to track." + }, + "journalFieldNameHint": "Magnesium, screen time, headache…", + "@journalFieldNameHint": { + "description": "Custom journal field sheet: text field placeholder with example things to track." + }, + "journalFieldKindQuestion": "What kind of number is it?", + "@journalFieldKindQuestion": { + "description": "Custom journal field sheet: label above the kind-selector chips." + }, + "journalFieldKindRating": "A 1–5 rating", + "@journalFieldKindRating": { + "description": "Custom journal field sheet: chip label for a 1-5 rating kind field." + }, + "journalFieldKindAmount": "An amount", + "@journalFieldKindAmount": { + "description": "Custom journal field sheet: chip label for an amount/dose kind field." + }, + "journalFieldKindMinutes": "Minutes", + "@journalFieldKindMinutes": { + "description": "Custom journal field sheet: chip label for a duration-in-minutes kind field." + }, + "journalFieldUnitLabel": "Unit", + "@journalFieldUnitLabel": { + "description": "Custom journal field sheet: text field label for the unit of an amount field." + }, + "journalFieldUnitHint": "mg, ml, cups…", + "@journalFieldUnitHint": { + "description": "Custom journal field sheet: text field placeholder with example units." + }, + "journalFieldStepSize": "Step size", + "@journalFieldStepSize": { + "description": "Custom journal field sheet: label above the step-size chips." + }, + "journalFieldMaxPerDay": "Most you would log in a day", + "@journalFieldMaxPerDay": { + "description": "Custom journal field sheet: label above the daily-ceiling chips." + }, + "journalFieldAskLastTime": "Ask when the last one was", + "@journalFieldAskLastTime": { + "description": "Custom journal field sheet: toggle pill asking whether to also record time of last occurrence." + }, + "journalFieldStartTracking": "Start tracking it", + "@journalFieldStartTracking": { + "description": "Custom journal field sheet: primary save button label." + }, + "aiBriefingForDay": "FOR {day}", + "@aiBriefingForDay": { + "description": "AI briefing screen subtitle naming the day the briefing covers.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "aiBriefingNoModelTitle": "No model is set up", + "@aiBriefingNoModelTitle": { + "description": "Status card title when no coach model is configured." + }, + "aiBriefingNoModelBody": "A briefing is written by a model you choose. Until you pick one there is nothing to generate and nothing has been sent anywhere.", + "@aiBriefingNoModelBody": { + "description": "Status card body when no coach model is configured." + }, + "aiBriefingChooseModel": "Choose a model", + "@aiBriefingChooseModel": { + "description": "Fix button label to go to model setup." + }, + "aiBriefingNothingTitle": "Nothing written for today", + "@aiBriefingNothingTitle": { + "description": "Status card title when no briefing exists for today." + }, + "aiBriefingNothingBody": "Briefings are generated on a schedule, or on demand here.", + "@aiBriefingNothingBody": { + "description": "Status card body when no briefing exists for today." + }, + "aiBriefingWriting": "Writing…", + "@aiBriefingWriting": { + "description": "Busy-state button/fix label while a briefing is being generated." + }, + "aiBriefingWriteNow": "Write one now", + "@aiBriefingWriteNow": { + "description": "Fix button label to generate a briefing on demand." + }, + "aiBriefingWriteAgain": "Write it again", + "@aiBriefingWriteAgain": { + "description": "Button label to regenerate an existing briefing." + }, + "aiBriefingFailedTitle": "That did not go through", + "@aiBriefingFailedTitle": { + "description": "Status card title when briefing generation failed." + }, + "aiBriefingFailedGeneric": "It failed: {error}", + "@aiBriefingFailedGeneric": { + "description": "Fallback error message shown when briefing generation throws a non-coach exception.", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "aiBriefingSentSection": "What was sent", + "@aiBriefingSentSection": { + "description": "Section header: payload was sent off-device." + }, + "aiBriefingReadSection": "What was read", + "@aiBriefingReadSection": { + "description": "Section header: payload was only read on-device (local endpoint)." + }, + "aiBriefingNoneBody": "Nothing. There was no request — the note above was written on this phone.", + "@aiBriefingNoneBody": { + "description": "Banner text when no model request was made at all (nightly sweep found nothing)." + }, + "aiBriefingLocalBody": "These numbers went to {host}, on this machine. Nothing left it.", + "@aiBriefingLocalBody": { + "description": "Banner text when the model endpoint is on the same machine (nothing left the device).", + "placeholders": { + "host": { + "type": "String" + } + } + }, + "aiBriefingCloudBody": "These numbers, and nothing else, were sent to {host} as {model}. No raw recordings, no name, no identifier.", + "@aiBriefingCloudBody": { + "description": "Banner text describing exactly what was sent to a remote model host.", + "placeholders": { + "host": { + "type": "String" + }, + "model": { + "type": "String" + } + } + }, + "aiBriefingNoneCardTitle": "Nothing stood out, so nothing was asked", + "@aiBriefingNoneCardTitle": { + "description": "Status card title when the nightly sweep found nothing worth asking a model about." + }, + "aiBriefingNoneCardBody": "The sweep runs on this phone. It only calls a model when it has a finding to hand it, and today it had none.", + "@aiBriefingNoneCardBody": { + "description": "Status card body explaining the sweep only calls a model when it has a finding." + }, + "aiBriefingEmptyCardTitle": "Nothing was available to send", + "@aiBriefingEmptyCardTitle": { + "description": "Status card title when the payload had no metric values to send." + }, + "aiBriefingEmptyCardBody": "No metric had a value when this was written, so the prompt carried none.", + "@aiBriefingEmptyCardBody": { + "description": "Status card body when the payload had no metric values to send." + }, + "napsFellAsleepHelp": "WHEN YOU FELL ASLEEP", + "@napsFellAsleepHelp": { + "description": "Time picker helper text for logging when a manual nap started." + }, + "napsWokeUpHelp": "WHEN YOU WOKE UP", + "@napsWokeUpHelp": { + "description": "Time picker helper text for logging when a manual nap ended." + }, + "napsInvalidWindow": "A nap is between 5 minutes and 6 hours. Anything longer is a sleep, and it belongs in the night where the stages can be read.", + "@napsInvalidWindow": { + "description": "Validation error when a manually logged nap window is outside 5 minutes to 6 hours." + }, + "napsOverlap": "That overlaps a nap already on this day. Remove that one first, rather than counting the same hour twice.", + "@napsOverlap": { + "description": "Validation error when a manually logged nap overlaps an existing one." + }, + "napsNotReanalysed": "The day was not re-analysed — another analysis was already running. Your edit is saved and will apply next time.", + "@napsNotReanalysed": { + "description": "Fallback error when a nap edit saved but the day could not be re-derived because another analysis was already running." + }, + "napsTitle": "Naps", + "@napsTitle": { + "description": "Naps screen nav bar title." + }, + "napsNoReadingTitle": "No nap reading for this day", + "@napsNoReadingTitle": { + "description": "Status card title when a day has no nap detection result." + }, + "napsNoReadingBody": "Naps are worked out from the same 1 Hz recording the rest of the day is, and this day does not have enough of it.", + "@napsNoReadingBody": { + "description": "Fallback status card body explaining naps require enough 1Hz recording." + }, + "napsEmptyTitle": "No naps on this day", + "@napsEmptyTitle": { + "description": "Status card title when a judged day detected zero naps." + }, + "napsEmptyBody": "Nothing on this day was still enough, for long enough, with the heart-rate dip that goes with sleeping through it.", + "@napsEmptyBody": { + "description": "Status card body explaining why no naps were detected that day." + }, + "napsCountsToward": "{mins} of nap counts toward tonight’s sleep need.", + "@napsCountsToward": { + "description": "Footnote stating how many minutes of nap reduce tonight's sleep need.", + "placeholders": { + "mins": { + "type": "String" + } + } + }, + "napsNotAppliedTitle": "That has not been applied", + "@napsNotAppliedTitle": { + "description": "Status card title when a nap edit failed to apply." + }, + "napsWorking": "Working…", + "@napsWorking": { + "description": "Busy-state label on the log-a-nap button while an edit is being written." + }, + "napsLogANap": "Log a nap", + "@napsLogANap": { + "description": "Button label to open the manual nap logging flow." + }, + "napsRemovedSection": "Removed", + "@napsRemovedSection": { + "description": "Section header listing naps the user has removed/rejected." + }, + "napsPutBackSemantic": "Put this nap back", + "@napsPutBackSemantic": { + "description": "Accessibility semantic label for the button that restores a removed nap." + }, + "napsPutBackLabel": "Put it back", + "@napsPutBackLabel": { + "description": "Visible button label to restore a removed nap." + }, + "napsRemovalKept": "A removal is kept as a window rather than an id, so it still applies after the detector’s edges move.", + "@napsRemovalKept": { + "description": "Footnote explaining removed naps are stored as a time window, not an id." + }, + "napsYouLoggedThis": "You logged this", + "@napsYouLoggedThis": { + "description": "Caption for a manually logged nap with no duration recorded." + }, + "napsDetected": "Detected", + "@napsDetected": { + "description": "Caption for a detector-found nap with no duration recorded." + }, + "napsLoggedWithMins": "{mins} · you logged this", + "@napsLoggedWithMins": { + "description": "Caption for a manually logged nap including its duration.", + "placeholders": { + "mins": { + "type": "String" + } + } + }, + "napsDetectedWithMins": "{mins} asleep · detected", + "@napsDetectedWithMins": { + "description": "Caption for a detector-found nap including its duration.", + "placeholders": { + "mins": { + "type": "String" + } + } + }, + "napsDeleteSemantic": "Delete this nap", + "@napsDeleteSemantic": { + "description": "Accessibility semantic label for deleting a manually logged nap." + }, + "napsNotANapSemantic": "This was not a nap", + "@napsNotANapSemantic": { + "description": "Accessibility semantic label for rejecting a detector-found nap." + }, + "napsDeleteLabel": "Delete", + "@napsDeleteLabel": { + "description": "Visible button label to delete a manually logged nap." + }, + "napsNotANapLabel": "Not a nap", + "@napsNotANapLabel": { + "description": "Visible button label to reject a detector-found nap." + }, + "readinessDetailTitle": "Readiness", + "@readinessDetailTitle": { + "description": "Readiness detail screen title, also used as the history chart title." + }, + "readinessDetailNotScoredTitle": "Readiness is not scored", + "@readinessDetailNotScoredTitle": { + "description": "Status card title when readiness has no score for today." + }, + "readinessDetailLastNightScored": "The last night scored was {day}.", + "@readinessDetailLastNightScored": { + "description": "Appended fact naming the last night that did produce a readiness score.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "readinessDetailWhatWasMissing": "What was missing", + "@readinessDetailWhatWasMissing": { + "description": "Section header for the readiness absence diagnostic breakdown." + }, + "readinessDetailWhatWentIntoIt": "What went into it", + "@readinessDetailWhatWentIntoIt": { + "description": "Section header for the readiness input breakdown (scored or empty-state)." + }, + "readinessDetailInputsFooter": "{used}/{total} inputs. Each one is ranked against your own history — a parallel view of the same inputs, not slices of the number above.", + "@readinessDetailInputsFooter": { + "description": "Footnote below the breakdown stating how many inputs were used out of the total.", + "placeholders": { + "used": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "readinessDetailNoBreakdownTitle": "No breakdown yet", + "@readinessDetailNoBreakdownTitle": { + "description": "Status card title when readiness scored but no breakdown exists yet." + }, + "readinessDetailNoBreakdownBody": "Ranking each input against your own history takes about two weeks of nights.", + "@readinessDetailNoBreakdownBody": { + "description": "Status card body explaining the breakdown needs about two weeks of history." + }, + "readinessDetailHistoryTitle": "History", + "@readinessDetailHistoryTitle": { + "description": "History section header when there are zero scored days." + }, + "readinessDetailLastNDays": "{n, plural, one{Last {n} day} other{Last {n} days}}", + "@readinessDetailLastNDays": { + "description": "History section header naming how many days of history are shown.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "readinessDetailNoHistoryTitle": "No readiness history", + "@readinessDetailNoHistoryTitle": { + "description": "Status card title when there is no readiness history at all." + }, + "readinessDetailNoHistoryBody": "0 days scored.", + "@readinessDetailNoHistoryBody": { + "description": "Status card body stating 0 days scored." + }, + "readinessDetailWearOvernight": "Wear the band overnight", + "@readinessDetailWearOvernight": { + "description": "Fix button label suggesting the user wear the band overnight." + }, + "readinessDetailUnit": "/100", + "@readinessDetailUnit": { + "description": "Unit suffix shown on the readiness history chart (score out of 100)." + }, + "readinessDetailDaysAgo": "{n, plural, one{{n} day ago} other{{n} days ago}}", + "@readinessDetailDaysAgo": { + "description": "X-axis label on the readiness history chart naming how many days ago the first plotted point is.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "readinessDetailToday": "Today", + "@readinessDetailToday": { + "description": "X-axis label on the readiness history chart for the most recent point." + }, + "readinessDetailMeasured": "Measured", + "@readinessDetailMeasured": { + "description": "Row value in the absence diagnostic: this input was measured." + }, + "readinessDetailNotMeasured": "Not measured", + "@readinessDetailNotMeasured": { + "description": "Row value in the absence diagnostic: this input was not measured." + }, + "readinessDetailNightsOfHistory": "{n, plural, one{{n} night of your own history} other{{n} nights of your own history}}", + "@readinessDetailNightsOfHistory": { + "description": "Row value in the absence diagnostic stating how many of the user's own nights back the baseline.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "readinessDetailNeedSuffix": "{need}. Each input is ranked against your own nights, so the score cannot start before there are enough of them.", + "@readinessDetailNeedSuffix": { + "description": "Sentence appended after the pipeline's own 'need N more nights' message.", + "placeholders": { + "need": { + "type": "String" + } + } + }, + "readinessDetailNoNoteFallback": "Everything above was present, and the comparison against your own history still could not be made.", + "@readinessDetailNoNoteFallback": { + "description": "Fallback sentence when readiness is absent but the pipeline left no specific note." + }, + "readinessDetailNotAvailable": "not available", + "@readinessDetailNotAvailable": { + "description": "Breakdown row tag: this input was not available/used." + }, + "readinessDetailContributionNotReported": "contribution not reported", + "@readinessDetailContributionNotReported": { + "description": "Breakdown row tag: input was used but its numeric contribution was not reported." + }, + "readinessDetailRelativeUncalibrated": "relative, uncalibrated", + "@readinessDetailRelativeUncalibrated": { + "description": "Breakdown row tag on the temperature input: it is a relative, uncalibrated sensor deviation." + }, + "readinessDetailWithinSpread": "within your usual spread", + "@readinessDetailWithinSpread": { + "description": "Breakdown row tag: the input's change was within the user's usual spread (below the smallest-worthwhile-change threshold)." + }, + "readinessDetailWeightPercent": "{pct}% weight", + "@readinessDetailWeightPercent": { + "description": "Breakdown row tag showing the percentage weight this input actually carried.", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "dayStepsTitle": "Steps", + "@dayStepsTitle": { + "description": "Screen title on the Day Steps detail screen." + }, + "dayStepsThroughDay": "Through the day", + "@dayStepsThroughDay": { + "description": "Section header above the list of step spans through the day." + }, + "dayStepsToday": "today", + "@dayStepsToday": { + "description": "Fragment meaning 'today', substituted into other Day Steps sentences as {when}." + }, + "dayStepsOnDay": "on {day}", + "@dayStepsOnDay": { + "description": "Fragment naming a specific past day, substituted into other Day Steps sentences as {when}.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "dayStepsNoTimesTitle": "No times behind the count {when}", + "@dayStepsNoTimesTitle": { + "description": "Empty-state title: the day has a step total but no timed spans behind it.", + "placeholders": { + "when": { + "type": "String" + } + } + }, + "dayStepsNoStepsTitle": "No steps counted {when}", + "@dayStepsNoStepsTitle": { + "description": "Empty-state title: nothing counted steps this day.", + "placeholders": { + "when": { + "type": "String" + } + } + }, + "dayStepsStrapCounterBody": "The {count} steps counted {when} came from the strap's own step counter, which reports a running day total and no times. There is nothing to place on a clock.", + "@dayStepsStrapCounterBody": { + "description": "Empty-state body explaining the strap's own running counter has no times.", + "placeholders": { + "count": { + "type": "String" + }, + "when": { + "type": "String" + } + } + }, + "dayStepsNothingCounted": "Nothing that can count steps recorded {when}.", + "@dayStepsNothingCounted": { + "description": "Empty-state body when no sensor recorded steps this day.", + "placeholders": { + "when": { + "type": "String" + } + } + }, + "dayStepsChartTitle": "WHEN THEY WERE COUNTED", + "@dayStepsChartTitle": { + "description": "Title of the hourly steps chart, shown in caps." + }, + "dayStepsUnit": "steps", + "@dayStepsUnit": { + "description": "Unit label for the steps chart." + }, + "dayStepsYourPhone": "Your phone", + "@dayStepsYourPhone": { + "description": "Label for the phone as a step source, used in legend, metrics and row labels." + }, + "dayStepsYourBand": "Your band", + "@dayStepsYourBand": { + "description": "Fallback label for the wearable band when no paired-device name is known." + }, + "dayStepsCounted": "Counted", + "@dayStepsCounted": { + "description": "Label for the combined total in the inline metrics row." + }, + "dayStepsHonestyMixed": "Counted at your wrist and by your phone, and the two miscount differently: a wrist reads a real walk low and can read rhythmic hand work as walking, while a phone counts only the steps you had it on you for.", + "@dayStepsHonestyMixed": { + "description": "Footnote explaining wrist vs phone step-counting error when both sources contributed." + }, + "dayStepsHonestyStrap": "Counted at your wrist, where a real walk tends to read low and rhythmic hand work can read as walking.", + "@dayStepsHonestyStrap": { + "description": "Footnote explaining wrist step-counting error when only the strap contributed." + }, + "dayStepsHonestyPhone": "Counted by your phone, so only the steps you had it on you for are here.", + "@dayStepsHonestyPhone": { + "description": "Footnote explaining phone step-counting coverage when only the phone contributed." + }, + "roughNightSignRhr": "your resting heart rate ran higher", + "@roughNightSignRhr": { + "description": "Rough Night: one of the moved-measurement clauses, resting heart rate ran higher." + }, + "roughNightSignHrv": "your HRV ran lower", + "@roughNightSignHrv": { + "description": "Rough Night: one of the moved-measurement clauses, HRV ran lower." + }, + "roughNightSignDip": "your heart rate dropped less overnight than it usually does", + "@roughNightSignDip": { + "description": "Rough Night: one of the moved-measurement clauses, overnight heart rate dip was blunted." + }, + "roughNightSignTemp": "your skin ran warmer", + "@roughNightSignTemp": { + "description": "Rough Night: one of the moved-measurement clauses, skin temperature ran warmer." + }, + "roughNightLateTraining": "You trained until {at}, which often does this on its own.", + "@roughNightLateTraining": { + "description": "Rough Night card: stated fact about a late training session that night.", + "placeholders": { + "at": { + "type": "String" + } + } + }, + "roughNightIllness": "The illness watch flagged this night too — a sustained rise against your own baseline, not a diagnosis.", + "@roughNightIllness": { + "description": "Rough Night card: stated fact when the illness watch also flagged the same night." + }, + "roughNightLuteal": "You are in the luteal phase, which lifts resting heart rate and skin temperature by itself.", + "@roughNightLuteal": { + "description": "Rough Night card: stated fact when the user is in the luteal phase." + }, + "roughNightWarmRoom": "Your skin ran warmer than your usual — a warm room does this too.", + "@roughNightWarmRoom": { + "description": "Rough Night card: stated fact when the skin-temperature sign fired." + }, + "roughNightDismiss": "Dismiss", + "@roughNightDismiss": { + "description": "Semantic label for the dismiss (X) button on the Rough Night card." + }, + "roughNightDefaultHeadline": "A rougher night than usual", + "@roughNightDefaultHeadline": { + "description": "Fallback headline for the Rough Night card when the analytics descriptor has no leading clause." + }, + "roughNightSummary": "{sentence}, against your own nights. This is a measurement of the night, not a verdict on you.", + "@roughNightSummary": { + "description": "Rough Night card body sentence, combining the moved-measurements clause with the closing disclaimer.", + "placeholders": { + "sentence": { + "type": "String" + } + } + }, + "roughNightTellWhatHappened": "Tell it what happened", + "@roughNightTellWhatHappened": { + "description": "Button inviting the user to open the optional attribution question on the Rough Night card." + }, + "roughNightNothingToAnswer": "Nothing to answer — this card only reports the night.", + "@roughNightNothingToAnswer": { + "description": "Rough Night card note shown after the user has permanently declined the attribution question." + }, + "roughNightWhatElse": "What else was going on?", + "@roughNightWhatElse": { + "description": "Rough Night attribution question heading when nothing else is already known about the night." + }, + "roughNightAnythingElse": "Anything else?", + "@roughNightAnythingElse": { + "description": "Rough Night attribution question heading when something is already known about the night." + }, + "roughNightSaving": "Saving", + "@roughNightSaving": { + "description": "Button label while the Rough Night attribution answer is being saved." + }, + "roughNightLogIt": "Log it for that night", + "@roughNightLogIt": { + "description": "Button label to save the picked attribution tags for that night." + }, + "roughNightAddHowMuch": "Add how much", + "@roughNightAddHowMuch": { + "description": "Link to the journal editor for entering amounts, from the Rough Night card." + }, + "roughNightDoNotAskAgain": "Do not ask again", + "@roughNightDoNotAskAgain": { + "description": "Link to permanently decline the Rough Night attribution question." + }, + "roughNightSeveralMoved": "Several overnight measurements moved together", + "@roughNightSeveralMoved": { + "description": "Fallback clause when the Rough Night card has no named moved measurements." + }, + "driverBreakdownHigherThanUsual": "Higher than your usual", + "@driverBreakdownHigherThanUsual": { + "description": "Driver breakdown: non-numeric input (e.g. skin temp) moved up vs usual." + }, + "driverBreakdownLowerThanUsual": "Lower than your usual", + "@driverBreakdownLowerThanUsual": { + "description": "Driver breakdown: non-numeric input (e.g. skin temp) moved down vs usual." + }, + "driverBreakdownRightOnUsual": "{now} · right on your usual", + "@driverBreakdownRightOnUsual": { + "description": "Driver value line when the reading equals the user's usual.", + "placeholders": { + "now": { + "type": "String" + } + } + }, + "driverBreakdownAboveUsual": "{now} · {delta} above your usual {usual}", + "@driverBreakdownAboveUsual": { + "description": "Driver value line: reading is above usual, with reading/delta/usual formatted values.", + "placeholders": { + "now": { + "type": "String" + }, + "delta": { + "type": "String" + }, + "usual": { + "type": "String" + } + } + }, + "driverBreakdownBelowUsual": "{now} · {delta} below your usual {usual}", + "@driverBreakdownBelowUsual": { + "description": "Driver value line: reading is below usual, with reading/delta/usual formatted values.", + "placeholders": { + "now": { + "type": "String" + }, + "delta": { + "type": "String" + }, + "usual": { + "type": "String" + } + } + }, + "driverBreakdownWeightPct": "{pct}% weight", + "@driverBreakdownWeightPct": { + "description": "Driver qualifier chip: the renormalised weight share as a rounded percent.", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "driverBreakdownNotAvailable": "not available", + "@driverBreakdownNotAvailable": { + "description": "Driver qualifier: the input was not usable." + }, + "driverBreakdownContributionNotReported": "contribution not reported", + "@driverBreakdownContributionNotReported": { + "description": "Driver qualifier: used but no contribution number was reported." + }, + "driverBreakdownRelativeUncalibrated": "relative, uncalibrated", + "@driverBreakdownRelativeUncalibrated": { + "description": "Driver qualifier: raw uncalibrated sensor deviation (e.g. skin temp)." + }, + "driverBreakdownWithinUsualSpread": "within your usual spread", + "@driverBreakdownWithinUsualSpread": { + "description": "Driver qualifier: change did not clear the smallest-worthwhile-change gate." + }, + "driverBreakdownBiggerThanNoise": "bigger than measurement noise", + "@driverBreakdownBiggerThanNoise": { + "description": "Driver qualifier: change is outside usual spread and bigger than measurement noise." + }, + "driverBreakdownSmallerThanNoise": "outside your usual spread, but small enough to be measurement noise", + "@driverBreakdownSmallerThanNoise": { + "description": "Driver qualifier: change is outside usual spread but still within measurement noise." + }, + "driverBreakdownWhatHelped": "What helped", + "@driverBreakdownWhatHelped": { + "description": "Driver breakdown group header: drivers that raised the score." + }, + "driverBreakdownWhatHeldYouBack": "What held you back", + "@driverBreakdownWhatHeldYouBack": { + "description": "Driver breakdown group header: drivers that lowered the score." + }, + "driverBreakdownNeither": "Neither", + "@driverBreakdownNeither": { + "description": "Driver breakdown group header: drivers unavailable or with zero net effect." + }, + "driverBreakdownFooter": "Each input is ranked against your own history — a parallel view of the same inputs, not slices of the score itself. \"Measurement noise\" is how far a reading can move on its own without anything having changed. Patterns in your own logs, not causes.", + "@driverBreakdownFooter": { + "description": "Driver breakdown screen footer explaining what the list means." + }, + "driverBreakdownHideHistory": "{label}, hide its history", + "@driverBreakdownHideHistory": { + "description": "Accessibility label for tapping an open driver row to collapse its chart.", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "driverBreakdownShowHistory": "{label}, show its history", + "@driverBreakdownShowHistory": { + "description": "Accessibility label for tapping a closed driver row to expand its chart.", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "driverBreakdownDaysAgo": "{n, plural, one{{n} day ago} other{{n} days ago}}", + "@driverBreakdownDaysAgo": { + "description": "X-axis start label on a driver's history chart, N days before today.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "driverBreakdownToday": "Today", + "@driverBreakdownToday": { + "description": "X-axis end label on a driver's history chart." + }, + "driverBreakdownUsualRange": "Your usual range {lo}–{hi}{unit}", + "@driverBreakdownUsualRange": { + "description": "Chart footnote stating the user's usual range for this driver.", + "placeholders": { + "lo": { + "type": "String" + }, + "hi": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "driverBreakdownAbsenceTitle": "No breakdown to show", + "@driverBreakdownAbsenceTitle": { + "description": "Title of the card shown when there is no readiness breakdown for the night." + }, + "driverBreakdownAbsenceAlgoVersion": "How readiness is worked out changed with the last update, and it is being rebuilt.", + "@driverBreakdownAbsenceAlgoVersion": { + "description": "Absence card body when the breakdown is missing because the algorithm version changed and is rebuilding." + }, + "driverBreakdownAbsenceStale": "The last rollup is too old to stand behind.", + "@driverBreakdownAbsenceStale": { + "description": "Absence card body when the last stored rollup is too old to show." + }, + "driverBreakdownAbsenceNoVersion": "The stored rollup carries no version stamp.", + "@driverBreakdownAbsenceNoVersion": { + "description": "Absence card body when the stored rollup has no version stamp." + }, + "driverBreakdownSyncTheBand": "Sync the band", + "@driverBreakdownSyncTheBand": { + "description": "Absence card action button: sync the band to refresh the rollup." + }, + "driverBreakdownAbsenceNoReason": "Nothing recorded says why last night has no breakdown.", + "@driverBreakdownAbsenceNoReason": { + "description": "Absence card body when nothing recorded explains the missing breakdown." + }, + "coachFiguresCouldNotBeDrawn": "A figure could not be drawn", + "@coachFiguresCouldNotBeDrawn": { + "description": "Fallback title when a coach-sent chart figure has an unrecognized type." + }, + "coachFiguresNoType": "The coach sent a figure with no type.", + "@coachFiguresNoType": { + "description": "Body text when the coach sent a figure spec with no 'type' field." + }, + "coachFiguresUnsupportedType": "The coach asked for a \"{type}\" figure, which this app does not draw.", + "@coachFiguresUnsupportedType": { + "description": "Body text when the coach sent a figure of a type this app cannot render.", + "placeholders": { + "type": { + "type": "String" + } + } + }, + "coachFiguresFigure": "Figure", + "@coachFiguresFigure": { + "description": "Fallback chart title when the coach's figure spec has no title." + }, + "coachFiguresSeriesN": "Series {n}", + "@coachFiguresSeriesN": { + "description": "Fallback legend name for an unnamed line/area series, numbered.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "coachFiguresLaneN": "Lane {n}", + "@coachFiguresLaneN": { + "description": "Fallback legend name for an unnamed dual-axis lane, numbered.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "coachFiguresNoSleepSegments": "No sleep segments", + "@coachFiguresNoSleepSegments": { + "description": "Empty-state message for a hypnogram figure with no sleep segments." + }, + "coachFiguresNoTimeInZone": "No time in zone", + "@coachFiguresNoTimeInZone": { + "description": "Empty-state message for a heart-rate-zone bar figure with no data." + }, + "coachFiguresMinTotal": "{n} min total", + "@coachFiguresMinTotal": { + "description": "Footnote under a zone bar figure stating total minutes across zones.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "coachFiguresGauge": "Gauge", + "@coachFiguresGauge": { + "description": "Fallback title/label for a gauge figure with no title or label." + }, + "coachFiguresGaugeNoValue": "The coach sent a gauge with no value.", + "@coachFiguresGaugeNoValue": { + "description": "Error card body when the coach sent a gauge figure with no value." + }, + "coachFiguresSummary": "Summary", + "@coachFiguresSummary": { + "description": "Fallback title for a KPI-grid figure with no title." + }, + "coachFiguresEmptySummary": "The coach sent an empty summary.", + "@coachFiguresEmptySummary": { + "description": "Error card body when the coach sent a KPI-grid figure with no cards." + }, + "coachFiguresTable": "Table", + "@coachFiguresTable": { + "description": "Fallback title for a table figure with no title." + }, + "coachFiguresTableNoRows": "The coach sent a table with no rows.", + "@coachFiguresTableNoRows": { + "description": "Error card body when the coach sent a table figure with no rows." + }, + "circadianDetailTitle": "Body clock", + "@circadianDetailTitle": { + "description": "Nav title of the body-clock (circadian) detail screen" + }, + "circadianDetailNoNightsTitle": "No nights to plot yet", + "@circadianDetailNoNightsTitle": { + "description": "Empty-state title when no nights have been scored yet" + }, + "circadianDetailNoNightsBody": "0 nights scored.", + "@circadianDetailNoNightsBody": { + "description": "Empty-state body under circadianDetailNoNightsTitle" + }, + "circadianDetailNoNightsFix": "Wear the band overnight", + "@circadianDetailNoNightsFix": { + "description": "Empty-state suggested fix action" + }, + "circadianDetailSleepTitle": "Sleep, night by night", + "@circadianDetailSleepTitle": { + "description": "Chart title for the per-night sleep actogram" + }, + "circadianDetailAsleep": "Asleep", + "@circadianDetailAsleep": { + "description": "Legend label for the actogram's asleep series" + }, + "circadianDetailSleepFootnote": "{count, plural, one{{count} night, one column each. Darker is more of that hour asleep.} other{{count} nights, one column each. Darker is more of that hour asleep.}}", + "@circadianDetailSleepFootnote": { + "description": "Footnote under the actogram stating how many nights are drawn", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "circadianDetailYourRhythm": "Your rhythm", + "@circadianDetailYourRhythm": { + "description": "Section title for the chronotype/jetlag/regularity table" + }, + "circadianDetailWhichNights": "Which nights", + "@circadianDetailWhichNights": { + "description": "Section action link to reveal the least-alike night pair" + }, + "circadianDetailHide": "Hide", + "@circadianDetailHide": { + "description": "Toggle action label to collapse an expanded section" + }, + "circadianDetailShow": "Show", + "@circadianDetailShow": { + "description": "Toggle action label to expand the rhythm-strength section" + }, + "circadianDetailTodayPredicted": "Today, predicted", + "@circadianDetailTodayPredicted": { + "description": "Section title for the alertness forecast card" + }, + "circadianDetailRhythmStrength": "Rhythm strength", + "@circadianDetailRhythmStrength": { + "description": "Section title for the non-parametric rhythm battery" + }, + "circadianDetailWhenStill": "When you are still", + "@circadianDetailWhenStill": { + "description": "Section title for the daytime HRV-while-still card" + }, + "circadianDetailNoStillTitle": "No still moments to read yet", + "@circadianDetailNoStillTitle": { + "description": "Empty-state title when no still-moment HRV data exists" + }, + "circadianDetailNoStillBody": "This reads beat timing only from the seconds you were not moving, and the last {days, plural, one{{days} day} other{{days} days}} had too few of them to build an hour from.", + "@circadianDetailNoStillBody": { + "description": "Empty-state body explaining too few quiet stretches were found", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "circadianDetailStillnessTitle": "Beat-to-beat variability while still", + "@circadianDetailStillnessTitle": { + "description": "Chart title for the hourly HRV-while-still chart" + }, + "circadianDetailStillnessFootnote": "Each hour is the middle value of {lo}–{hi} five-minute stretches you were actually still, over the last {days, plural, one{{days} day} other{{days} days}} — never today's alone. {drawn} of 24 hours had at least three stretches; the rest are blank. Not a stress score — sitting up, a warm room or a coffee move it just as much.", + "@circadianDetailStillnessFootnote": { + "description": "Footnote explaining the HRV-while-still chart's methodology and coverage", + "placeholders": { + "lo": { + "type": "int" + }, + "hi": { + "type": "int" + }, + "days": { + "type": "int" + }, + "drawn": { + "type": "int" + } + } + }, + "circadianDetailForecastTitle": "How today is likely to run", + "@circadianDetailForecastTitle": { + "description": "Chart title for the alertness-shape forecast" + }, + "circadianDetailForecastFootnote": "No scale — the shape is the whole output.", + "@circadianDetailForecastFootnote": { + "description": "Footnote noting the forecast chart has no numeric scale" + }, + "circadianDetailTroughText": "The flattest stretch lands in {troughLabel}, around {start}–{end}.", + "@circadianDetailTroughText": { + "description": "Sentence naming the predicted flattest/lowest-alertness window of the day", + "placeholders": { + "troughLabel": { + "type": "String" + }, + "start": { + "type": "String" + }, + "end": { + "type": "String" + } + } + }, + "circadianDetailPredictionDisclaimer": "This is a prediction, not a reading. Nothing on the band measures how alert you are, and it knows last night and nothing else — a nap, coffee, or anything that happens today never reaches it.", + "@circadianDetailPredictionDisclaimer": { + "description": "Disclaimer that the alertness forecast is a prediction, not a measurement" + }, + "circadianDetailAssumedPhaseNote": "Your own clock peak is not worked out yet, so this uses an average one.", + "@circadianDetailAssumedPhaseNote": { + "description": "Appended note when the user's own circadian acrophase is not yet known and an average is used" + }, + "circadianDetailNotADrivingCheck": "It is not a fitness-to-drive check and not a shift-safety tool, and it does not say you are impaired.", + "@circadianDetailNotADrivingCheck": { + "description": "Safety disclaimer that the forecast is not a fitness-to-drive or shift-safety tool" + }, + "circadianDetailChronotype": "Chronotype", + "@circadianDetailChronotype": { + "description": "Row label: chronotype classification" + }, + "circadianDetailMidSleepFree": "Mid-sleep, free days", + "@circadianDetailMidSleepFree": { + "description": "Row label: mid-sleep time on free days" + }, + "circadianDetailMidSleepWork": "Mid-sleep, working days", + "@circadianDetailMidSleepWork": { + "description": "Row label: mid-sleep time on working days" + }, + "circadianDetailSocialJetlag": "Social jetlag", + "@circadianDetailSocialJetlag": { + "description": "Row label: social jetlag magnitude" + }, + "circadianDetailLater": "later", + "@circadianDetailLater": { + "description": "Suffix word appended to the social jetlag value when the free-day clock runs later" + }, + "circadianDetailEarlier": "earlier", + "@circadianDetailEarlier": { + "description": "Suffix word appended to the social jetlag value when the free-day clock runs earlier" + }, + "circadianDetailNightsCompared": "Free / working nights compared", + "@circadianDetailNightsCompared": { + "description": "Row label: count of free vs working nights compared" + }, + "circadianDetailRegularityIndex": "Regularity index", + "@circadianDetailRegularityIndex": { + "description": "Row label: sleep regularity index score" + }, + "circadianDetailNightsLeastAlike": "Nights least alike", + "@circadianDetailNightsLeastAlike": { + "description": "Row label: the pair of nights with lowest regularity agreement" + }, + "circadianDetailSamePairScale": "That pair, same scale", + "@circadianDetailSamePairScale": { + "description": "Row label: that least-alike pair's score on the same 0-100 scale" + }, + "circadianDetailRhythmNotEstablished": "Your rhythm is not established yet", + "@circadianDetailRhythmNotEstablished": { + "description": "Empty-state title when the rhythm table has no rows yet" + }, + "circadianDetailPairFootnote": "The pair that matched least, out of {count}. A weekend that runs late is a different schedule, not a worse night. Pairs where too little of either day was recorded are left out.", + "@circadianDetailPairFootnote": { + "description": "Footnote explaining the least-alike night pair table, out of how many pairs total", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "circadianDetailStability": "Day-to-day stability", + "@circadianDetailStability": { + "description": "Row label: interdaily stability (IS)" + }, + "circadianDetailFragmentation": "Hour-to-hour fragmentation", + "@circadianDetailFragmentation": { + "description": "Row label: intradaily variability (IV)" + }, + "circadianDetailAmplitude": "Relative amplitude", + "@circadianDetailAmplitude": { + "description": "Row label: relative amplitude (RA)" + }, + "circadianDetailM10Start": "Highest-HR 10 hours start", + "@circadianDetailM10Start": { + "description": "Row label: start time of the highest-HR 10-hour window (M10)" + }, + "circadianDetailL5Start": "Lowest-HR 5 hours start", + "@circadianDetailL5Start": { + "description": "Row label: start time of the lowest-HR 5-hour window (L5)" + }, + "circadianDetailRhythmPeak": "Rhythm peak", + "@circadianDetailRhythmPeak": { + "description": "Row label: cosinor-fit rhythm peak time (acrophase)" + }, + "circadianDetailPeakSwing": "Peak-to-mean swing", + "@circadianDetailPeakSwing": { + "description": "Row label: cosinor amplitude, peak-to-mean swing" + }, + "circadianDetailFitCurve": "Fit to a 24 h curve", + "@circadianDetailFitCurve": { + "description": "Row label: adjusted R² of the 24h cosinor fit" + }, + "circadianDetailStrengthNotMeasured": "Rhythm strength is not measured yet", + "@circadianDetailStrengthNotMeasured": { + "description": "Empty-state title when the rhythm-strength battery has no rows yet" + }, + "circadianDetailStrengthWhy": "Needs consecutive days with all 24 hours recorded.", + "@circadianDetailStrengthWhy": { + "description": "Empty-state reason: needs consecutive fully-recorded days" + }, + "circadianDetailStrengthFootnoteKnown": "From {used, plural, one{{used} fully-recorded day} other{{used} fully-recorded days}} of heart rate. These are your highest and lowest heart-rate hours, not your busiest.", + "@circadianDetailStrengthFootnoteKnown": { + "description": "Footnote citing how many fully-recorded days the rhythm-strength battery used", + "placeholders": { + "used": { + "type": "int" + } + } + }, + "circadianDetailStrengthFootnoteUnknown": "From a run of fully-recorded days of heart rate. These are your highest and lowest heart-rate hours, not your busiest.", + "@circadianDetailStrengthFootnoteUnknown": { + "description": "Footnote for the rhythm-strength battery when the exact day count is unavailable" + }, + "beatsTitle": "Beats", + "@beatsTitle": { + "description": "Nav title of the Beats (RR-interval) detail screen" + }, + "beatsNoNightTitle": "No night to draw yet", + "@beatsNoNightTitle": { + "description": "Empty-state title when no derived night exists yet" + }, + "beatsNoNightBody": "Nothing on this phone has produced a derived night, so there are no beat intervals to plot.", + "@beatsNoNightBody": { + "description": "Empty-state body explaining there are no beat intervals to plot" + }, + "beatsNoNightFix": "Wear the band overnight, then sync", + "@beatsNoNightFix": { + "description": "Empty-state suggested fix action" + }, + "beatsNightOf": "Night of {date}", + "@beatsNightOf": { + "description": "Subtitle stating which night's date all panels describe", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "beatsPoincareSection": "Every beat against the one before it", + "@beatsPoincareSection": { + "description": "Section title for the Poincaré scatter plot" + }, + "beatsBeatsGoneTitle": "The beats for this night are no longer on this phone", + "@beatsBeatsGoneTitle": { + "description": "Empty-state title when raw beat intervals have been pruned from the phone" + }, + "beatsBeatsGoneBody": "Individual beat intervals are kept for a few days after the night is scored, then deleted. The numbers taken from them are kept for good", + "@beatsBeatsGoneBody": { + "description": "Empty-state body explaining beat intervals are pruned after a few days but derived numbers persist" + }, + "beatsBeatsGoneMeasured": " — this night measured SD1 {sd1} ms, SD2 {sd2} ms", + "@beatsBeatsGoneMeasured": { + "description": "Appended clause reporting the SD1/SD2 values still available even though raw beats are gone", + "placeholders": { + "sd1": { + "type": "String" + }, + "sd2": { + "type": "String" + } + } + }, + "beatsScatterTitle": "Each interval, plotted against the previous one", + "@beatsScatterTitle": { + "description": "Chart title for the Poincaré (beat vs previous beat) scatter" + }, + "beatsScatterFootnote": "The diagonal is where a beat came out the same length as the one before it. Spread across that line is SD1, beat to beat; spread along it is SD2, the slower drift.", + "@beatsScatterFootnote": { + "description": "Footnote explaining how to read the Poincaré scatter's SD1/SD2 axes" + }, + "beatsSd1Label": "SD1", + "@beatsSd1Label": { + "description": "Inline metric label: SD1 (short-term Poincaré axis)" + }, + "beatsSd2Label": "SD2", + "@beatsSd2Label": { + "description": "Inline metric label: SD2 (long-term Poincaré axis)" + }, + "beatsIntervalsLabel": "Intervals", + "@beatsIntervalsLabel": { + "description": "Inline metric label: count of intervals plotted" + }, + "beatsIntervalsSurvived": "{count, plural, one{{count} interval survived correction} other{{count} intervals survived correction}}", + "@beatsIntervalsSurvived": { + "description": "Note fragment: how many beat intervals survived artifact correction", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "beatsDroppedArtifact": " — {count, plural, one{{count} was} other{{count} were}} rejected as artifact and {count, plural, one{is} other{are}} not in the cloud", + "@beatsDroppedArtifact": { + "description": "Note fragment appended when some beats were rejected as artifact", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "beatsPulseNotEcg": "Pulse, not ECG — real and yours, but not the picture an ECG draws.", + "@beatsPulseNotEcg": { + "description": "Disclaimer that this is pulse-derived data, not an ECG reading" + }, + "beatsMeasuredOn": "Measured on {device}; straps do not read the same numbers as each other.", + "@beatsMeasuredOn": { + "description": "Note fragment naming which strap/device family measured this night", + "placeholders": { + "device": { + "type": "String" + } + } + }, + "beatsVariabilitySection": "Variability across the night", + "@beatsVariabilitySection": { + "description": "Section title for the night HRV curve panel" + }, + "beatsUnitNights": "nights", + "@beatsUnitNights": { + "description": "Unit label on the night-variability status card" + }, + "beatsUnitScreenedNotScreened": "screened / not screened", + "@beatsUnitScreenedNotScreened": { + "description": "Unit label on the rhythm-screen chart (legend describes the two per-day states)" + }, + "beatsVariabilityWhy": "No half-hour bin of this night held enough clean beats to publish an RMSSD.", + "@beatsVariabilityWhy": { + "description": "Empty-state reason: no half-hour bin had enough clean beats" + }, + "beatsNoBinsStored": "No bins were stored for this night.", + "@beatsNoBinsStored": { + "description": "Empty-state body when literally no bins were stored for the night" + }, + "beatsRmssdTitle": "RMSSD in half-hour bins", + "@beatsRmssdTitle": { + "description": "Chart title for the half-hour RMSSD band chart" + }, + "beatsStart": "Start", + "@beatsStart": { + "description": "X-axis label for the start of the night when no wall-clock origin is known" + }, + "beatsBandFootnote": "The bar is how sure we are of the bin, not a range your body passed through. The mark inside it is the value.", + "@beatsBandFootnote": { + "description": "Footnote explaining the RMSSD band chart's bar-vs-mark meaning" + }, + "beatsHolesFootnote": "{count, plural, one{ {count} bin holds too few clean beats to publish one, and is left empty rather than joined across.} other{ {count} bins hold too few clean beats to publish one, and are left empty rather than joined across.}}", + "@beatsHolesFootnote": { + "description": "Footnote fragment noting how many bins were left empty for too few clean beats", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "beatsFirstThird": "First third", + "@beatsFirstThird": { + "description": "Inline metric label: RMSSD in the first third of the night" + }, + "beatsLastThird": "Last third", + "@beatsLastThird": { + "description": "Inline metric label: RMSSD in the last third of the night" + }, + "beatsDcSection": "Deceleration capacity", + "@beatsDcSection": { + "description": "Section title for the deceleration-capacity (PRSA DC) panel" + }, + "beatsDcWhy": "No stored night has produced one yet.", + "@beatsDcWhy": { + "description": "Empty-state reason: no stored night has produced a DC value" + }, + "beatsDcNoData": "No night has produced one yet.", + "@beatsDcNoData": { + "description": "Empty-state body fallback for deceleration capacity" + }, + "beatsDcChartTitle": "Your own nights, in order", + "@beatsDcChartTitle": { + "description": "Chart title for the deceleration-capacity trend chart" + }, + "beatsDaysAgo": "{count} days ago", + "@beatsDaysAgo": { + "description": "X-axis label for the oldest day shown in a rolling-window chart", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "beatsTodayLabel": "Today", + "@beatsTodayLabel": { + "description": "X-axis label for the most recent day in a rolling-window chart" + }, + "beatsAnchorsLastNight": "Anchors last night", + "@beatsAnchorsLastNight": { + "description": "Inline metric label: PRSA anchor count for the most recent night" + }, + "beatsCleanBeats": "Clean beats", + "@beatsCleanBeats": { + "description": "Inline metric label: percentage of beats that were clean" + }, + "beatsDcNote": "Yours only. Compare it against your own other nights and nothing else — there is no reference band for a wrist.\n\nIt averages the beats around each moment your heart slowed. A rising line can be a cleaner signal rather than a different heart, so read it beside the anchor count and clean-beat share above. If you changed straps inside this window, the two halves do not compare.", + "@beatsDcNote": { + "description": "Explanatory note under the deceleration-capacity chart about how to interpret it" + }, + "beatsRhythmSection": "Rhythm screen", + "@beatsRhythmSection": { + "description": "Section title for the irregular-rhythm screening panel" + }, + "beatsRhythmChartTitle": "One cell per day", + "@beatsRhythmChartTitle": { + "description": "Chart title for the per-day rhythm-screen strip" + }, + "beatsScreenNotFired": "Screen did not fire", + "@beatsScreenNotFired": { + "description": "Legend label: the rhythm screen ran and did not flag anything" + }, + "beatsScreenFired": "Screen fired", + "@beatsScreenFired": { + "description": "Legend label: the rhythm screen ran and flagged the day" + }, + "beatsNotScreened": "Not screened", + "@beatsNotScreened": { + "description": "Legend label: the rhythm screen did not run that day" + }, + "beatsNoDayScreened": "No day in this window was screened", + "@beatsNoDayScreened": { + "description": "Empty-state message when no day in the window was screened at all" + }, + "beatsScreenNote": "A screen, not a test.\n\nA day the screen did not fire is not a day you were cleared — it cannot rule anything out, and it never could. Outlined days were not screened at all: too few clean beats, or too much movement.\n\nWrist pulse is not an ECG. If symptoms are what brought you here, a clinician can test that properly.", + "@beatsScreenNote": { + "description": "Multi-paragraph disclaimer that the rhythm strip is a screen, not a diagnosis, and to see a clinician for symptoms" + }, + "beatsScreenedSummary": "{screened} of the last {win} days were screened", + "@beatsScreenedSummary": { + "description": "Summary sentence: how many of the last N days were screened", + "placeholders": { + "screened": { + "type": "int" + }, + "win": { + "type": "int" + } + } + }, + "beatsFiredSummary": "; the screen fired on {fired}", + "@beatsFiredSummary": { + "description": "Appended clause: how many days the screen fired on", + "placeholders": { + "fired": { + "type": "int" + } + } + }, + "beatsNotScreenedNote": " Last night was not screened: {note}", + "@beatsNotScreenedNote": { + "description": "Appended clause giving the reason last night specifically was not screened", + "placeholders": { + "note": { + "type": "String" + } + } + }, + "logWorkoutCouldNotLog": "Could not log this one — try again.", + "@logWorkoutCouldNotLog": { + "description": "Error snack bar when confirming a detected workout suggestion fails to save" + }, + "logWorkoutCouldNotDismiss": "Could not dismiss this one — try again.", + "@logWorkoutCouldNotDismiss": { + "description": "Error snack bar when dismissing a detected workout suggestion fails" + }, + "logWorkoutAdjustTimes": "Adjust the times", + "@logWorkoutAdjustTimes": { + "description": "Button/screen-title for widening the times on a detected workout before logging it" + }, + "logWorkoutDetectedActivityTitle": "Detected activity", + "@logWorkoutDetectedActivityTitle": { + "description": "Nav bar title on the detected-workout review screen" + }, + "logWorkoutYoursToConfirmSub": "YOURS TO CONFIRM", + "@logWorkoutYoursToConfirmSub": { + "description": "Nav bar subtitle (all caps) on the detected-workout review screen" + }, + "logWorkoutReadFailedTitle": "Could not read your detected activity", + "@logWorkoutReadFailedTitle": { + "description": "Status card title when reading detected workout suggestions fails" + }, + "logWorkoutReadFailedBody": "The store did not answer. Nothing has been logged or dismissed.", + "@logWorkoutReadFailedBody": { + "description": "Status card body when reading detected workout suggestions fails" + }, + "logWorkoutTryAgain": "Try again", + "@logWorkoutTryAgain": { + "description": "Retry action label on the failed-read status card" + }, + "logWorkoutReadingSpotted": "Reading what the band spotted…", + "@logWorkoutReadingSpotted": { + "description": "Loading message while detected workout suggestions are being read" + }, + "logWorkoutNothingToReviewTitle": "Nothing to review", + "@logWorkoutNothingToReviewTitle": { + "description": "Status card title when there are no detected workouts left to review" + }, + "logWorkoutNothingToReviewBody": "This one may already have been logged or dismissed.", + "@logWorkoutNothingToReviewBody": { + "description": "Status card body when there are no detected workouts left to review" + }, + "logWorkoutHardMinutesTitle": "These are the hard minutes, not the whole session", + "@logWorkoutHardMinutesTitle": { + "description": "Status card title explaining that detection only reports the hard-effort core of a session" + }, + "logWorkoutHardMinutesBody": "Detection reports the sustained effort it could see, so a warm-up and the rest between sets fall outside it. Adjust the times before logging if the window is short.", + "@logWorkoutHardMinutesBody": { + "description": "Status card body explaining that detection only reports the hard-effort core of a session" + }, + "logWorkoutMinutesOfEffort": "{mins} min of effort", + "@logWorkoutMinutesOfEffort": { + "description": "Duration line on a detected-workout card, e.g. '25 min of effort'", + "placeholders": { + "mins": { + "type": "int" + } + } + }, + "logWorkoutAvgHr": "Avg HR", + "@logWorkoutAvgHr": { + "description": "Metric label: average heart rate over a detected bout" + }, + "logWorkoutPeakHr": "Peak HR", + "@logWorkoutPeakHr": { + "description": "Metric label: peak heart rate over a detected bout" + }, + "logWorkoutLooksLike": "Looks like", + "@logWorkoutLooksLike": { + "description": "Metric label naming the guessed activity type of a detected bout" + }, + "logWorkoutLogIt": "Log it", + "@logWorkoutLogIt": { + "description": "Primary button to confirm/log a workout" + }, + "logWorkoutNotAWorkout": "Not a workout", + "@logWorkoutNotAWorkout": { + "description": "Button to dismiss a detected bout as not a workout" + }, + "logWorkoutToday": "Today", + "@logWorkoutToday": { + "description": "Relative day label for the current calendar day" + }, + "logWorkoutYesterday": "Yesterday", + "@logWorkoutYesterday": { + "description": "Relative day label for the previous calendar day" + }, + "logWorkoutDefaultTitle": "Log a past workout", + "@logWorkoutDefaultTitle": { + "description": "Default screen title for the manual workout log form when no title is supplied" + }, + "logWorkoutWindowRescoredSub": "THE WINDOW, RE-SCORED", + "@logWorkoutWindowRescoredSub": { + "description": "Nav bar subtitle (all caps) when retiming an existing logged workout" + }, + "logWorkoutYourOwnTimesSub": "YOUR OWN TIMES", + "@logWorkoutYourOwnTimesSub": { + "description": "Nav bar subtitle (all caps) when logging a brand-new manual workout" + }, + "logWorkoutWhenGroup": "When", + "@logWorkoutWhenGroup": { + "description": "Settings group heading over the date/start/end/length rows on the workout form" + }, + "logWorkoutActivityLabel": "Activity", + "@logWorkoutActivityLabel": { + "description": "Form row label for the workout activity/type picker" + }, + "logWorkoutDateLabel": "Date", + "@logWorkoutDateLabel": { + "description": "Form row label for the workout date picker" + }, + "logWorkoutStartedLabel": "Started", + "@logWorkoutStartedLabel": { + "description": "Form row label for the workout start time picker" + }, + "logWorkoutEndedLabel": "Ended", + "@logWorkoutEndedLabel": { + "description": "Form row label for the workout end time picker" + }, + "logWorkoutLengthLabel": "Length", + "@logWorkoutLengthLabel": { + "description": "Form row label showing the computed workout duration" + }, + "logWorkoutNextMorningSub": "the next morning", + "@logWorkoutNextMorningSub": { + "description": "Small note under the end-time row when the end falls on the next calendar day" + }, + "logWorkoutWindowInvalidTitle": "That window will not save", + "@logWorkoutWindowInvalidTitle": { + "description": "Status card title when the chosen time window fails validation" + }, + "logWorkoutTimesUpdatedTitle": "Times updated", + "@logWorkoutTimesUpdatedTitle": { + "description": "Status card title after successfully retiming an existing workout" + }, + "logWorkoutLoggedTitle": "Workout logged", + "@logWorkoutLoggedTitle": { + "description": "Status card title after successfully logging a new workout" + }, + "logWorkoutUnscoredSaved": "Saved. No heart rate was recorded over that window, so it has no strain and no calorie figure — the times are all this one carries.", + "@logWorkoutUnscoredSaved": { + "description": "Status card body when a saved workout has no heart-rate data so it could not be scored" + }, + "logWorkoutCouldNotSave": "Could not save that — try again.", + "@logWorkoutCouldNotSave": { + "description": "Status card body when saving the workout form fails" + }, + "logWorkoutScoredTitle": "Scored from what the band recorded", + "@logWorkoutScoredTitle": { + "description": "Status card title explaining that strain/calories are computed from recorded heart rate" + }, + "logWorkoutScoredBody": "Strain and calories come from the 1-second heart rate inside these times, through the same method the day uses. Nothing is estimated from the duration.", + "@logWorkoutScoredBody": { + "description": "Status card body explaining that strain/calories are computed from recorded heart rate, not estimated" + }, + "logWorkoutSaving": "Saving…", + "@logWorkoutSaving": { + "description": "Save button label while the workout form is submitting" + }, + "logWorkoutSaveNewTimes": "Save the new times", + "@logWorkoutSaveNewTimes": { + "description": "Save button label when retiming an existing workout" + }, + "logWorkoutSearchActivities": "Search activities", + "@logWorkoutSearchActivities": { + "description": "Search field hint text in the activity-type picker sheet" + }, + "logWorkoutNoActivityByName": "No activity by that name", + "@logWorkoutNoActivityByName": { + "description": "Empty-state message when the activity search finds no matches" + }, + "logFoodTitle": "Log an eating occasion", + "@logFoodTitle": { + "description": "Heading of the log-food bottom sheet" + }, + "logFoodClose": "Close", + "@logFoodClose": { + "description": "Accessibility label for the close button on the log-food sheet" + }, + "logFoodIAte": "I ate {meal}", + "@logFoodIAte": { + "description": "Primary button to log a bare eating occasion, e.g. 'I ate breakfast'", + "placeholders": { + "meal": { + "type": "String" + } + } + }, + "logFoodAgain": "Again", + "@logFoodAgain": { + "description": "Section heading listing recently logged foods for one-tap re-logging" + }, + "logFoodAddNumbers": "Add the numbers", + "@logFoodAddNumbers": { + "description": "Expandable row label to reveal the macro-detail fields" + }, + "logFoodScanBarcode": "Scan a barcode", + "@logFoodScanBarcode": { + "description": "Row label to scan a product barcode" + }, + "logFoodScanSubOn": "Asks openfoodfacts.org about the barcode, and fills in what it can stand behind", + "@logFoodScanSubOn": { + "description": "Subtitle under the scan-barcode row when online lookups are already allowed" + }, + "logFoodScanSubOff": "Looks the pack up online. Asks first", + "@logFoodScanSubOff": { + "description": "Subtitle under the scan-barcode row when online lookups are off and will be asked about first" + }, + "logFoodLookingUpTitle": "Looking it up", + "@logFoodLookingUpTitle": { + "description": "Status card title while a barcode lookup is in progress" + }, + "logFoodLookingUpBody": "The boxes fill as soon as the answer is here.", + "@logFoodLookingUpBody": { + "description": "Status card body while a barcode lookup is in progress" + }, + "logFoodWhatLabel": "What", + "@logFoodWhatLabel": { + "description": "Text field label for the food description" + }, + "logFoodWhatHint": "Chicken and rice", + "@logFoodWhatHint": { + "description": "Text field placeholder example for the food description" + }, + "logFoodPortionLabel": "Portion", + "@logFoodPortionLabel": { + "description": "Number field label for the eaten portion size in grams" + }, + "logFoodUnknownHint": "unknown", + "@logFoodUnknownHint": { + "description": "Placeholder text in a macro number field when no value is known" + }, + "logFoodEnergyLabel": "Energy", + "@logFoodEnergyLabel": { + "description": "Number field label for energy/calories" + }, + "logFoodProteinLabel": "Protein", + "@logFoodProteinLabel": { + "description": "Number field label for protein" + }, + "logFoodCarbsLabel": "Carbs", + "@logFoodCarbsLabel": { + "description": "Number field label for carbohydrates" + }, + "logFoodFatLabel": "Fat", + "@logFoodFatLabel": { + "description": "Number field label for fat" + }, + "logFoodFibreLabel": "Fibre", + "@logFoodFibreLabel": { + "description": "Number field label for fibre" + }, + "logFoodBlankHint": "A blank number stays blank. Only \"What\" is needed.", + "@logFoodBlankHint": { + "description": "Helper text under the macro fields explaining blank values are fine" + }, + "logFoodSayWhatFirst": "Say what it was first.", + "@logFoodSayWhatFirst": { + "description": "Snack bar shown when trying to save a food entry with no description" + }, + "logFoodConsentTitle": "Look barcodes up online?", + "@logFoodConsentTitle": { + "description": "Heading of the barcode-lookup consent sheet" + }, + "logFoodConsentBody1": "A scan sends the barcode to openfoodfacts.org, a free, open food database. They see the barcode and your IP address. Nothing about you, your meals or your health leaves this phone, and a barcode you have scanned before is answered from your own copy without asking them again.", + "@logFoodConsentBody1": { + "description": "First paragraph of the barcode-lookup consent sheet, explaining what data leaves the phone" + }, + "logFoodConsentBody2": "Their numbers are typed in by the public and a fair few of them are wrong, so anything that fails a sanity check is left blank rather than filled in. Everything it does fill in is yours to edit before you save.", + "@logFoodConsentBody2": { + "description": "Second paragraph of the barcode-lookup consent sheet, explaining data quality and editability" + }, + "logFoodConsentBody3": "You can turn this back off in Settings › Privacy. Typing the numbers off the pack works either way.", + "@logFoodConsentBody3": { + "description": "Third paragraph of the barcode-lookup consent sheet, explaining how to revoke consent" + }, + "logFoodAllowLookups": "Allow lookups", + "@logFoodAllowLookups": { + "description": "Consent sheet button to allow online barcode lookups" + }, + "logFoodNotNow": "Not now", + "@logFoodNotNow": { + "description": "Consent sheet button to decline online barcode lookups" + }, + "logFoodBreakfast": "Breakfast", + "@logFoodBreakfast": { + "description": "Meal-type label: breakfast" + }, + "logFoodLunch": "Lunch", + "@logFoodLunch": { + "description": "Meal-type label: lunch" + }, + "logFoodDinner": "Dinner", + "@logFoodDinner": { + "description": "Meal-type label: dinner" + }, + "logFoodSnack": "Snack", + "@logFoodSnack": { + "description": "Meal-type label: snack (also the default/fallback meal type)" + }, + "logFoodNoNumbersTitle": "No numbers for this one", + "@logFoodNoNumbersTitle": { + "description": "Status card title when a barcode lookup found the product but no usable nutrition numbers" + }, + "logFoodNoNumbersBody": "Open Food Facts has the product but nothing usable on its nutrition — or what it had did not survive a sanity check.", + "@logFoodNoNumbersBody": { + "description": "Status card body when a barcode lookup found the product but no usable nutrition numbers" + }, + "logFoodNotFoundTitle": "Not in Open Food Facts", + "@logFoodNotFoundTitle": { + "description": "Status card title when a scanned barcode is not in the product database" + }, + "logFoodNotFoundBody": "Nobody has added this barcode yet.", + "@logFoodNotFoundBody": { + "description": "Status card body when a scanned barcode is not in the product database" + }, + "logFoodFlaggedTitle": "This record is flagged as wrong", + "@logFoodFlaggedTitle": { + "description": "Status card title when Open Food Facts flags the scanned product record as wrong" + }, + "logFoodFlaggedBody": "Open Food Facts marks this product as containing errors, so none of its numbers were filled in.", + "@logFoodFlaggedBody": { + "description": "Status card body when Open Food Facts flags the scanned product record as wrong" + }, + "logFoodUnreachableTitle": "No answer from Open Food Facts", + "@logFoodUnreachableTitle": { + "description": "Status card title when the barcode lookup service could not be reached" + }, + "logFoodUnreachableBody": "The lookup could not reach openfoodfacts.org.", + "@logFoodUnreachableBody": { + "description": "Status card body when the barcode lookup service could not be reached" + }, + "logFoodRefusedTitle": "Barcode lookup is off", + "@logFoodRefusedTitle": { + "description": "Status card title when barcode lookup is turned off in privacy settings" + }, + "logFoodRefusedBody": "Nothing was sent. You can turn it on in Settings › Privacy.", + "@logFoodRefusedBody": { + "description": "Status card body when barcode lookup is turned off in privacy settings" + }, + "logFoodPortionNoteBase": "Open Food Facts lists this per 100 g. Change the portion and the numbers follow.", + "@logFoodPortionNoteBase": { + "description": "Note under the portion field explaining nutrition numbers are per 100g and rescale with portion, used when the product has no stated serving size" + }, + "logFoodPortionNoteServing": "Open Food Facts lists this per 100 g. Change the portion and the numbers follow. The pack’s own serving is {serving}.", + "@logFoodPortionNoteServing": { + "description": "Note under the portion field explaining nutrition numbers are per 100g and rescale with portion, including the pack's own stated serving size", + "placeholders": { + "serving": { + "type": "String" + } + } + }, + "logFoodPillOpenFoodFacts": "Open Food Facts", + "@logFoodPillOpenFoodFacts": { + "description": "Provenance pill on a food row: number came from an Open Food Facts barcode scan" + }, + "logFoodPillYours": "Yours", + "@logFoodPillYours": { + "description": "Provenance pill on a food row: numbers were typed in by the user" + }, + "logFoodBareOccasion": "LOGGED · ENERGY NOT RECORDED", + "@logFoodBareOccasion": { + "description": "Detail line on a food row that was logged with no energy/macro numbers, all caps" + }, + "logFoodOpensInBrowser": "{label}, opens in your browser", + "@logFoodOpensInBrowser": { + "description": "Accessibility label appended to an external link, e.g. an Open Food Facts credit link", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "dayTimelineChargerOn": "On the charger", + "@dayTimelineChargerOn": { + "description": "Timeline event: the band was placed on the charger." + }, + "dayTimelineChargerOff": "Off the charger", + "@dayTimelineChargerOff": { + "description": "Timeline event: the band was taken off the charger." + }, + "dayTimelineDoubleTap": "You double-tapped the band", + "@dayTimelineDoubleTap": { + "description": "Timeline event: user double-tapped the band." + }, + "dayTimelineRestarted": "The band restarted", + "@dayTimelineRestarted": { + "description": "Timeline event: the band restarted itself." + }, + "dayTimelineBatteryPackAttached": "Battery pack attached", + "@dayTimelineBatteryPackAttached": { + "description": "Timeline event: an external battery pack was attached to the band." + }, + "dayTimelineBatteryPackRemoved": "Battery pack removed", + "@dayTimelineBatteryPackRemoved": { + "description": "Timeline event: an external battery pack was removed from the band." + }, + "dayTimelineAlarmWentOff": "Alarm went off", + "@dayTimelineAlarmWentOff": { + "description": "Timeline event: the band's alarm fired." + }, + "dayTimelineAsleep": "Asleep", + "@dayTimelineAsleep": { + "description": "Timeline/legend title for a night's sleep span." + }, + "dayTimelineNap": "Nap", + "@dayTimelineNap": { + "description": "Timeline title for a nap span." + }, + "dayTimelineWorkout": "Workout", + "@dayTimelineWorkout": { + "description": "Fallback timeline/legend title for a workout session with no known type." + }, + "dayTimelineBandOffWrist": "Band off your wrist", + "@dayTimelineBandOffWrist": { + "description": "Timeline title for a stretch the band was off the wrist." + }, + "dayTimelineHighestHr": "Highest heart rate", + "@dayTimelineHighestHr": { + "description": "Timeline title for the day's highest heart-rate reading." + }, + "dayTimelineLowestHr": "Lowest heart rate", + "@dayTimelineLowestHr": { + "description": "Timeline title for the day's lowest heart-rate reading." + }, + "dayTimelineBpmAt": "{bpm} bpm at {time}", + "@dayTimelineBpmAt": { + "description": "Timeline detail line for a heart-rate extreme, e.g. '142 bpm at 3:14 PM'.", + "placeholders": { + "bpm": { + "type": "int" + }, + "time": { + "type": "String" + } + } + }, + "dayTimelineTakenAt": "Taken at {time}", + "@dayTimelineTakenAt": { + "description": "Timeline detail line for a medication dose taken at a given time.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "dayTimelineLastAt": "last at {time}", + "@dayTimelineLastAt": { + "description": "Timeline detail suffix for the last occurrence of a timed journal field, e.g. 'last at 9:30 PM'.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "dayTimelineTaggedTitle": "Tagged", + "@dayTimelineTaggedTitle": { + "description": "Fallback day-note title when a journal entry has tags but no written note." + }, + "dayTimelineTitle": "Breakdown of your day", + "@dayTimelineTitle": { + "description": "Day timeline screen title." + }, + "dayTimelineSub": "MIDNIGHT TO MIDNIGHT", + "@dayTimelineSub": { + "description": "Day timeline screen subtitle, all-caps." + }, + "dayTimelineHeartRateTitle": "Heart rate", + "@dayTimelineHeartRateTitle": { + "description": "Title of the day's heart-rate chart." + }, + "dayTimelineMidnight": "Midnight", + "@dayTimelineMidnight": { + "description": "X-axis label for midnight on the day chart." + }, + "dayTimelineNoon": "Noon", + "@dayTimelineNoon": { + "description": "X-axis label for noon on the day chart." + }, + "dayTimelineMoving": "Moving", + "@dayTimelineMoving": { + "description": "Legend label for the movement lane on the day chart." + }, + "dayTimelineNotRecorded": "Not recorded", + "@dayTimelineNotRecorded": { + "description": "Legend label for unmeasured stretches on the day chart." + }, + "dayTimelineNothingRecordedTitle": "Nothing was recorded on this day", + "@dayTimelineNothingRecordedTitle": { + "description": "Empty-state title when a day has no chart and no timeline entries at all." + }, + "dayTimelineNothingRecordedBody": "No sleep, no session, no log and no band event carrying a time. A day with nothing on it is usually a day the band was off.", + "@dayTimelineNothingRecordedBody": { + "description": "Empty-state body explaining a day with nothing recorded." + }, + "dayTimelineNoTimeTitle": "Nothing on this day carries a time", + "@dayTimelineNoTimeTitle": { + "description": "Empty-state title when a day has notes but nothing with a time attached." + }, + "dayTimelineNoTimeBody": "What was logged for it is below.", + "@dayTimelineNoTimeBody": { + "description": "Empty-state body pointing to the notes section below." + }, + "dayTimelineWhatHappenedSection": "What happened", + "@dayTimelineWhatHappenedSection": { + "description": "Section heading over the day's timed events list; also reused as a section heading on the journal compose screen for the day's tags." + }, + "dayTimelineAlsoLoggedSection": "Also logged on this day", + "@dayTimelineAlsoLoggedSection": { + "description": "Section heading over the day's untimed notes." + }, + "dayTimelineNoTimeNote": "These were recorded against the day and carry no time of day, so they are not placed on it.", + "@dayTimelineNoTimeNote": { + "description": "Footnote explaining why untimed notes are not placed on the timeline." + }, + "dayTimelinePatternsNote": "Patterns in your own logs, not causes. Two things next to each other here happened near each other, which is all this page claims.", + "@dayTimelinePatternsNote": { + "description": "Footnote reminding the user the timeline shows adjacency, not causation." + }, + "journalComposeNotReady": "Not ready yet — open the app first.", + "@journalComposeNotReady": { + "description": "Snackbar shown when trying to add a custom journal field before the app has a repository ready." + }, + "journalComposeSaveFailed": "Could not save it — check storage and retry.", + "@journalComposeSaveFailed": { + "description": "Snackbar shown when saving a new custom journal field fails." + }, + "journalComposeWhenWasLastOne": "When was the last one?", + "@journalComposeWhenWasLastOne": { + "description": "Time picker help text when logging the time of the last dose of a journal field." + }, + "journalComposeTitle": "Journal", + "@journalComposeTitle": { + "description": "Journal compose screen title." + }, + "journalComposeTodaySection": "Today", + "@journalComposeTodaySection": { + "description": "Section heading over today's journal field steppers." + }, + "journalComposeTrackSomethingElse": "Track something else", + "@journalComposeTrackSomethingElse": { + "description": "Button to define a new custom journal field." + }, + "journalComposeAnythingElseLabel": "Anything else", + "@journalComposeAnythingElseLabel": { + "description": "Label for the free-text journal note field." + }, + "journalComposeAnythingElseHint": "A line about the day.", + "@journalComposeAnythingElseHint": { + "description": "Placeholder hint for the free-text journal note field." + }, + "journalComposeSavingLabel": "Saving", + "@journalComposeSavingLabel": { + "description": "Save button label while the journal entry is being saved." + }, + "journalComposeHowAreYouFeeling": "How are you feeling?", + "@journalComposeHowAreYouFeeling": { + "description": "Mood picker heading." + }, + "journalComposeNotAnsweredYet": "Not answered yet", + "@journalComposeNotAnsweredYet": { + "description": "Mood picker subtitle when no mood has been picked yet." + }, + "journalComposeMoodOfFive": "Mood {value} of 5 · tap it again to clear", + "@journalComposeMoodOfFive": { + "description": "Mood picker subtitle once a mood is picked, e.g. 'Mood 3 of 5 · tap it again to clear'.", + "placeholders": { + "value": { + "type": "int" + } + } + }, + "journalComposeMoodOfFiveSelected": "Mood {n} of 5, selected. Activate to clear.", + "@journalComposeMoodOfFiveSelected": { + "description": "Accessibility label for the currently selected mood face, which can be tapped again to clear it.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "journalComposeMoodOfFiveLabel": "Mood {n} of 5", + "@journalComposeMoodOfFiveLabel": { + "description": "Accessibility label for an unselected mood face.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "journalComposeNotLogged": "Not logged", + "@journalComposeNotLogged": { + "description": "Field stepper value shown when a journal field has no value logged." + }, + "journalComposeWhenWasLastField": "When was the last {field}", + "@journalComposeWhenWasLastField": { + "description": "Accessibility label for the button that opens a time picker for a journal field's last dose, e.g. 'When was the last Caffeine'.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "journalComposeAddTimeOfLastOne": "Add the time of the last one", + "@journalComposeAddTimeOfLastOne": { + "description": "Button prompting the user to add the time of the last dose of a field." + }, + "journalComposeLastAt": "Last at {time}", + "@journalComposeLastAt": { + "description": "Button label showing the recorded time of the last dose, e.g. 'Last at 9:30 PM'.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "journalComposeIncrease": "Increase", + "@journalComposeIncrease": { + "description": "Accessibility label for the stepper's increase button." + }, + "journalComposeDecrease": "Decrease", + "@journalComposeDecrease": { + "description": "Accessibility label for the stepper's decrease button." + }, + "journalComposeWeightLabel": "Weight", + "@journalComposeWeightLabel": { + "description": "Label for the weight row in the journal, and fallback name for the weight field used in error messages." + }, + "journalComposeNotEntered": "Not entered", + "@journalComposeNotEntered": { + "description": "Weight row subtitle when no weight has been entered for the day." + }, + "journalComposeEnteredNotMeasured": "{value} · entered, not measured", + "@journalComposeEnteredNotMeasured": { + "description": "Weight row subtitle showing the entered value with a note that it is self-reported, e.g. '70.0 kg · entered, not measured'.", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "journalComposeEnterWeight": "Enter weight", + "@journalComposeEnterWeight": { + "description": "Accessibility label for the button opening the weight-entry dialog." + }, + "journalComposeEnter": "Enter", + "@journalComposeEnter": { + "description": "Weight row button label when no weight is entered yet." + }, + "journalComposeChange": "Change", + "@journalComposeChange": { + "description": "Weight row button label when a weight is already entered." + }, + "journalComposeSeeWeightTrend": "See the weight trend", + "@journalComposeSeeWeightTrend": { + "description": "Accessibility label for the link to the weight trend screen." + }, + "journalComposeSeeTheTrend": "See the trend", + "@journalComposeSeeTheTrend": { + "description": "Link text to the weight trend screen." + }, + "journalComposeWeightToday": "Weight today", + "@journalComposeWeightToday": { + "description": "Title of the dialog for entering today's weight." + }, + "journalComposeWeightKgLabel": "Weight (kg)", + "@journalComposeWeightKgLabel": { + "description": "Fallback text-field label for weight in kilograms when no units controller is available." + }, + "journalComposeWeightScaleNote": "What you or your scale read. The band does not measure this.", + "@journalComposeWeightScaleNote": { + "description": "Note in the weight-entry dialog clarifying the band does not measure weight." + }, + "journalComposeClear": "Clear", + "@journalComposeClear": { + "description": "Button to clear today's weight entry." + }, + "journalComposeNotEnoughEntriesTitle": "Not enough entries for a trend", + "@journalComposeNotEnoughEntriesTitle": { + "description": "Empty-state title on the weight trend screen when fewer than two entries exist." + }, + "journalComposeNotEnoughEntriesBody": "The line is a seven-day average through what you entered, so it needs at least two days. Nothing is filled in between them.", + "@journalComposeNotEnoughEntriesBody": { + "description": "Empty-state body explaining the weight trend needs at least two entries." + }, + "journalComposeSevenDayTrend": "Seven-day trend", + "@journalComposeSevenDayTrend": { + "description": "Title of the weight trend chart." + }, + "journalComposeTrendFootnote": "Entered by you. Days with no entry are left empty.", + "@journalComposeTrendFootnote": { + "description": "Footnote under the weight trend chart." + }, + "journalComposeWeightTrendExplainer": "Entered by you or your scale — the band does not measure weight. What is drawn is a seven-day average, because a scale moves one to two kilos on water and food alone and the raw readings would show that as something happening to your body. {count, plural, one{{count} day entered.} other{{count} days entered.}}", + "@journalComposeWeightTrendExplainer": { + "description": "Closing explainer under the weight trend chart, ending in a count of days entered.", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "nutritionTabToday": "Today", + "@nutritionTabToday": { + "description": "Nutrition screen: sub-tab label for today's log" + }, + "nutritionTabWeek": "Week", + "@nutritionTabWeek": { + "description": "Nutrition screen: sub-tab label for the weekly view" + }, + "nutritionTabGoals": "Goals", + "@nutritionTabGoals": { + "description": "Nutrition screen: sub-tab label for nutrition goals" + }, + "nutritionLogFood": "Log food", + "@nutritionLogFood": { + "description": "Nutrition screen: semantic label on the add-food button in the title row" + }, + "nutritionTitle": "Nutrition", + "@nutritionTitle": { + "description": "Nutrition screen title" + }, + "nutritionEmptyTodayTitle": "Nothing logged today", + "@nutritionEmptyTodayTitle": { + "description": "Nutrition Today tab: title when nothing has been logged today" + }, + "nutritionEmptyTodayBody": "One tap is a complete log.", + "@nutritionEmptyTodayBody": { + "description": "Nutrition Today tab: body text under the empty-today title" + }, + "nutritionLogOccasionFix": "Log an eating occasion", + "@nutritionLogOccasionFix": { + "description": "Nutrition screen: call-to-action button text to log an eating occasion (used in two empty/low-data states)" + }, + "nutritionOccasionsSection": "Occasions", + "@nutritionOccasionsSection": { + "description": "Nutrition Today tab: section header above the meal rows" + }, + "nutritionAddAction": "Add", + "@nutritionAddAction": { + "description": "Nutrition Today tab: 'Add' action link on the Occasions section header" + }, + "nutritionFloorTitle": "Today's energy is a floor, not a total", + "@nutritionFloorTitle": { + "description": "Nutrition Today tab: title warning that today's energy total is a lower bound" + }, + "nutritionFloorBody": "{unknown} of {total} occasions were logged without an energy figure, so the number above is the least you ate rather than what you ate.", + "@nutritionFloorBody": { + "description": "Nutrition Today tab: body explaining how many occasions lack an energy figure", + "placeholders": { + "unknown": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "nutritionAddNumbersFix": "Add the numbers to an occasion", + "@nutritionAddNumbersFix": { + "description": "Nutrition screen: call-to-action to add missing energy numbers to an occasion (used in two places)" + }, + "nutritionDaysNotCounted": "{excluded} of the last {span} days could not be counted", + "@nutritionDaysNotCounted": { + "description": "Nutrition Today tab: observation noting how many of the last N days could not be counted in averages", + "placeholders": { + "excluded": { + "type": "int" + }, + "span": { + "type": "int" + } + } + }, + "nutritionDayCountsRule": "A day counts once every occasion carries an energy figure.", + "@nutritionDayCountsRule": { + "description": "Nutrition Today tab: short rule explaining what makes a day count for averages" + }, + "nutritionDaysLoggedLabel": "Days with something logged", + "@nutritionDaysLoggedLabel": { + "description": "Nutrition Week tab: consistency-card label when there are no partial days" + }, + "nutritionPartialExcluded": "{partial} logged but partial, so excluded from every average below", + "@nutritionPartialExcluded": { + "description": "Nutrition Week tab: consistency-card label naming how many days were partial and excluded from averages", + "placeholders": { + "partial": { + "type": "int" + } + } + }, + "nutritionEnergyByDay": "Energy, day by day", + "@nutritionEnergyByDay": { + "description": "Nutrition Week tab: section header above the daily energy bar chart" + }, + "nutritionSevenDayAvg": "Seven-day average", + "@nutritionSevenDayAvg": { + "description": "Nutrition Week tab: section header above the seven-day nutrient means" + }, + "nutritionNoCompleteDayTitle": "No complete day to average yet", + "@nutritionNoCompleteDayTitle": { + "description": "Nutrition Week tab: title shown when there is no complete day yet to average" + }, + "nutritionNoCompleteDayBody": "You have none.", + "@nutritionNoCompleteDayBody": { + "description": "Nutrition Week tab: body text under the no-complete-day title" + }, + "nutritionLabelEnergy": "Energy", + "@nutritionLabelEnergy": { + "description": "Nutrition Week tab: row label for the energy mean" + }, + "nutritionLabelProtein": "Protein", + "@nutritionLabelProtein": { + "description": "Nutrition Week tab: row label for the protein mean" + }, + "nutritionLabelCarbs": "Carbs", + "@nutritionLabelCarbs": { + "description": "Nutrition Week tab: row label for the carbs mean" + }, + "nutritionLabelFat": "Fat", + "@nutritionLabelFat": { + "description": "Nutrition Week tab: row label for the fat mean" + }, + "nutritionLabelFibre": "Fibre", + "@nutritionLabelFibre": { + "description": "Nutrition Week tab: row label for the fibre mean" + }, + "nutritionEnergyBalance": "Energy balance", + "@nutritionEnergyBalance": { + "description": "Nutrition Week tab: section header above the eaten/burned/balance metrics" + }, + "nutritionLabelEaten": "EATEN", + "@nutritionLabelEaten": { + "description": "Nutrition Week tab: all-caps inline metric label for energy eaten" + }, + "nutritionLabelBurned": "BURNED", + "@nutritionLabelBurned": { + "description": "Nutrition screen: all-caps inline metric label for energy burned (used in Week tab and Today card)" + }, + "nutritionLabelBalance": "BALANCE", + "@nutritionLabelBalance": { + "description": "Nutrition Week tab: all-caps inline metric label for the eaten-minus-burned balance" + }, + "nutritionEatenMeanNote": "Eaten is the mean of {days} complete days. Burned is today only.", + "@nutritionEatenMeanNote": { + "description": "Nutrition Week tab: footnote explaining the eaten mean is over N complete days while burned is today only", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "nutritionEnergyLoggedTitle": "Energy logged", + "@nutritionEnergyLoggedTitle": { + "description": "Nutrition Week tab: chart title above the daily energy bars" + }, + "nutritionPartialFootnote": "{n} partial, left out of the averages below.", + "@nutritionPartialFootnote": { + "description": "Nutrition Week tab: chart footnote naming how many partial days were left out of averages", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionNothingLoggedYet": "Nothing logged yet", + "@nutritionNothingLoggedYet": { + "description": "Nutrition Week tab: chart empty-state message when no days have been logged at all" + }, + "nutritionNoEnergyFiguresYet": "No energy figures yet", + "@nutritionNoEnergyFiguresYet": { + "description": "Nutrition Week tab: chart empty-state message when days are logged but none carry energy figures" + }, + "nutritionDailyEnergy": "Daily energy", + "@nutritionDailyEnergy": { + "description": "Nutrition Goals tab: name of the daily energy target" + }, + "nutritionDailyProtein": "Daily protein", + "@nutritionDailyProtein": { + "description": "Nutrition Goals tab: name of the daily protein target" + }, + "nutritionEnergyWord": "energy", + "@nutritionEnergyWord": { + "description": "Nutrition Goals tab: bare noun for energy, used inside sentences (distinct from the 'Daily energy' label so word order/gender in other languages doesn't leak the label's adjective into the noun slot)" + }, + "nutritionProteinWord": "protein", + "@nutritionProteinWord": { + "description": "Nutrition Goals tab: bare noun for protein, used inside sentences (see nutritionEnergyWord)" + }, + "nutritionYourTargetsSection": "Your targets", + "@nutritionYourTargetsSection": { + "description": "Nutrition Goals tab: section header for the user's set targets, also the edit-targets sheet title" + }, + "nutritionHintNone": "none", + "@nutritionHintNone": { + "description": "Nutrition Goals tab: placeholder hint text in the empty target number field" + }, + "nutritionNoTargetsTitle": "No targets set", + "@nutritionNoTargetsTitle": { + "description": "Nutrition Goals tab: title shown when the user has set no targets" + }, + "nutritionNoTargetsBody": "A target here is one you type.", + "@nutritionNoTargetsBody": { + "description": "Nutrition Goals tab: body text under the no-targets title" + }, + "nutritionSetTargetFix": "Set a target", + "@nutritionSetTargetFix": { + "description": "Nutrition Goals tab: call-to-action button to set a target" + }, + "nutritionEditAction": "Edit", + "@nutritionEditAction": { + "description": "Nutrition Goals tab: 'Edit' action link on the Your Targets section header" + }, + "nutritionBodySpentToday": "What your body spent today", + "@nutritionBodySpentToday": { + "description": "Nutrition Goals tab: heading over the estimated-expenditure card" + }, + "nutritionEstimatedExpenditure": "Estimated expenditure", + "@nutritionEstimatedExpenditure": { + "description": "Nutrition Goals tab: metric row label for estimated calories burned" + }, + "nutritionNotMeasured": "Not measured", + "@nutritionNotMeasured": { + "description": "Nutrition Goals tab: value shown when estimated expenditure has no data" + }, + "nutritionExpenditureSub": "TODAY, FROM HEART RATE AND YOUR PROFILE", + "@nutritionExpenditureSub": { + "description": "Nutrition Goals tab: all-caps subtext explaining the expenditure figure's source" + }, + "nutritionRemoveTitle": "Remove {label}?", + "@nutritionRemoveTitle": { + "description": "Nutrition screen: confirmation sheet title when removing a logged food entry", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "nutritionRemoveBody": "It leaves the day and every average that counted it. There is no undo.", + "@nutritionRemoveBody": { + "description": "Nutrition screen: confirmation sheet body warning the removal cannot be undone" + }, + "nutritionNothingToMeasure": "Nothing to measure {label} against yet", + "@nutritionNothingToMeasure": { + "description": "Nutrition Goals tab: title shown when a target has no mean to compare against yet", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "nutritionFloorAverageBody": "Every complete day had an occasion logged without a {nutrient} figure, so the average would only be a lower bound.", + "@nutritionFloorAverageBody": { + "description": "Nutrition Goals tab: explanation that every complete day lacked this nutrient's figure", + "placeholders": { + "nutrient": { + "type": "String" + } + } + }, + "nutritionCountedNoFigureBody": "{count} of the last {span} days counted, but none of them carried a {nutrient} figure.", + "@nutritionCountedNoFigureBody": { + "description": "Nutrition Goals tab: explanation that counted days carried no figure for this nutrient", + "placeholders": { + "count": { + "type": "int" + }, + "span": { + "type": "int" + }, + "nutrient": { + "type": "String" + } + } + }, + "nutritionDayCountsRuleFull": "A day counts once every occasion carries a figure and the log reaches the evening. None of the last {span} days has.", + "@nutritionDayCountsRuleFull": { + "description": "Nutrition Goals tab: full rule explaining what makes a day count, when none of the recent days qualify", + "placeholders": { + "span": { + "type": "int" + } + } + }, + "nutritionOnTarget": "On target", + "@nutritionOnTarget": { + "description": "Nutrition Goals tab: goal-trajectory status text when the mean is within 1 unit of target" + }, + "nutritionRateAbove": "{amount} {unit}/day above", + "@nutritionRateAbove": { + "description": "Nutrition Goals tab: goal-trajectory rate text when the mean is above target", + "placeholders": { + "amount": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "nutritionRateBelow": "{amount} {unit}/day below", + "@nutritionRateBelow": { + "description": "Nutrition Goals tab: goal-trajectory rate text when the mean is below target", + "placeholders": { + "amount": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "nutritionMeanOfDays": "{n, plural, one{mean of {n} complete day} other{mean of {n} complete days}}", + "@nutritionMeanOfDays": { + "description": "Nutrition Goals tab: goal-trajectory footnote naming how many complete days the mean covers", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionLoggedToday": "LOGGED TODAY", + "@nutritionLoggedToday": { + "description": "Nutrition Today card: all-caps header when no energy figure exists, only an occasion count" + }, + "nutritionEatenToday": "EATEN TODAY", + "@nutritionEatenToday": { + "description": "Nutrition Today card: all-caps header when an energy figure exists" + }, + "nutritionAtLeast": "At least", + "@nutritionAtLeast": { + "description": "Nutrition Today card: pill label shown when the energy total is a floor, not an exact total" + }, + "nutritionOccasionsUnit": "{n, plural, one{occasion} other{occasions}}", + "@nutritionOccasionsUnit": { + "description": "Nutrition Today card: unit word next to the raw occasion count when no energy figure exists", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionOccasionsCount": "{n, plural, one{{n} occasion} other{{n} occasions}}", + "@nutritionOccasionsCount": { + "description": "Nutrition Today card: '· N occasions' note shown alongside the energy total", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionLabelBalanceAtLeast": "BALANCE AT LEAST", + "@nutritionLabelBalanceAtLeast": { + "description": "Nutrition Today card: all-caps inline metric label for the balance when eaten is only a floor" + }, + "nutritionNotLogged": "Not logged", + "@nutritionNotLogged": { + "description": "Nutrition screen: 'Not logged' status text (used for a meal row and for the water row)" + }, + "nutritionLoggedNoEnergy": "{n} logged · energy not recorded", + "@nutritionLoggedNoEnergy": { + "description": "Nutrition meal row: status text when entries exist but none carry an energy figure", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionAtLeastPrefix": "at least ", + "@nutritionAtLeastPrefix": { + "description": "Nutrition meal row: lowercase prefix shown before a kcal total that is a floor, not exact (includes trailing space)" + }, + "nutritionMealBreakfast": "Breakfast", + "@nutritionMealBreakfast": { + "description": "Nutrition screen: meal occasion name, breakfast" + }, + "nutritionMealLunch": "Lunch", + "@nutritionMealLunch": { + "description": "Nutrition screen: meal occasion name, lunch" + }, + "nutritionMealDinner": "Dinner", + "@nutritionMealDinner": { + "description": "Nutrition screen: meal occasion name, dinner" + }, + "nutritionMealSnacks": "Snacks", + "@nutritionMealSnacks": { + "description": "Nutrition screen: meal occasion name, snacks (catch-all bucket)" + }, + "nutritionNotCounted": "Not counted", + "@nutritionNotCounted": { + "description": "Nutrition Week tab: nutrient mean value when the mean could not be counted at all" + }, + "nutritionNotRecorded": "Not recorded", + "@nutritionNotRecorded": { + "description": "Nutrition Week tab: nutrient mean value when no complete day recorded this nutrient" + }, + "nutritionEveryDayNoFigure": "EVERY COMPLETE DAY HAD AN OCCASION WITH NO {label} FIGURE", + "@nutritionEveryDayNoFigure": { + "description": "Nutrition Week tab: all-caps subtext when every complete day lacked this nutrient's figure", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "nutritionNoDayRecorded": "NO COMPLETE DAY RECORDED {label}", + "@nutritionNoDayRecorded": { + "description": "Nutrition Week tab: all-caps subtext when no complete day recorded this nutrient", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "nutritionMeanOfCompleteDaysCaps": "{n, plural, one{MEAN OF {n} COMPLETE DAY} other{MEAN OF {n} COMPLETE DAYS}}", + "@nutritionMeanOfCompleteDaysCaps": { + "description": "Nutrition Week tab: all-caps subtext naming how many complete days the nutrient mean covers", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionLeftOutAsFloor": " · {n} LEFT OUT AS A FLOOR", + "@nutritionLeftOutAsFloor": { + "description": "Nutrition Week tab: all-caps suffix appended to the mean subtext naming how many days were left out as floors (includes leading ' · ')", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "nutritionWaterLabel": "Water", + "@nutritionWaterLabel": { + "description": "Nutrition Today tab: label on the water tracking row" + }, + "nutritionTapToChange": "Tap − or + to change", + "@nutritionTapToChange": { + "description": "Nutrition Today tab: hint under the water label once a value is logged" + }, + "nutritionNoneYet": "None yet", + "@nutritionNoneYet": { + "description": "Nutrition Today tab: water amount text when nothing is logged yet" + }, + "nutritionAddWater": "Add water", + "@nutritionAddWater": { + "description": "Nutrition Today tab: semantic label on the water plus-step button" + }, + "nutritionRemoveWater": "Remove water", + "@nutritionRemoveWater": { + "description": "Nutrition Today tab: semantic label on the water minus-step button" + }, + "coachApiKeyLabel": "API key", + "@coachApiKeyLabel": { + "description": "Coach AI settings: text field label for a cloud provider's API key" + }, + "coachApiKeyLocalLabel": "API key (not needed locally)", + "@coachApiKeyLocalLabel": { + "description": "Coach AI settings: text field label for the API key field when a local provider is selected" + }, + "coachAsking": "Asking…", + "@coachAsking": { + "description": "Coach AI settings: button label while models are being fetched" + }, + "coachAskLabel": "Ask the coach", + "@coachAskLabel": { + "description": "Coach screen: semantic label on the chat input text field" + }, + "coachBaseUrlLabel": "Base URL", + "@coachBaseUrlLabel": { + "description": "Coach AI settings: text field label for the provider base URL" + }, + "coachBriefingMenuSub": "The exact snapshot that left this device", + "@coachBriefingMenuSub": { + "description": "Coach screen: subtitle on the menu row that opens the AI briefing detail" + }, + "coachBriefingMenuTitle": "Briefing, and what was sent", + "@coachBriefingMenuTitle": { + "description": "Coach screen: menu row title that opens the briefing and what-was-sent detail" + }, + "coachChooseModelFix": "Choose a model", + "@coachChooseModelFix": { + "description": "Coach screen: call-to-action button to open AI setup when no model is configured" + }, + "coachCloudDataNote": "Your questions and the rows the coach reads are sent to this endpoint. See exactly what that is on \"What was sent\".", + "@coachCloudDataNote": { + "description": "Coach AI settings: privacy note shown when a cloud provider is selected" + }, + "coachDeleteChat": "Delete {title}", + "@coachDeleteChat": { + "description": "Coach screen: semantic label on the delete-chat icon in the past-chats menu list", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "coachDeleteIt": "Delete it", + "@coachDeleteIt": { + "description": "Coach screen: confirm-dialog button text for a destructive write action" + }, + "coachDestructiveWarning": "This removes data from this device and cannot be undone.", + "@coachDestructiveWarning": { + "description": "Coach screen: confirm-dialog warning text for a destructive write action" + }, + "coachEndpointUnreachable": "Could not reach that endpoint: {error}", + "@coachEndpointUnreachable": { + "description": "Coach AI settings: error message when fetching models from the endpoint fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "coachErrorTitle": "That did not go through", + "@coachErrorTitle": { + "description": "Coach screen: title on a chat bubble reporting that a tool call failed" + }, + "coachInputHint": "Ask about your health…", + "@coachInputHint": { + "description": "Coach screen: placeholder text in the empty chat input field" + }, + "coachIntroBody": "Ask about anything the app measures, and it can log food, water, workouts, doses and how you felt — always asking first.", + "@coachIntroBody": { + "description": "Coach screen: intro paragraph shown in the empty-chat welcome card" + }, + "coachKeychainRefused": "The keychain refused the key: {error}", + "@coachKeychainRefused": { + "description": "Coach AI settings: error message when the keychain refuses to save the API key", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "coachKeyStillSavedBody": "It could not be read from the keychain this time, which happens when the app is woken while the phone is locked.", + "@coachKeyStillSavedBody": { + "description": "Coach screen: body text explaining why the saved API key could not be read this time" + }, + "coachKeyStillSavedTitle": "Your key is still saved", + "@coachKeyStillSavedTitle": { + "description": "Coach screen: title shown when the API key exists but could not be read from the keychain" + }, + "coachListModels": "List models", + "@coachListModels": { + "description": "Coach AI settings: button label to fetch the list of available models" + }, + "coachLocalDataNote": "Your questions and the rows the coach reads stay on your own machine.", + "@coachLocalDataNote": { + "description": "Coach AI settings: privacy note shown when a local provider is selected" + }, + "coachLocalSub": "On this network. Nothing leaves your machine.", + "@coachLocalSub": { + "description": "Coach AI settings: subtitle shown under local provider presets (Ollama, LM Studio)" + }, + "coachMenuSemantic": "Chats and AI settings", + "@coachMenuSemantic": { + "description": "Coach screen: semantic label on the top-right menu button (chats and AI settings)" + }, + "coachModelHint": "search, or type an id", + "@coachModelHint": { + "description": "Coach AI settings: placeholder hint in the model search field" + }, + "coachModelLabel": "Model", + "@coachModelLabel": { + "description": "Coach AI settings: text field label for searching or typing a model id" + }, + "coachModelsFound": "{n, plural, one{{n} model. Tap one.} other{{n} models. Tap one.}}", + "@coachModelsFound": { + "description": "Coach AI settings: status message after fetching the model list, showing how many were found", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "coachNavTitle": "Coach", + "@coachNavTitle": { + "description": "Coach screen: nav bar title" + }, + "coachNewChat": "New chat", + "@coachNewChat": { + "description": "Coach screen: menu row to start a new chat" + }, + "coachNoChatsYet": "Nothing yet — this is your first conversation.", + "@coachNoChatsYet": { + "description": "Coach screen: message in the past-chats menu list when there is no chat history yet" + }, + "coachNoDataBody": "The coach answers from your own derived days, and there are none on this device yet.", + "@coachNoDataBody": { + "description": "Coach screen: body text explaining there is no derived data on this device yet" + }, + "coachNoDataTitle": "No data to read yet", + "@coachNoDataTitle": { + "description": "Coach screen: title shown when the coach is configured but there is no data to read yet" + }, + "coachNoModelsListed": "That endpoint listed no models. Type one below instead.", + "@coachNoModelsListed": { + "description": "Coach AI settings: status message when the endpoint returned zero models" + }, + "coachNotSetUp": "Not set up", + "@coachNotSetUp": { + "description": "Coach screen: subtitle shown wherever the coach has no model configured yet" + }, + "coachNotSetUpBody": "It runs on a model you choose — one on your own machine, or any OpenAI-compatible provider with your own key. Nothing goes through OpenStrap either way.", + "@coachNotSetUpBody": { + "description": "Coach screen: body text explaining how to set up the coach and its privacy stance" + }, + "coachNotSetUpTitle": "The coach is not set up", + "@coachNotSetUpTitle": { + "description": "Coach screen: title shown when the coach has not been set up" + }, + "coachPastChats": "PAST CHATS", + "@coachPastChats": { + "description": "Coach screen: all-caps section header above the list of past chat sessions" + }, + "coachPickModelFirst": "Pick or type a model first.", + "@coachPickModelFirst": { + "description": "Coach AI settings: validation message when saving without a chosen model" + }, + "coachSafeWarning": "Nothing is written until you tap below.", + "@coachSafeWarning": { + "description": "Coach screen: confirm-dialog reassurance text for a non-destructive write action" + }, + "coachSaveIt": "Save it", + "@coachSaveIt": { + "description": "Coach screen: confirm-dialog button text for a non-destructive write action" + }, + "coachSendLabel": "Send", + "@coachSendLabel": { + "description": "Coach screen: semantic label on the send-message button" + }, + "coachSetupNavSub": "Bring your own model", + "@coachSetupNavSub": { + "description": "Coach AI settings: nav bar subtitle" + }, + "coachSetupNavTitle": "AI settings", + "@coachSetupNavTitle": { + "description": "Coach AI settings: nav bar title" + }, + "coachSomethingWrong": "Something went wrong: {error}", + "@coachSomethingWrong": { + "description": "Coach screen: generic error message shown in a chat bubble for an unexpected exception", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "coachStarterAteYesterday": "What did I eat yesterday?", + "@coachStarterAteYesterday": { + "description": "Coach screen: example starter prompt about what was eaten yesterday" + }, + "coachStarterHrvChart": "Chart my HRV over the last month", + "@coachStarterHrvChart": { + "description": "Coach screen: example starter prompt to chart HRV over the last month" + }, + "coachStarterLogRun": "I ran for 40 minutes this morning — log it", + "@coachStarterLogRun": { + "description": "Coach screen: example starter prompt to log a run" + }, + "coachStarterLogWater": "Log 500 ml of water for today", + "@coachStarterLogWater": { + "description": "Coach screen: example starter prompt to log water intake" + }, + "coachStarterRecovery": "How recovered am I today, and why?", + "@coachStarterRecovery": { + "description": "Coach screen: example starter prompt about today's recovery" + }, + "coachStarterSleep": "How has my sleep been this week?", + "@coachStarterSleep": { + "description": "Coach screen: example starter prompt about this week's sleep" + }, + "coachTryAgainFix": "Try again", + "@coachTryAgainFix": { + "description": "Coach screen: retry button when the saved API key could not be read" + }, + "coachTryAsking": "Try asking", + "@coachTryAsking": { + "description": "Coach screen: section header above the list of example starter prompts" + }, + "coachUntitledChat": "Untitled chat", + "@coachUntitledChat": { + "description": "Coach screen: fallback title for a past chat session with no generated title" + }, + "coachWhereModelRuns": "Where the model runs", + "@coachWhereModelRuns": { + "description": "Coach AI settings: section header above the provider preset list" + }, + "coachYourDataYourModel": "YOUR DATA, YOUR MODEL", + "@coachYourDataYourModel": { + "description": "Coach screen: all-caps headline in the empty-chat welcome card" + }, + "investigateNerdStatsLabel": "NERD STATS", + "@investigateNerdStatsLabel": { + "description": "Subtitle on the Investigate (nerd stats) screen header" + }, + "investigateProvenanceLabel": "Provenance", + "@investigateProvenanceLabel": { + "description": "Table title: where this day's data came from" + }, + "investigateDayLabel": "Day", + "@investigateDayLabel": { + "description": "Provenance table row label: the derived day" + }, + "investigateCoverageLabel": "Coverage", + "@investigateCoverageLabel": { + "description": "Provenance table row label: wear coverage percent" + }, + "investigateSleepWindowLabel": "Sleep window", + "@investigateSleepWindowLabel": { + "description": "Provenance table row label: sleep window times" + }, + "investigateSourceLabel": "Source", + "@investigateSourceLabel": { + "description": "Provenance table row label: data source" + }, + "investigateSourceOnDevice": "Band records · derived on this phone", + "@investigateSourceOnDevice": { + "description": "Provenance value: data derived on-device from band records" + }, + "investigateSourceImported": "Imported · {source}", + "@investigateSourceImported": { + "description": "Provenance value: data imported from another source", + "placeholders": { + "source": { + "type": "String" + } + } + }, + "investigateAlgoVersionLabel": "Algorithm version", + "@investigateAlgoVersionLabel": { + "description": "Provenance table row label: algorithm version" + }, + "investigateWhatHappenedTitle": "What happened that day", + "@investigateWhatHappenedTitle": { + "description": "Link row title to the day timeline screen" + }, + "investigateWhatHappenedSub": "Sleep, sessions, meals and logs in time order", + "@investigateWhatHappenedSub": { + "description": "Link row subtitle describing the day timeline" + }, + "investigateWhichSensorCounted": "Which sensor counted", + "@investigateWhichSensorCounted": { + "description": "Table title for step-source breakdown" + }, + "investigateStrapPedometer": "strap · 100 Hz pedometer", + "@investigateStrapPedometer": { + "description": "Step source row: strap's 100 Hz pedometer" + }, + "investigateStrapOnChipCounter": "strap · on-chip counter", + "@investigateStrapOnChipCounter": { + "description": "Step source row: strap's on-chip step counter" + }, + "investigatePhonePedometer": "phone · pedometer", + "@investigatePhonePedometer": { + "description": "Step source row: phone pedometer" + }, + "investigateDayTotal": "day total", + "@investigateDayTotal": { + "description": "Step source row: total steps for the day" + }, + "investigateStrapChipReported": "strap chip reported", + "@investigateStrapChipReported": { + "description": "Step source row: what the strap chip itself reported" + }, + "investigateTimeDomain": "Time domain", + "@investigateTimeDomain": { + "description": "HRV table title: time-domain metrics" + }, + "investigateRmssd": "RMSSD", + "@investigateRmssd": { + "description": "HRV metric label: RMSSD (kept as scientific abbreviation)" + }, + "investigateSdnn": "SDNN", + "@investigateSdnn": { + "description": "HRV metric label: SDNN (kept as scientific abbreviation)" + }, + "investigateSdann": "SDANN", + "@investigateSdann": { + "description": "HRV metric label: SDANN (kept as scientific abbreviation)" + }, + "investigateSdnnIndex": "SDNN index", + "@investigateSdnnIndex": { + "description": "HRV metric label: SDNN index" + }, + "investigatePnn50": "pNN50", + "@investigatePnn50": { + "description": "HRV metric label: pNN50 (kept as scientific abbreviation)" + }, + "investigateLnRmssd": "ln RMSSD", + "@investigateLnRmssd": { + "description": "HRV metric label: natural log of RMSSD" + }, + "investigateBaselineRmssd": "Your baseline RMSSD", + "@investigateBaselineRmssd": { + "description": "HRV metric label: the user's own baseline RMSSD" + }, + "investigateStabilityCv": "Stability (CV)", + "@investigateStabilityCv": { + "description": "HRV metric label: coefficient of variation stability" + }, + "investigateFrequencyDomain": "Frequency domain", + "@investigateFrequencyDomain": { + "description": "HRV table title: frequency-domain metrics" + }, + "investigateUlfPower": "ULF power", + "@investigateUlfPower": { + "description": "HRV metric label: ultra-low-frequency power" + }, + "investigateVlfPower": "VLF power", + "@investigateVlfPower": { + "description": "HRV metric label: very-low-frequency power" + }, + "investigateLfPower": "LF power", + "@investigateLfPower": { + "description": "HRV metric label: low-frequency power" + }, + "investigateHfPower": "HF power", + "@investigateHfPower": { + "description": "HRV metric label: high-frequency power" + }, + "investigateTotalPower": "Total power", + "@investigateTotalPower": { + "description": "HRV metric label: total spectral power" + }, + "investigateLfHf": "LF / HF", + "@investigateLfHf": { + "description": "HRV metric label: LF/HF ratio" + }, + "investigateLfNormalised": "LF, normalised", + "@investigateLfNormalised": { + "description": "HRV metric label: normalised LF power" + }, + "investigateHfNormalised": "HF, normalised", + "@investigateHfNormalised": { + "description": "HRV metric label: normalised HF power" + }, + "investigateHfGated": "HF gated", + "@investigateHfGated": { + "description": "HRV metric label: whether HF power passed the artifact gate" + }, + "investigateYes": "yes", + "@investigateYes": { + "description": "Generic yes value for a nerd-stats table row" + }, + "investigateNo": "no", + "@investigateNo": { + "description": "Generic no value for a nerd-stats table row" + }, + "investigateNoFrequencySpectrum": "No frequency-domain spectrum for this night", + "@investigateNoFrequencySpectrum": { + "description": "Status card title: no frequency-domain spectrum available" + }, + "investigateRecordingTooShort": "The recording was too short to resolve the bands.", + "@investigateRecordingTooShort": { + "description": "Status card fallback body: recording too short for spectrum" + }, + "investigateNonLinear": "Non-linear", + "@investigateNonLinear": { + "description": "HRV table title: non-linear metrics" + }, + "investigateSd1Sleep": "SD1, sleep", + "@investigateSd1Sleep": { + "description": "HRV metric label: Poincare SD1 during sleep window" + }, + "investigateSd2Sleep": "SD2, sleep", + "@investigateSd2Sleep": { + "description": "HRV metric label: Poincare SD2 during sleep window" + }, + "investigateSd124h": "SD1, 24 h", + "@investigateSd124h": { + "description": "HRV metric label: Poincare SD1 over 24 hours" + }, + "investigateSd224h": "SD2, 24 h", + "@investigateSd224h": { + "description": "HRV metric label: Poincare SD2 over 24 hours" + }, + "investigateSd1Sd224h": "SD1 / SD2, 24 h", + "@investigateSd1Sd224h": { + "description": "HRV metric label: SD1/SD2 ratio over 24 hours" + }, + "investigateSuccessiveIntervalsOver70ms": "Successive intervals over 70 ms", + "@investigateSuccessiveIntervalsOver70ms": { + "description": "HRV metric label: percent of successive beat intervals differing by over 70 ms" + }, + "investigateIrregularRhythmFlagSleep": "Irregular-rhythm flag, sleep", + "@investigateIrregularRhythmFlagSleep": { + "description": "HRV metric label: irregular-rhythm flag during sleep" + }, + "investigateIrregularRhythmFlag24h": "Irregular-rhythm flag, 24 h", + "@investigateIrregularRhythmFlag24h": { + "description": "HRV metric label: irregular-rhythm flag over 24 hours" + }, + "investigateFlagRaised": "raised", + "@investigateFlagRaised": { + "description": "Value shown when the irregular-rhythm flag was raised" + }, + "investigateFlagClear": "clear", + "@investigateFlagClear": { + "description": "Value shown when the irregular-rhythm flag was clear" + }, + "investigateDecelerationCapacity": "Deceleration capacity", + "@investigateDecelerationCapacity": { + "description": "HRV metric label / chart title: deceleration capacity" + }, + "investigateAccelerationCapacity": "Acceleration capacity", + "@investigateAccelerationCapacity": { + "description": "HRV metric label: acceleration capacity" + }, + "investigateDcAnchors": "DC anchors", + "@investigateDcAnchors": { + "description": "HRV metric label: number of deceleration-capacity anchor points" + }, + "investigateSignalQuality": "Signal quality", + "@investigateSignalQuality": { + "description": "HRV table title: signal quality metrics" + }, + "investigateBeatsAnalysed": "Beats analysed", + "@investigateBeatsAnalysed": { + "description": "Signal quality row: beats analysed" + }, + "investigateBeatsAnalysed24h": "Beats analysed, 24 h", + "@investigateBeatsAnalysed24h": { + "description": "Signal quality row: beats analysed over 24 hours" + }, + "investigateNoShapeForNight": "No shape for this night", + "@investigateNoShapeForNight": { + "description": "Status card title: no night-shape data available" + }, + "investigateTooFewBeatsToBin": "The night carried too few clean beats to bin.", + "@investigateTooFewBeatsToBin": { + "description": "Status card fallback body: too few beats to bin the night" + }, + "investigateShapeOfTheNight": "Shape of the night", + "@investigateShapeOfTheNight": { + "description": "Chart title: RMSSD shape across the night" + }, + "investigateBinRmssd": "Bin RMSSD", + "@investigateBinRmssd": { + "description": "Chart legend label: per-bin RMSSD line" + }, + "investigateSamplingRange": "Sampling range", + "@investigateSamplingRange": { + "description": "Chart legend label: sampling spread band" + }, + "investigateShapeFootnote": "{drawn} of {total} bins carried enough beats to read; the rest are gaps, not zeroes. The outer pair is the estimator's own sampling spread, not a range you were in. This describes the night and cannot explain it — a low first third is equally consistent with alcohol, a late meal, late training, a warm room, an illness starting, or nothing at all.", + "@investigateShapeFootnote": { + "description": "Chart footnote explaining night-shape bin coverage and caveats", + "placeholders": { + "drawn": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "investigateNightShape": "Night shape", + "@investigateNightShape": { + "description": "Table title: night-shape summary stats" + }, + "investigateBinWidth": "Bin width", + "@investigateBinWidth": { + "description": "Night-shape row: minutes per bin" + }, + "investigateBinsRead": "Bins read", + "@investigateBinsRead": { + "description": "Night-shape row: bins with readable data" + }, + "investigateFirstThird": "First third", + "@investigateFirstThird": { + "description": "Night-shape row: RMSSD in the first third of the night" + }, + "investigateLastThird": "Last third", + "@investigateLastThird": { + "description": "Night-shape row: RMSSD in the last third of the night" + }, + "investigateLastThirdOverFirst": "Last third ÷ first", + "@investigateLastThirdOverFirst": { + "description": "Night-shape row: ratio of last third to first third" + }, + "investigate29DaysAgo": "29 days ago", + "@investigate29DaysAgo": { + "description": "Chart x-axis label: 29 days before today" + }, + "investigateToday": "Today", + "@investigateToday": { + "description": "Chart x-axis label: today" + }, + "investigateDcFootnoteWithBeats": "Your own nights only — no reference range, and none exists for pulse arrivals. Night-to-night signal quality moves this line on its own, and last night was {beats} beats.", + "@investigateDcFootnoteWithBeats": { + "description": "Deceleration capacity chart footnote, including last night's beat count", + "placeholders": { + "beats": { + "type": "String" + } + } + }, + "investigateDcFootnote": "Your own nights only — no reference range, and none exists for pulse arrivals. Night-to-night signal quality moves this line on its own.", + "@investigateDcFootnote": { + "description": "Deceleration capacity chart footnote, no beat count available" + }, + "investigateIrregularRhythmScreen": "Irregular-rhythm screen", + "@investigateIrregularRhythmScreen": { + "description": "Chart title: irregular-rhythm screening heat map" + }, + "investigateOneSquarePerDay": "one square per day", + "@investigateOneSquarePerDay": { + "description": "Chart unit label: one square per day" + }, + "investigate12WeeksAgo": "12 weeks ago", + "@investigate12WeeksAgo": { + "description": "Chart x-axis label: 12 weeks before today" + }, + "investigateThisWeek": "This week", + "@investigateThisWeek": { + "description": "Chart x-axis label: this week" + }, + "investigateScreenRan": "Screen ran", + "@investigateScreenRan": { + "description": "Chart legend label: day the irregular-rhythm screen ran" + }, + "investigateRhythmStripFootnote": "Ran on {ran} {ran, plural, one{day} other{days}}, raised its flag on {raised}. An outlined square is a day it did not run. A clear strip is not a negative result: this is a screen on pulse timing, and it cannot tell an ectopic beat from a dropped beat from the band moving on your wrist.", + "@investigateRhythmStripFootnote": { + "description": "Footnote summarising how many days the irregular-rhythm screen ran and flagged", + "placeholders": { + "ran": { + "type": "int" + }, + "raised": { + "type": "int" + } + } + }, + "investigateNoRestingBreathingRate": "No resting breathing rate away from sleep", + "@investigateNoRestingBreathingRate": { + "description": "Status card title: no resting breathing rate outside sleep" + }, + "investigateNoRestingBreathingRateBody": "This reads breathing only from three-minute stretches where the band saw you almost completely still, outside the sleep window. Most days have none — a day with none is a day you were moving, not a day anything went wrong.", + "@investigateNoRestingBreathingRateBody": { + "description": "Status card body explaining resting breathing rate requires stillness" + }, + "investigateBreathingAtRestAwake": "Breathing at rest, awake", + "@investigateBreathingAtRestAwake": { + "description": "Table title: resting breathing rate while awake" + }, + "investigateStillStretchesOutsideSleep": "Still stretches outside sleep", + "@investigateStillStretchesOutsideSleep": { + "description": "Table row: count of still stretches found outside sleep" + }, + "investigateLowest": "Lowest", + "@investigateLowest": { + "description": "Table row: lowest resting breathing rate reading" + }, + "investigateNextLowest": "Next lowest", + "@investigateNextLowest": { + "description": "Table row: second-lowest resting breathing rate reading" + }, + "investigateHighestOfThem": "Highest of them", + "@investigateHighestOfThem": { + "description": "Table row: highest of the still-stretch readings" + }, + "investigateFloorNotRateBody": "A floor, not a rate for the day. Only stretches where you were almost completely still can be read at all, so these are the stillest few minutes the band saw outside your sleep — nothing here describes the rest of your day, and breathing while you move cannot be recovered from beat timing.", + "@investigateFloorNotRateBody": { + "description": "Explanatory paragraph: resting breathing rate is a floor, not a daily average" + }, + "investigateCycleScreenDidNotRun": "The cycle screen did not run for this night", + "@investigateCycleScreenDidNotRun": { + "description": "Status card title: the heart-rate-cycling screen abstained for the night" + }, + "investigateNotEnoughCleanBeats": "Not enough clean beats to run it.", + "@investigateNotEnoughCleanBeats": { + "description": "Status card fallback body: not enough clean beats to run the cycle screen" + }, + "investigateHeartRateCycles": "Heart-rate cycles", + "@investigateHeartRateCycles": { + "description": "Table title: heart-rate cycling stats for the night" + }, + "investigateCyclesCounted": "Cycles counted", + "@investigateCyclesCounted": { + "description": "Table row: number of heart-rate cycles counted" + }, + "investigateObservedHoursAnalysed": "Observed hours analysed", + "@investigateObservedHoursAnalysed": { + "description": "Table row: hours of data analysed for cycling" + }, + "investigateCyclesPerObservedHour": "Cycles per observed hour", + "@investigateCyclesPerObservedHour": { + "description": "Table row: cycles per observed hour" + }, + "investigateMeanCycleLength": "Mean cycle length", + "@investigateMeanCycleLength": { + "description": "Table row: mean length of a heart-rate cycle" + }, + "investigateMeanDipDepth": "Mean dip depth", + "@investigateMeanDipDepth": { + "description": "Table row: mean depth of the heart-rate dip per cycle" + }, + "investigateCycleLengthQuartiles": "Cycle length, quartiles", + "@investigateCycleLengthQuartiles": { + "description": "Table row: quartiles of cycle length" + }, + "investigateDipDepthQuartiles": "Dip depth, quartiles", + "@investigateDipDepthQuartiles": { + "description": "Table row: quartiles of dip depth" + }, + "investigateNotEnoughNightsAcross": "Not enough nights for the across-nights view", + "@investigateNotEnoughNightsAcross": { + "description": "Status card title: not enough nights for the multi-night distribution view" + }, + "investigateNeedsSeveralNights": "This needs several nights with a few observed hours each.", + "@investigateNeedsSeveralNights": { + "description": "Status card fallback body: needs several qualifying nights" + }, + "investigateDroppedIrregular": "{count, plural, one{# night left out because the irregular-rhythm screen flagged it} other{# nights left out because the irregular-rhythm screen flagged them}}", + "@investigateDroppedIrregular": { + "description": "Note listing nights excluded because the irregular-rhythm screen flagged them", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "investigateDroppedThin": "{count, plural, one{# night left out for too few observed hours} other{# nights left out for too few observed hours}}", + "@investigateDroppedThin": { + "description": "Note listing nights excluded for too few observed hours", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "investigateAcrossNOwnNights": "ACROSS {n} OF YOUR OWN NIGHTS", + "@investigateAcrossNOwnNights": { + "description": "Section header: summary across the user's own recent nights", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "investigateCvhrAboveUsual": "Over your most recent nights, the heart-rate cycling this screen counts has been running higher than across the {n} nights behind it.", + "@investigateCvhrAboveUsual": { + "description": "Sentence: recent heart-rate cycling is running above the user's own usual range", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "investigateCvhrInsideUsual": "Over your most recent nights, the heart-rate cycling this screen counts has stayed inside the range of the {n} nights behind it.", + "@investigateCvhrInsideUsual": { + "description": "Sentence: recent heart-rate cycling has stayed within the user's own usual range", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "investigateCvhrExplainer": "It is a pattern in your pulse, not a measurement of your breathing, and it is not a test for anything. The same cycling comes from an irregular rhythm, from being at altitude, and from any broken-up night — and beta-blockers, diabetes and nerve conditions flatten it, so genuinely disturbed breathing often leaves nothing here at all.", + "@investigateCvhrExplainer": { + "description": "Paragraph explaining what heart-rate cycling is and is not a measure of" + }, + "investigateCvhrNotNegativeResult": "So nothing here is a negative result and nothing here clears anything, and none of it says anything about any one night — a single night's count moves for a dozen reasons on its own.", + "@investigateCvhrNotNegativeResult": { + "description": "Paragraph clarifying this is not a negative clinical result" + }, + "investigateCvhrSeeClinicianIfSymptoms": "If you snore, wake unrefreshed, or someone has seen you stop breathing in your sleep, a clinician can test that properly.", + "@investigateCvhrSeeClinicianIfSymptoms": { + "description": "Paragraph advising the user to see a clinician if they have relevant symptoms" + }, + "investigateStageMinutesAsCounted": "Stage minutes, as counted", + "@investigateStageMinutesAsCounted": { + "description": "Table title: raw sleep-stage minute counts" + }, + "investigateLight": "Light", + "@investigateLight": { + "description": "Sleep stage label: light sleep" + }, + "investigateDeep": "Deep", + "@investigateDeep": { + "description": "Sleep stage label: deep sleep" + }, + "investigateRem": "REM", + "@investigateRem": { + "description": "Sleep stage label: REM sleep" + }, + "investigateAwake": "Awake", + "@investigateAwake": { + "description": "Sleep stage label: awake" + }, + "investigateTotalSleep": "Total sleep", + "@investigateTotalSleep": { + "description": "Table row: total sleep time" + }, + "investigateSegmentationConfidence": "Segmentation confidence", + "@investigateSegmentationConfidence": { + "description": "Table row: confidence of the sleep-stage segmentation" + }, + "investigateNotPublished": "not published", + "@investigateNotPublished": { + "description": "Value shown when segmentation confidence is not published" + }, + "investigateNothingComputedForKey": "Nothing computed for this key", + "@investigateNothingComputedForKey": { + "description": "Status card title: nothing computed for this metric key" + }, + "investigateNoStoredSeries": "No stored series", + "@investigateNoStoredSeries": { + "description": "Status card title: no stored data series for this metric" + }, + "investigateNothingStoredYet": "Nothing stored for {metric} yet.", + "@investigateNothingStoredYet": { + "description": "Status card body: nothing stored yet for this metric", + "placeholders": { + "metric": { + "type": "String" + } + } + }, + "investigateSeries": "Series", + "@investigateSeries": { + "description": "Table title: generic stored-series summary stats" + }, + "investigateDaysDerived": "Days derived", + "@investigateDaysDerived": { + "description": "Table row: number of derived days in the series" + }, + "investigateLatest": "Latest", + "@investigateLatest": { + "description": "Table row: latest value in the series" + }, + "investigateMean": "Mean", + "@investigateMean": { + "description": "Table row: mean value of the series" + }, + "investigateMedian": "Median", + "@investigateMedian": { + "description": "Table row: median value of the series" + }, + "investigateSd": "SD", + "@investigateSd": { + "description": "Table row: standard deviation of the series" + }, + "investigateMin": "Min", + "@investigateMin": { + "description": "Table row: minimum value of the series" + }, + "investigateMax": "Max", + "@investigateMax": { + "description": "Table row: maximum value of the series" + }, + "investigateUnit": "Unit", + "@investigateUnit": { + "description": "Table row: unit of the series" + }, + "investigateUnitless": "unitless", + "@investigateUnitless": { + "description": "Value shown when the series has no unit" + }, + "investigateStorage": "Storage", + "@investigateStorage": { + "description": "Table row: how the series is stored" + }, + "investigateOneValuePerDerivedDay": "one value per derived day", + "@investigateOneValuePerDerivedDay": { + "description": "Value: one stored value per derived day" + }, + "investigateMethodLabel": "METHOD", + "@investigateMethodLabel": { + "description": "Section header for the method/citation panel" + }, + "investigateNotDocumented": "Not documented.", + "@investigateNotDocumented": { + "description": "Fallback text when the method is not documented" + }, + "calmBreathingResonanceLabel": "Resonance", + "@calmBreathingResonanceLabel": { + "description": "Pattern name for a custom-paced resonance breathing exercise" + }, + "calmBreathingResonanceDescription": "Even in and out at about {rate} breaths a minute. The one with a coherence score.", + "@calmBreathingResonanceDescription": { + "description": "Description of a custom-paced resonance breathing exercise", + "placeholders": { + "rate": { + "type": "String" + } + } + }, + "calmBreathingCloseBreathing": "Close breathing", + "@calmBreathingCloseBreathing": { + "description": "Semantic label for the close button on the breathing screen" + }, + "calmBreathingFinishNow": "Finish now", + "@calmBreathingFinishNow": { + "description": "Button label to finish the post-session quiet window immediately" + }, + "calmBreathingStop": "Stop", + "@calmBreathingStop": { + "description": "Button label to stop a breathing session or pre-window" + }, + "calmBreathingEndSession": "End session", + "@calmBreathingEndSession": { + "description": "Button label to end an ordinary (non-sweep) breathing session" + }, + "calmBreathingBegin": "Begin", + "@calmBreathingBegin": { + "description": "Button label to begin a breathing session" + }, + "calmBreathingTakeABreath": "Take a breath.", + "@calmBreathingTakeABreath": { + "description": "Headline on the breathing setup screen" + }, + "calmBreathingRingLeads": "The ring leads. Put the phone down.", + "@calmBreathingRingLeads": { + "description": "Subtitle instructing the user to follow the breathing ring" + }, + "calmBreathingScoredPill": "Scored", + "@calmBreathingScoredPill": { + "description": "Pill label marking a pattern as coherence-scored" + }, + "calmBreathingHowLong": "How long", + "@calmBreathingHowLong": { + "description": "Section title for choosing session length" + }, + "calmBreathingMinutesSemantic": "{m, plural, one{# minute} other{# minutes}}", + "@calmBreathingMinutesSemantic": { + "description": "Semantic label for a minute-duration option button", + "placeholders": { + "m": { + "type": "int" + } + } + }, + "calmBreathingMinutesAbbrev": "{m} min", + "@calmBreathingMinutesAbbrev": { + "description": "Abbreviated minute label on a duration option button", + "placeholders": { + "m": { + "type": "int" + } + } + }, + "calmBreathingYourOwnPace": "Your own pace", + "@calmBreathingYourOwnPace": { + "description": "Section title for the personal-pace sweep feature" + }, + "calmBreathingWindowRowSemantic": "Measure before and after, adds four minutes", + "@calmBreathingWindowRowSemantic": { + "description": "Semantic label for the before/after quiet-window toggle row" + }, + "calmBreathingMeasureBeforeAfter": "Measure before and after · adds 4 min", + "@calmBreathingMeasureBeforeAfter": { + "description": "Row title for the before/after quiet-window toggle" + }, + "calmBreathingNeedsBandBeatTiming": "Needs the band on — the comparison is made from beat timing.", + "@calmBreathingNeedsBandBeatTiming": { + "description": "Body text explaining a feature needs the band connected for beat timing" + }, + "calmBreathingFindYourPace": "Find the pace your heart follows", + "@calmBreathingFindYourPace": { + "description": "Row title inviting the user to run the personal-pace sweep" + }, + "calmBreathingSweepIntro": "Six minutes: {rates} breaths a minute, two minutes each. It takes two sittings that agree before anything changes.", + "@calmBreathingSweepIntro": { + "description": "Body text describing the personal-pace sweep before it has ever run", + "placeholders": { + "rates": { + "type": "String" + } + } + }, + "calmBreathingSweepAgreed": "Two sittings agreed on {rate} breaths a minute, and Resonance is paced there. Run it again to check.", + "@calmBreathingSweepAgreed": { + "description": "Body text describing the pace two sittings agreed on", + "placeholders": { + "rate": { + "type": "String" + } + } + }, + "calmBreathingPaceOfRate": "PACE {block} OF {total} · {rate} BREATHS A MINUTE", + "@calmBreathingPaceOfRate": { + "description": "Header shown during a sweep block naming its position and pace", + "placeholders": { + "block": { + "type": "int" + }, + "total": { + "type": "int" + }, + "rate": { + "type": "String" + } + } + }, + "calmBreathingOfClock": "of {clock}", + "@calmBreathingOfClock": { + "description": "Label showing the target duration next to the elapsed clock", + "placeholders": { + "clock": { + "type": "String" + } + } + }, + "calmBreathingNoScoreForSession": "No coherence score for this session", + "@calmBreathingNoScoreForSession": { + "description": "Status card / title: no coherence score for this session" + }, + "calmBreathingScoringNeedsBand": "Scoring needs beat timing from the band. Not connected, so this one paces you but is not saved.", + "@calmBreathingScoringNeedsBand": { + "description": "Status card body: scoring needs the band connected" + }, + "calmBreathingBeforeLabel": "BEFORE", + "@calmBreathingBeforeLabel": { + "description": "Header shown during the pre-session quiet window" + }, + "calmBreathingAfterLabel": "AFTER", + "@calmBreathingAfterLabel": { + "description": "Header shown during the post-session quiet window" + }, + "calmBreathingSitStill": "Sit still for a moment.", + "@calmBreathingSitStill": { + "description": "Instruction during the pre-session quiet window" + }, + "calmBreathingStaySitting": "Stay sitting.", + "@calmBreathingStaySitting": { + "description": "Instruction during the post-session quiet window" + }, + "calmBreathingNothingPacingScored": "Breathe however you normally would. Nothing is pacing you and nothing is being scored.", + "@calmBreathingNothingPacingScored": { + "description": "Body text reassuring the quiet window is unpaced and unscored" + }, + "calmBreathingPatternNotScored": "{pattern} is not scored. Resonance is the one paced at the rate the score is built for.", + "@calmBreathingPatternNotScored": { + "description": "Explanation that the current breathing pattern is not coherence-scored", + "placeholders": { + "pattern": { + "type": "String" + } + } + }, + "calmBreathingTooFewBeatTimings": "Too few clean beat timings across the session to score it.", + "@calmBreathingTooFewBeatTimings": { + "description": "Fallback reason: too few clean beat timings to score the session" + }, + "calmBreathingThatIsDone": "That is done.", + "@calmBreathingThatIsDone": { + "description": "Headline shown when a session finishes normally" + }, + "calmBreathingCardiacCoherence": "Cardiac coherence", + "@calmBreathingCardiacCoherence": { + "description": "Result card title for the coherence score" + }, + "calmBreathingHowStronglyFollowedPace": "HOW STRONGLY YOUR HEART RATE FOLLOWED THE PACE", + "@calmBreathingHowStronglyFollowedPace": { + "description": "Result card subtitle explaining what the coherence score measures" + }, + "calmBreathingStoppedThere": "Stopped there.", + "@calmBreathingStoppedThere": { + "description": "Headline shown when a sweep was aborted early" + }, + "calmBreathingHowStronglyEachPace": "HOW STRONGLY YOUR HEART RATE FOLLOWED EACH PACE", + "@calmBreathingHowStronglyEachPace": { + "description": "Header for the sweep results table" + }, + "calmBreathingBreathsAMinute": "{rate} breaths a minute", + "@calmBreathingBreathsAMinute": { + "description": "A pace expressed in breaths per minute", + "placeholders": { + "rate": { + "type": "String" + } + } + }, + "calmBreathingNotReached": "not reached", + "@calmBreathingNotReached": { + "description": "Sweep result row value: this block was never reached" + }, + "calmBreathingTooFewCleanBeats": "too few clean beats", + "@calmBreathingTooFewCleanBeats": { + "description": "Sweep result row value: this block ran but could not be scored" + }, + "calmBreathingRankingExplainer": "A ranking of three paces from one sitting. The blocks run back to back, so each pace is measured while you are still settling out of the one before. It says which pace your heart rate followed most strongly, and nothing else.", + "@calmBreathingRankingExplainer": { + "description": "Footnote explaining the sweep is a ranking with real limits, not a diagnosis" + }, + "calmBreathingVerdictAborted": "You stopped part way, so there was nothing to compare. Nothing has changed.", + "@calmBreathingVerdictAborted": { + "description": "Verdict text when the sweep was stopped before comparing any paces" + }, + "calmBreathingVerdictCouldNotScore": "At least one pace could not be scored, so there is nothing to rank. Nothing has changed.", + "@calmBreathingVerdictCouldNotScore": { + "description": "Verdict text when at least one pace could not be scored" + }, + "calmBreathingVerdictTied": "Two of the paces scored the same, so this sitting cannot separate them. Nothing has changed.", + "@calmBreathingVerdictTied": { + "description": "Verdict text when two paces scored the same and cannot be separated" + }, + "calmBreathingVerdictConfirmed": "Of the paces tested, {w} gave your strongest response — and that is now two sittings in a row. Resonance is paced there.", + "@calmBreathingVerdictConfirmed": { + "description": "Verdict text when the winning pace matches the prior agreed pace", + "placeholders": { + "w": { + "type": "String" + } + } + }, + "calmBreathingVerdictFirstWin": "Of the paces tested, {w} gave your strongest response. Nothing is set yet: the pace only changes when two sittings pick the same one.", + "@calmBreathingVerdictFirstWin": { + "description": "Verdict text when a pace won but has not yet been confirmed by a second sitting", + "placeholders": { + "w": { + "type": "String" + } + } + }, + "breathPatternBoxName": "Box", + "@breathPatternBoxName": { + "description": "Pattern name for box breathing (4-4-4-4 count)" + }, + "breathPatternBoxDesc": "Four counts each way, holds included. Steadying when your head is racing.", + "@breathPatternBoxDesc": { + "description": "Description of box breathing on the pattern picker" + }, + "breathPattern478Name": "4-7-8", + "@breathPattern478Name": { + "description": "Pattern name for the 4-7-8 breathing exercise" + }, + "breathPattern478Desc": "A long hold and a longer exhale. Usually used to get to sleep.", + "@breathPattern478Desc": { + "description": "Description of the 4-7-8 pattern on the pattern picker" + }, + "breathPatternExtendedExhaleName": "Long exhale", + "@breathPatternExtendedExhaleName": { + "description": "Pattern name for the extended-exhale breathing exercise" + }, + "breathPatternExtendedExhaleDesc": "Out for twice as long as in. No holds, so it is easy to keep up for a while.", + "@breathPatternExtendedExhaleDesc": { + "description": "Description of the extended-exhale pattern on the pattern picker" + }, + "breathPhaseInhale": "Inhale", + "@breathPhaseInhale": { + "description": "Instruction shown on the breathing ring during the inhale phase" + }, + "breathPhaseHold": "Hold", + "@breathPhaseHold": { + "description": "Instruction shown on the breathing ring during a hold phase" + }, + "breathPhaseExhale": "Exhale", + "@breathPhaseExhale": { + "description": "Instruction shown on the breathing ring during the exhale phase" + }, + "breathPhaseWork": "Work", + "@breathPhaseWork": { + "description": "Instruction shown on the breathing ring during an interval-timer work phase" + }, + "breathPhaseRest": "Rest", + "@breathPhaseRest": { + "description": "Instruction shown on the breathing ring during an interval-timer rest phase" + }, + "metricDetailToday": "Today", + "@metricDetailToday": { + "description": "Range picker chip and headline label for the current day, on the shared metric trend/detail screen." + }, + "metricDetailRange7Days": "7 days", + "@metricDetailRange7Days": { + "description": "Range picker chip: 7-day window, on the metric detail screen." + }, + "metricDetailRange30Days": "30 days", + "@metricDetailRange30Days": { + "description": "Range picker chip: 30-day window, on the metric detail screen." + }, + "metricDetailRange6Months": "6 months", + "@metricDetailRange6Months": { + "description": "Range picker chip: 6-month window, on the metric detail screen." + }, + "metricDetailRangeYear": "Year", + "@metricDetailRangeYear": { + "description": "Range picker chip: 1-year window, on the metric detail screen." + }, + "metricDetailLockedNote": "{label} needs {needed} days of history. You have {have}.", + "@metricDetailLockedNote": { + "description": "Note under the range picker explaining why a wider range is not yet unlocked.", + "placeholders": { + "label": { + "type": "String" + }, + "needed": { + "type": "int" + }, + "have": { + "type": "int" + } + } + }, + "metricDetailNotShownTitle": "Not shown as a trend", + "@metricDetailNotShownTitle": { + "description": "StatusCard title for a metric that cannot be charted as a trend (e.g. skin temperature)." + }, + "metricDetailNothingRecordedToday": "Nothing recorded today", + "@metricDetailNothingRecordedToday": { + "description": "Empty-state title when Today's window has no value for this metric." + }, + "metricDetailNoHistoryYet": "No history for {metric} yet", + "@metricDetailNoHistoryYet": { + "description": "Empty-state title when a wider window has no value for this metric yet.", + "placeholders": { + "metric": { + "type": "String" + } + } + }, + "metricDetailNoValueYet": "Today has not produced a value yet.", + "@metricDetailNoValueYet": { + "description": "Empty-state body: today has not produced a value and the install has no data at all yet." + }, + "metricDetailNoValueYetWiderRanges": "Today has not produced a value yet. The wider ranges above hold the days that did.", + "@metricDetailNoValueYetWiderRanges": { + "description": "Empty-state body: today has not produced a value but wider ranges above have data." + }, + "metricDetailNoValueInWindow": "No day in this window produced a value.", + "@metricDetailNoValueInWindow": { + "description": "Empty-state body for a multi-day window with no value in it." + }, + "metricDetailWearBandFix": "Wear the band overnight to start the series", + "@metricDetailWearBandFix": { + "description": "Empty-state fix suggestion telling the user to wear the band overnight." + }, + "metricDetailBeatsLinkTitle": "Beats", + "@metricDetailBeatsLinkTitle": { + "description": "Title of the doorway row from the HRV metric screen into the Beats (Poincare/rhythm) screen." + }, + "metricDetailBeatsLinkSub": "The intervals a night is made of, drawn", + "@metricDetailBeatsLinkSub": { + "description": "Subtitle of the doorway row into the Beats screen." + }, + "metricDetailBreakdownLinkTitle": "Breakdown", + "@metricDetailBreakdownLinkTitle": { + "description": "Title of the doorway row from the Steps metric screen (Today only) into the day's step breakdown." + }, + "metricDetailBreakdownLinkSub": "Each stretch of today, and what counted it", + "@metricDetailBreakdownLinkSub": { + "description": "Subtitle of the doorway row into the step breakdown screen." + }, + "metricDetailNerdStatsTitle": "Nerd stats", + "@metricDetailNerdStatsTitle": { + "description": "Title of the doorway row into the Nerd stats (Investigate) screen, shown on every metric detail screen." + }, + "metricDetailNerdStatsSub": "The figures behind the picture", + "@metricDetailNerdStatsSub": { + "description": "Subtitle of the doorway row into Nerd stats." + }, + "metricDetailDailyAverage": "Daily average · {count} of {win} days", + "@metricDetailDailyAverage": { + "description": "Caption under the hero number on a multi-day window, showing how many of the window's days had data.", + "placeholders": { + "count": { + "type": "int" + }, + "win": { + "type": "int" + } + } + }, + "metricDetailLatestReading": "Latest {value} {unit} · {asOf}", + "@metricDetailLatestReading": { + "description": "Caption showing the most recent reading and the day it is from, under the daily-average headline.", + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + }, + "asOf": { + "type": "String" + } + } + }, + "metricDetailAlgoBreakFootnote": "{n, plural, one{The dotted line is a change in how these days were computed. Readings either side of it came from different versions.} other{The dotted lines are changes in how these days were computed. Readings either side of one came from different versions.}}", + "@metricDetailAlgoBreakFootnote": { + "description": "Chart footnote explaining the dotted vertical line(s) marking an algorithm-version change.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "metricDetailDaysAgoLabel": "{n, plural, one{{n} day ago} other{{n} days ago}}", + "@metricDetailDaysAgoLabel": { + "description": "Left x-axis label on the trend chart, naming how many days ago the leftmost point is.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "metricDetailWornChartTitle": "Worn", + "@metricDetailWornChartTitle": { + "description": "Title of the small wear-time bar chart shown under a long trend." + }, + "metricDetailHoursADayUnit": "h a day", + "@metricDetailHoursADayUnit": { + "description": "Unit label on the wear-time bar chart (hours per day)." + }, + "metricDetailWearFootnote": "{have} of these {win} days have a wear record. The rest are gaps in both charts — the line above is not carried across one.", + "@metricDetailWearFootnote": { + "description": "Footnote under the wear-time chart explaining coverage gaps.", + "placeholders": { + "have": { + "type": "int" + }, + "win": { + "type": "int" + } + } + }, + "metricDetailSlotNoRecord": "{day}, no record", + "@metricDetailSlotNoRecord": { + "description": "Screen-reader readout for a chart slot with no data, when scrubbing the trend line.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "metricDetailSlotWithValue": "{day}, {value} {unit}", + "@metricDetailSlotWithValue": { + "description": "Screen-reader readout for a chart slot with a value, when scrubbing the trend line.", + "placeholders": { + "day": { + "type": "String" + }, + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "metricDetailOpenDay": "Open {day}", + "@metricDetailOpenDay": { + "description": "Semantic label for the tappable row that opens a touched day's detail screen.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "metricDetailNoRecordLabel": "No record", + "@metricDetailNoRecordLabel": { + "description": "Value text shown in the touched-day row when that day has no record." + }, + "metricDetailLowest": "Lowest", + "@metricDetailLowest": { + "description": "Label under the lowest value in the 'your normal range' 3-stat row." + }, + "metricDetailTypical": "Typical", + "@metricDetailTypical": { + "description": "Label under the median value in the 'your normal range' 3-stat row." + }, + "metricDetailHighest": "Highest", + "@metricDetailHighest": { + "description": "Label under the highest value in the 'your normal range' 3-stat row." + }, + "metricDetailFromDaysCount": "From {n} of your own days.", + "@metricDetailFromDaysCount": { + "description": "Fallback sentence under the 3-stat row when no percentile rank is available.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "metricDetailPercentileTodayNoBand": "Today sits at the {ordinal} percentile of your own history.", + "@metricDetailPercentileTodayNoBand": { + "description": "Sentence stating today's percentile rank within the user's own history, with no qualitative band.", + "placeholders": { + "ordinal": { + "type": "String" + } + } + }, + "metricDetailPercentileTodayBand": "Today sits at the {ordinal} percentile of your own history — {band}.", + "@metricDetailPercentileTodayBand": { + "description": "Sentence stating today's percentile rank within the user's own history, plus a qualitative band label.", + "placeholders": { + "ordinal": { + "type": "String" + }, + "band": { + "type": "String" + } + } + }, + "metricDetailPercentileFromNoBand": "Your reading from {date} sits at the {ordinal} percentile of your own history.", + "@metricDetailPercentileFromNoBand": { + "description": "Sentence stating a past day's percentile rank within the user's own history, with no qualitative band.", + "placeholders": { + "date": { + "type": "String" + }, + "ordinal": { + "type": "String" + } + } + }, + "metricDetailPercentileFromBand": "Your reading from {date} sits at the {ordinal} percentile of your own history — {band}.", + "@metricDetailPercentileFromBand": { + "description": "Sentence stating a past day's percentile rank within the user's own history, plus a qualitative band label.", + "placeholders": { + "date": { + "type": "String" + }, + "ordinal": { + "type": "String" + }, + "band": { + "type": "String" + } + } + }, + "metricDetailDaysWithWithout": "{withCount} days with · {withoutCount} without", + "@metricDetailDaysWithWithout": { + "description": "Row subtitle in the 'what moves it' list: how many logged days had vs. lacked a journal tag.", + "placeholders": { + "withCount": { + "type": "int" + }, + "withoutCount": { + "type": "int" + } + } + }, + "metricDetailPatternsNotCauses": "Patterns in your own logs, not causes.", + "@metricDetailPatternsNotCauses": { + "description": "Disclaimer under the 'what moves it' journal-correlation list." + }, + "metricDetailChooseDayHelp": "Choose a day", + "@metricDetailChooseDayHelp": { + "description": "Help text on the native date picker used to jump to a specific derived day." + }, + "metricDetailPreviousDay": "Previous day", + "@metricDetailPreviousDay": { + "description": "Semantic label for the back arrow on the day-stepper control." + }, + "metricDetailNextDay": "Next day", + "@metricDetailNextDay": { + "description": "Semantic label for the forward arrow on the day-stepper control." + }, + "metricDetailChooseDayShowing": "Choose a day. Showing {day}", + "@metricDetailChooseDayShowing": { + "description": "Semantic label for the day-stepper's centre label, announcing the day currently shown.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "metricDetailNormalRangeSection": "Your normal range", + "@metricDetailNormalRangeSection": { + "description": "Section heading above the lowest/typical/highest 3-stat card." + }, + "metricDetailWhatMovesItSection": "What moves it", + "@metricDetailWhatMovesItSection": { + "description": "Section heading above the journal-correlation 'movers' list." + }, + "cycleRemoveLogTitle": "Remove {date}?", + "@cycleRemoveLogTitle": { + "description": "Confirmation dialog title when deleting a logged cycle day.", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "cycleRemoveLogBody": "Cycle day, phase and the predicted next date are all counted from the days you log. Only today can be logged, so this one cannot be put back.", + "@cycleRemoveLogBody": { + "description": "Confirmation dialog body when deleting a logged cycle day." + }, + "cycleWhatAppliesToYou": "What applies to you", + "@cycleWhatAppliesToYou": { + "description": "Title of the reproductive-state picker sheet, and label of the settings row that opens it." + }, + "cyclePreferNotToSay": "Prefer not to say", + "@cyclePreferNotToSay": { + "description": "Reproductive-state option: decline to answer." + }, + "cyclePreferNotToSayWhy": "The app keeps the phase off.", + "@cyclePreferNotToSayWhy": { + "description": "Explanation shown under 'Prefer not to say' in the reproductive-state picker." + }, + "cycleReproCyclingLabel": "I have natural cycles", + "@cycleReproCyclingLabel": { + "description": "Reproductive-state option: has natural menstrual cycles." + }, + "cycleReproCyclingWhy": "Counts a phase from your logged starts.", + "@cycleReproCyclingWhy": { + "description": "Explanation shown under the 'natural cycles' reproductive-state option." + }, + "cycleReproContraceptionLabel": "Hormonal contraception", + "@cycleReproContraceptionLabel": { + "description": "Reproductive-state option: uses hormonal contraception." + }, + "cycleReproContraceptionWhy": "No ovulation to count from, so no phase. Bleeds are still logged.", + "@cycleReproContraceptionWhy": { + "description": "Explanation shown under the 'hormonal contraception' reproductive-state option." + }, + "cycleReproNoneLabel": "Pregnant, postpartum, or not cycling", + "@cycleReproNoneLabel": { + "description": "Reproductive-state option: pregnant, postpartum, or not cycling." + }, + "cycleReproNoneWhy": "No phase and no predicted next. Your biometrics still show.", + "@cycleReproNoneWhy": { + "description": "Explanation shown under the 'pregnant/postpartum/not cycling' reproductive-state option." + }, + "cycleReproNotSet": "Not set", + "@cycleReproNotSet": { + "description": "Fallback label when no reproductive state has been chosen." + }, + "cycleTrackingOffTitle": "Cycle tracking is off", + "@cycleTrackingOffTitle": { + "description": "Status card title when cycle tracking is disabled." + }, + "cycleTrackingOffBody": "It stays on this phone.", + "@cycleTrackingOffBody": { + "description": "Status card body when cycle tracking is disabled." + }, + "cycleTurnOnTracking": "Turn on cycle tracking", + "@cycleTurnOnTracking": { + "description": "Button label to enable cycle tracking." + }, + "cycleNoPeriodTitle": "No period logged yet", + "@cycleNoPeriodTitle": { + "description": "Status card title when no period has ever been logged." + }, + "cycleNoPeriodBody": "Counted from the days you log.", + "@cycleNoPeriodBody": { + "description": "Status card body when no period has ever been logged." + }, + "cycleLogPeriodButton": "Log a period start today", + "@cycleLogPeriodButton": { + "description": "Button label to log a period start today (used both as a status-card fix action and the main button)." + }, + "cycleLogKindStart": "START", + "@cycleLogKindStart": { + "description": "Recent-logs row: this entry is a period start (shown uppercased already, so translate as-is)" + }, + "cycleLogKindEnd": "END", + "@cycleLogKindEnd": { + "description": "Recent-logs row: this entry is a period end (shown uppercased already, so translate as-is)" + }, + "cycleAcrossCyclesTitle": "Across your cycles", + "@cycleAcrossCyclesTitle": { + "description": "Title for the cycle-history deep-dive card and its detail screen." + }, + "cycleUnitCompleteCycle": "complete cycle", + "@cycleUnitCompleteCycle": { + "description": "Unit label under the completed-cycle count, singular." + }, + "cycleUnitCompleteCycles": "complete cycles", + "@cycleUnitCompleteCycles": { + "description": "Unit label under the completed-cycle count, plural." + }, + "cycleOpenAction": "Open", + "@cycleOpenAction": { + "description": "Action label on the 'Across your cycles' deep-dive card." + }, + "cycleWhatYouNoticedToday": "What you noticed today", + "@cycleWhatYouNoticedToday": { + "description": "Section title above today's symptom chips." + }, + "cycleLoggedDays": "Logged days", + "@cycleLoggedDays": { + "description": "Section title above the list of recently logged cycle days." + }, + "cycleReproOptionalHint": "Optional. Until you say, the app leaves the phase off.", + "@cycleReproOptionalHint": { + "description": "Subtitle shown when no reproductive state has been set." + }, + "cycleReproPrivateHint": "Only you and this phone. Never exported.", + "@cycleReproPrivateHint": { + "description": "Subtitle shown once a reproductive state has been set, noting privacy." + }, + "cycleTurnOffTracking": "Turn off cycle tracking", + "@cycleTurnOffTracking": { + "description": "Text button to disable cycle tracking." + }, + "cycleDayInThisCycle": "DAY IN THIS CYCLE", + "@cycleDayInThisCycle": { + "description": "Overline label above the current cycle-day number." + }, + "cycleCountedFromLastStart": "counted from your last logged start", + "@cycleCountedFromLastStart": { + "description": "Caption shown next to the cycle-day number when no median cycle length is known yet." + }, + "cycleOfAboutDays": "of about {days}", + "@cycleOfAboutDays": { + "description": "Caption shown next to the cycle-day number, giving the typical cycle length.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "cyclePhaseMenstrual": "Menstrual", + "@cyclePhaseMenstrual": { + "description": "Cycle phase pill label: menstrual." + }, + "cyclePhaseFollicular": "Follicular", + "@cyclePhaseFollicular": { + "description": "Cycle phase pill label: follicular." + }, + "cyclePhaseOvulation": "Ovulation window", + "@cyclePhaseOvulation": { + "description": "Cycle phase pill label: ovulation window." + }, + "cyclePhaseLuteal": "Luteal", + "@cyclePhaseLuteal": { + "description": "Cycle phase pill label: luteal." + }, + "cycleNextPeriodBetween": "NEXT PERIOD, EXPECTED BETWEEN", + "@cycleNextPeriodBetween": { + "description": "Overline label when the next-period prediction is a date range." + }, + "cycleNextPeriodAround": "NEXT PERIOD, EXPECTED AROUND", + "@cycleNextPeriodAround": { + "description": "Overline label when the next-period prediction is a single point date." + }, + "cycleFromOneMeasuredGap": "from your one measured gap, which cannot show how much your own cycle varies", + "@cycleFromOneMeasuredGap": { + "description": "Trailing clause of the prediction footnote when only one cycle gap has been measured." + }, + "cyclePastEndOfIt": "{days} days past the end of it · ", + "@cyclePastEndOfIt": { + "description": "Leading clause of the prediction footnote: the predicted window has already passed.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "cycleInsideItNow": "you are inside it now · ", + "@cycleInsideItNow": { + "description": "Leading clause of the prediction footnote: today falls inside the predicted window." + }, + "cycleInDaysRange": "in {lo}–{hi} days · ", + "@cycleInDaysRange": { + "description": "Leading clause of the prediction footnote: the predicted window is still ahead, given as a day range.", + "placeholders": { + "lo": { + "type": "int" + }, + "hi": { + "type": "int" + } + } + }, + "cycleHalfOfMeasuredGaps": "half of your {n} measured gaps landed inside a range this wide", + "@cycleHalfOfMeasuredGaps": { + "description": "Trailing clause of the prediction footnote, stating how many measured gaps back the range.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "cycleLeadDaysLate": "{days} days late · ", + "@cycleLeadDaysLate": { + "description": "Leading clause used when the single measured-gap prediction is overdue.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "cycleLeadToday": "today · ", + "@cycleLeadToday": { + "description": "Leading clause used when the single measured-gap prediction is due today." + }, + "cycleLeadInDays": "in {days} days · ", + "@cycleLeadInDays": { + "description": "Leading clause used when the single measured-gap prediction is still ahead.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "cycleWhatYouUsuallyNotice": "What you usually notice", + "@cycleWhatYouUsuallyNotice": { + "description": "Expandable row label/semantic label for the symptom-shape look-back." + }, + "cycleSymptomShapeSummary": "Four numbers, one per week of the cycle, counted back to your own logged starts. You logged something on {daysByWeek} days of each week across {cycles} cycles — those are the only days in any of this.", + "@cycleSymptomShapeSummary": { + "description": "Explanatory footer under the symptom-shape breakdown.", + "placeholders": { + "daysByWeek": { + "type": "String" + }, + "cycles": { + "type": "int" + } + } + }, + "cycleRemoveLoggedDay": "Remove {date}", + "@cycleRemoveLoggedDay": { + "description": "Semantic label on the remove (X) button next to a logged cycle day.", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "cycleSymptomCramps": "cramps", + "@cycleSymptomCramps": { + "description": "Symptom chip: cramps." + }, + "cycleSymptomHeadache": "headache", + "@cycleSymptomHeadache": { + "description": "Symptom chip: headache." + }, + "cycleSymptomBloating": "bloating", + "@cycleSymptomBloating": { + "description": "Symptom chip: bloating." + }, + "cycleSymptomFatigue": "fatigue", + "@cycleSymptomFatigue": { + "description": "Symptom chip: fatigue." + }, + "cycleSymptomLowMood": "low mood", + "@cycleSymptomLowMood": { + "description": "Symptom chip: low mood." + }, + "cycleSymptomAcne": "acne", + "@cycleSymptomAcne": { + "description": "Symptom chip: acne." + }, + "cycleSymptomTenderBreasts": "tender breasts", + "@cycleSymptomTenderBreasts": { + "description": "Symptom chip: tender breasts." + }, + "cycleSymptomNausea": "nausea", + "@cycleSymptomNausea": { + "description": "Symptom chip: nausea." + }, + "cycleThisCycle": "This cycle", + "@cycleThisCycle": { + "description": "Section title for the current-cycle chart on the cycle-history screen." + }, + "cycleByDayOfYourCycle": "By day of your cycle", + "@cycleByDayOfYourCycle": { + "description": "Section title for the by-cycle-day comparison charts." + }, + "cycleHowLongCyclesBeen": "How long your cycles have been", + "@cycleHowLongCyclesBeen": { + "description": "Section title for the cycle-lengths-over-time chart." + }, + "cycleRestingHeartRate": "Resting heart rate", + "@cycleRestingHeartRate": { + "description": "Chart title: resting heart rate." + }, + "cycleUnitBpm": "bpm", + "@cycleUnitBpm": { + "description": "Unit abbreviation for beats per minute." + }, + "cycleHrvRmssdTitle": "HRV (RMSSD)", + "@cycleHrvRmssdTitle": { + "description": "Chart title: heart-rate variability (RMSSD)." + }, + "cycleUnitMs": "ms", + "@cycleUnitMs": { + "description": "Unit abbreviation for milliseconds." + }, + "cycleNotEnoughDescribeDayTitle": "Not enough cycles to describe a cycle day yet", + "@cycleNotEnoughDescribeDayTitle": { + "description": "Status card title when not enough cycles exist to chart cycle-day medians." + }, + "cycleNotEnoughDescribeDayBody": "Every point here is the middle of the same day across two or more of your own cycles. Nothing has two behind it yet.", + "@cycleNotEnoughDescribeDayBody": { + "description": "Status card body when not enough cycles exist to chart cycle-day medians." + }, + "cycleOwnPastCyclesDescribed": "Your own past cycles, described. Days that only one cycle reached are left empty rather than drawn — one night is not a middle. It describes what happened, not what will.", + "@cycleOwnPastCyclesDescribed": { + "description": "Explanatory footer under the by-cycle-day charts." + }, + "cycleDayOneLabel": "Day 1", + "@cycleDayOneLabel": { + "description": "X-axis label: day 1 of the cycle." + }, + "cycleDayNLabel": "Day {n}", + "@cycleDayNLabel": { + "description": "X-axis label: the last drawn day of the cycle.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "cycleMiddleOfNCycles": "Middle of {n} cycles at each day.", + "@cycleMiddleOfNCycles": { + "description": "Chart footnote stating how many cycles each median point is drawn from, when the count is the same for every point.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "cycleMiddleOfRangeCycles": "Middle of between {lo} and {hi} cycles at each day.", + "@cycleMiddleOfRangeCycles": { + "description": "Chart footnote stating the range of cycles each median point is drawn from, when the count varies.", + "placeholders": { + "lo": { + "type": "int" + }, + "hi": { + "type": "int" + } + } + }, + "cycleNotEnoughCompareTitle": "Not enough cycles to compare a day against itself", + "@cycleNotEnoughCompareTitle": { + "description": "Status card title when there aren't enough previous cycles to compare today against." + }, + "cycleCompareBodyGeneric": "This puts today next to the same day of your own previous cycles. It needs three of them that got that far.", + "@cycleCompareBodyGeneric": { + "description": "Status card body for the day-against-itself comparison when today's cycle day is unknown." + }, + "cycleCompareBodyWithDay": "This puts today next to the same day of your own previous cycles. It needs three of them that reached day {day}.", + "@cycleCompareBodyWithDay": { + "description": "Status card body for the day-against-itself comparison, naming today's cycle day.", + "placeholders": { + "day": { + "type": "int" + } + } + }, + "cycleNightOfLabel": "NIGHT OF {date}", + "@cycleNightOfLabel": { + "description": "Overline label naming the night the day-against-itself comparison is about.", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "cycleComparisonNotCorrection": "A comparison, not a correction. Nothing on your readiness has been rescaled by this, and nothing here is a training instruction.", + "@cycleComparisonNotCorrection": { + "description": "Disclaimer under the day-against-itself comparison." + }, + "cycleCompareHrvLabel": "HRV", + "@cycleCompareHrvLabel": { + "description": "Short label for HRV used in the day-against-itself comparison line." + }, + "cycleCompareLine": "{label} {z1} vs your last 3 weeks, {z2} vs your last {n} day-{cycleDay}s.", + "@cycleCompareLine": { + "description": "One line of the day-against-itself comparison, e.g. 'HRV +1.2 vs your last 3 weeks, -0.3 vs your last 4 day-22s.'", + "placeholders": { + "label": { + "type": "String" + }, + "z1": { + "type": "String" + }, + "z2": { + "type": "String" + }, + "n": { + "type": "int" + }, + "cycleDay": { + "type": "int" + } + } + }, + "cycleLengthsTitle": "Your cycle lengths against a published range", + "@cycleLengthsTitle": { + "description": "Status card title offering to show cycle-length-vs-published-range chart (opt-in)." + }, + "cycleLengthsBody": "Off unless you ask for it. It draws the days between your own logged starts next to the range published for an adult cycle, and says nothing else about them.", + "@cycleLengthsBody": { + "description": "Status card body explaining the cycle-length review is opt-in." + }, + "cycleShowIt": "Show it", + "@cycleShowIt": { + "description": "Button label to opt in and show the cycle-lengths chart." + }, + "cycleNotEnoughLoggedTitle": "Not enough logged cycles yet", + "@cycleNotEnoughLoggedTitle": { + "description": "Status card title when too few cycle-length gaps have been logged." + }, + "cycleNotEnoughLoggedBody": "This needs a long run: {n} of {total} gaps so far, which is about a year of logging every start.", + "@cycleNotEnoughLoggedBody": { + "description": "Status card body when too few cycle-length gaps have been logged.", + "placeholders": { + "n": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "cycleGapTitle": "There is a gap in your logged starts", + "@cycleGapTitle": { + "description": "Status card title when a suspiciously long gap breaks the cycle-length chart." + }, + "cycleGapBody": "One of them is more than {days} days after the one before it. A start you never logged and a cycle that genuinely ran that long look the same from here, so nothing is drawn.", + "@cycleGapBody": { + "description": "Status card body when a suspiciously long gap breaks the cycle-length chart.", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "cycleDaysBetweenStarts": "Days between your logged starts", + "@cycleDaysBetweenStarts": { + "description": "Chart title: days between logged period starts." + }, + "cycleUnitDays": "days", + "@cycleUnitDays": { + "description": "Unit label for days." + }, + "cycleLegendYourCycles": "Your cycles", + "@cycleLegendYourCycles": { + "description": "Chart legend entry for the user's own cycle-length bars." + }, + "cycleLegendPublishedRange": "Published range", + "@cycleLegendPublishedRange": { + "description": "Chart legend entry for the published cycle-length reference range." + }, + "cycleTwoLinesFootnote": "The two lines are {low} and {high} days.", + "@cycleTwoLinesFootnote": { + "description": "Chart footnote naming the two published reference-range boundary days.", + "placeholders": { + "low": { + "type": "int" + }, + "high": { + "type": "int" + } + } + }, + "cycleLengthChangesReasons": "Cycle length changes for many reasons — thyroid, stress, weight change, contraception, PCOS and others. This is your own logged data next to a published range. It is a reason to ask a clinician, not an answer from one.", + "@cycleLengthChangesReasons": { + "description": "Disclaimer under the cycle-length chart about medical causes of variation." + }, + "cycleHideLengths": "Hide cycle lengths", + "@cycleHideLengths": { + "description": "Button label to opt back out of the cycle-lengths chart." + }, + "cycleDescriptiveOnly": "Descriptive only.", + "@cycleDescriptiveOnly": { + "description": "Footnote on the current-cycle resting-heart-rate chart." + }, + "cycleNotEnoughDerivedNights": "Not enough derived nights this cycle yet", + "@cycleNotEnoughDerivedNights": { + "description": "Empty-state message on the current-cycle chart when too few nights have derived data." + }, + "cycleMdcNoteInsideSpread": " Every day drawn here is inside your own night-to-night spread: the biggest gap between two of them is {s}, and {n} is the smallest change this can tell from noise. A shape, not a shift.", + "@cycleMdcNoteInsideSpread": { + "description": "Chart footnote addendum when the drawn swing is smaller than the user's own night-to-night noise.", + "placeholders": { + "s": { + "type": "String" + }, + "n": { + "type": "String" + } + } + }, + "cycleMdcNoteVaries": " Your nights vary by {n} on their own, so days closer together than that are not separated. The biggest gap here is {s}.", + "@cycleMdcNoteVaries": { + "description": "Chart footnote addendum when the drawn swing exceeds the user's own night-to-night noise.", + "placeholders": { + "n": { + "type": "String" + }, + "s": { + "type": "String" + } + } + }, + "healthTabOverview": "Overview", + "@healthTabOverview": { + "description": "Health screen sub-tab label: Overview" + }, + "healthTabExplore": "Explore", + "@healthTabExplore": { + "description": "Health screen sub-tab label: Explore" + }, + "healthTabTrends": "Trends", + "@healthTabTrends": { + "description": "Health screen sub-tab label: Trends" + }, + "healthTabVitals": "Vitals", + "@healthTabVitals": { + "description": "Health screen sub-tab label: Vitals" + }, + "healthTabLabs": "Labs", + "@healthTabLabs": { + "description": "Health screen sub-tab label: Labs" + }, + "healthTitle": "Health", + "@healthTitle": { + "description": "Health screen page title" + }, + "healthCouldNotRead": "Could not read your {what}", + "@healthCouldNotRead": { + "description": "Read-failure card title, e.g. Could not read your vitals", + "placeholders": { + "what": { + "type": "String" + } + } + }, + "healthReadFailedBody": "The stored rows failed to load. Nothing was deleted — this is a read that went wrong.", + "@healthReadFailedBody": { + "description": "Read-failure card body" + }, + "healthTryAgain": "Try again", + "@healthTryAgain": { + "description": "Read-failure card retry button" + }, + "healthWhatVitals": "vitals", + "@healthWhatVitals": { + "description": "Noun 'vitals' plugged into the read-failure title" + }, + "healthWhatLabResults": "lab results", + "@healthWhatLabResults": { + "description": "Noun 'lab results' plugged into the read-failure title" + }, + "healthMeasuresUnit": "measures", + "@healthMeasuresUnit": { + "description": "Noun 'measures' — used both as the read-failure title noun and as the Explore consistency-tile unit" + }, + "healthRowRestingHr": "Resting heart rate", + "@healthRowRestingHr": { + "description": "Overview/Trends/Vitals row label: resting heart rate" + }, + "healthRowHrv": "HRV", + "@healthRowHrv": { + "description": "Overview/Trends row label: HRV" + }, + "healthRowSleep": "Sleep", + "@healthRowSleep": { + "description": "Overview row label and Explore catalogue category title: Sleep" + }, + "healthRowStress": "Stress", + "@healthRowStress": { + "description": "Overview row label and sub-label fallback: Stress" + }, + "healthRowRespRate": "Respiratory rate", + "@healthRowRespRate": { + "description": "Overview/Vitals row label: respiratory rate" + }, + "healthSubOvernight": "Overnight", + "@healthSubOvernight": { + "description": "Overview row sub-label: overnight reading" + }, + "healthSubRmssdAsleep": "RMSSD, asleep", + "@healthSubRmssdAsleep": { + "description": "Overview HRV row sub-label" + }, + "healthSubLastNight": "Last night", + "@healthSubLastNight": { + "description": "Sub-label / chart axis label meaning last night" + }, + "healthSubAsleep": "Asleep", + "@healthSubAsleep": { + "description": "Overview/Vitals row sub-label: asleep reading" + }, + "healthNoMetric": "No {name}", + "@healthNoMetric": { + "description": "Empty-state card title for a metric with no value, e.g. 'No resting heart rate'", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "healthWhyReadFromSleep": "Read from sleep, and no night was scored.", + "@healthWhyReadFromSleep": { + "description": "Why-absent reason: resting HR is read from sleep and no night was scored" + }, + "healthWhyReadOnlyFromSleep": "Read only from sleep, and no night was scored.", + "@healthWhyReadOnlyFromSleep": { + "description": "Why-absent reason: HRV/respiratory rate read only from sleep and no night was scored" + }, + "healthWhySleepNotLongEnough": "No sleep period long enough to score was recorded.", + "@healthWhySleepNotLongEnough": { + "description": "Why-absent reason: no sleep period long enough to score" + }, + "healthWhyReadFromNight": "Read from the night, and no night was scored.", + "@healthWhyReadFromNight": { + "description": "Why-absent reason: stress read from the night and no night was scored" + }, + "healthWhyNoReadingLastNight": "No reading from last night.", + "@healthWhyNoReadingLastNight": { + "description": "Why-absent reason: no respiratory reading from last night" + }, + "healthIllnessRedTitle": "Several nights in a row are away from your normal", + "@healthIllnessRedTitle": { + "description": "Illness observation card title, red/severe state" + }, + "healthIllnessLastNightTitle": "Last night sat outside your normal range", + "@healthIllnessLastNightTitle": { + "description": "Illness observation card title when the flagged night is last night" + }, + "healthIllnessDayTitle": "{day} sat outside your normal range", + "@healthIllnessDayTitle": { + "description": "Illness observation card title when the flagged night is an earlier day", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "healthIllnessBodyNoZ": "Your nocturnal resting heart rate has been running above your own baseline. This watches one signal only. It names a pattern, not a cause.", + "@healthIllnessBodyNoZ": { + "description": "Illness observation card body when no z-score is available" + }, + "healthIllnessBodyWithZ": "Your nocturnal resting heart rate has been running above your own baseline; that night sat {z} standard deviations {direction} it. This watches one signal only. It names a pattern, not a cause.", + "@healthIllnessBodyWithZ": { + "description": "Illness observation card body including the standard-deviation figure and direction", + "placeholders": { + "z": { + "type": "String" + }, + "direction": { + "type": "String" + } + } + }, + "healthDirectionAbove": "above", + "@healthDirectionAbove": { + "description": "Direction word: above (baseline)" + }, + "healthDirectionBelow": "below", + "@healthDirectionBelow": { + "description": "Direction word: below (baseline)" + }, + "healthIllnessAdvice": "Worth noting if it continues past a couple of days.", + "@healthIllnessAdvice": { + "description": "Illness observation card advice line" + }, + "healthObservationsTitle": "Observations", + "@healthObservationsTitle": { + "description": "Section title over the illness/findings observation card" + }, + "healthSeeAll": "See all", + "@healthSeeAll": { + "description": "Section action button: open the full findings log" + }, + "healthNapsTitle": "Naps", + "@healthNapsTitle": { + "description": "Section title: Naps" + }, + "healthNoNapReading": "No nap reading", + "@healthNoNapReading": { + "description": "Empty-state title when there is no nap day to name" + }, + "healthNoNapReadingFor": "No nap reading for {day}", + "@healthNoNapReadingFor": { + "description": "Empty-state title when there is no nap reading for a named day", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "healthNapsBody": "Naps come off the same second-by-second recording as the rest of the day, and this day does not have enough of it.", + "@healthNapsBody": { + "description": "Empty-state body explaining why naps may be missing" + }, + "healthDaytimeSleep": "Daytime sleep", + "@healthDaytimeSleep": { + "description": "Nap row label" + }, + "healthValueNone": "None", + "@healthValueNone": { + "description": "Value shown when zero naps were measured (a measured zero, not absence)" + }, + "healthNoneDetectedOn": "None detected · {day}", + "@healthNoneDetectedOn": { + "description": "Nap row sub-label when zero naps were detected on a day", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "healthNapCountLabel": "{n, plural, one{{n} nap} other{{n} naps}}", + "@healthNapCountLabel": { + "description": "Nap row sub-label: N naps counted, ICU plural", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "healthAddOrCorrect": "Add or correct", + "@healthAddOrCorrect": { + "description": "Naps section action button" + }, + "healthNoTrendYet": "No {label} trend yet", + "@healthNoTrendYet": { + "description": "Trend card empty-state title", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "healthZeroDaysStored": "0 days stored.", + "@healthZeroDaysStored": { + "description": "Trend card empty-state body" + }, + "healthVsDayAverage": "vs your {days}-day average", + "@healthVsDayAverage": { + "description": "Trend card comparison window label", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "healthAsOf": " · as of {date}", + "@healthAsOf": { + "description": "Trend card suffix noting the hero number's date when it is not today", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "healthNoBaseline": "no baseline", + "@healthNoBaseline": { + "description": "Trend card delta label when there is no baseline to compare to" + }, + "healthFirstReadings": "first readings", + "@healthFirstReadings": { + "description": "Trend card window label on the very first readings" + }, + "healthTimeAsleep": "Time asleep", + "@healthTimeAsleep": { + "description": "Trends tab sleep trend card label" + }, + "healthVsNeed": "vs your {need} need", + "@healthVsNeed": { + "description": "Trend card comparison label against the computed sleep need", + "placeholders": { + "need": { + "type": "String" + } + } + }, + "healthBodyClockTitle": "Body clock", + "@healthBodyClockTitle": { + "description": "Section title: Body clock" + }, + "healthChronotypeJetlagRegularity": "Chronotype, jetlag and regularity", + "@healthChronotypeJetlagRegularity": { + "description": "Body clock card subtitle" + }, + "healthChronotypeLabel": "CHRONOTYPE", + "@healthChronotypeLabel": { + "description": "Body clock inline-metric label: CHRONOTYPE" + }, + "healthSocialJetlagLabel": "SOCIAL JETLAG", + "@healthSocialJetlagLabel": { + "description": "Body clock inline-metric label: SOCIAL JETLAG" + }, + "healthRegularityLabel": "REGULARITY", + "@healthRegularityLabel": { + "description": "Body clock inline-metric label: REGULARITY" + }, + "healthConsistencyTitle": "Consistency", + "@healthConsistencyTitle": { + "description": "Section title: Consistency" + }, + "healthDaysWithRecord": "Days with a derived record in the last 30 days", + "@healthDaysWithRecord": { + "description": "Consistency tile caption" + }, + "healthToday": "Today", + "@healthToday": { + "description": "Word 'Today' used as a day label" + }, + "healthRowHeartRate": "Heart rate", + "@healthRowHeartRate": { + "description": "Vitals row label: heart rate range" + }, + "healthRowSkinTemp": "Skin temperature", + "@healthRowSkinTemp": { + "description": "Vitals row label: skin temperature" + }, + "healthVsOwnNights": "vs your own nights", + "@healthVsOwnNights": { + "description": "Skin temperature row sub-label, no dated night" + }, + "healthVsOwnNightsOn": "vs your own nights · {day}", + "@healthVsOwnNightsOn": { + "description": "Skin temperature row sub-label with a dated night", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "healthRowWearTime": "Wear time", + "@healthRowWearTime": { + "description": "Vitals row label: wear time" + }, + "healthTheDay": "the day", + "@healthTheDay": { + "description": "Phrase 'the day' used in the wear-time coverage sentence when today" + }, + "healthCoverageOf": "{pct}% of {day}", + "@healthCoverageOf": { + "description": "Wear-time coverage sentence: N% of the day / of a named day", + "placeholders": { + "pct": { + "type": "int" + }, + "day": { + "type": "String" + } + } + }, + "healthNothingMeasuredDay": "Nothing measured for this day", + "@healthNothingMeasuredDay": { + "description": "Vitals empty-state title: nothing measured this day" + }, + "healthNoBandRecordings": "No band recordings reached this day.", + "@healthNoBandRecordings": { + "description": "Vitals empty-state body" + }, + "healthSyncTheBand": "Sync the band", + "@healthSyncTheBand": { + "description": "Vitals empty-state fix button: sync the band" + }, + "healthDeepDivesTitle": "Deep dives", + "@healthDeepDivesTitle": { + "description": "Section title: Deep dives" + }, + "healthHeartRateVariability": "Heart rate variability", + "@healthHeartRateVariability": { + "description": "Deep-dive card title: heart rate variability" + }, + "healthTimeFrequencyNonLinear": "Time, frequency and non-linear", + "@healthTimeFrequencyNonLinear": { + "description": "Deep-dive card subtitle" + }, + "healthRmssdOfLastNights": "RMSSD, {have} of the last {days} nights", + "@healthRmssdOfLastNights": { + "description": "HRV preview chart title: RMSSD, N of the last M nights", + "placeholders": { + "have": { + "type": "int" + }, + "days": { + "type": "int" + } + } + }, + "healthNightsAgo": "{n} nights ago", + "@healthNightsAgo": { + "description": "HRV preview chart x-axis label: N nights ago", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "healthOneNightNotTrend": "One night is not a trend yet", + "@healthOneNightNotTrend": { + "description": "HRV preview chart empty message when fewer than two nights are stored" + }, + "healthMeasuresWithHistory": "Measures with stored history on this device", + "@healthMeasuresWithHistory": { + "description": "Explore tab consistency tile caption" + }, + "healthEachOneOpens": "Each one opens its chart, your own range, and how it is worked out.", + "@healthEachOneOpens": { + "description": "Explore tab intro line explaining what a tap gets you" + }, + "healthCatHeartRhythm": "Heart & rhythm", + "@healthCatHeartRhythm": { + "description": "Explore catalogue category title: Heart & rhythm" + }, + "healthCatBreathing": "Breathing", + "@healthCatBreathing": { + "description": "Explore catalogue category title: Breathing" + }, + "healthCatMovementLoad": "Movement & load", + "@healthCatMovementLoad": { + "description": "Explore catalogue category title: Movement & load" + }, + "healthCatBodyWear": "Body & wear", + "@healthCatBodyWear": { + "description": "Explore catalogue category title: Body & wear" + }, + "healthBlurbRestingHr": "The lowest sustained rate of the night", + "@healthBlurbRestingHr": { + "description": "Explore catalogue one-line description for resting heart rate" + }, + "healthBlurbHrv": "RMSSD over the cleanest window of sleep", + "@healthBlurbHrv": { + "description": "Explore catalogue one-line description for HRV" + }, + "healthBlurbHrvCv": "How much that swings from night to night", + "@healthBlurbHrvCv": { + "description": "Explore catalogue one-line description for HRV night-to-night variability" + }, + "healthBlurbLfHf": "Where beat-timing power sits across frequencies", + "@healthBlurbLfHf": { + "description": "Explore catalogue one-line description for LF/HF" + }, + "healthBlurbDip": "How far your heart rate falls while you sleep", + "@healthBlurbDip": { + "description": "Explore catalogue one-line description for heart-rate dip" + }, + "healthBlurbHrr": "How fast it falls in the minute after a bout", + "@healthBlurbHrr": { + "description": "Explore catalogue one-line description for heart-rate recovery" + }, + "healthBlurbSleep": "Time asleep, from motion and beat timing", + "@healthBlurbSleep": { + "description": "Explore catalogue one-line description for sleep duration" + }, + "healthBlurbEfficiency": "Asleep as a share of time in bed", + "@healthBlurbEfficiency": { + "description": "Explore catalogue one-line description for sleep efficiency" + }, + "healthBlurbDeep": "Heart-rate flatness inside NREM", + "@healthBlurbDeep": { + "description": "Explore catalogue one-line description for deep sleep" + }, + "healthBlurbRem": "Staged from beat variability and movement", + "@healthBlurbRem": { + "description": "Explore catalogue one-line description for REM sleep" + }, + "healthBlurbNapMin": "Sleep detected outside the main night", + "@healthBlurbNapMin": { + "description": "Explore catalogue one-line description for nap minutes" + }, + "healthBlurbRespRate": "Breaths per minute, recovered from beat timing", + "@healthBlurbRespRate": { + "description": "Explore catalogue one-line description for respiratory rate" + }, + "healthBlurbBrv": "How much that rate varies across the night", + "@healthBlurbBrv": { + "description": "Explore catalogue one-line description for breathing rate variability" + }, + "healthBlurbSteps": "Counted by a pedometer, never modelled", + "@healthBlurbSteps": { + "description": "Explore catalogue one-line description for steps" + }, + "healthBlurbActiveMin": "Minutes of movement volume, not locomotion", + "@healthBlurbActiveMin": { + "description": "Explore catalogue one-line description for active minutes" + }, + "healthBlurbCalories": "Active energy from heart rate and your profile", + "@healthBlurbCalories": { + "description": "Explore catalogue one-line description for calories" + }, + "healthBlurbStrain": "Cardiovascular load over the day, on 0–21", + "@healthBlurbStrain": { + "description": "Explore catalogue one-line description for strain" + }, + "healthBlurbTrimp": "Time in each zone, weighted by its cost", + "@healthBlurbTrimp": { + "description": "Explore catalogue one-line description for TRIMP" + }, + "healthBlurbSkinTemp": "Distance from your own recent nights", + "@healthBlurbSkinTemp": { + "description": "Explore catalogue one-line description for skin temperature" + }, + "healthBlurbWear": "Minutes with a band record present", + "@healthBlurbWear": { + "description": "Explore catalogue one-line description for wear time" + }, + "healthNothingMeasuredHere": "Nothing measured here yet", + "@healthNothingMeasuredHere": { + "description": "Explore family empty-state title when no row in the family has history" + }, + "healthNotMeasuredYet": "Not measured yet", + "@healthNotMeasuredYet": { + "description": "Explore family empty-state title when some rows in the family have history" + }, + "healthNoDayProduced": "No day on this device has produced one yet.", + "@healthNoDayProduced": { + "description": "Explore family empty-state trailing sentence" + }, + "healthNoLabResults": "No lab results", + "@healthNoLabResults": { + "description": "Labs tab empty-state title" + }, + "healthNoLabResultsBody": "Nothing logged. Anything you add here stays on this device, and anything you remove is gone from it.", + "@healthNoLabResultsBody": { + "description": "Labs tab empty-state body" + }, + "healthLastPanel": "Last panel {date} · logged by hand", + "@healthLastPanel": { + "description": "Labs tab footer: date of the last logged panel", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "healthMarkersYouNamed": "Markers you named", + "@healthMarkersYouNamed": { + "description": "Section title over the user's custom lab markers" + }, + "healthAddAResult": "Add a result", + "@healthAddAResult": { + "description": "Button/dialog title: add a lab result" + }, + "healthRangesDifferByLab": "Ranges differ by lab. Use the one on your report.", + "@healthRangesDifferByLab": { + "description": "Labs tab footer caveat about reference ranges" + }, + "healthRemoveMarkerFrom": "Remove {marker} from {date}", + "@healthRemoveMarkerFrom": { + "description": "Accessibility label for a lab-result row's remove action", + "placeholders": { + "marker": { + "type": "String" + }, + "date": { + "type": "String" + } + } + }, + "healthNoReferenceInterval": "No reference interval · {date}", + "@healthNoReferenceInterval": { + "description": "Lab result row sub-label when the marker has no reference range", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "healthTypicalRange": "Typical {low}–{high} · {date}", + "@healthTypicalRange": { + "description": "Lab result row sub-label showing the typical reference range", + "placeholders": { + "low": { + "type": "String" + }, + "high": { + "type": "String" + }, + "date": { + "type": "String" + } + } + }, + "healthRemoveLabelFrom": "Remove {label} from {date}?", + "@healthRemoveLabelFrom": { + "description": "Confirm-remove dialog title for a lab result", + "placeholders": { + "label": { + "type": "String" + }, + "date": { + "type": "String" + } + } + }, + "healthRemoveLabBody": "The {value} {unit} you logged for that draw. It leaves this device and there is no undo.", + "@healthRemoveLabBody": { + "description": "Confirm-remove dialog body for a lab result, first sentence", + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "healthRemoveLabOlderNote": " Your {date} draw stays, and shows here instead.", + "@healthRemoveLabOlderNote": { + "description": "Confirm-remove dialog body addendum noting an older draw remains", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "healthRemovedNoneLeft": "Removed {label} from {date}. No {label} results left.", + "@healthRemovedNoneLeft": { + "description": "Snackbar after removing a lab result when none remain for that marker", + "placeholders": { + "label": { + "type": "String" + }, + "date": { + "type": "String" + } + } + }, + "healthRemovedShowingOlder": "Removed {label} from {date}. Showing your {older} draw now.", + "@healthRemovedShowingOlder": { + "description": "Snackbar after removing a lab result when an older draw now shows instead", + "placeholders": { + "label": { + "type": "String" + }, + "date": { + "type": "String" + }, + "older": { + "type": "String" + } + } + }, + "healthRemoveTheMarker": "Remove the {label} marker", + "@healthRemoveTheMarker": { + "description": "Accessibility label for removing a custom marker definition", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "healthNothingLoggedUnderIt": "Nothing logged under it", + "@healthNothingLoggedUnderIt": { + "description": "Custom marker row sub-label when it holds zero results" + }, + "healthResultsCount": "{n, plural, one{{n} result · {unit}} other{{n} results · {unit}}}", + "@healthResultsCount": { + "description": "Custom marker row sub-label: N results, ICU plural", + "placeholders": { + "n": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "healthStillHoldsResults": "{count, plural, one{{label} still holds {count} result. Remove those first — the marker is what labels them.} other{{label} still holds {count} results. Remove those first — the marker is what labels them.}}", + "@healthStillHoldsResults": { + "description": "Snackbar refusing to remove a marker definition that still holds results, ICU plural", + "placeholders": { + "count": { + "type": "int" + }, + "label": { + "type": "String" + } + } + }, + "healthRemoveMarkerQ": "Remove {label}?", + "@healthRemoveMarkerQ": { + "description": "Confirm-remove dialog title for a custom marker definition", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "healthRemoveMarkerBody": "It leaves the marker list, so you can no longer log it. Nothing measured goes with it — you have no results under it.", + "@healthRemoveMarkerBody": { + "description": "Confirm-remove dialog body for a custom marker definition" + }, + "healthMarkerLabel": "Marker", + "@healthMarkerLabel": { + "description": "Add-result dialog: accessibility label for the marker dropdown" + }, + "healthValueUnit": "Value ({unit})", + "@healthValueUnit": { + "description": "Add-result dialog: value field label with unit", + "placeholders": { + "unit": { + "type": "String" + } + } + }, + "healthDateDrawn": "Date drawn (YYYY-MM-DD)", + "@healthDateDrawn": { + "description": "Add-result dialog: date field label" + }, + "healthValueMustBeNumber": "The value needs to be a number on its own, without the unit. Nothing was saved.", + "@healthValueMustBeNumber": { + "description": "Add-result dialog validation error: value must be a plain number" + }, + "healthDateFormatError": "The date needs to be YYYY-MM-DD. Nothing was saved.", + "@healthDateFormatError": { + "description": "Add-result dialog validation error: date format" + }, + "healthCouldNotSaveIt": "Could not save it: {error}", + "@healthCouldNotSaveIt": { + "description": "Add-result dialog snackbar on save failure", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "homeStepSensorStrapPhone": "Strap + phone", + "@homeStepSensorStrapPhone": { + "description": "Step count sensor label: both the strap and phone counted." + }, + "homeStepSensorStrap": "Strap", + "@homeStepSensorStrap": { + "description": "Step count sensor label: the strap counted." + }, + "homeStepSensorPhone": "Phone", + "@homeStepSensorPhone": { + "description": "Step count sensor label: the phone counted." + }, + "homeOvernightBuilding": "Last night is still being worked out.", + "@homeOvernightBuilding": { + "description": "Home ring gap reason: last night's data is still being derived." + }, + "homeOvernightNothingYet": "Nothing from last night has reached the app yet.", + "@homeOvernightNothingYet": { + "description": "Home ring gap reason: nothing from last night has synced yet." + }, + "homeMonthJanuary": "January", + "@homeMonthJanuary": { + "description": "Full month name: January, used in the Home date line." + }, + "homeMonthFebruary": "February", + "@homeMonthFebruary": { + "description": "Full month name: February, used in the Home date line." + }, + "homeMonthMarch": "March", + "@homeMonthMarch": { + "description": "Full month name: March, used in the Home date line." + }, + "homeMonthApril": "April", + "@homeMonthApril": { + "description": "Full month name: April, used in the Home date line." + }, + "homeMonthMay": "May", + "@homeMonthMay": { + "description": "Full month name: May, used in the Home date line." + }, + "homeMonthJune": "June", + "@homeMonthJune": { + "description": "Full month name: June, used in the Home date line." + }, + "homeMonthJuly": "July", + "@homeMonthJuly": { + "description": "Full month name: July, used in the Home date line." + }, + "homeMonthAugust": "August", + "@homeMonthAugust": { + "description": "Full month name: August, used in the Home date line." + }, + "homeMonthSeptember": "September", + "@homeMonthSeptember": { + "description": "Full month name: September, used in the Home date line." + }, + "homeMonthOctober": "October", + "@homeMonthOctober": { + "description": "Full month name: October, used in the Home date line." + }, + "homeMonthNovember": "November", + "@homeMonthNovember": { + "description": "Full month name: November, used in the Home date line." + }, + "homeMonthDecember": "December", + "@homeMonthDecember": { + "description": "Full month name: December, used in the Home date line." + }, + "homeMonthJanuaryShort": "Jan", + "@homeMonthJanuaryShort": { + "description": "Abbreviated month name: January, for compact date chips (e.g. \"Tue 11 Jan\")." + }, + "homeMonthFebruaryShort": "Feb", + "@homeMonthFebruaryShort": { + "description": "Abbreviated month name: February, for compact date chips." + }, + "homeMonthMarchShort": "Mar", + "@homeMonthMarchShort": { + "description": "Abbreviated month name: March, for compact date chips." + }, + "homeMonthAprilShort": "Apr", + "@homeMonthAprilShort": { + "description": "Abbreviated month name: April, for compact date chips." + }, + "homeMonthMayShort": "May", + "@homeMonthMayShort": { + "description": "Abbreviated month name: May, for compact date chips." + }, + "homeMonthJuneShort": "Jun", + "@homeMonthJuneShort": { + "description": "Abbreviated month name: June, for compact date chips." + }, + "homeMonthJulyShort": "Jul", + "@homeMonthJulyShort": { + "description": "Abbreviated month name: July, for compact date chips." + }, + "homeMonthAugustShort": "Aug", + "@homeMonthAugustShort": { + "description": "Abbreviated month name: August, for compact date chips." + }, + "homeMonthSeptemberShort": "Sep", + "@homeMonthSeptemberShort": { + "description": "Abbreviated month name: September, for compact date chips." + }, + "homeMonthOctoberShort": "Oct", + "@homeMonthOctoberShort": { + "description": "Abbreviated month name: October, for compact date chips." + }, + "homeMonthNovemberShort": "Nov", + "@homeMonthNovemberShort": { + "description": "Abbreviated month name: November, for compact date chips." + }, + "homeMonthDecemberShort": "Dec", + "@homeMonthDecemberShort": { + "description": "Abbreviated month name: December, for compact date chips." + }, + "homeWeekdayMonday": "Monday", + "@homeWeekdayMonday": { + "description": "Full weekday name: Monday, used in the Home date line." + }, + "homeWeekdayTuesday": "Tuesday", + "@homeWeekdayTuesday": { + "description": "Full weekday name: Tuesday, used in the Home date line." + }, + "homeWeekdayWednesday": "Wednesday", + "@homeWeekdayWednesday": { + "description": "Full weekday name: Wednesday, used in the Home date line." + }, + "homeWeekdayThursday": "Thursday", + "@homeWeekdayThursday": { + "description": "Full weekday name: Thursday, used in the Home date line." + }, + "homeWeekdayFriday": "Friday", + "@homeWeekdayFriday": { + "description": "Full weekday name: Friday, used in the Home date line." + }, + "homeWeekdaySaturday": "Saturday", + "@homeWeekdaySaturday": { + "description": "Full weekday name: Saturday, used in the Home date line." + }, + "homeWeekdaySunday": "Sunday", + "@homeWeekdaySunday": { + "description": "Full weekday name: Sunday, used in the Home date line." + }, + "homeReadinessNotScored": "Not scored", + "@homeReadinessNotScored": { + "description": "Readiness band label when there is no score." + }, + "homeReadinessGoodToGo": "Good to go", + "@homeReadinessGoodToGo": { + "description": "Readiness band label, top tier." + }, + "homeReadinessSteady": "Steady", + "@homeReadinessSteady": { + "description": "Readiness band label, second-highest tier." + }, + "homeReadinessTakeItEasy": "Take it easy", + "@homeReadinessTakeItEasy": { + "description": "Readiness band label, second-lowest tier." + }, + "homeReadinessRestToday": "Rest today", + "@homeReadinessRestToday": { + "description": "Readiness band label, lowest tier." + }, + "homeDriverHrv": "HRV", + "@homeDriverHrv": { + "description": "Readiness driver name: heart rate variability, shown in the Why? row on Home." + }, + "homeDriverRhr": "Resting heart rate", + "@homeDriverRhr": { + "description": "Readiness driver name: resting heart rate, shown in the Why? row on Home." + }, + "homeDriverResp": "Breathing rate", + "@homeDriverResp": { + "description": "Readiness driver name: breathing rate, shown in the Why? row on Home." + }, + "homeDriverTemp": "Skin temperature", + "@homeDriverTemp": { + "description": "Readiness driver name: skin temperature, shown in the Why? row on Home." + }, + "homeDbRebuiltTitle": "Your database was rebuilt to start the app", + "@homeDbRebuiltTitle": { + "description": "Title of the card shown when the local database had to be rebuilt to start the app." + }, + "homeDbRebuiltNothingRecovered": "Nothing could be read back.", + "@homeDbRebuiltNothingRecovered": { + "description": "Database-rebuilt card: nothing could be salvaged from the old file." + }, + "homeDbRebuiltRecovered": "Recovered: {list}.", + "@homeDbRebuiltRecovered": { + "description": "Database-rebuilt card: list of tables and row counts that were recovered.", + "placeholders": { + "list": { + "type": "String", + "example": "sleep 42 · workouts 3" + } + } + }, + "homeDbRebuiltEmpty": "Empty: {list}.", + "@homeDbRebuiltEmpty": { + "description": "Database-rebuilt card: list of tables that came back empty.", + "placeholders": { + "list": { + "type": "String", + "example": "nutrition" + } + } + }, + "homeDbRebuiltKept": "The original file is kept at {path} — nothing was deleted.", + "@homeDbRebuiltKept": { + "description": "Database-rebuilt card: reassurance that the original file was kept, with its path.", + "placeholders": { + "path": { + "type": "String", + "example": "/data/quarantine/old.db" + } + } + }, + "homeWorkoutHoldTitle": "A workout is still running", + "@homeWorkoutHoldTitle": { + "description": "Title of the card shown on Home while a workout session is live and derivation is paused." + }, + "homeWorkoutHoldBody": "Today is on hold while a workout is live: the band keeps recording, but the numbers are computed once the session ends. Finish the workout from the bar below and today fills in — syncing will not.", + "@homeWorkoutHoldBody": { + "description": "Body of the card shown on Home while a workout session is live and derivation is paused." + }, + "homeInsightsRebuildingTitle": "Your cross-day insights are being rebuilt", + "@homeInsightsRebuildingTitle": { + "description": "Title of the card shown when the cross-day insights rollup was withheld rather than shown." + }, + "homeInsightsRebuildingAlgoVersion": "How these are computed changed with the last update.", + "@homeInsightsRebuildingAlgoVersion": { + "description": "Reason the cross-day rollup is being rebuilt: the algorithm version changed." + }, + "homeInsightsStaleOverWeek": "The last rollup was built over a week ago, which is too old to stand behind.", + "@homeInsightsStaleOverWeek": { + "description": "Reason the cross-day rollup is being rebuilt: the last rollup is more than a week old." + }, + "homeInsightsStaleOnDay": "The last rollup was built on {day}, which is too old to stand behind.", + "@homeInsightsStaleOnDay": { + "description": "Reason the cross-day rollup is being rebuilt: names the day the last rollup was built.", + "placeholders": { + "day": { + "type": "String", + "example": "Saturday, 20 May" + } + } + }, + "homeInsightsNoVersionStamp": "The stored rollup carries no version stamp.", + "@homeInsightsNoVersionStamp": { + "description": "Reason the cross-day rollup is being rebuilt: the stored rollup has no version stamp." + }, + "homeSyncBand": "Sync the band", + "@homeSyncBand": { + "description": "Call-to-action button label: sync the band." + }, + "homeWhyLabel": "Why?", + "@homeWhyLabel": { + "description": "Short label introducing the readiness driver list on Home." + }, + "homeCalibrating": "Calibrating", + "@homeCalibrating": { + "description": "Ring value shown while a metric is still building its baseline." + }, + "homeCalibratingNights": "{have} of {need} nights", + "@homeCalibratingNights": { + "description": "Calibration progress caption under a ring, counted in nights.", + "placeholders": { + "have": { + "type": "int" + }, + "need": { + "type": "int" + } + } + }, + "homeCalibratingDays": "{have} of {need} days", + "@homeCalibratingDays": { + "description": "Calibration progress caption under a ring, counted in days.", + "placeholders": { + "have": { + "type": "int" + }, + "need": { + "type": "int" + } + } + }, + "homeGapNoReason": "Nothing recorded says why this is missing.", + "@homeGapNoReason": { + "description": "Fallback reason shown when a ring is empty and nothing explains why." + }, + "homeRingRecovery": "Recovery", + "@homeRingRecovery": { + "description": "Home ring label: Recovery (readiness)." + }, + "homeRingStrain": "Strain", + "@homeRingStrain": { + "description": "Home ring label: Strain." + }, + "homeRingSleep": "Sleep", + "@homeRingSleep": { + "description": "Home ring label: Sleep." + }, + "homeRingNoStrain": "No strain", + "@homeRingNoStrain": { + "description": "Strain ring value when there is no strain reading." + }, + "homeRingNoSleep": "No sleep", + "@homeRingNoSleep": { + "description": "Sleep ring value when there is no sleep reading." + }, + "homeSleepGapFallback": "No night long enough to score was recorded.", + "@homeSleepGapFallback": { + "description": "Fallback reason the sleep ring is empty: no night long enough to score." + }, + "homeStrainOf21": "of 21", + "@homeStrainOf21": { + "description": "Strain ring subtitle: out of the fixed 21-point scale." + }, + "homeSleepNoTarget": "No target yet", + "@homeSleepNoTarget": { + "description": "Sleep ring subtitle when no computed sleep-need target exists yet." + }, + "homeOfSpan": "of {duration}", + "@homeOfSpan": { + "description": "Sleep ring subtitle: duration measured against a computed sleep-need target.", + "placeholders": { + "duration": { + "type": "String", + "example": "7h 45m" + } + } + }, + "homeLoadFailedTitle": "Today could not be read", + "@homeLoadFailedTitle": { + "description": "Title of the card shown when today's data failed to load from the database." + }, + "homeLoadFailedBody": "The stored day failed to load. Nothing was deleted — this is a read that went wrong, not missing data.", + "@homeLoadFailedBody": { + "description": "Body of the card shown when today's data failed to load from the database." + }, + "homeTryAgain": "Try again", + "@homeTryAgain": { + "description": "Button label to retry loading today's data." + }, + "homeNothingDerivedTitle": "Nothing derived yet", + "@homeNothingDerivedTitle": { + "description": "Title shown on a first-run day with nothing derived yet." + }, + "homeNothingDerivedBody": "No band recordings processed yet.", + "@homeNothingDerivedBody": { + "description": "Body shown on a first-run day with nothing derived yet." + }, + "homeAskCoach": "Ask the coach", + "@homeAskCoach": { + "description": "Semantic label for the button that opens the AI coach on Home." + }, + "homeProfileSettings": "Profile and settings", + "@homeProfileSettings": { + "description": "Semantic label for the avatar button that opens Profile and settings." + }, + "homeNothingTodayTitle": "Nothing recorded for today", + "@homeNothingTodayTitle": { + "description": "Title shown when there is history but nothing recorded for today." + }, + "homeNothingTodayBody": "The last night this app scored was {day}. Nothing has reached it since.", + "@homeNothingTodayBody": { + "description": "Body shown when there is history but nothing recorded for today, naming the last scored night.", + "placeholders": { + "day": { + "type": "String", + "example": "Saturday, 20 May" + } + } + }, + "homeReadinessNotScoredTitle": "Readiness is not scored today", + "@homeReadinessNotScoredTitle": { + "description": "Title of the card shown when readiness has no score today." + }, + "homeReadinessNeedBody": "{need} to know what normal looks like for you.", + "@homeReadinessNeedBody": { + "description": "Body of the readiness-not-scored card, naming what is still needed.", + "placeholders": { + "need": { + "type": "String", + "example": "Needs 2 more nights" + } + } + }, + "homeReadinessNoReason": "Nothing recorded says why.", + "@homeReadinessNoReason": { + "description": "Fallback body of the readiness-not-scored card when no reason is recorded." + }, + "homeSeeWhatWasMissing": "See what was missing", + "@homeSeeWhatWasMissing": { + "description": "Button label opening the readiness detail screen to see what was missing." + }, + "homeAtAGlance": "At a glance", + "@homeAtAGlance": { + "description": "Section title on Home for the row of signal cards (heart rate, steps, energy)." + }, + "homeTodaysPlan": "Today's plan", + "@homeTodaysPlan": { + "description": "Section title on Home for the day's remaining plan items." + }, + "homeBreakdownTitle": "Breakdown of your day", + "@homeBreakdownTitle": { + "description": "Link row title: opens the hour-by-hour breakdown of the day." + }, + "homeBreakdownSubtitle": "Hour by hour", + "@homeBreakdownSubtitle": { + "description": "Link row subtitle under the breakdown-of-your-day title." + }, + "homeIllnessRedTitle": "Several nights in a row are away from your normal", + "@homeIllnessRedTitle": { + "description": "Illness watch headline when several nights in a row are away from baseline (red state)." + }, + "homeIllnessAmberSameNight": "Last night sat outside your normal range", + "@homeIllnessAmberSameNight": { + "description": "Illness watch headline when last night sat outside the normal range (amber state, same night)." + }, + "homeIllnessAmberOtherNight": "{day} sat outside your normal range", + "@homeIllnessAmberOtherNight": { + "description": "Illness watch headline naming a specific past night that sat outside the normal range.", + "placeholders": { + "day": { + "type": "String", + "example": "Saturday, 20 May" + } + } + }, + "homeIllnessBodyNoZ": "Your nocturnal resting heart rate has been running above your own baseline. This reads one signal. It names a pattern, and it does not name a cause.", + "@homeIllnessBodyNoZ": { + "description": "Illness watch body text when no standardised-deviation figure is available." + }, + "homeIllnessBodyAbove": "Your nocturnal resting heart rate has been running above your own baseline; that night sat {z} standardised deviations above it. This reads one signal. It names a pattern, and it does not name a cause.", + "@homeIllnessBodyAbove": { + "description": "Illness watch body text when the flagged night's heart rate sat above baseline, with the deviation amount.", + "placeholders": { + "z": { + "type": "String", + "example": "1.8" + } + } + }, + "homeIllnessBodyBelow": "Your nocturnal resting heart rate has been running above your own baseline; that night sat {z} standardised deviations below it. This reads one signal. It names a pattern, and it does not name a cause.", + "@homeIllnessBodyBelow": { + "description": "Illness watch body text when the flagged night's heart rate sat below baseline, with the deviation amount.", + "placeholders": { + "z": { + "type": "String", + "example": "1.8" + } + } + }, + "homeIllnessAdvice": "Worth noting if it continues past a couple of days.", + "@homeIllnessAdvice": { + "description": "Illness watch advice line." + }, + "homeHeartRate": "Heart rate", + "@homeHeartRate": { + "description": "Signal card title: Heart rate." + }, + "homeRestingSub": "Resting", + "@homeRestingSub": { + "description": "Signal card subtitle: Resting, under the heart rate reading." + }, + "homeNoRestingHr": "No resting heart rate", + "@homeNoRestingHr": { + "description": "Title of the empty-state card when there is no resting heart rate." + }, + "homeNoRestingHrWhy": "Resting heart rate is read from sleep, and no sleep was recorded.", + "@homeNoRestingHrWhy": { + "description": "Reason shown when resting heart rate is absent because no sleep was recorded." + }, + "homeSteps": "Steps", + "@homeSteps": { + "description": "Signal card title: Steps." + }, + "homeStepsNone": "None", + "@homeStepsNone": { + "description": "Steps card value when no step count was recorded at all." + }, + "homeStepsNotRecorded": "NOT RECORDED", + "@homeStepsNotRecorded": { + "description": "Steps card subtitle when nothing counted steps at all." + }, + "homeStepsPercentGoal": "{pct}% of goal", + "@homeStepsPercentGoal": { + "description": "Steps card subtitle: percent of the daily step goal reached.", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "homeActiveEnergy": "Active energy", + "@homeActiveEnergy": { + "description": "Signal card title: Active energy." + }, + "homeCaloriesEstimated": "Estimated", + "@homeCaloriesEstimated": { + "description": "Active energy card subtitle when the total is estimated." + }, + "homeCaloriesTotal": "{total} total", + "@homeCaloriesTotal": { + "description": "Active energy card subtitle showing the total calories for the day.", + "placeholders": { + "total": { + "type": "String", + "example": "2,340" + } + } + }, + "homeNoEnergyEstimate": "No energy estimate", + "@homeNoEnergyEstimate": { + "description": "Title of the empty-state card when there is no energy estimate." + }, + "homeStepsLeft": "{left} steps left", + "@homeStepsLeft": { + "description": "Plan row title: number of steps left to reach the goal.", + "placeholders": { + "left": { + "type": "String", + "example": "2,300" + } + } + }, + "homeMovement": "Movement", + "@homeMovement": { + "description": "Plan row category label: Movement." + }, + "homeGoalSteps": "Goal {goal}", + "@homeGoalSteps": { + "description": "Plan row meta text: the step goal total.", + "placeholders": { + "goal": { + "type": "String", + "example": "10,000" + } + } + }, + "homeStepGoalMet": "Step goal met", + "@homeStepGoalMet": { + "description": "Plan row title when the step goal has been met." + }, + "homeStrainTargetMet": "Strain target met", + "@homeStrainTargetMet": { + "description": "Plan row title when the strain target has been met." + }, + "homeAimForStrain": "Aim for {aim} strain", + "@homeAimForStrain": { + "description": "Plan row title: strain target to aim for.", + "placeholders": { + "aim": { + "type": "String", + "example": "11.4" + } + } + }, + "homeTraining": "Training", + "@homeTraining": { + "description": "Plan row category label: Training." + }, + "homeSleepNeedRow": "{duration} of sleep", + "@homeSleepNeedRow": { + "description": "Plan row title: computed sleep need for tonight.", + "placeholders": { + "duration": { + "type": "String", + "example": "7h 45m" + } + } + }, + "homeTonight": "Tonight", + "@homeTonight": { + "description": "Plan row category label: Tonight." + }, + "homeNeed": "Need", + "@homeNeed": { + "description": "Plan row meta text when no bedtime has been computed yet." + }, + "homeBedTime": "Bed {time}", + "@homeBedTime": { + "description": "Plan row meta text: the recommended bedtime.", + "placeholders": { + "time": { + "type": "String", + "example": "10:40 PM" + } + } + }, + "homeNoPlanTitle": "No plan for today yet", + "@homeNoPlanTitle": { + "description": "Title of the empty-state card when there is no plan for today yet." + }, + "homeNoPlanWhyStale": "The cross-day rollup they come from is being rebuilt.", + "@homeNoPlanWhyStale": { + "description": "Reason there is no plan: the cross-day rollup they come from is being rebuilt." + }, + "homeNoPlanWhyNone": "None are established yet.", + "@homeNoPlanWhyNone": { + "description": "Reason there is no plan: no baselines are established yet." + }, + "homeGreetingStillUp": "Still up", + "@homeGreetingStillUp": { + "description": "Home greeting for very late night hours (before 5am)." + }, + "homeGreetingMorning": "Good morning", + "@homeGreetingMorning": { + "description": "Home greeting for morning hours." + }, + "homeGreetingAfternoon": "Good afternoon", + "@homeGreetingAfternoon": { + "description": "Home greeting for afternoon hours." + }, + "homeGreetingEvening": "Good evening", + "@homeGreetingEvening": { + "description": "Home greeting for evening hours." + }, + "wellnessTitle": "Wellness", + "@wellnessTitle": { + "description": "Wellness screen title" + }, + "wellnessTabMind": "Mind", + "@wellnessTabMind": { + "description": "Wellness sub-tab label: Mind" + }, + "wellnessTabRecovery": "Recovery", + "@wellnessTabRecovery": { + "description": "Wellness sub-tab label: Recovery" + }, + "wellnessTabHabits": "Habits", + "@wellnessTabHabits": { + "description": "Wellness sub-tab label: Habits" + }, + "wellnessTabMedication": "Medication", + "@wellnessTabMedication": { + "description": "Wellness sub-tab label: Medication" + }, + "wellnessTabCycle": "Cycle", + "@wellnessTabCycle": { + "description": "Wellness sub-tab label: Cycle" + }, + "wellnessStartASitting": "START A SITTING", + "@wellnessStartASitting": { + "description": "Hero card label to start a breathing sitting, all caps" + }, + "wellnessExercisesNoun": "exercises", + "@wellnessExercisesNoun": { + "description": "Noun for the count of breathing exercises on the start card" + }, + "wellnessPickOneAndGo": "Pick one and go", + "@wellnessPickOneAndGo": { + "description": "Start card subtitle when no breathing sitting has been done yet" + }, + "wellnessLastMinutes": "Last: {count} min", + "@wellnessLastMinutes": { + "description": "Start card subtitle showing minutes of the last breathing sitting", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "wellnessWriteTheDayDown": "Write the day down", + "@wellnessWriteTheDayDown": { + "description": "Action card title opening the journal compose screen" + }, + "wellnessOpen": "Open", + "@wellnessOpen": { + "description": "Generic 'Open' button/CTA label on action cards" + }, + "wellnessStressLastNight": "Stress last night", + "@wellnessStressLastNight": { + "description": "Section title for last night's stress reading" + }, + "wellnessNoStressTitle": "No stress reading last night", + "@wellnessNoStressTitle": { + "description": "Status card title when there is no stress reading for last night" + }, + "wellnessNoStressBody": "Stress is read from beat timing while you were resting overnight, and last night produced no reading.", + "@wellnessNoStressBody": { + "description": "Status card body explaining why there is no stress reading" + }, + "wellnessAutonomicTension": "Autonomic tension", + "@wellnessAutonomicTension": { + "description": "Label on the stress signal card" + }, + "wellnessStressLevelLow": "low", + "@wellnessStressLevelLow": { + "description": "Stress card sub-label: the 'low' band (shown uppercased)" + }, + "wellnessStressLevelNormal": "normal", + "@wellnessStressLevelNormal": { + "description": "Stress card sub-label: the 'normal' band (shown uppercased)" + }, + "wellnessStressLevelElevated": "elevated", + "@wellnessStressLevelElevated": { + "description": "Stress card sub-label: the 'elevated' band (shown uppercased)" + }, + "wellnessStressLevelHigh": "high", + "@wellnessStressLevelHigh": { + "description": "Stress card sub-label: the 'high' band (shown uppercased)" + }, + "wellnessJournalDefaultSubtitle": "Anything you want to remember about today", + "@wellnessJournalDefaultSubtitle": { + "description": "Journal subtitle when no custom fields are configured" + }, + "wellnessJournalSubtitleShort": "{fields} and a note", + "@wellnessJournalSubtitleShort": { + "description": "Journal subtitle listing up to 4 field names before 'and a note'", + "placeholders": { + "fields": { + "type": "String" + } + } + }, + "wellnessJournalSubtitleLong": "{fields} and {more} more, plus a note", + "@wellnessJournalSubtitleLong": { + "description": "Journal subtitle listing first 4 field names plus a count of more fields", + "placeholders": { + "fields": { + "type": "String" + }, + "more": { + "type": "int" + } + } + }, + "wellnessTurnInBy": "Turn in by {time}", + "@wellnessTurnInBy": { + "description": "Sleep recommendation headline naming a target bedtime", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "wellnessDebtBody": "You are {debt} down against your own need, and tonight's is {need}.", + "@wellnessDebtBody": { + "description": "Sleep recommendation body stating sleep debt and tonight's need", + "placeholders": { + "debt": { + "type": "String" + }, + "need": { + "type": "String" + } + } + }, + "wellnessSeeWhatLastNightCost": "See what last night cost you", + "@wellnessSeeWhatLastNightCost": { + "description": "Sleep recommendation call-to-action text" + }, + "wellnessWhatChargedAndDrained": "What charged and drained you", + "@wellnessWhatChargedAndDrained": { + "description": "Section title for readiness drivers" + }, + "wellnessNoDriversTitle": "No readiness drivers yet", + "@wellnessNoDriversTitle": { + "description": "Status card title when no readiness drivers are available yet" + }, + "wellnessNoDriversBody": "Needs enough nights to know what normal looks like for you.", + "@wellnessNoDriversBody": { + "description": "Status card body explaining readiness drivers need more nights of data" + }, + "wellnessSleepNeedTonight": "Sleep need tonight", + "@wellnessSleepNeedTonight": { + "description": "Section title for tonight's sleep need" + }, + "wellnessNoSleepNeedTitle": "No sleep need yet", + "@wellnessNoSleepNeedTitle": { + "description": "Status card title when no sleep need estimate exists" + }, + "wellnessNoSleepNeedBody": "Nothing recorded says why there is no need for tonight.", + "@wellnessNoSleepNeedBody": { + "description": "Status card fallback body when there is no reason recorded for missing sleep need" + }, + "wellnessTonightsNeed": "Tonight's need", + "@wellnessTonightsNeed": { + "description": "Metric row label for tonight's sleep need duration" + }, + "wellnessSleepDebt": "Sleep debt", + "@wellnessSleepDebt": { + "description": "Metric row label for accumulated sleep debt" + }, + "wellnessAddedForStrain": "Added for strain", + "@wellnessAddedForStrain": { + "description": "Metric row label for minutes added to sleep need for strain" + }, + "wellnessCreditedFromNaps": "Credited from naps", + "@wellnessCreditedFromNaps": { + "description": "Metric row label for minutes credited from naps" + }, + "wellnessTargetBedtime": "Target bedtime", + "@wellnessTargetBedtime": { + "description": "Metric row label for the recommended bedtime" + }, + "wellnessTargetWake": "Target wake", + "@wellnessTargetWake": { + "description": "Metric row label for the recommended wake time" + }, + "wellnessRemoveHabitSemantic": "Remove {label}", + "@wellnessRemoveHabitSemantic": { + "description": "Accessibility label on the trash icon that removes a habit", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "wellnessDaysYouDidIt": "Days you did it", + "@wellnessDaysYouDidIt": { + "description": "Consistency widget label counting days a habit was done" + }, + "wellnessAddAHabit": "Add a habit", + "@wellnessAddAHabit": { + "description": "Button label to add a new habit, also used as the add-habit dialog title" + }, + "wellnessWhatYouLogTitle": "What you log, against your numbers", + "@wellnessWhatYouLogTitle": { + "description": "Action card title linking to journal findings analysis" + }, + "wellnessWhatYouLogSubtitle": "Dose, habit difference, and the day of the week", + "@wellnessWhatYouLogSubtitle": { + "description": "Action card subtitle describing what the journal findings screen covers" + }, + "wellnessRemoveHabitConfirmTitle": "Remove {label}?", + "@wellnessRemoveHabitConfirmTitle": { + "description": "Confirmation sheet title asking to remove a habit", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "wellnessRemoveHabitConfirmBody": "It stops being asked. The days you already recorded stay.", + "@wellnessRemoveHabitConfirmBody": { + "description": "Confirmation sheet body explaining removing a habit keeps recorded history" + }, + "wellnessHabitHint": "Walk after lunch", + "@wellnessHabitHint": { + "description": "Placeholder example text in the add-habit name field" + }, + "wellnessAlreadyTrack": "You already track \"{name}\".", + "@wellnessAlreadyTrack": { + "description": "Snackbar message shown when a duplicate habit/field name is entered", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "wellnessNothingScheduledTitle": "Nothing scheduled", + "@wellnessNothingScheduledTitle": { + "description": "Status card title when no medications are scheduled at all" + }, + "wellnessNothingScheduledBody": "Add what you take and when.", + "@wellnessNothingScheduledBody": { + "description": "Status card body prompting to add a medication" + }, + "wellnessAddAMedication": "Add a medication", + "@wellnessAddAMedication": { + "description": "Button/fix label to add a medication, also used as the add-medication dialog title" + }, + "wellnessNothingDueTodayTitle": "Nothing due today", + "@wellnessNothingDueTodayTitle": { + "description": "Status card title when medications exist but none are due today" + }, + "wellnessNothingDueTodayBody": "What you take is scheduled for other days or times.", + "@wellnessNothingDueTodayBody": { + "description": "Status card body explaining medications are scheduled for other days/times" + }, + "wellnessAdherence": "Adherence", + "@wellnessAdherence": { + "description": "Section title for medication adherence" + }, + "wellnessNothingToScoreTitle": "Nothing to score yet", + "@wellnessNothingToScoreTitle": { + "description": "Status card title when no doses have come due yet for adherence" + }, + "wellnessNothingToScoreBody": "No scheduled doses have come due yet.", + "@wellnessNothingToScoreBody": { + "description": "Status card body explaining adherence has nothing to score yet" + }, + "wellnessTakenOfScheduled": "Taken, of those scheduled in the last seven days.", + "@wellnessTakenOfScheduled": { + "description": "Consistency widget label describing doses taken vs scheduled in the last 7 days" + }, + "wellnessDosesUnit": "doses", + "@wellnessDosesUnit": { + "description": "Unit word for the adherence consistency bar, counting doses not days" + }, + "wellnessUndoSkipped": "Undo skipped", + "@wellnessUndoSkipped": { + "description": "Sheet action label to undo marking a dose as skipped" + }, + "wellnessSkippedOnPurpose": "Skipped on purpose", + "@wellnessSkippedOnPurpose": { + "description": "Sheet action label to mark a dose as deliberately skipped" + }, + "wellnessBackToNotTaken": "Back to not taken.", + "@wellnessBackToNotTaken": { + "description": "Sheet action subtitle when undoing a skipped dose" + }, + "wellnessRecordedAsDecision": "Recorded as a decision, not a miss.", + "@wellnessRecordedAsDecision": { + "description": "Sheet action subtitle when marking a dose as skipped on purpose" + }, + "wellnessWhichDaysDue": "Which days it is due", + "@wellnessWhichDaysDue": { + "description": "Sheet action label to view/edit which days a medication is due" + }, + "wellnessRemoveMedTitle": "Remove {label}", + "@wellnessRemoveMedTitle": { + "description": "Sheet action label to remove a medication", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "wellnessRemoveMedBody": "It stops being scheduled. Marked doses stay.", + "@wellnessRemoveMedBody": { + "description": "Sheet action subtitle explaining removing a medication keeps marked doses" + }, + "wellnessRemoveMedConfirmTitle": "Remove {label}?", + "@wellnessRemoveMedConfirmTitle": { + "description": "Confirmation sheet title asking to remove a medication", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "wellnessRemoveMedConfirmBody": "It stops being scheduled and stops counting towards adherence. The doses you already marked stay.", + "@wellnessRemoveMedConfirmBody": { + "description": "Confirmation sheet body explaining medication removal stops scheduling and adherence counting but keeps marked doses" + }, + "wellnessMedHint": "Vitamin D", + "@wellnessMedHint": { + "description": "Placeholder example text in the add-medication name field" + }, + "wellnessNameLabel": "Name", + "@wellnessNameLabel": { + "description": "Text field label for entering a habit or medication name" + }, + "wellnessAdd": "Add", + "@wellnessAdd": { + "description": "Confirm button label in the add-name dialog" + }, + "wellnessEveryDay": "Every day", + "@wellnessEveryDay": { + "description": "Days-due summary meaning every day of the week" + }, + "wellnessWeekdays": "Weekdays", + "@wellnessWeekdays": { + "description": "Days-due summary meaning Monday through Friday" + }, + "wellnessWeekends": "Weekends", + "@wellnessWeekends": { + "description": "Days-due summary meaning Saturday and Sunday" + }, + "wellnessMon": "Mon", + "@wellnessMon": { + "description": "Abbreviated weekday name: Monday" + }, + "wellnessTue": "Tue", + "@wellnessTue": { + "description": "Abbreviated weekday name: Tuesday" + }, + "wellnessWed": "Wed", + "@wellnessWed": { + "description": "Abbreviated weekday name: Wednesday" + }, + "wellnessThu": "Thu", + "@wellnessThu": { + "description": "Abbreviated weekday name: Thursday" + }, + "wellnessFri": "Fri", + "@wellnessFri": { + "description": "Abbreviated weekday name: Friday" + }, + "wellnessSat": "Sat", + "@wellnessSat": { + "description": "Abbreviated weekday name: Saturday" + }, + "wellnessSun": "Sun", + "@wellnessSun": { + "description": "Abbreviated weekday name: Sunday" + }, + "wellnessWhenYouTakeIt": "When you take it", + "@wellnessWhenYouTakeIt": { + "description": "Heading in the medication schedule picker sheet" + }, + "wellnessChangeTheTime": "Change the time", + "@wellnessChangeTheTime": { + "description": "Accessibility label for the time picker row in the schedule sheet" + }, + "wellnessWhichDays": "WHICH DAYS", + "@wellnessWhichDays": { + "description": "All-caps section label in the schedule picker for choosing weekdays" + }, + "wellnessPickAtLeastOneDay": "Pick at least one day.", + "@wellnessPickAtLeastOneDay": { + "description": "Validation hint when no weekday is selected in the schedule picker" + }, + "wellnessDueDays": "Due {days}.", + "@wellnessDueDays": { + "description": "Schedule picker summary of which days the dose is due", + "placeholders": { + "days": { + "type": "String" + } + } + }, + "wellnessMoreForMed": "More for {label}", + "@wellnessMoreForMed": { + "description": "Accessibility label for the overflow/more menu button on a medication row", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "wellnessMedAtTime": "{label} at {time}", + "@wellnessMedAtTime": { + "description": "Accessibility label for a medication dose row: the medication name and its scheduled time", + "placeholders": { + "label": { + "type": "String" + }, + "time": { + "type": "String" + } + } + }, + "wellnessStateTaken": "taken", + "@wellnessStateTaken": { + "description": "Dose state word: taken" + }, + "wellnessStateSkipped": "skipped", + "@wellnessStateSkipped": { + "description": "Dose state word: skipped" + }, + "wellnessStateNotTaken": "not taken", + "@wellnessStateNotTaken": { + "description": "Dose state word: not taken (missed)" + }, + "wellnessStateDueLater": "due later", + "@wellnessStateDueLater": { + "description": "Dose state word: due later (upcoming)" + }, + "wellnessMarkDone": "Mark done", + "@wellnessMarkDone": { + "description": "Accessibility label on an unchecked dose/habit check control" + }, + "wellnessWhatYouLogScreenTitle": "What you log", + "@wellnessWhatYouLogScreenTitle": { + "description": "Screen title for the journal findings detail screen" + }, + "wellnessNothingSeparatedTitle": "Nothing separated itself yet", + "@wellnessNothingSeparatedTitle": { + "description": "Status card title when no journal findings cleared statistical significance" + }, + "wellnessNothingSeparatedBody": "Everything you log is tested against your recovery, HRV, resting heart rate and sleep efficiency. Nothing has cleared the bar yet.", + "@wellnessNothingSeparatedBody": { + "description": "Status card body explaining what journal findings are tested against" + }, + "wellnessTheDaysYouDidIt": "The days you did it", + "@wellnessTheDaysYouDidIt": { + "description": "Section title for binary habit findings in journal findings" + }, + "wellnessHowMuchAndWhatFollowed": "How much, and what followed", + "@wellnessHowMuchAndWhatFollowed": { + "description": "Section title for numeric dose-response findings in journal findings" + }, + "wellnessLinkNeverCause": "A link on your own days — never a cause. The days you do a thing are days you were already that kind of day.", + "@wellnessLinkNeverCause": { + "description": "Disclaimer text below journal findings emphasizing correlation not causation" + }, + "wellnessWhichDayOfWeek": "Which day of the week", + "@wellnessWhichDayOfWeek": { + "description": "Section title for the weekday effect card" + }, + "wellnessHigher": "higher", + "@wellnessHigher": { + "description": "Direction word: higher, used when composing a finding sentence" + }, + "wellnessLower": "lower", + "@wellnessLower": { + "description": "Direction word: lower, used when composing a finding sentence" + }, + "wellnessHeadlineBinary": "On the {n} days you logged {field}, {outcome} ran {amount} {direction}", + "@wellnessHeadlineBinary": { + "description": "Finding headline for a binary (habit) journal field, e.g. 'On the 5 days you logged X, Y ran 3 higher'", + "placeholders": { + "n": { + "type": "String" + }, + "field": { + "type": "String" + }, + "outcome": { + "type": "String" + }, + "amount": { + "type": "String" + }, + "direction": { + "type": "String" + } + } + }, + "wellnessHeadlineNoSlope": "On the {n} days you logged {field}, more of it went with {direction} {outcome}", + "@wellnessHeadlineNoSlope": { + "description": "Finding headline for a numeric field with only a rank correlation (no slope)", + "placeholders": { + "n": { + "type": "String" + }, + "field": { + "type": "String" + }, + "direction": { + "type": "String" + }, + "outcome": { + "type": "String" + } + } + }, + "wellnessHeadlineSlope": "On the {n} days you logged {field}, {outcome} ran {amount} {direction} per {step}", + "@wellnessHeadlineSlope": { + "description": "Finding headline for a numeric field with a per-unit slope", + "placeholders": { + "n": { + "type": "String" + }, + "field": { + "type": "String" + }, + "outcome": { + "type": "String" + }, + "amount": { + "type": "String" + }, + "direction": { + "type": "String" + }, + "step": { + "type": "String" + } + } + }, + "wellnessMatchedSameDay": "Matched against the same day's numbers.", + "@wellnessMatchedSameDay": { + "description": "Finding detail: the field was matched against the same day's numbers" + }, + "wellnessMatchedNightFollowed": "Matched against the night that followed.", + "@wellnessMatchedNightFollowed": { + "description": "Finding detail: the field was matched against the following night" + }, + "wellnessMatchedNightEnded": "Matched against the night that ended that morning.", + "@wellnessMatchedNightEnded": { + "description": "Finding detail: the field was matched against the night that just ended" + }, + "wellnessAgainstDaysYouDidNot": "Against the {n} days you did not", + "@wellnessAgainstDaysYouDidNot": { + "description": "Finding detail prefix comparing against days a habit was not done", + "placeholders": { + "n": { + "type": "String" + } + } + }, + "wellnessRangeTo": "{lo} to {hi}", + "@wellnessRangeTo": { + "description": "Confidence interval range phrase, e.g. '0.10 to 0.40'", + "placeholders": { + "lo": { + "type": "String" + }, + "hi": { + "type": "String" + } + } + }, + "wellnessRankCorrelation": "Rank correlation {rho}{ci}. ", + "@wellnessRankCorrelation": { + "description": "Finding detail stating the rank correlation coefficient and its confidence interval", + "placeholders": { + "rho": { + "type": "String" + }, + "ci": { + "type": "String" + } + } + }, + "wellnessCaffeineCaveat": "This is your last caffeine of the day only — two cups and five look identical here, so \"later\" can quietly mean \"more\". A long, stressful day produces both the late coffee and the poor night.", + "@wellnessCaffeineCaveat": { + "description": "Extra caveat paragraph shown for the last-caffeine-of-day finding" + }, + "wellnessHourLater": "hour later", + "@wellnessHourLater": { + "description": "Unit phrase for caffeine timing slope, e.g. 'per hour later'" + }, + "wellnessPointUnit": "point", + "@wellnessPointUnit": { + "description": "Fallback singular unit word when a journal field has no unit" + }, + "wellnessNotEnoughWeeksTitle": "Not enough weeks yet", + "@wellnessNotEnoughWeeksTitle": { + "description": "Status card title when there is not yet enough data for the weekday effect" + }, + "wellnessNotEnoughWeeksBody": "Comparing seven weekdays needs at least eight weeks of days, with five of every weekday in them.", + "@wellnessNotEnoughWeeksBody": { + "description": "Status card body explaining the weekday effect needs eight weeks of data" + }, + "wellnessNoDayStandsOutTitle": "No day of the week stands out", + "@wellnessNoDayStandsOutTitle": { + "description": "Status card title when no weekday stands out as significant" + }, + "wellnessNoDayStandsOutBody": "No day stands apart from the other six once we account for having checked all seven.", + "@wellnessNoDayStandsOutBody": { + "description": "Status card body explaining no weekday cleared the multiple-comparison bar" + }, + "wellnessWeekdayHeadline": "{weekday}: readiness runs {delta} {direction} than your overall median", + "@wellnessWeekdayHeadline": { + "description": "Finding headline naming which weekday's readiness differs from the median", + "placeholders": { + "weekday": { + "type": "String" + }, + "delta": { + "type": "String" + }, + "direction": { + "type": "String" + } + } + }, + "wellnessWeekdayDetail": "From {n} of them. A weekday is not a cause — it is a container for what you do on it. Nothing here is advice.", + "@wellnessWeekdayDetail": { + "description": "Finding detail giving the sample size and the correlation-not-causation caveat for the weekday effect", + "placeholders": { + "n": { + "type": "String" + } + } + }, + "wellnessPluralMonday": "Mondays", + "@wellnessPluralMonday": { + "description": "Plural weekday name used in the weekday-effect headline: Mondays" + }, + "wellnessPluralTuesday": "Tuesdays", + "@wellnessPluralTuesday": { + "description": "Plural weekday name used in the weekday-effect headline: Tuesdays" + }, + "wellnessPluralWednesday": "Wednesdays", + "@wellnessPluralWednesday": { + "description": "Plural weekday name used in the weekday-effect headline: Wednesdays" + }, + "wellnessPluralThursday": "Thursdays", + "@wellnessPluralThursday": { + "description": "Plural weekday name used in the weekday-effect headline: Thursdays" + }, + "wellnessPluralFriday": "Fridays", + "@wellnessPluralFriday": { + "description": "Plural weekday name used in the weekday-effect headline: Fridays" + }, + "wellnessPluralSaturday": "Saturdays", + "@wellnessPluralSaturday": { + "description": "Plural weekday name used in the weekday-effect headline: Saturdays" + }, + "wellnessPluralSunday": "Sundays", + "@wellnessPluralSunday": { + "description": "Plural weekday name used in the weekday-effect headline: Sundays" + }, + "sleepDetailNavTitle": "Sleep", + "@sleepDetailNavTitle": { + "description": "Nav bar title of the Sleep detail screen." + }, + "sleepDetailNoNightTitle": "No night to show", + "@sleepDetailNoNightTitle": { + "description": "Status card title when the stepped-to day has no night recorded." + }, + "sleepDetailNoNightBody": "No stretch of band recordings long enough to score.", + "@sleepDetailNoNightBody": { + "description": "Status card body explaining why there is no night." + }, + "sleepDetailNoNightFix": "Wear the band overnight and sync in the morning", + "@sleepDetailNoNightFix": { + "description": "Suggested fix line on the no-night status card." + }, + "sleepDetailStagesSection": "Stages", + "@sleepDetailStagesSection": { + "description": "Section header for the sleep stage breakdown." + }, + "sleepDetailVersusUsualSection": "Against your usual", + "@sleepDetailVersusUsualSection": { + "description": "Section header comparing last night to the user's own recent nights." + }, + "sleepDetailUnusualLastNight": "Unusual last night", + "@sleepDetailUnusualLastNight": { + "description": "Section header when the unusual-findings section is about last night." + }, + "sleepDetailUnusualOnDay": "Unusual on {day}", + "@sleepDetailUnusualOnDay": { + "description": "Section header when the unusual-findings section is about a past day, not last night.", + "placeholders": { + "day": { + "type": "String" + } + } + }, + "sleepDetailOvernightSection": "Overnight signals", + "@sleepDetailOvernightSection": { + "description": "Section header for the overnight signal charts (HR, HRV, breathing, temp)." + }, + "sleepDetailTonightSection": "Tonight", + "@sleepDetailTonightSection": { + "description": "Section header for the one-takeaway sleep coaching card." + }, + "sleepDetailTotalSleep": "Total sleep", + "@sleepDetailTotalSleep": { + "description": "Caption under the big total-sleep-duration number." + }, + "sleepDetailInBed": "IN BED", + "@sleepDetailInBed": { + "description": "Inline metric label: time spent in bed." + }, + "sleepDetailWatched": "WATCHED", + "@sleepDetailWatched": { + "description": "Inline metric label: time the band actually recorded." + }, + "sleepDetailAsleepOfThat": "ASLEEP OF THAT", + "@sleepDetailAsleepOfThat": { + "description": "Inline metric label for asleep percentage when a 'watched' window is also shown." + }, + "sleepDetailAsleep": "ASLEEP", + "@sleepDetailAsleep": { + "description": "Inline metric label for asleep percentage (efficiency)." + }, + "sleepDetailWatchedExplain": "We watched {watched} of your {inBed} in bed; the rest is not a measurement. Asleep, and the stage shares below, are out of the time we watched.", + "@sleepDetailWatchedExplain": { + "description": "Explanatory sentence under the total-sleep card about the watched vs. in-bed window.", + "placeholders": { + "watched": { + "type": "String" + }, + "inBed": { + "type": "String" + } + } + }, + "sleepDetailWindowMine": "You set this window", + "@sleepDetailWindowMine": { + "description": "Label when the user has set/confirmed the sleep window themselves." + }, + "sleepDetailWindowFallback": "This window was inferred from heart rate", + "@sleepDetailWindowFallback": { + "description": "Label when the sleep window was inferred from heart rate as a fallback." + }, + "sleepDetailWindowAuto": "This window was staged from the signals", + "@sleepDetailWindowAuto": { + "description": "Label when the sleep window was auto-staged from the signals." + }, + "sleepDetailWindowFallbackBody": "Staging could not find the edges, so the times are a best guess.", + "@sleepDetailWindowFallbackBody": { + "description": "Explanation shown under the fallback window label." + }, + "sleepDetailWindowSol": "From the start of your window to asleep: {band}.", + "@sleepDetailWindowSol": { + "description": "Sentence stating sleep-onset latency for a user-confirmed window.", + "placeholders": { + "band": { + "type": "String" + } + } + }, + "sleepDetailConfirmTimes": "These times are right", + "@sleepDetailConfirmTimes": { + "description": "Button: confirm the inferred sleep window times are correct." + }, + "sleepDetailChangeTimes": "Change the times", + "@sleepDetailChangeTimes": { + "description": "Button: change an already user-set sleep window." + }, + "sleepDetailSetTimesMyself": "Set the times myself", + "@sleepDetailSetTimesMyself": { + "description": "Button: set the sleep window times manually for the first time." + }, + "sleepDetailBackToAutomatic": "Back to automatic", + "@sleepDetailBackToAutomatic": { + "description": "Button: revert a manual sleep window override back to automatic staging." + }, + "sleepDetailReanalysing": "Re-analysing the night…", + "@sleepDetailReanalysing": { + "description": "Progress label shown while the night is being re-derived after a window edit." + }, + "sleepDetailCorrectionFailedTitle": "That correction has not been applied", + "@sleepDetailCorrectionFailedTitle": { + "description": "Status card title when a sleep window correction failed to apply." + }, + "sleepDetailBedTimeHelp": "WHEN YOU GOT INTO BED", + "@sleepDetailBedTimeHelp": { + "description": "Help text on the time picker for when the user got into bed." + }, + "sleepDetailWakeTimeHelp": "WHEN YOU GOT UP", + "@sleepDetailWakeTimeHelp": { + "description": "Help text on the time picker for when the user got up." + }, + "sleepDetailReanalyseFailed": "The night was not re-analysed — another re-analysis was already running, or it failed. The times you set are saved; Re-analyze everything on Your data applies them.", + "@sleepDetailReanalyseFailed": { + "description": "Fallback error message when re-analysis after a window edit could not be confirmed." + }, + "sleepDetailNoHypnogramTitle": "No hypnogram for this night", + "@sleepDetailNoHypnogramTitle": { + "description": "Status card title when no hypnogram data exists for the night." + }, + "sleepDetailNoHypnogramBody": "Staging needs movement and beat timing. One was missing.", + "@sleepDetailNoHypnogramBody": { + "description": "Status card body explaining why no hypnogram is available." + }, + "sleepDetailThroughTheNight": "Through the night", + "@sleepDetailThroughTheNight": { + "description": "Chart title for the hypnogram and the overnight signals chart." + }, + "sleepDetailUnitStage": "stage", + "@sleepDetailUnitStage": { + "description": "Unit label on the hypnogram chart frame." + }, + "sleepDetailTapDragCycles": "{n, plural, one{Tap or drag the chart for any moment. {n} cycle.} other{Tap or drag the chart for any moment. {n} cycles.}}", + "@sleepDetailTapDragCycles": { + "description": "Hint sentence under the hypnogram, stating the sleep-cycle count with no average available.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "sleepDetailTapDragCyclesAvg": "{n, plural, one{Tap or drag the chart for any moment. {n} cycle, {avg} on average.} other{Tap or drag the chart for any moment. {n} cycles, {avg} on average.}}", + "@sleepDetailTapDragCyclesAvg": { + "description": "Hint sentence under the hypnogram, stating the sleep-cycle count and its average length.", + "placeholders": { + "n": { + "type": "int" + }, + "avg": { + "type": "String" + } + } + }, + "sleepDetailTapDragNone": "Tap or drag the chart for any moment of the night.", + "@sleepDetailTapDragNone": { + "description": "Hint sentence under the hypnogram when there were no complete sleep cycles." + }, + "sleepDetailNoWakeups": "No wake-ups of 5 minutes or more; shorter ones are invisible to a wrist.", + "@sleepDetailNoWakeups": { + "description": "Sentence describing a night with zero qualifying wake-ups." + }, + "sleepDetailAtLeastWakeups": "{n, plural, one{At least {n} wake-up of 5 minutes or more; shorter ones are invisible to a wrist.} other{At least {n} wake-ups of 5 minutes or more; shorter ones are invisible to a wrist.}}", + "@sleepDetailAtLeastWakeups": { + "description": "Sentence stating a floor count of wake-ups of 5+ minutes.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "sleepDetailLongestStretch": "Longest unbroken stretch {longest}.", + "@sleepDetailLongestStretch": { + "description": "Sentence stating the longest unbroken sleep stretch of the night.", + "placeholders": { + "longest": { + "type": "String" + } + } + }, + "sleepDetailHypnogramLabel": "Hypnogram", + "@sleepDetailHypnogramLabel": { + "description": "Accessibility label for the hypnogram scrubber control." + }, + "sleepDetailPercentThroughNight": "{pct}% through the night", + "@sleepDetailPercentThroughNight": { + "description": "Screen-reader description of scrub position when the night's clock times are unknown.", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "sleepDetailNotMeasured": "not measured", + "@sleepDetailNotMeasured": { + "description": "Lowercase, mid-sentence: describes an instant the band was not recording, used in the scrubber's spoken description." + }, + "sleepDetailScrubAt": "{at}, {stage}", + "@sleepDetailScrubAt": { + "description": "Combines the scrubbed time and the stage/measurement state into one spoken sentence.", + "placeholders": { + "at": { + "type": "String" + }, + "stage": { + "type": "String" + } + } + }, + "sleepDetailHeartRate": "Heart rate", + "@sleepDetailHeartRate": { + "description": "Label for the heart-rate signal, used as a metric label and as a chart lane legend." + }, + "sleepDetailHrv": "HRV", + "@sleepDetailHrv": { + "description": "Label for the heart-rate-variability signal." + }, + "sleepDetailBreathing": "Breathing", + "@sleepDetailBreathing": { + "description": "Label for the breathing-rate signal (title case)." + }, + "sleepDetailTemp": "Temp", + "@sleepDetailTemp": { + "description": "Short label for skin temperature in the scrub readout card." + }, + "sleepDetailNotMeasuredCap": "Not measured", + "@sleepDetailNotMeasuredCap": { + "description": "Capitalized, standalone label shown in place of a stage pill when the instant was not recorded." + }, + "sleepDetailNoSignalAtMoment": "No signal recorded at this moment.", + "@sleepDetailNoSignalAtMoment": { + "description": "Message shown in the scrub card when no signal is available at the scrubbed instant." + }, + "sleepDetailStageAwake": "Awake", + "@sleepDetailStageAwake": { + "description": "Sleep stage name: Awake." + }, + "sleepDetailStageRem": "REM", + "@sleepDetailStageRem": { + "description": "Sleep stage name: REM." + }, + "sleepDetailStageLight": "Light sleep", + "@sleepDetailStageLight": { + "description": "Sleep stage name: Light sleep." + }, + "sleepDetailStageDeep": "Deep sleep", + "@sleepDetailStageDeep": { + "description": "Sleep stage name: Deep sleep." + }, + "sleepDetailDeep": "Deep", + "@sleepDetailDeep": { + "description": "Short row label for deep sleep in the stage breakdown table." + }, + "sleepDetailLight": "Light", + "@sleepDetailLight": { + "description": "Short row label for light sleep in the stage breakdown table." + }, + "sleepDetailNoStageSplitTitle": "No stage split for this night", + "@sleepDetailNoStageSplitTitle": { + "description": "Status card title when no stage breakdown could be computed." + }, + "sleepDetailNoStageSplitBody": "No beat timing across the whole window.", + "@sleepDetailNoStageSplitBody": { + "description": "Status card body explaining why no stage breakdown is available." + }, + "sleepDetailStageRangeExplain": "Each stage is a range, not a count — the better we saw the night, the narrower it is. Deep is the widest. Awake stays one figure. Nerd stats has the exact counts.", + "@sleepDetailStageRangeExplain": { + "description": "Explanatory footnote under the stage breakdown table about ranges vs. counts." + }, + "sleepDetailTimeAsleep": "Time asleep", + "@sleepDetailTimeAsleep": { + "description": "Row label in the 'against your usual' comparison: total time asleep." + }, + "sleepDetailShorterThanUsual": "shorter than usual", + "@sleepDetailShorterThanUsual": { + "description": "Verdict phrase: this night's sleep duration was shorter than usual." + }, + "sleepDetailLongerThanUsual": "longer than usual", + "@sleepDetailLongerThanUsual": { + "description": "Verdict phrase: this night's sleep duration was longer than usual." + }, + "sleepDetailLessThanUsual": "less than usual", + "@sleepDetailLessThanUsual": { + "description": "Verdict phrase: this night's deep sleep was less than usual." + }, + "sleepDetailMoreThanUsual": "more than usual", + "@sleepDetailMoreThanUsual": { + "description": "Verdict phrase: this night's deep sleep was more than usual." + }, + "sleepDetailAsleepWhileInBed": "Asleep while in bed", + "@sleepDetailAsleepWhileInBed": { + "description": "Row label in the 'against your usual' comparison: sleep efficiency." + }, + "sleepDetailLowerThanUsual": "lower than usual", + "@sleepDetailLowerThanUsual": { + "description": "Verdict phrase: sleep efficiency was lower than usual." + }, + "sleepDetailHigherThanUsual": "higher than usual", + "@sleepDetailHigherThanUsual": { + "description": "Verdict phrase: sleep efficiency was higher than usual." + }, + "sleepDetailFellAsleep": "Fell asleep", + "@sleepDetailFellAsleep": { + "description": "Row label in the 'against your usual' comparison: sleep onset time." + }, + "sleepDetailEarlierThanUsual": "earlier than usual", + "@sleepDetailEarlierThanUsual": { + "description": "Verdict phrase: fell asleep earlier than usual." + }, + "sleepDetailLaterThanUsual": "later than usual", + "@sleepDetailLaterThanUsual": { + "description": "Verdict phrase: fell asleep later than usual." + }, + "sleepDetailNotEnoughNightsTitle": "Not enough nights to compare", + "@sleepDetailNotEnoughNightsTitle": { + "description": "Status card title when there is not yet enough history to compare against." + }, + "sleepDetailNightsSoFar": "{have} of {min} nights so far", + "@sleepDetailNightsSoFar": { + "description": "Progress line: how many nights of history exist versus the minimum needed.", + "placeholders": { + "have": { + "type": "int" + }, + "min": { + "type": "int" + } + } + }, + "sleepDetailBarExplain": "The bar is the middle half of your own nights.", + "@sleepDetailBarExplain": { + "description": "Footnote explaining that the comparison bar is the user's own middle-50% range." + }, + "sleepDetailLessThanAny": "{noun} {value} — less than any of your last {count} nights, the lowest of which was {lowest}.", + "@sleepDetailLessThanAny": { + "description": "Insight card body: a measure was the lowest across the recent-night history.", + "placeholders": { + "noun": { + "type": "String" + }, + "value": { + "type": "String" + }, + "count": { + "type": "int" + }, + "lowest": { + "type": "String" + } + } + }, + "sleepDetailMoreThanAny": "{noun} {value} — more than any of your last {count} nights, the highest of which was {highest}.", + "@sleepDetailMoreThanAny": { + "description": "Insight card body: a measure was the highest across the recent-night history.", + "placeholders": { + "noun": { + "type": "String" + }, + "value": { + "type": "String" + }, + "count": { + "type": "int" + }, + "highest": { + "type": "String" + } + } + }, + "sleepDetailYouSlept": "You slept", + "@sleepDetailYouSlept": { + "description": "Noun phrase used as the subject of the total-sleep-duration extreme sentence, e.g. 'You slept 5h 10m'." + }, + "sleepDetailShortestNightLately": "Your shortest night lately", + "@sleepDetailShortestNightLately": { + "description": "Insight card title: shortest night in the recent record." + }, + "sleepDetailLongestNightLately": "Your longest night lately", + "@sleepDetailLongestNightLately": { + "description": "Insight card title: longest night in the recent record." + }, + "sleepDetailSleepingHrHighTitle": "Sleeping heart rate ran high", + "@sleepDetailSleepingHrHighTitle": { + "description": "Insight card title: sleeping heart rate was elevated versus baseline." + }, + "sleepDetailSleepingHrHighBody": "{bpm} bpm above your own baseline. Common after alcohol, a late meal, a hard session or an infection starting — this is a measurement, not a diagnosis.", + "@sleepDetailSleepingHrHighBody": { + "description": "Insight card body explaining the elevated sleeping heart rate.", + "placeholders": { + "bpm": { + "type": "String" + } + } + }, + "sleepDetailNothingStoodOut": "Nothing stood out.", + "@sleepDetailNothingStoodOut": { + "description": "Reassurance line shown when nothing unusual was found in the night." + }, + "sleepDetailSleepingHr": "SLEEPING HR", + "@sleepDetailSleepingHr": { + "description": "All-caps inline metric label: average sleeping heart rate." + }, + "sleepDetailLowest": "LOWEST", + "@sleepDetailLowest": { + "description": "All-caps inline metric label: lowest heart rate reached overnight." + }, + "sleepDetailBreathingCaps": "BREATHING", + "@sleepDetailBreathingCaps": { + "description": "All-caps inline metric label for breathing rate summary." + }, + "sleepDetailSkinTemp": "Skin temp", + "@sleepDetailSkinTemp": { + "description": "Chart lane legend label for skin temperature." + }, + "sleepDetailSleepNeedNotEstablished": "Sleep need not established", + "@sleepDetailSleepNeedNotEstablished": { + "description": "Status card title when sleep need has not yet been established." + }, + "sleepDetailYourNeedIs": "Your need is {need}", + "@sleepDetailYourNeedIs": { + "description": "Reason fragment stating the user's computed sleep need.", + "placeholders": { + "need": { + "type": "String" + } + } + }, + "sleepDetailYouAreDown": "you are {debt} down", + "@sleepDetailYouAreDown": { + "description": "Reason fragment stating how far behind the user's sleep debt is.", + "placeholders": { + "debt": { + "type": "String" + } + } + }, + "sleepDetailLightsOut": "lights out", + "@sleepDetailLightsOut": { + "description": "Small caption under the recommended bedtime clock time." + }, + "sleepDetailToAimFor": "to aim for", + "@sleepDetailToAimFor": { + "description": "Small caption under the sleep-need duration when no bedtime is recommended." + }, + "sleepDetailNoPersonalRangeYet": "No personal range yet — {count} of {min} nights.", + "@sleepDetailNoPersonalRangeYet": { + "description": "Message shown on a comparison row before enough personal history exists.", + "placeholders": { + "count": { + "type": "int" + }, + "min": { + "type": "int" + } + } + }, + "sleepDetailNotFarEnoughToCall": "Not far enough from usual to call", + "@sleepDetailNotFarEnoughToCall": { + "description": "Verdict phrase when a fuzzy measurement's interval overlaps the usual band." + }, + "sleepDetailTypicalForYou": "Typical for you", + "@sleepDetailTypicalForYou": { + "description": "Verdict phrase when a measured value falls within the user's usual range." + }, + "sleepDetailVerdictSummary": "{verdict} · usual {lo}–{hi} over {n} nights", + "@sleepDetailVerdictSummary": { + "description": "Footnote combining the verdict phrase with the usual range and night count.", + "placeholders": { + "verdict": { + "type": "String" + }, + "lo": { + "type": "String" + }, + "hi": { + "type": "String" + }, + "n": { + "type": "int" + } + } + }, + "sleepDetailNoOvernightTitle": "No overnight signal lines", + "@sleepDetailNoOvernightTitle": { + "description": "Status card title when no overnight signal lines were recorded." + }, + "sleepDetailNoOvernightBody": "No overnight recordings reached this day.", + "@sleepDetailNoOvernightBody": { + "description": "Status card body explaining why no overnight signals exist." + }, + "sleepDetailSolUnder15": "under 15 minutes", + "@sleepDetailSolUnder15": { + "description": "Sleep-onset-latency band label for under 15 minutes." + }, + "sleepDetailSolOverHour": "over an hour", + "@sleepDetailSolOverHour": { + "description": "Sleep-onset-latency band label for over an hour." + }, + "sleepDetailSolRange": "{lo}–{hi} minutes", + "@sleepDetailSolRange": { + "description": "15-minute sleep-onset-latency band label, e.g. '15–30 minutes'.", + "placeholders": { + "lo": { + "type": "int" + }, + "hi": { + "type": "int" + } + } + }, + "workoutTabForYou": "For you", + "@workoutTabForYou": { + "description": "Workout screen sub-tab label" + }, + "workoutTabActivities": "Activities", + "@workoutTabActivities": { + "description": "Workout screen sub-tab label" + }, + "workoutTabHistory": "History", + "@workoutTabHistory": { + "description": "Workout screen sub-tab label" + }, + "workoutScreenTitle": "Workout", + "@workoutScreenTitle": { + "description": "Workout screen title" + }, + "workoutStartSessionLabel": "START A SESSION", + "@workoutStartSessionLabel": { + "description": "Hero start card label on For You tab" + }, + "workoutActivitiesNoun": "activities", + "@workoutActivitiesNoun": { + "description": "Noun used beside the activity count on the start card" + }, + "workoutThisWeek": "This week", + "@workoutThisWeek": { + "description": "Section header and stat label for the current week" + }, + "workoutTrainingLoad": "Training load", + "@workoutTrainingLoad": { + "description": "Section header over the fitness/fatigue card" + }, + "workoutTodaysStrainAction": "Today's strain", + "@workoutTodaysStrainAction": { + "description": "Link action under Training load section, opens day strain detail" + }, + "workoutMechanicalLoadTitle": "MECHANICAL LOAD", + "@workoutMechanicalLoadTitle": { + "description": "Chart title for weekly strength tonnage" + }, + "workoutKgLiftedUnit": "kg lifted", + "@workoutKgLiftedUnit": { + "description": "Unit label on the tonnage chart" + }, + "workoutTonnageFootnoteIntro": "Reps × load over the sets you logged with a weight. ", + "@workoutTonnageFootnoteIntro": { + "description": "First sentence of the tonnage chart footnote" + }, + "workoutTonnageFootnotePartial": "Sets logged without one are not in it, so this is a floor rather than a total. ", + "@workoutTonnageFootnotePartial": { + "description": "Conditional clause inserted into the tonnage footnote when some sets had no weight" + }, + "workoutTonnageFootnoteOutro": "Exact for what you typed and worthless across exercises — kept out of strain and recovery for that reason.", + "@workoutTonnageFootnoteOutro": { + "description": "Closing sentence of the tonnage chart footnote" + }, + "workoutOverreachHeadline": "Your last 7 days of load are {ratio}× your usual six weeks, and your resting heart rate was above your usual on {nightsElevated} of {nightsConsidered} nights.", + "@workoutOverreachHeadline": { + "description": "Overreach coincidence card headline, ratio is a formatted decimal string", + "placeholders": { + "ratio": { + "type": "String" + }, + "nightsElevated": { + "type": "int" + }, + "nightsConsidered": { + "type": "int" + } + } + }, + "workoutOverreachBody": "Two measurements that happen to point the same way. Illness, travel, altitude, alcohol and a run of poor sleep all produce this same pair, and nothing here can tell them apart.", + "@workoutOverreachBody": { + "description": "Overreach coincidence card body text" + }, + "workoutNoLoadTitle": "No training load yet", + "@workoutNoLoadTitle": { + "description": "StatusCard title when fitness/fatigue has not computed yet" + }, + "workoutNoLoadBody": "Fitness and fatigue are 42-day and 7-day averages. They need about two weeks of sessions.", + "@workoutNoLoadBody": { + "description": "Default StatusCard body when fitness/fatigue has not computed yet" + }, + "workoutFitnessLabel": "fitness", + "@workoutFitnessLabel": { + "description": "Small caption under the CTL number on the load card" + }, + "workoutDailyLoadTitle": "DAILY LOAD", + "@workoutDailyLoadTitle": { + "description": "Chart title for daily TRIMP" + }, + "workoutTrimpUnit": "TRIMP", + "@workoutTrimpUnit": { + "description": "Chart unit label, training-impulse acronym" + }, + "workoutDailyLoadFootnoteIntro": "Banister training impulse — minutes weighted by heart-rate reserve. ", + "@workoutDailyLoadFootnoteIntro": { + "description": "First sentence of the daily load chart footnote" + }, + "workoutDailyLoadAllDays": "Last seven days.", + "@workoutDailyLoadAllDays": { + "description": "Daily load footnote when all seven days have data" + }, + "workoutDailyLoadPartialDays": "{days} of the last seven days produced a figure.", + "@workoutDailyLoadPartialDays": { + "description": "Daily load footnote when only some of the last seven days have data", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "workoutFatigueLabel": "Fatigue", + "@workoutFatigueLabel": { + "description": "Inline metric label on the load card" + }, + "workoutFormLabel": "Form", + "@workoutFormLabel": { + "description": "Inline metric label on the load card" + }, + "workoutNotYet": "Not yet", + "@workoutNotYet": { + "description": "Placeholder value when fatigue/form has not computed" + }, + "workoutFormFresh": "Fresh", + "@workoutFormFresh": { + "description": "Coggan form band name" + }, + "workoutFormSteady": "Steady", + "@workoutFormSteady": { + "description": "Coggan form band name" + }, + "workoutFormBuilding": "Building", + "@workoutFormBuilding": { + "description": "Coggan form band name" + }, + "workoutFormOverreaching": "Overreaching", + "@workoutFormOverreaching": { + "description": "Coggan form band name" + }, + "workoutSearchActivitiesLabel": "Search activities", + "@workoutSearchActivitiesLabel": { + "description": "Semantic label on the activity search bar" + }, + "workoutSearchActivitiesCount": "{count, plural, one{Search {count} activity} other{Search {count} activities}}", + "@workoutSearchActivitiesCount": { + "description": "Search bar placeholder text showing the catalogue size", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workoutQuickStartHeader": "QUICK START", + "@workoutQuickStartHeader": { + "description": "Section header over the quick-start activity tiles" + }, + "workoutCalorieNeedWeightTitle": "Calorie estimates need your weight", + "@workoutCalorieNeedWeightTitle": { + "description": "StatusCard title shown when profile has no weight set" + }, + "workoutAddWeightFix": "Add weight in profile", + "@workoutAddWeightFix": { + "description": "Fix action label on the missing-weight card" + }, + "workoutCalorieEstimatesTitle": "Calorie figures are estimates", + "@workoutCalorieEstimatesTitle": { + "description": "StatusCard title reminding calories are estimated" + }, + "workoutSuggestionsTitle": "{n, plural, one{{n} effort we spotted but did not log} other{{n} efforts we spotted but did not log}}", + "@workoutSuggestionsTitle": { + "description": "Title of the detected-but-unlogged effort card on History", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "workoutSuggestionsBody": "The band saw sustained work and nothing was started for it. Nothing is logged until you say so.", + "@workoutSuggestionsBody": { + "description": "Body text on the detected-but-unlogged effort card" + }, + "workoutReviewFix": "{n, plural, one{Review it} other{Review them}}", + "@workoutReviewFix": { + "description": "Fix action label on the detected-but-unlogged effort card", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "workoutLogPastTitle": "Did something the band missed?", + "@workoutLogPastTitle": { + "description": "Title of the log-a-past-workout card" + }, + "workoutLogPastBody": "Enter the times yourself and it is scored from the heart rate recorded across them, like any other session.", + "@workoutLogPastBody": { + "description": "Body text of the log-a-past-workout card" + }, + "workoutLogPastFix": "Log a past workout", + "@workoutLogPastFix": { + "description": "Fix action label on the log-a-past-workout card" + }, + "workoutNoSessionsTitle": "No sessions recorded yet", + "@workoutNoSessionsTitle": { + "description": "Empty-state title on History tab" + }, + "workoutNoSessionsBody": "Sessions appear here once you start one.", + "@workoutNoSessionsBody": { + "description": "Empty-state body on History tab" + }, + "workoutStartWorkoutFix": "Start a workout", + "@workoutStartWorkoutFix": { + "description": "Fix action label on the empty History state" + }, + "workoutTrackedLabel": "Tracked", + "@workoutTrackedLabel": { + "description": "Summary stat tile label, total sessions tracked" + }, + "workoutWeeklyLoadLabel": "Weekly load", + "@workoutWeeklyLoadLabel": { + "description": "Summary stat tile label, this week's strain load" + }, + "workoutNoneLabel": "None", + "@workoutNoneLabel": { + "description": "Summary stat tile value when weekly load is absent" + }, + "workoutImportedThisWeekNote": "{count} of this week's sessions came from {storeName}. They count here, and they are left out of weekly load — an imported workout arrives with no heart-rate trace, and a load number without one would be invented.", + "@workoutImportedThisWeekNote": { + "description": "Explanatory note under the weekly summary row when some sessions were imported", + "placeholders": { + "count": { + "type": "int" + }, + "storeName": { + "type": "String" + } + } + }, + "workoutAutoImportOnLabel": "Auto-import on. Tap to turn off.", + "@workoutAutoImportOnLabel": { + "description": "Semantic label on the auto-import toggle when on" + }, + "workoutAutoImportOffLabel": "Auto-import off. Tap to turn on.", + "@workoutAutoImportOffLabel": { + "description": "Semantic label on the auto-import toggle when off" + }, + "workoutImportFromStore": "Import from {storeName}", + "@workoutImportFromStore": { + "description": "Import card row title naming the health store", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutFetchNowLabel": "Fetch workouts now", + "@workoutFetchNowLabel": { + "description": "Semantic label on the manual refresh icon" + }, + "workoutImportDenied": "{storeName} did not grant workouts. Nothing was read.", + "@workoutImportDenied": { + "description": "Import status note when the health store denies permission", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutImportEmpty": "Nothing came back. {storeName} holds no workouts inside the window it will share.", + "@workoutImportEmpty": { + "description": "Import status note when the health store has nothing to sync", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutImportNoRoutes": " {storeName} will not share routes, so none have coordinates.", + "@workoutImportNoRoutes": { + "description": "Import result clause when the store cannot share GPS routes at all", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutImportNoneWithRoute": " None of them had a route recorded.", + "@workoutImportNoneWithRoute": { + "description": "Import result clause when routes are supported but none of the imported workouts had one" + }, + "workoutImportSomeWithRoute": " {count} came with a route.", + "@workoutImportSomeWithRoute": { + "description": "Import result clause reporting how many imported workouts had a route", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workoutImportBroughtIn": "{count, plural, one{{count} workout brought in.} other{{count} workouts brought in.}}", + "@workoutImportBroughtIn": { + "description": "Import result headline reporting how many workouts were imported", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workoutImportFailed": "Failed: {error}", + "@workoutImportFailed": { + "description": "Import status note when the sync throws", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "workoutMorningAfterTitle": "The morning after", + "@workoutMorningAfterTitle": { + "description": "Section header over the session-cost history" + }, + "workoutMorningAfterBody": "Your own history, not a rule about the activity — these mornings also had whatever evening came with them. Nothing here is a reason to skip a session.", + "@workoutMorningAfterBody": { + "description": "Disclaimer under the morning-after rows" + }, + "workoutAfterActivity": "After {name}", + "@workoutAfterActivity": { + "description": "Morning-after row label naming the activity type", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "workoutUnchangedLabel": "Unchanged", + "@workoutUnchangedLabel": { + "description": "Morning-after row value when the effect is inside noise" + }, + "workoutRestingHeartRateLabel": "Resting heart rate", + "@workoutRestingHeartRateLabel": { + "description": "Morning-after row sub-label for the RHR metric" + }, + "workoutHrvLabel": "HRV", + "@workoutHrvLabel": { + "description": "Morning-after row sub-label for the HRV metric" + }, + "workoutMorningCount": "{n, plural, one{{n} morning} other{{n} mornings}}", + "@workoutMorningCount": { + "description": "Sample-size count in the morning-after row sub-label", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "workoutInsideRangeSuffix": " · inside your night-to-night range", + "@workoutInsideRangeSuffix": { + "description": "Suffix appended to a morning-after row when the effect does not exceed MDC" + }, + "workoutDeleteSessionLabel": "Delete this session", + "@workoutDeleteSessionLabel": { + "description": "Semantic label on the history row delete icon" + }, + "workoutStrainLabel": "strain", + "@workoutStrainLabel": { + "description": "Small caption beside the strain number on a history row" + }, + "workoutTimeInZonesTitle": "TIME IN ZONES", + "@workoutTimeInZonesTitle": { + "description": "Chart title on the history row zone bar" + }, + "workoutMinutesUnit": "minutes", + "@workoutMinutesUnit": { + "description": "Chart unit label on the history row zone bar" + }, + "workoutFixTimesOnSessionLabel": "Fix the times on this session", + "@workoutFixTimesOnSessionLabel": { + "description": "Semantic label on the retime control of a history row" + }, + "workoutFixTimes": "Fix the times", + "@workoutFixTimes": { + "description": "Retime button text on a history row, and title of the retime screen" + }, + "workoutTimeStatLabel": "Time", + "@workoutTimeStatLabel": { + "description": "Session stat row label for duration" + }, + "workoutDistanceStatLabel": "Distance", + "@workoutDistanceStatLabel": { + "description": "Session stat row label for distance" + }, + "workoutCaloriesStatLabel": "Calories", + "@workoutCaloriesStatLabel": { + "description": "Session stat row label for calories" + }, + "workoutNotCostedValue": "Not costed", + "@workoutNotCostedValue": { + "description": "Session stat row value when calories were not estimated" + }, + "workoutMaxHrStatLabel": "Max HR", + "@workoutMaxHrStatLabel": { + "description": "Session stat row label for peak heart rate" + }, + "workoutNoReadingValue": "No reading", + "@workoutNoReadingValue": { + "description": "Session stat row value when the band recorded nothing" + }, + "workoutConfirmDeleteTitle": "Delete this {activity}?", + "@workoutConfirmDeleteTitle": { + "description": "Confirm-delete dialog title naming the session's activity type", + "placeholders": { + "activity": { + "type": "String" + } + } + }, + "workoutDeleteBodyOwn": "It disappears from OpenStrap. A copy in {storeName}, if there is one, stays where it is.", + "@workoutDeleteBodyOwn": { + "description": "Confirm-delete dialog body for a session this band recorded", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutDeleteBodyImported": "It disappears from OpenStrap and will not be re-imported. The original in {storeName} stays.", + "@workoutDeleteBodyImported": { + "description": "Confirm-delete dialog body for an imported session", + "placeholders": { + "storeName": { + "type": "String" + } + } + }, + "workoutWhenToday": "Today, {time}", + "@workoutWhenToday": { + "description": "History row timestamp for a session started today", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "workoutWhenYesterday": "Yesterday, {time}", + "@workoutWhenYesterday": { + "description": "History row timestamp for a session started yesterday", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "workoutWeekdayLetterMon": "M", + "@workoutWeekdayLetterMon": { + "description": "Single-letter weekday abbreviation (Mon) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterTue": "T", + "@workoutWeekdayLetterTue": { + "description": "Single-letter weekday abbreviation (Tue) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterWed": "W", + "@workoutWeekdayLetterWed": { + "description": "Single-letter weekday abbreviation (Wed) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterThu": "T", + "@workoutWeekdayLetterThu": { + "description": "Single-letter weekday abbreviation (Thu) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterFri": "F", + "@workoutWeekdayLetterFri": { + "description": "Single-letter weekday abbreviation (Fri) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterSat": "S", + "@workoutWeekdayLetterSat": { + "description": "Single-letter weekday abbreviation (Sat) used in the This-week strip and small charts" + }, + "workoutWeekdayLetterSun": "S", + "@workoutWeekdayLetterSun": { + "description": "Single-letter weekday abbreviation (Sun) used in the This-week strip and small charts" + }, + "workoutWeekdayAbbrMon": "Mon", + "@workoutWeekdayAbbrMon": { + "description": "Three-letter weekday abbreviation (Mon) used in the History row date" + }, + "workoutWeekdayAbbrTue": "Tue", + "@workoutWeekdayAbbrTue": { + "description": "Three-letter weekday abbreviation (Tue) used in the History row date" + }, + "workoutWeekdayAbbrWed": "Wed", + "@workoutWeekdayAbbrWed": { + "description": "Three-letter weekday abbreviation (Wed) used in the History row date" + }, + "workoutWeekdayAbbrThu": "Thu", + "@workoutWeekdayAbbrThu": { + "description": "Three-letter weekday abbreviation (Thu) used in the History row date" + }, + "workoutWeekdayAbbrFri": "Fri", + "@workoutWeekdayAbbrFri": { + "description": "Three-letter weekday abbreviation (Fri) used in the History row date" + }, + "workoutWeekdayAbbrSat": "Sat", + "@workoutWeekdayAbbrSat": { + "description": "Three-letter weekday abbreviation (Sat) used in the History row date" + }, + "workoutWeekdayAbbrSun": "Sun", + "@workoutWeekdayAbbrSun": { + "description": "Three-letter weekday abbreviation (Sun) used in the History row date" + }, + "activitySetupRouteLabel": "Route", + "@activitySetupRouteLabel": { + "description": "Label for the GPS route row on the activity setup screen" + }, + "activitySetupRouteDetail": "Recorded if location is available, and kept on this phone", + "@activitySetupRouteDetail": { + "description": "Detail text under the route row on the activity setup screen" + }, + "activitySetupHeartRateLabel": "Heart rate", + "@activitySetupHeartRateLabel": { + "description": "Label for the heart rate row on the activity setup screen" + }, + "activitySetupBandConnected": "Band connected", + "@activitySetupBandConnected": { + "description": "Detail text when the band is connected, shown under the heart rate row" + }, + "activitySetupNoBandConnected": "No band connected", + "@activitySetupNoBandConnected": { + "description": "Detail text when no band is connected, shown under the heart rate row" + }, + "activitySetupPrivateLabel": "Private session", + "@activitySetupPrivateLabel": { + "description": "Label for the private-session toggle on the activity setup screen" + }, + "activitySetupPrivateDetail": "Hidden from summaries and exports", + "@activitySetupPrivateDetail": { + "description": "Detail text under the private-session toggle" + }, + "activitySetupCaloriesNeedWeight": "Calories need your weight.", + "@activitySetupCaloriesNeedWeight": { + "description": "Shown instead of a calorie estimate when the user has no weight on file" + }, + "activitySetupCalorieEstimate": "About {est} kcal per {minutes} min, from {met} MET and your weight.", + "@activitySetupCalorieEstimate": { + "description": "Calorie estimate line on the activity setup screen, e.g. 'About 250 kcal per 30 min, from 8.0 MET and your weight.'", + "placeholders": { + "est": { + "type": "int" + }, + "minutes": { + "type": "int" + }, + "met": { + "type": "String" + } + } + }, + "activitySetupTrackSets": "Sets, reps and load — logged by you", + "@activitySetupTrackSets": { + "description": "What this session type will record — strength training tracked by sets" + }, + "activitySetupTrackDistanceGps": "Distance, pace and heart rate", + "@activitySetupTrackDistanceGps": { + "description": "What this session type will record — distance-based activity with GPS" + }, + "activitySetupTrackTime": "Time and heart rate", + "@activitySetupTrackTime": { + "description": "What this session type will record — time-based activity" + }, + "activitySetupTrackInterval": "Rounds and heart rate", + "@activitySetupTrackInterval": { + "description": "What this session type will record — interval/rounds-based activity" + }, + "activitySetupTrackStillness": "Time, breathing and heart rate", + "@activitySetupTrackStillness": { + "description": "What this session type will record — stillness activity like yoga or breathwork" + }, + "activitySetupSessionRunningTitle": "A session is already running", + "@activitySetupSessionRunningTitle": { + "description": "Status card title when the user tries to start a session while one is already live" + }, + "activitySetupSessionRunningBody": "Only one can be live at a time.", + "@activitySetupSessionRunningBody": { + "description": "Status card body when the user tries to start a session while one is already live" + }, + "activitySetupOpenRunningSession": "Open the running session", + "@activitySetupOpenRunningSession": { + "description": "Status card action button to jump to the already-running session" + }, + "activitySetupStart": "Start", + "@activitySetupStart": { + "description": "Button label to start a new activity session" + }, + "activityPickerTitle": "Choose activity", + "@activityPickerTitle": { + "description": "Nav bar title on the activity picker screen" + }, + "activityPickerSearchLabel": "Search activities", + "@activityPickerSearchLabel": { + "description": "Accessibility label for the activity search field" + }, + "activityPickerSearchHint": "Search {count} activities", + "@activityPickerSearchHint": { + "description": "Placeholder hint in the activity search field, e.g. 'Search 73 activities'", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "activityPickerNoMatchTitle": "No activity matches that", + "@activityPickerNoMatchTitle": { + "description": "Status card title when a search finds no matching activity" + }, + "activityPickerNoMatchBody": "The catalogue covers about seventy activities with a published energy cost. Pick the closest one.", + "@activityPickerNoMatchBody": { + "description": "Status card body when a search finds no matching activity" + }, + "activityPickerQuickStart": "QUICK START", + "@activityPickerQuickStart": { + "description": "Section heading over the quick-start row when the user has no activity history" + }, + "activityPickerRecent": "RECENT", + "@activityPickerRecent": { + "description": "Section heading over the recent-activities row" + }, + "activityPickerCalorieEstimatesTitle": "Calorie figures are estimates", + "@activityPickerCalorieEstimatesTitle": { + "description": "Status card title explaining that calorie figures are estimates" + }, + "activityPickerMetValue": "{met} MET", + "@activityPickerMetValue": { + "description": "MET value shown on an activity row when no calorie estimate is available, e.g. '8.0 MET'", + "placeholders": { + "met": { + "type": "String" + } + } + }, + "activityPickerKcalPer30": "{kcal} kcal / 30 min", + "@activityPickerKcalPer30": { + "description": "Calorie estimate shown on an activity row, e.g. '250 kcal / 30 min'", + "placeholders": { + "kcal": { + "type": "int" + } + } + }, + "dayStrainToday": "TODAY", + "@dayStrainToday": { + "description": "Day strain screen sub-header when the shown day is today" + }, + "dayStrainTitle": "Day strain", + "@dayStrainTitle": { + "description": "Day strain screen title, and the strain metric label shown inline" + }, + "dayStrainNoTraceTitle": "No strain trace for this day", + "@dayStrainNoTraceTitle": { + "description": "Status card title when a day has no strain score and no per-minute trace" + }, + "dayStrainNoMinuteTraceTitle": "No minute-by-minute trace for this day", + "@dayStrainNoMinuteTraceTitle": { + "description": "Status card title when a day has a strain score but no per-minute trace stored" + }, + "dayStrainNoReasonBody": "Nothing recorded says why this day produced no strain.", + "@dayStrainNoReasonBody": { + "description": "Fallback body text when no strain trace exists and no reason was given by the backend" + }, + "dayStrainScoredNoTraceBody": "The day strain is {strain}. The waking minutes it was built from are not stored for this day.", + "@dayStrainScoredNoTraceBody": { + "description": "Body text when a day has a strain score but no per-minute curve stored", + "placeholders": { + "strain": { + "type": "String" + } + } + }, + "dayStrainWearBandFix": "Wear the band through the day", + "@dayStrainWearBandFix": { + "description": "Suggested fix action on the no-strain-trace card, shown when the band never saw the day" + }, + "dayStrainChartTitle": "STRAIN THROUGH THE DAY", + "@dayStrainChartTitle": { + "description": "Chart title above the day's strain-over-time curve" + }, + "dayStrainChartFootnote": "Accumulated, so it only ever climbs — the STEEP parts are where the effort was. Built from {drawn} recorded waking minutes.", + "@dayStrainChartFootnote": { + "description": "Footnote under the strain curve chart explaining it is cumulative", + "placeholders": { + "drawn": { + "type": "int" + } + } + }, + "dayStrainPeakHr": "Peak HR", + "@dayStrainPeakHr": { + "description": "Inline metric label for peak heart rate on the day strain screen" + }, + "dayStrainWorn": "Worn", + "@dayStrainWorn": { + "description": "Inline metric label for minutes the band was worn on the day strain screen" + }, + "dayStrainLowCoverageTitle": "The band saw {pct}% of this day", + "@dayStrainLowCoverageTitle": { + "description": "Status card title warning that band coverage was low for this day", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "dayStrainLowCoverageBody": "Strain is a total over the minutes that were recorded, so a partly-worn day reads lower than a full one and the two are not comparable.", + "@dayStrainLowCoverageBody": { + "description": "Status card body explaining why a partly-worn day is not comparable to a full one" + }, + "dayStrainTimeInZonesSection": "Time in zones", + "@dayStrainTimeInZonesSection": { + "description": "Section header above the heart-rate zone bar for the day" + }, + "dayStrainZonesChartTitle": "TIME IN ZONES", + "@dayStrainZonesChartTitle": { + "description": "Chart title for the day's time-in-zones bar" + }, + "dayStrainZoneFootnoteKarvonen": "Zone edges span the gap between your measured resting heart rate and the highest we have seen ({maxHr} bpm). Both measured on you.", + "@dayStrainZoneFootnoteKarvonen": { + "description": "Footnote under the day's zone bar when both resting HR and ceiling are measured (karvonen)", + "placeholders": { + "maxHr": { + "type": "int" + } + } + }, + "dayStrainZoneFootnoteObserved": "Zone edges are percentages of the highest heart rate we have seen ({maxHr} bpm) — measured, not estimated.", + "@dayStrainZoneFootnoteObserved": { + "description": "Footnote under the day's zone bar when only the ceiling is measured", + "placeholders": { + "maxHr": { + "type": "int" + } + } + }, + "dayStrainHowSet": "How these are set", + "@dayStrainHowSet": { + "description": "Link/action label leading to the full zones detail screen" + }, + "dayStrainInputsSection": "What this is made of", + "@dayStrainInputsSection": { + "description": "Section header above the paragraph explaining what the strain score is built from" + }, + "dayStrainInputsBase": "Banister TRIMP over your waking heart rate, scaled to 0–21.", + "@dayStrainInputsBase": { + "description": "First sentence of the day strain 'what this is made of' explanation" + }, + "dayStrainInputsMaxHr": "It was integrated against an assumed maximum of {maxHr} bpm — estimated from your age and your strap, not measured.", + "@dayStrainInputsMaxHr": { + "description": "Sentence naming the assumed max heart rate the strain score was integrated against", + "placeholders": { + "maxHr": { + "type": "int" + } + } + }, + "dayStrainInputsMeasuredCeilingNote": "The zone bar above uses the measured ceiling instead; strain has not been moved onto it, because that would rewrite every strain score you have ever seen.", + "@dayStrainInputsMeasuredCeilingNote": { + "description": "Sentence noting the zone bar uses a different, measured ceiling than the strain score" + }, + "dayStrainInputsRhrAnchor": "The other anchor is your resting heart rate from the night before, so a night the band missed moves the whole day.", + "@dayStrainInputsRhrAnchor": { + "description": "Sentence naming resting heart rate as the strain score's other anchor" + }, + "activityZonesTitle": "Heart-rate zones", + "@activityZonesTitle": { + "description": "Heart-rate zones detail screen title" + }, + "activityZonesYourZonesSection": "Your zones", + "@activityZonesYourZonesSection": { + "description": "Section header above the list of zone rows" + }, + "activityZonesIntensitySection": "Where your intensity went", + "@activityZonesIntensitySection": { + "description": "Section header above the 28-day intensity distribution chart" + }, + "activityZonesNoCeilingTitle": "No measured ceiling yet", + "@activityZonesNoCeilingTitle": { + "description": "Status card title when no measured heart-rate ceiling exists yet" + }, + "activityZonesNoCeilingTanakaTail": " Until one is measured, the zones below come off your age.", + "@activityZonesNoCeilingTanakaTail": { + "description": "Trailing sentence fragment appended when age-estimated zones are shown below (note the leading space, appended directly after another sentence)" + }, + "activityZonesNoCeilingDefaultBody": "We only count a high reading the band held for 15 seconds while you were moving. A one-second spike is not a heart rate.", + "@activityZonesNoCeilingDefaultBody": { + "description": "Default explanation of the 15-second hold rule for a measured ceiling, shown when no other reason was given" + }, + "activityZonesWearBandFix": "Wear the band for your normal hard sessions", + "@activityZonesWearBandFix": { + "description": "Suggested fix action on the no-ceiling card" + }, + "activityZonesHighestSeenLabel": "HIGHEST WE HAVE SEEN", + "@activityZonesHighestSeenLabel": { + "description": "Label above the highest measured heart rate figure" + }, + "activityZonesBpmUnit": "bpm", + "@activityZonesBpmUnit": { + "description": "Unit label 'bpm' shown next to a heart-rate number" + }, + "activityZonesCeilingOnDate": "on {date}", + "@activityZonesCeilingOnDate": { + "description": "Fragment naming the date the heart-rate ceiling was recorded, e.g. 'on 3 Aug'", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "activityZonesCeilingDuringSession": "during {session}", + "@activityZonesCeilingDuringSession": { + "description": "Fragment naming the session type the ceiling was recorded during, e.g. 'during running'", + "placeholders": { + "session": { + "type": "String" + } + } + }, + "activityZonesHighestSeenFootnote": "The highest we have measured, not a limit — it creeps up as the band sees harder efforts. Do not go and test it.", + "@activityZonesHighestSeenFootnote": { + "description": "Footnote clarifying the highest-seen heart rate is not a limit and should not be deliberately tested" + }, + "activityZonesNoZonesTitle": "No zones yet", + "@activityZonesNoZonesTitle": { + "description": "Status card title when there are no zone edges at all" + }, + "activityZonesNoAgeBody": "Zone edges are percentages of a maximum heart rate, and without your age there is nothing to take a percentage of.", + "@activityZonesNoAgeBody": { + "description": "Explanation shown when zone edges are missing because the user's age is not set" + }, + "catalogueZonesWhy": "Zone edges are percentages of a maximum heart rate estimated from your age — not one measured on you.", + "@catalogueZonesWhy": { + "description": "Shared footnote under the zone chart on the workout summary and day-strain screens, when the ceiling is a Tanaka age estimate rather than a measured one" + }, + "activityZonesNoZonesDefaultBody": "Nothing recorded says why there are no zone edges yet.", + "@activityZonesNoZonesDefaultBody": { + "description": "Fallback explanation when zone edges are missing and no reason was given" + }, + "activityZonesAddAgeFix": "Add your age in Profile", + "@activityZonesAddAgeFix": { + "description": "Suggested fix action to add age in Profile, shown only when age truly is missing" + }, + "activityZonesAnchorKarvonen": "Built from two numbers the band measured on you: your resting rate ({restingHr}, the middle of your last {restingDays} nights) and the highest we have seen ({maxHr}). A low resting rate makes zone 1 wide. These are the usual bands, not your own measured thresholds.", + "@activityZonesAnchorKarvonen": { + "description": "Explanation of how zone edges were built when both resting HR and ceiling are measured", + "placeholders": { + "restingHr": { + "type": "int" + }, + "restingDays": { + "type": "int" + }, + "maxHr": { + "type": "int" + } + } + }, + "activityZonesAnchorObserved": "Built from the highest heart rate we have seen ({maxHr}). After {restingMinDays} nights of resting rate (you have {restingDays}) your resting rate joins it, which fits you better. These are the usual bands, not your own measured thresholds.", + "@activityZonesAnchorObserved": { + "description": "Explanation of how zone edges were built when only the ceiling is measured and resting HR history is still short", + "placeholders": { + "maxHr": { + "type": "int" + }, + "restingMinDays": { + "type": "int" + }, + "restingDays": { + "type": "int" + } + } + }, + "activityZonesAnchorTanaka": "Built from {maxHr} bpm, estimated from your age rather than measured on you — it can be 20 bpm out either way. The edges move to a measured ceiling once the band sees a hard enough session.", + "@activityZonesAnchorTanaka": { + "description": "Explanation of how zone edges were built from an age-estimated maximum heart rate", + "placeholders": { + "maxHr": { + "type": "int" + } + } + }, + "activityZonesAnchorDefault": "Zone edges are percentages of a maximum heart rate.", + "@activityZonesAnchorDefault": { + "description": "Generic fallback explanation of what zone edges are" + }, + "activityZonesNotShownTitle": "Not shown yet", + "@activityZonesNotShownTitle": { + "description": "Status card title when the 28-day intensity distribution is not yet shown" + }, + "activityZonesNeedsMonthBody": "Needs about a month of recorded sessions, each with a minute-by-minute heart rate.", + "@activityZonesNeedsMonthBody": { + "description": "Explanation of what is needed before the intensity distribution chart appears, when zones are measured" + }, + "activityZonesAgeEstimateBody": "The bars would be a picture of the age estimate, not of your training. They appear once the zone edges above are measured.", + "@activityZonesAgeEstimateBody": { + "description": "Explanation of why the intensity distribution is withheld when zones are only age-estimated" + }, + "activityZonesSessionMinutesChartTitle": "SESSION MINUTES, LAST 28 DAYS", + "@activityZonesSessionMinutesChartTitle": { + "description": "Chart title for the 28-day session-minutes-by-zone bar" + }, + "activityZonesShapePyramidal": "Most of your minutes are easy, fewer in the middle, fewest hard — a pyramid.", + "@activityZonesShapePyramidal": { + "description": "Description of a pyramidal training-intensity distribution shape" + }, + "activityZonesShapePolarised": "Most of your minutes are easy and the rest are hard, with little in between.", + "@activityZonesShapePolarised": { + "description": "Description of a polarised training-intensity distribution shape" + }, + "activityZonesShapeMiddleHeavy": "Most of your minutes sit in the middle rather than easy or hard.", + "@activityZonesShapeMiddleHeavy": { + "description": "Description of a middle-heavy training-intensity distribution shape" + }, + "activityZonesShapeSummary": "{easy} min easy, {moderate} moderate, {hard} hard, over {sessions} recorded sessions. A description, not a target.", + "@activityZonesShapeSummary": { + "description": "Sentence summarizing minutes spent easy/moderate/hard across recorded sessions", + "placeholders": { + "easy": { + "type": "int" + }, + "moderate": { + "type": "int" + }, + "hard": { + "type": "int" + }, + "sessions": { + "type": "int" + } + } + }, + "activityShareTitle": "Share", + "@activityShareTitle": { + "description": "Nav bar title on the share screen, and the label of its main share button" + }, + "activityShareOpenFailed": "Could not open the share sheet.", + "@activityShareOpenFailed": { + "description": "Snackbar shown when the OS share sheet fails to open" + }, + "activitySharePhotoHeader": "YOUR PHOTO", + "@activitySharePhotoHeader": { + "description": "Section eyebrow header above the photo picker row on the share screen" + }, + "activityShareAddPhoto": "Add a photo", + "@activityShareAddPhoto": { + "description": "Row label when no photo has been chosen yet for the share card" + }, + "activityShareChangePhoto": "Change photo", + "@activityShareChangePhoto": { + "description": "Row label when a photo has already been chosen, to pick a different one" + }, + "activitySharePhotoHint": "From this phone. Nothing is uploaded", + "@activitySharePhotoHint": { + "description": "Subtitle under the photo row explaining the photo stays on-device" + }, + "activityShareRemovePhoto": "Remove the photo", + "@activityShareRemovePhoto": { + "description": "Row label/button to remove the chosen photo from the share card" + }, + "activityShareBasemapHeader": "BASEMAP", + "@activityShareBasemapHeader": { + "description": "Section eyebrow header above the basemap toggle row on the share screen" + }, + "activityShareDrawMap": "Draw the real map", + "@activityShareDrawMap": { + "description": "Row label for the toggle that fetches real map tiles for the route" + }, + "activityShareMapHint": "Asks openstreetmap.org for the tiles covering this route. Off, the route draws on its own", + "@activityShareMapHint": { + "description": "Subtitle explaining the basemap toggle asks openstreetmap.org for tiles" + }, + "activityShareFetchingMapTitle": "Fetching the map", + "@activityShareFetchingMapTitle": { + "description": "Status card title while map tiles are being fetched" + }, + "activityShareFetchingMapBody": "The card draws as soon as every tile is here.", + "@activityShareFetchingMapBody": { + "description": "Status card body while map tiles are being fetched" + }, + "activityShareNoMapTitle": "No map for this card", + "@activityShareNoMapTitle": { + "description": "Status card title when the map tile fetch failed" + }, + "activityShareNoMapBody": "The map tiles could not be fetched, so the route is drawn on its own. Everything else on the card is unchanged.", + "@activityShareNoMapBody": { + "description": "Status card body explaining the map tile fetch failed and the route drew on its own" + }, + "activityShareStatusPrivateTitle": "This session is private", + "@activityShareStatusPrivateTitle": { + "description": "Status card title shown when the session being shared is marked private" + }, + "activityShareStatusPrivateBody": "Hidden from summaries and exports.", + "@activityShareStatusPrivateBody": { + "description": "Status card body explaining a private session is hidden from summaries and exports" + }, + "activityPosterFormatPost": "Post", + "@activityPosterFormatPost": { + "description": "Format switcher label for the 1:1 feed post crop of the share card" + }, + "activityPosterFormatStory": "Story", + "@activityPosterFormatStory": { + "description": "Format switcher label for the 9:16 portrait story crop of the share card" + }, + "activitySummaryRpeHeadline": "HOW HARD DID THAT FEEL?", + "@activitySummaryRpeHeadline": { + "description": "Header of the TS-09 post-session effort rating card" + }, + "activitySummaryRpeBody": "Your own rating of the effort. It is a feeling, not a measurement — which is the point, because it can disagree with the numbers above.", + "@activitySummaryRpeBody": { + "description": "Explanatory body text under the effort rating card header" + }, + "activitySummaryRateEffort": "Rate this effort {n} of 10", + "@activitySummaryRateEffort": { + "description": "Semantic label on each of the 10 effort-rating tap targets", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryRpeVeryEasy": "1 · very easy", + "@activitySummaryRpeVeryEasy": { + "description": "Left-hand caption under the 1-10 effort rating scale" + }, + "activitySummaryRpeMaximal": "10 · maximal", + "@activitySummaryRpeMaximal": { + "description": "Right-hand caption under the 1-10 effort rating scale" + }, + "activitySummaryNotNow": "Not now", + "@activitySummaryNotNow": { + "description": "Button to dismiss the effort rating card without answering" + }, + "activitySummaryShareThis": "Share this {name}", + "@activitySummaryShareThis": { + "description": "Semantic label on the share icon in the summary nav bar", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "activitySummaryChangeType": "Change activity type", + "@activitySummaryChangeType": { + "description": "Semantic label on the pencil icon that lets the user correct the activity type" + }, + "activitySummaryUnsavedTitle": "This session is not saved yet", + "@activitySummaryUnsavedTitle": { + "description": "Title of the status card shown when a finished session failed to save" + }, + "activitySummaryUnsavedBody": "Writing it to this phone failed.", + "@activitySummaryUnsavedBody": { + "description": "Body of the status card shown when a finished session failed to save" + }, + "activitySummarySaving": "Saving", + "@activitySummarySaving": { + "description": "Fix button label while a retried save is in flight" + }, + "activitySummaryTryAgain": "Try again", + "@activitySummaryTryAgain": { + "description": "Fix button label to retry saving an unsaved session" + }, + "activitySummaryPrivate": "Private", + "@activitySummaryPrivate": { + "description": "Pill label shown next to the hero stat when the session is marked private" + }, + "activitySummaryStepsBasis": "Steps came from the strap's own motion sensor, which only counts them on foot.", + "@activitySummaryStepsBasis": { + "description": "Sentence explaining where the step count came from, appended to the calorie basis note" + }, + "activitySummaryCaloriesNeedWeight": "Calories need your weight.", + "@activitySummaryCaloriesNeedWeight": { + "description": "Calorie basis note when the user has no weight on file" + }, + "activitySummaryNoCalorieNoStrain": "No calorie figure for this session. An energy estimate from heart rate needs your maximum and resting heart rates, and one of them is not set.", + "@activitySummaryNoCalorieNoStrain": { + "description": "Calorie basis note when there is no calorie figure and no strain figure either" + }, + "activitySummaryNoCalorieWithStrain": "No calorie figure for this session — an energy estimate from heart rate needs your maximum and resting heart rates, and one of them is not set. Strain above is the effort that was measured, on its own 0–21 scale.", + "@activitySummaryNoCalorieWithStrain": { + "description": "Calorie basis note when there is no calorie figure but a strain figure exists" + }, + "activitySummaryCalorieNoHr": "Estimated from {met} MET and your weight. No heart rate reached this session, so none of it is in the figure.", + "@activitySummaryCalorieNoHr": { + "description": "Calorie basis note when calories were estimated from MET and weight with no heart rate", + "placeholders": { + "met": { + "type": "String" + } + } + }, + "activitySummaryCalorieWithHr": "Estimated from {met} MET, your weight and heart rate.", + "@activitySummaryCalorieWithHr": { + "description": "Calorie basis note when calories were estimated using heart rate too", + "placeholders": { + "met": { + "type": "String" + } + } + }, + "activitySummaryNothingLoggedWithLoad": "Nothing was logged with a load", + "@activitySummaryNothingLoggedWithLoad": { + "description": "Hero caption for a strength session with sets logged but no load recorded" + }, + "activitySummarySetUnit": "{n, plural, one{set} other{sets}}", + "@activitySummarySetUnit": { + "description": "Unit word next to the hero set count on a strength session with no volume", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryVolumeLoadedSets": "Volume of the loaded sets", + "@activitySummaryVolumeLoadedSets": { + "description": "Hero caption for total volume when some sets had no recorded load" + }, + "activitySummaryTotalVolume": "Total volume", + "@activitySummaryTotalVolume": { + "description": "Hero caption for total volume when every set had a recorded load" + }, + "activitySummaryElapsedTime": "Elapsed time", + "@activitySummaryElapsedTime": { + "description": "Fallback hero caption showing plain elapsed time" + }, + "activitySummaryClimbed": "+{m} m climbed", + "@activitySummaryClimbed": { + "description": "Hero caption showing metres climbed on a journey/hike session", + "placeholders": { + "m": { + "type": "int" + } + } + }, + "activitySummaryLapsCaption": "{n, plural, one{{n} lap} other{{n} laps}}", + "@activitySummaryLapsCaption": { + "description": "Hero caption showing lap count on a swim/row session", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryNoRouteTitle": "No route for this session", + "@activitySummaryNoRouteTitle": { + "description": "Title of the status card when a route session recorded no GPS track" + }, + "activitySummaryNoRouteBody": "Location was off, or this activity was not recorded with GPS.", + "@activitySummaryNoRouteBody": { + "description": "Body of the status card when a route session recorded no GPS track" + }, + "activitySummaryRouteTitle": "ROUTE", + "@activitySummaryRouteTitle": { + "description": "Chart title above the route map" + }, + "activitySummarySlower": "Slower", + "@activitySummarySlower": { + "description": "Route map legend entry for the slow end of the pace colour scale" + }, + "activitySummaryFaster": "Faster", + "@activitySummaryFaster": { + "description": "Route map legend entry for the fast end of the pace colour scale" + }, + "activitySummaryStartFinishPinned": "Start and finish are pinned.", + "@activitySummaryStartFinishPinned": { + "description": "Route map footnote when there is no distance to report" + }, + "activitySummaryRouteFootnote": "{distance} {unit}, start and finish pinned.", + "@activitySummaryRouteFootnote": { + "description": "Route map footnote reporting distance plus that start/finish are pinned", + "placeholders": { + "distance": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "activitySummaryNoSetsTitle": "No sets logged", + "@activitySummaryNoSetsTitle": { + "description": "Title of the status card when a strength session has no sets logged" + }, + "activitySummaryNoSetsBody": "Nothing was entered for this session, so there is no load and no volume to total.", + "@activitySummaryNoSetsBody": { + "description": "Body of the status card on the overview tab when a strength session has no sets logged" + }, + "activitySummaryNoRoundsTitle": "No rounds recorded", + "@activitySummaryNoRoundsTitle": { + "description": "Title of the status card when an interval session has no rounds recorded" + }, + "activitySummaryNoRoundsBody": "0 rounds logged.", + "@activitySummaryNoRoundsBody": { + "description": "Body of the status card when an interval session has no rounds recorded" + }, + "activitySummaryIntervalLadderTitle": "INTERVAL LADDER", + "@activitySummaryIntervalLadderTitle": { + "description": "Chart title above the interval work/rest ladder" + }, + "activitySummaryWork": "Work", + "@activitySummaryWork": { + "description": "Interval ladder legend entry for work intervals" + }, + "activitySummaryRest": "Rest", + "@activitySummaryRest": { + "description": "Interval ladder legend entry for rest intervals" + }, + "activitySummaryRoundLabel": "Round {n}", + "@activitySummaryRoundLabel": { + "description": "X-axis label naming a round number on the interval ladder chart", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryLongestBlock": "Longest block {time}.", + "@activitySummaryLongestBlock": { + "description": "Interval ladder footnote naming the longest work or rest block", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activitySummaryPosesCount": "{n, plural, one{{n} pose} other{{n} poses}}", + "@activitySummaryPosesCount": { + "description": "Caption on the flow session's defining-object card counting logged poses", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryNoLapsTitle": "No laps counted", + "@activitySummaryNoLapsTitle": { + "description": "Title of the status card when a laps session has no laps counted" + }, + "activitySummaryNoLapsBody": "0 laps tapped.", + "@activitySummaryNoLapsBody": { + "description": "Body of the status card when a laps session has no laps counted" + }, + "activitySummaryLapsTitle": "LAPS", + "@activitySummaryLapsTitle": { + "description": "Chart title above the lap-time bar chart" + }, + "activitySummarySecondsPerLap": "seconds per lap", + "@activitySummarySecondsPerLap": { + "description": "Unit label on the lap-time bar chart" + }, + "activitySummaryLapLabel": "Lap {n}", + "@activitySummaryLapLabel": { + "description": "X-axis label naming a lap number on the lap-time chart", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryPoolLength": "{m} m pool", + "@activitySummaryPoolLength": { + "description": "Lap chart footnote fragment naming the pool length", + "placeholders": { + "m": { + "type": "int" + } + } + }, + "activitySummaryFastest": "fastest {time}", + "@activitySummaryFastest": { + "description": "Lap chart footnote fragment naming the fastest lap time", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activitySummarySlowest": "slowest {time}", + "@activitySummarySlowest": { + "description": "Lap chart footnote fragment naming the slowest lap time", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activitySummaryNoElevationTitle": "No elevation profile", + "@activitySummaryNoElevationTitle": { + "description": "Title of the status card when a journey session has no elevation profile" + }, + "activitySummaryNoElevationBody": "No route, or the route carried no altitude.", + "@activitySummaryNoElevationBody": { + "description": "Body of the status card when a journey session has no elevation profile" + }, + "activitySummaryElevationTitle": "ELEVATION", + "@activitySummaryElevationTitle": { + "description": "Chart title above the elevation profile" + }, + "activitySummaryStart": "Start", + "@activitySummaryStart": { + "description": "X-axis / footnote label meaning the start of the session" + }, + "activitySummaryFinish": "Finish", + "@activitySummaryFinish": { + "description": "X-axis label meaning the finish of the session, paired with Start" + }, + "activitySummaryGain": "Gain", + "@activitySummaryGain": { + "description": "Inline metric label for elevation gained on a journey session" + }, + "activitySummaryLoss": "Loss", + "@activitySummaryLoss": { + "description": "Inline metric label for elevation lost on a journey session" + }, + "activitySummaryPeak": "Peak", + "@activitySummaryPeak": { + "description": "Inline metric label for peak elevation reached on a journey session" + }, + "activitySummaryColdPlungeWhy": "Cold closes the blood vessels the sensor reads through. Finding nothing here is expected, not a fault.", + "@activitySummaryColdPlungeWhy": { + "description": "Explanation shown on a cold plunge session for why the sensor found no pulse" + }, + "activitySummaryHeatWhy": "Heat, sweat and a strap that loosens as you warm up all stop the sensor seeing a pulse. Finding nothing here is ordinary, not a fault.", + "@activitySummaryHeatWhy": { + "description": "Explanation shown on a sauna/heat session for why the sensor found no pulse" + }, + "activitySummaryNoPulseTitle": "No pulse reading for this {activity}", + "@activitySummaryNoPulseTitle": { + "description": "Status card title when a heat/cold session found no pulse at all", + "placeholders": { + "activity": { + "type": "String" + } + } + }, + "activitySummaryOneMinutePulse": "One minute of pulse, and no more", + "@activitySummaryOneMinutePulse": { + "description": "Status card title when a heat/cold session found only one minute of pulse" + }, + "activitySummaryPulseGapNote": "The band found a pulse in {have} of {total} minutes. The gaps are expected, so what is drawn is the part it could see.", + "@activitySummaryPulseGapNote": { + "description": "Footnote on the heart-rate chart of a heat/cold session noting partial pulse coverage", + "placeholders": { + "have": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "activitySummaryTooShortTitle": "Too short to chart", + "@activitySummaryTooShortTitle": { + "description": "Status card title when a session is too short to chart heart rate" + }, + "activitySummaryTooShortBody": "One minute of heart rate is a point, not a line.", + "@activitySummaryTooShortBody": { + "description": "Status card body when a session is too short to chart heart rate" + }, + "activitySummaryNoHrTitle": "No heart rate for this session", + "@activitySummaryNoHrTitle": { + "description": "Status card title when a non-thermal session recorded no heart rate at all" + }, + "activitySummaryNoHrBody": "The band reported nothing while this was running.", + "@activitySummaryNoHrBody": { + "description": "Status card body when a non-thermal session recorded no heart rate at all" + }, + "activitySummaryCheckBandConnection": "Check band connection", + "@activitySummaryCheckBandConnection": { + "description": "Fix button label pointing the user at the profile's sources list" + }, + "activitySummaryPartialTrace": "Partial trace — the band handed over {pct}% of these minutes.", + "@activitySummaryPartialTrace": { + "description": "Heart-rate chart footnote when the stored trace covers less than 90% of the session", + "placeholders": { + "pct": { + "type": "int" + } + } + }, + "activitySummaryHeartRateTitle": "HEART RATE", + "@activitySummaryHeartRateTitle": { + "description": "Chart title above the heart-rate trace" + }, + "activitySummaryHardMinutesNote": "{min} min above 80% of your maximum.", + "@activitySummaryHardMinutesNote": { + "description": "Heart-rate chart footnote naming minutes spent above 80% of max heart rate", + "placeholders": { + "min": { + "type": "int" + } + } + }, + "activitySummaryTimeInZonesTitle": "TIME IN ZONES", + "@activitySummaryTimeInZonesTitle": { + "description": "Chart title above the heart-rate zone bar" + }, + "activitySummaryTopSet": "Top set", + "@activitySummaryTopSet": { + "description": "Section title above the heaviest logged set on a strength session" + }, + "activitySummaryOneRepMax": "1RM estimate {kg} kg", + "@activitySummaryOneRepMax": { + "description": "Estimated one-rep-max caption under the top set exercise name", + "placeholders": { + "kg": { + "type": "int" + } + } + }, + "activitySummarySomeSetsNoLoadTitle": "Some sets had no load", + "@activitySummarySomeSetsNoLoadTitle": { + "description": "Status card title when some strength sets were logged with no load" + }, + "activitySummarySomeSetsNoLoadBody": "Counted in sets and reps, but left out of volume.", + "@activitySummarySomeSetsNoLoadBody": { + "description": "Status card body when some strength sets were logged with no load" + }, + "activitySummaryScore": "Score", + "@activitySummaryScore": { + "description": "Section title above the match game score table" + }, + "activitySummaryGameSetLabel": "Set {n}", + "@activitySummaryGameSetLabel": { + "description": "Row label naming a game set number in the match score table", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryNoSplitsTitle": "No splits for this session", + "@activitySummaryNoSplitsTitle": { + "description": "Status card title on the Splits tab when a route session has no splits" + }, + "activitySummaryNoSplitsBody": "Splits need a recorded distance.", + "@activitySummaryNoSplitsBody": { + "description": "Status card body on the Splits tab when a route session has no splits" + }, + "activitySummaryKm": "KM", + "@activitySummaryKm": { + "description": "Column header for kilometre number in the route splits table" + }, + "activitySummaryPace": "PACE", + "@activitySummaryPace": { + "description": "Column header for pace in the route splits table" + }, + "activitySummaryHr": "HR", + "@activitySummaryHr": { + "description": "Column header for average heart rate in the route splits table" + }, + "activitySummarySetsLoggedZero": "0 sets logged.", + "@activitySummarySetsLoggedZero": { + "description": "Status card body on the Splits tab when a strength session has no sets logged" + }, + "activitySummaryRoundHeader": "R", + "@activitySummaryRoundHeader": { + "description": "Column header for round number in the interval splits table" + }, + "activitySummaryWorkHeader": "WORK", + "@activitySummaryWorkHeader": { + "description": "Column header for work duration in the interval splits table" + }, + "activitySummaryRestHeader": "REST", + "@activitySummaryRestHeader": { + "description": "Column header for rest duration in the interval splits table" + }, + "activitySummaryAvgBpm": "AVG BPM", + "@activitySummaryAvgBpm": { + "description": "Column header for average heart rate in the interval splits table" + }, + "activitySummaryLapHeader": "LAP", + "@activitySummaryLapHeader": { + "description": "Column header for lap number in the laps splits table" + }, + "activitySummaryTimeHeader": "TIME", + "@activitySummaryTimeHeader": { + "description": "Column header for lap time in the laps splits table" + }, + "activitySummarySpeedVsFastest": "SPEED vs FASTEST", + "@activitySummarySpeedVsFastest": { + "description": "Column header for relative lap speed in the laps splits table" + }, + "activitySummaryBodyweightReps": "{n, plural, one{{n} rep · bodyweight} other{{n} reps · bodyweight}}", + "@activitySummaryBodyweightReps": { + "description": "Set row text for a logged set with no recorded load", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activitySummaryRpeValue": "RPE {v}", + "@activitySummaryRpeValue": { + "description": "Set-level RPE value shown next to a logged set", + "placeholders": { + "v": { + "type": "int" + } + } + }, + "activitySummaryNothingToPlot": "Nothing to plot for this {activity}", + "@activitySummaryNothingToPlot": { + "description": "Status card title on the Graphs tab for a heat/cold session with no trace to plot", + "placeholders": { + "activity": { + "type": "String" + } + } + }, + "activitySummaryNoSeriesTitle": "No series to plot", + "@activitySummaryNoSeriesTitle": { + "description": "Status card title on the Graphs tab when a session recorded no per-minute series" + }, + "activitySummaryNoSeriesBody": "This session recorded no per-minute streams.", + "@activitySummaryNoSeriesBody": { + "description": "Status card body on the Graphs tab when a session recorded no per-minute series" + }, + "activitySummaryHeartRateZones": "Heart-rate zones", + "@activitySummaryHeartRateZones": { + "description": "Section title above the heart-rate zone breakdown chart" + }, + "activitySummaryTabOverview": "Overview", + "@activitySummaryTabOverview": { + "description": "Sub-tab label for the Overview tab on the activity summary screen" + }, + "activitySummaryTabSplits": "Splits", + "@activitySummaryTabSplits": { + "description": "Sub-tab label for the Splits tab on the activity summary screen" + }, + "activitySummaryTabGraphs": "Graphs", + "@activitySummaryTabGraphs": { + "description": "Sub-tab label for the Graphs tab on the activity summary screen" + }, + "activityLiveAddALap": "Add a lap", + "@activityLiveAddALap": { + "description": "Live swim screen: accessibility label for the tap-a-lap button." + }, + "activityLiveAddExerciseTitle": "Add exercise", + "@activityLiveAddExerciseTitle": { + "description": "Live strength screen: title of the sheet for adding another exercise." + }, + "activityLiveAllowLocation": "Allow location", + "@activityLiveAllowLocation": { + "description": "Live measured session: fix action when location permission was denied." + }, + "activityLiveBestLabel": "Best", + "@activityLiveBestLabel": { + "description": "Live strength screen: header over the lifter's best-ever set for this exercise." + }, + "activityLiveBodyweightExcludedNote": "Bodyweight — left out of volume", + "@activityLiveBodyweightExcludedNote": { + "description": "Live strength screen: note shown when the set is logged as bodyweight-only." + }, + "activityLiveBodyweightOnly": "bodyweight only", + "@activityLiveBodyweightOnly": { + "description": "Live strength screen: unit label under the volume total when it is bodyweight-only." + }, + "activityLiveBpmUnit": "bpm", + "@activityLiveBpmUnit": { + "description": "Live heart-rate block: unit label next to the live beats-per-minute reading." + }, + "activityLiveBwAbbrev": "BW", + "@activityLiveBwAbbrev": { + "description": "Live strength screen: abbreviation for bodyweight, shown instead of a load in kg." + }, + "activityLiveChangeStroke": "Change stroke", + "@activityLiveChangeStroke": { + "description": "Live swim screen: accessibility label for the cycle-stroke button." + }, + "activityLiveDecrease": "{label} down", + "@activityLiveDecrease": { + "description": "Generic accessibility label for a decrease/minus control, e.g. 'Weight down'.", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "activityLiveDeniedForeverBody": "Location is denied for this app, which only Settings can change.", + "@activityLiveDeniedForeverBody": { + "description": "Live measured session: explanation when location is permanently denied for the app." + }, + "activityLiveDurationHeader": "DURATION", + "@activityLiveDurationHeader": { + "description": "Live measured session: section header above the elapsed-time display." + }, + "activityLiveEffortRpeHeader": "EFFORT (RPE)", + "@activityLiveEffortRpeHeader": { + "description": "Live strength screen: header above the RPE (rate of perceived exertion) picker." + }, + "activityLiveEndSet": "End set", + "@activityLiveEndSet": { + "description": "Live match screen: button that closes the current set and starts a new one." + }, + "activityLiveExerciseOf": "EXERCISE {index} OF {total}", + "@activityLiveExerciseOf": { + "description": "Live strength screen: 'EXERCISE {index} OF {total}' progress header.", + "placeholders": { + "index": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "activityLiveFinishSessionLabel": "Finish session", + "@activityLiveFinishSessionLabel": { + "description": "Live session shell: accessibility label for the stop/finish control." + }, + "activityLiveHoldTime": "Hold · {time}", + "@activityLiveHoldTime": { + "description": "Live flow (yoga) screen: 'Hold · {time}' countdown under the current pose.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activityLiveIncrease": "{label} up", + "@activityLiveIncrease": { + "description": "Generic accessibility label for an increase/plus control, e.g. 'Weight up'.", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "activityLiveIntervalSubtitle": "{workSec} S WORK · {restSec} S REST", + "@activityLiveIntervalSubtitle": { + "description": "Live interval screen: shell subtitle stating the work/rest split, e.g. '45 S WORK · 30 S REST'.", + "placeholders": { + "workSec": { + "type": "int" + }, + "restSec": { + "type": "int" + } + } + }, + "activityLiveKcalEstUnit": "kcal · est", + "@activityLiveKcalEstUnit": { + "description": "Common stats row: unit label for the estimated-calories figure." + }, + "activityLiveKgVolumeUnit": "kg volume", + "@activityLiveKgVolumeUnit": { + "description": "Live strength screen: unit label under the total training volume in kg." + }, + "activityLiveLapButtonLabel": "LAP", + "@activityLiveLapButtonLabel": { + "description": "Live swim screen: label on the big circular tap-a-lap button." + }, + "activityLiveLapsChartTitle": "LAPS", + "@activityLiveLapsChartTitle": { + "description": "Live swim screen: chart title for the per-lap time bars." + }, + "activityLiveLapsCount": "{count, plural, one{{count} lap} other{{count} laps}} · {stroke}", + "@activityLiveLapsCount": { + "description": "Live swim screen: 'N laps · stroke' summary line under the distance figure.", + "placeholders": { + "count": { + "type": "int" + }, + "stroke": { + "type": "String" + } + } + }, + "activityLiveLapsFootnote": "Fastest {time} · bar length is speed against it.", + "@activityLiveLapsFootnote": { + "description": "Live swim screen: footnote naming the fastest lap under the laps chart.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activityLiveLapXLabel": "Lap {n}", + "@activityLiveLapXLabel": { + "description": "Live swim screen: x-axis label on the laps chart, e.g. 'Lap 1'.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveLogAsBodyweight": "Log as bodyweight", + "@activityLiveLogAsBodyweight": { + "description": "Live strength screen: toggle label to log the current set as bodyweight-only." + }, + "activityLiveMatchSetSubtitle": "SET {n}", + "@activityLiveMatchSetSubtitle": { + "description": "Live match screen: shell subtitle showing the set number in progress.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveMetrePoolLabel": "{len} metre pool", + "@activityLiveMetrePoolLabel": { + "description": "Live swim screen: accessibility label for a pool-length choice, e.g. '25 metre pool'.", + "placeholders": { + "len": { + "type": "int" + } + } + }, + "activityLiveMinimiseLabel": "Minimise", + "@activityLiveMinimiseLabel": { + "description": "Live session shell: accessibility label for the minimise chevron." + }, + "activityLiveNextExercise": "Next exercise", + "@activityLiveNextExercise": { + "description": "Live strength screen: accessibility label for the next-exercise arrow." + }, + "activityLiveNextLabel": "NEXT", + "@activityLiveNextLabel": { + "description": "Live interval screen: 'NEXT' header before the upcoming work/rest phase." + }, + "activityLiveNextPose": "Next pose", + "@activityLiveNextPose": { + "description": "Live flow (yoga) screen: button to advance to the next pose." + }, + "activityLiveNextRest": "Rest · {time}", + "@activityLiveNextRest": { + "description": "Live interval screen: 'Rest · {time}' preview of the upcoming rest phase.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activityLiveNextWork": "Work · {time}", + "@activityLiveNextWork": { + "description": "Live interval screen: 'Work · {time}' preview of the upcoming work phase.", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "activityLiveNoHrBody": "The band is not connected, so nothing is arriving for this session.", + "@activityLiveNoHrBody": { + "description": "Live heart-rate block: explanation shown when the band is not connected at all." + }, + "activityLiveNoHrTitle": "No heart rate", + "@activityLiveNoHrTitle": { + "description": "Live heart-rate block: title shown when the band is not connected." + }, + "activityLiveNoHrYetBody": "The band is connected but has not reported a beat, so it needs to be snug, a finger-width above the wrist bone.", + "@activityLiveNoHrYetBody": { + "description": "Live heart-rate block: explanation shown when the band is connected but has no beat yet." + }, + "activityLiveNoHrYetTitle": "No heart rate yet", + "@activityLiveNoHrYetTitle": { + "description": "Live heart-rate block: title shown when the band is connected but has no beat yet." + }, + "activityLiveNoneYet": "None yet", + "@activityLiveNoneYet": { + "description": "Live strength screen: placeholder for a previous/best reference that has no data." + }, + "activityLiveNoRouteFailedBody": "The phone returned an error when asked for a fix.", + "@activityLiveNoRouteFailedBody": { + "description": "Live measured session: explanation when a GPS fix request errored out." + }, + "activityLiveNoRouteFailedTitle": "No route: location failed", + "@activityLiveNoRouteFailedTitle": { + "description": "Live measured session: title when a GPS fix request errored out." + }, + "activityLiveNoRouteNotAllowedTitle": "No route: location not allowed", + "@activityLiveNoRouteNotAllowedTitle": { + "description": "Live measured session: title when location permission is denied (once or forever)." + }, + "activityLiveNoRouteOffBody": "Location services are off on this phone, so no fixes are arriving.", + "@activityLiveNoRouteOffBody": { + "description": "Live measured session: explanation when the phone's location services are off." + }, + "activityLiveNoRouteOffTitle": "No route: location is off", + "@activityLiveNoRouteOffTitle": { + "description": "Live measured session: title when the phone's location services are off." + }, + "activityLiveOneLapFewer": "One lap fewer", + "@activityLiveOneLapFewer": { + "description": "Live swim screen: accessibility label for the undo-last-lap button." + }, + "activityLiveOpenSettings": "Open Settings", + "@activityLiveOpenSettings": { + "description": "Live measured session: fix action when location is permanently denied." + }, + "activityLiveOpponentLabel": "OPPONENT", + "@activityLiveOpponentLabel": { + "description": "Live match screen: label above the opponent's score." + }, + "activityLivePauseLabel": "Pause", + "@activityLivePauseLabel": { + "description": "Live session shell: accessibility label for the pause control." + }, + "activityLivePerLapUnit": "per lap", + "@activityLivePerLapUnit": { + "description": "Live swim screen: unit label for the per-lap pace stat." + }, + "activityLivePointLabel": "{side} point", + "@activityLivePointLabel": { + "description": "Live match screen: accessibility label for tapping to add a point, e.g. 'YOU point'.", + "placeholders": { + "side": { + "type": "String" + } + } + }, + "activityLivePoolSubtitle": "{len}M POOL · {stroke}", + "@activityLivePoolSubtitle": { + "description": "Live swim screen: shell subtitle showing pool length and stroke.", + "placeholders": { + "len": { + "type": "int" + }, + "stroke": { + "type": "String" + } + } + }, + "activityLivePoseBridge": "Bridge", + "@activityLivePoseBridge": { + "description": "Live flow (yoga) screen: name of the Bridge pose." + }, + "activityLivePoseChair": "Chair", + "@activityLivePoseChair": { + "description": "Live flow (yoga) screen: name of the Chair pose." + }, + "activityLivePoseChildsPose": "Child's pose", + "@activityLivePoseChildsPose": { + "description": "Live flow (yoga) screen: name of Child's pose." + }, + "activityLivePoseForwardFold": "Forward fold", + "@activityLivePoseForwardFold": { + "description": "Live flow (yoga) screen: name of the Forward fold pose." + }, + "activityLivePoseMountain": "Mountain", + "@activityLivePoseMountain": { + "description": "Live flow (yoga) screen: name of the Mountain pose." + }, + "activityLivePoseOf": "POSE {index} OF {total}", + "@activityLivePoseOf": { + "description": "Live flow (yoga) screen: 'POSE {index} OF {total}' progress header.", + "placeholders": { + "index": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "activityLivePosePigeon": "Pigeon", + "@activityLivePosePigeon": { + "description": "Live flow (yoga) screen: name of the Pigeon pose." + }, + "activityLivePosePlank": "Plank", + "@activityLivePosePlank": { + "description": "Live flow (yoga) screen: name of the Plank pose." + }, + "activityLivePoseSavasana": "Savasana", + "@activityLivePoseSavasana": { + "description": "Live flow (yoga) screen: name of the Savasana pose." + }, + "activityLivePoseTriangle": "Triangle", + "@activityLivePoseTriangle": { + "description": "Live flow (yoga) screen: name of the Triangle pose." + }, + "activityLivePoseWarriorTwo": "Warrior II", + "@activityLivePoseWarriorTwo": { + "description": "Live flow (yoga) screen: name of the Warrior II pose." + }, + "activityLivePreviousExercise": "Previous exercise", + "@activityLivePreviousExercise": { + "description": "Live strength screen: accessibility label for the previous-exercise arrow." + }, + "activityLivePreviousLabel": "Previous", + "@activityLivePreviousLabel": { + "description": "Shared label for a 'Previous' reference or navigation button." + }, + "activityLivePrivateSession": "Private session", + "@activityLivePrivateSession": { + "description": "Live measured session: badge shown for a private session." + }, + "activityLiveRecordingRoute": "Recording route", + "@activityLiveRecordingRoute": { + "description": "Live measured session: pill shown while a GPS route is actively recording." + }, + "activityLiveRepsBodyweightRow": "{n, plural, one{{n} rep · bodyweight} other{{n} reps · bodyweight}}", + "@activityLiveRepsBodyweightRow": { + "description": "Live strength screen: 'N reps · bodyweight' row in the this-exercise set list.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveRepsLabel": "REPS", + "@activityLiveRepsLabel": { + "description": "Live strength screen: header above the reps stepper." + }, + "activityLiveRepsLoggedBodyweight": "{n, plural, one{{n} rep logged} other{{n} reps logged}}", + "@activityLiveRepsLoggedBodyweight": { + "description": "Live strength screen: 'N reps logged' shown during the rest countdown for a bodyweight set.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveRepsOnly": "{n, plural, one{{n} rep} other{{n} reps}}", + "@activityLiveRepsOnly": { + "description": "Live strength screen: 'N reps' reference figure with no load recorded.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveRepsUnit": "reps", + "@activityLiveRepsUnit": { + "description": "Live strength screen: unit label under the total reps figure." + }, + "activityLiveRestingHeader": "RESTING", + "@activityLiveRestingHeader": { + "description": "Live strength screen: header shown during the rest countdown between sets." + }, + "activityLiveRestWord": "Rest", + "@activityLiveRestWord": { + "description": "The word 'Rest', used standalone for interval announcements and labels." + }, + "activityLiveResumeLabel": "Resume", + "@activityLiveResumeLabel": { + "description": "Live session shell: accessibility label for the resume control." + }, + "activityLiveRoundLabel": "ROUND {n}", + "@activityLiveRoundLabel": { + "description": "Live interval screen: 'ROUND N' header.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveRouteFootnoteNoDistance": "Start pinned; distance appears once the fixes settle.", + "@activityLiveRouteFootnoteNoDistance": { + "description": "Live measured session: route-chart footnote before any distance has been computed." + }, + "activityLiveRouteFootnoteWithDistance": "{distance} from the fixes recorded so far.", + "@activityLiveRouteFootnoteWithDistance": { + "description": "Live measured session: route-chart footnote naming the distance recorded so far.", + "placeholders": { + "distance": { + "type": "String" + } + } + }, + "activityLiveRouteSoFarTitle": "ROUTE SO FAR", + "@activityLiveRouteSoFarTitle": { + "description": "Live measured session: chart title for the in-progress GPS route." + }, + "activityLiveSetNumber": "Set {n}", + "@activityLiveSetNumber": { + "description": "Shared 'Set N' label used for the current set count and past-set rows.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveSetsCountSubtitle": "{n, plural, one{{n} SET} other{{n} SETS}}", + "@activityLiveSetsCountSubtitle": { + "description": "Live strength screen: shell subtitle showing the number of sets logged so far.", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "activityLiveSetsListHeader": "SETS", + "@activityLiveSetsListHeader": { + "description": "Live match screen: header above the list of finished sets." + }, + "activityLiveSetsUnit": "sets", + "@activityLiveSetsUnit": { + "description": "Live strength screen: unit label under the total sets figure." + }, + "activityLiveStepsUnit": "steps", + "@activityLiveStepsUnit": { + "description": "Common stats row: unit label for the live step count." + }, + "activityLiveStrainUnit": "strain", + "@activityLiveStrainUnit": { + "description": "Common stats row: unit label for the live strain figure." + }, + "activityLiveStrokeBack": "Back", + "@activityLiveStrokeBack": { + "description": "Live swim screen: name of the backstroke." + }, + "activityLiveStrokeBreast": "Breast", + "@activityLiveStrokeBreast": { + "description": "Live swim screen: name of breaststroke." + }, + "activityLiveStrokeFly": "Fly", + "@activityLiveStrokeFly": { + "description": "Live swim screen: name of butterfly stroke." + }, + "activityLiveStrokeFree": "Free", + "@activityLiveStrokeFree": { + "description": "Live swim screen: name of freestyle stroke." + }, + "activityLiveThisExerciseLabel": "THIS EXERCISE", + "@activityLiveThisExerciseLabel": { + "description": "Live strength screen: header above the current exercise's logged sets." + }, + "activityLiveTimeInZonesTitle": "TIME IN ZONES", + "@activityLiveTimeInZonesTitle": { + "description": "Live heart-rate block: chart title for time spent in each heart-rate zone." + }, + "activityLiveTimeUnit": "time", + "@activityLiveTimeUnit": { + "description": "Live swim screen: unit label for the elapsed-time stat." + }, + "activityLiveTryAgain": "Try again", + "@activityLiveTryAgain": { + "description": "Live measured session: fix action when a GPS fix request errored out." + }, + "activityLiveTurnOnLocation": "Turn on location", + "@activityLiveTurnOnLocation": { + "description": "Live measured session: fix action when location services are off." + }, + "activityLiveVolumeSetsSubtitle": "{kg} KG · {n, plural, one{{n} SET} other{{n} SETS}}", + "@activityLiveVolumeSetsSubtitle": { + "description": "Live strength screen: shell subtitle showing training volume and set count.", + "placeholders": { + "kg": { + "type": "String" + }, + "n": { + "type": "int" + } + } + }, + "activityLiveWeightLabel": "WEIGHT", + "@activityLiveWeightLabel": { + "description": "Live strength screen: header above the weight stepper." + }, + "activityLiveWeightRepsLogged": "{kg} kg × {n} logged", + "@activityLiveWeightRepsLogged": { + "description": "Live strength screen: 'kg × N logged' shown during the rest countdown for a loaded set.", + "placeholders": { + "kg": { + "type": "String" + }, + "n": { + "type": "int" + } + } + }, + "activityLiveWorkWord": "Work", + "@activityLiveWorkWord": { + "description": "The word 'Work', used standalone for interval announcements and labels." + }, + "activityLiveYouLabel": "YOU", + "@activityLiveYouLabel": { + "description": "Live match screen: label above the user's own score." + }, + "activityLiveZoneLabel": "Zone {z}", + "@activityLiveZoneLabel": { + "description": "Live heart-rate block: pill showing the current heart-rate zone number.", + "placeholders": { + "z": { + "type": "int" + } + } + }, + "activityLiveLogSet": "Log set", + "@activityLiveLogSet": { + "description": "Live strength screen: button that banks the current set." + }, + "activityLiveRestOverAnnounce": "Rest over", + "@activityLiveRestOverAnnounce": { + "description": "Live strength screen: screen-reader announcement when the rest countdown ends." + }, + "activityLiveSkipRest": "Skip rest", + "@activityLiveSkipRest": { + "description": "Live strength screen: button that skips the remaining rest countdown." + }, + "gesturesNavTitle": "Double-tap", + "@gesturesNavTitle": { + "description": "Title of the double-tap gesture settings screen" + }, + "gesturesSectionTitle": "Tap the band twice", + "@gesturesSectionTitle": { + "description": "Section heading explaining the double-tap gesture on the band" + }, + "gesturesSectionBody": "Only while the app is connected and awake. A tap the band stored while your phone was away arrives later with an old timestamp, and is ignored rather than fired hours after you meant it.", + "@gesturesSectionBody": { + "description": "Body text explaining when the double-tap gesture actually works" + }, + "gesturesItDoesTitle": "It does", + "@gesturesItDoesTitle": { + "description": "Heading for the group of selectable double-tap actions" + }, + "gesturesNoPhoneActionsTitle": "Nothing on the phone?", + "@gesturesNoPhoneActionsTitle": { + "description": "Heading shown when no phone-side actions are available" + }, + "gesturesNoPhoneActionsBody": "Ringing your phone and the flashlight are missing because the app could not reach the system to ask what this device allows. Reopen the app and come back; the in-app actions above work either way.", + "@gesturesNoPhoneActionsBody": { + "description": "Body text explaining why phone-native double-tap actions are missing" + }, + "settingsBarcodeSaveFailed": "That could not be saved — it may be back next time you open the app.", + "@settingsBarcodeSaveFailed": { + "description": "Snackbar shown when the barcode-lookup preference fails to save" + }, + "settingsIconRowTitle": "Icon", + "@settingsIconRowTitle": { + "description": "Row title for the home-screen app icon picker" + }, + "settingsIconRowConfirmHint": "iPhone will ask you to confirm", + "@settingsIconRowConfirmHint": { + "description": "Hint under the app icon row noting iOS confirms every icon change" + }, + "settingsIconChoiceLabel": "{label} icon.", + "@settingsIconChoiceLabel": { + "description": "Accessibility label for one app-icon choice", + "placeholders": { + "label": { + "type": "String" + } + } + }, + "settingsSelectedSuffix": " Selected.", + "@settingsSelectedSuffix": { + "description": "Accessibility suffix appended when an option is the current selection" + }, + "settingsHealthSyncOff": "Off. Nothing is written to {store}", + "@settingsHealthSyncOff": { + "description": "Health-sync subtitle when the write toggle is off", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsHealthSyncReady": "Writes each day’s sleep, resting heart rate, HRV, respiratory rate, energy and workouts to {store} once it is final", + "@settingsHealthSyncReady": { + "description": "Health-sync subtitle when the write is active and ready", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsHealthSyncNeedsPermission": "{store} has not granted write access. Tap to open it", + "@settingsHealthSyncNeedsPermission": { + "description": "Health-sync subtitle when write permission is missing", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsHealthSyncNotInstalled": "Health Connect is not installed. Tap to get it", + "@settingsHealthSyncNotInstalled": { + "description": "Health-sync subtitle when Health Connect is missing (Android)" + }, + "settingsHealthSyncNeedsUpdate": "Health Connect is too old to write to. Tap to update it", + "@settingsHealthSyncNeedsUpdate": { + "description": "Health-sync subtitle when Health Connect needs an update" + }, + "settingsHealthSyncUnsupported": "This device has no health store to write to", + "@settingsHealthSyncUnsupported": { + "description": "Health-sync subtitle when the device has no health store at all" + }, + "settingsHealthSyncChecking": "Checking {store}…", + "@settingsHealthSyncChecking": { + "description": "Health-sync subtitle while the state is being determined", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsWriteToHealthStoreRowTitle": "Write to {store}", + "@settingsWriteToHealthStoreRowTitle": { + "description": "Settings row title to enable writing to the platform health store", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsHealthShareOffTitle": "Contribution off", + "@settingsHealthShareOffTitle": { + "description": "Dialog title after withdrawing health-data contribution consent" + }, + "settingsHealthShareOffNeverUploaded": "Nothing was ever uploaded. Nothing will be.", + "@settingsHealthShareOffNeverUploaded": { + "description": "Dialog body when consent is withdrawn and no upload had ever happened" + }, + "settingsHealthShareOffDetail": "Nothing further will be uploaded.\n\nOne copy of your database was uploaded on {date}. The server keeps only the most recent copy per device. We tried to tell it your consent is withdrawn — that message is sent once and is not retried, so if this phone is offline it will not have arrived, and we cannot show you that the copy is gone either.", + "@settingsHealthShareOffDetail": { + "description": "Dialog body when consent is withdrawn and a previous upload exists, {date} is a local date/time string", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "settingsOk": "OK", + "@settingsOk": { + "description": "Generic OK button on the health-contribution-off dialog" + }, + "settingsHealthShareOnTitle": "Contribute your health data?", + "@settingsHealthShareOnTitle": { + "description": "Dialog title asking to grant whole-database health contribution" + }, + "settingsHealthShareOnBody": "Once a day, on Wi-Fi and while charging, a compressed copy of your ENTIRE database is uploaded — every derived day and every raw sensor row the band has sent. It is used to improve the algorithms.\n\nIt is not anonymous in any meaningful sense: it is your whole health history. You can switch this off at any time, and nothing further is sent from that moment.", + "@settingsHealthShareOnBody": { + "description": "Dialog body explaining what the whole-database health-share upload does" + }, + "settingsNo": "No", + "@settingsNo": { + "description": "Decline button on the health-contribution consent dialog" + }, + "settingsContribute": "Contribute", + "@settingsContribute": { + "description": "Accept button on the health-contribution consent dialog" + }, + "settingsResetTitle": "Delete everything?", + "@settingsResetTitle": { + "description": "Confirmation dialog title for resetting all app data" + }, + "settingsResetBody": "This deletes, permanently and with no copy anywhere else:\n\n· every measured day, sleep, workout and route\n· every lab result, meal, medication dose, habit, breathing session and logged set\n· your journal, cycle log and rolling baselines\n· your profile, every preference and any stored AI key\n· the home-screen widget and every scheduled reminder\n\nThe band is unpaired, and it cannot re-send history it has already handed over. Export from Your data first if you want a copy.", + "@settingsResetBody": { + "description": "Confirmation dialog body listing exactly what a full reset deletes" + }, + "settingsResetKeepData": "Keep my data", + "@settingsResetKeepData": { + "description": "Cancel button on the reset-everything confirmation dialog" + }, + "settingsResetDeleteEverything": "Delete everything", + "@settingsResetDeleteEverything": { + "description": "Confirm button on the reset-everything confirmation dialog" + }, + "settingsNavTitle": "Settings", + "@settingsNavTitle": { + "description": "Title of the main settings screen" + }, + "settingsGroupTheBand": "The band", + "@settingsGroupTheBand": { + "description": "Settings group heading for band-related rows" + }, + "settingsAlarmRowTitle": "Alarm", + "@settingsAlarmRowTitle": { + "description": "Settings row title opening the alarm screen" + }, + "settingsAlarmRowSub": "Buzzes on your wrist, on the band’s own clock", + "@settingsAlarmRowSub": { + "description": "Settings row subtitle describing the band alarm" + }, + "settingsGroupThisPhone": "This phone", + "@settingsGroupThisPhone": { + "description": "Settings group heading for phone-side sensing rows" + }, + "settingsStepsRowTitle": "Steps", + "@settingsStepsRowTitle": { + "description": "Settings row title for the phone's own step counter" + }, + "settingsStepsRowSub": "This phone’s own step counter, for the hours the band doesn’t cover. Nothing leaves the device", + "@settingsStepsRowSub": { + "description": "Settings row subtitle explaining the phone step counter" + }, + "settingsGroupNotifications": "Notifications", + "@settingsGroupNotifications": { + "description": "Settings group heading for the notifications row" + }, + "settingsManageNotificationsRowTitle": "Manage notifications", + "@settingsManageNotificationsRowTitle": { + "description": "Settings row title opening the notification settings screen" + }, + "settingsManageNotificationsRowSub": "What may interrupt you, quiet hours, and off switches for all of them", + "@settingsManageNotificationsRowSub": { + "description": "Settings row subtitle for the manage-notifications row" + }, + "settingsGroupPreferences": "Preferences", + "@settingsGroupPreferences": { + "description": "Settings group heading for units/appearance/cycle-tracking" + }, + "settingsUnitsRowTitle": "Units", + "@settingsUnitsRowTitle": { + "description": "Settings row title cycling the measurement unit system" + }, + "settingsAppearanceRowTitle": "Appearance", + "@settingsAppearanceRowTitle": { + "description": "Settings row title cycling the app theme" + }, + "settingsCycleTrackingRowTitle": "Cycle tracking", + "@settingsCycleTrackingRowTitle": { + "description": "Settings row title toggling cycle tracking" + }, + "settingsCycleTrackingRowSub": "Adds the Cycle tab to Wellness. Off hides it and keeps everything already logged", + "@settingsCycleTrackingRowSub": { + "description": "Settings row subtitle for the cycle-tracking toggle" + }, + "settingsGroupYourData": "Your data", + "@settingsGroupYourData": { + "description": "Settings group heading for export/backup/health-write rows" + }, + "settingsExportBackupImportRowTitle": "Export, backup, import", + "@settingsExportBackupImportRowTitle": { + "description": "Settings row title opening the data screen" + }, + "settingsExportBackupImportRowSub": "Spreadsheets, a full copy, and bringing history in", + "@settingsExportBackupImportRowSub": { + "description": "Settings row subtitle for the data screen entry" + }, + "settingsGroupAutomation": "Automation", + "@settingsGroupAutomation": { + "description": "Settings group heading for double-tap and Tasker/Shortcuts rows" + }, + "settingsDoubleTapRowTitle": "Double-tap", + "@settingsDoubleTapRowTitle": { + "description": "Settings row title opening the double-tap gesture screen" + }, + "settingsDoubleTapRowSub": "What a double-tap on the band does", + "@settingsDoubleTapRowSub": { + "description": "Settings row subtitle for the double-tap gesture entry" + }, + "settingsTaskerShortcutsRowTitle": "Tasker and Shortcuts", + "@settingsTaskerShortcutsRowTitle": { + "description": "Settings row title opening the automation screen" + }, + "settingsTaskerShortcutsRowSub": "Android only for events out. iOS can buzz the band but cannot be triggered by it", + "@settingsTaskerShortcutsRowSub": { + "description": "Settings row subtitle stating the Android/iOS automation asymmetry" + }, + "settingsGroupPrivacy": "Privacy", + "@settingsGroupPrivacy": { + "description": "Settings group heading for telemetry/barcode/health-share/update rows" + }, + "settingsCrashReportsRowTitle": "Crash reports", + "@settingsCrashReportsRowTitle": { + "description": "Settings row title toggling crash telemetry" + }, + "settingsCrashReportsRowSub": "Nothing is sent until you say so", + "@settingsCrashReportsRowSub": { + "description": "Settings row subtitle for the crash-reports toggle" + }, + "settingsBarcodeLookupRowTitle": "Look barcodes up online", + "@settingsBarcodeLookupRowTitle": { + "description": "Settings row title toggling online barcode lookup" + }, + "settingsBarcodeLookupRowSub": "Sends a scanned barcode to openfoodfacts.org. Nothing about you goes with it", + "@settingsBarcodeLookupRowSub": { + "description": "Settings row subtitle for the barcode-lookup toggle" + }, + "settingsContributeHealthDataRowTitle": "Contribute my health data", + "@settingsContributeHealthDataRowTitle": { + "description": "Settings row title toggling whole-database health contribution" + }, + "settingsContributeHealthDataRowSub": "Uploads your whole database once a day, on Wi-Fi and charging, to improve the algorithms", + "@settingsContributeHealthDataRowSub": { + "description": "Settings row subtitle for the health-data-contribution toggle" + }, + "settingsCheckForUpdatesRowTitle": "Check for updates", + "@settingsCheckForUpdatesRowTitle": { + "description": "Settings row title toggling background update checks" + }, + "settingsUpdateBelowMinimum": "This build is below the minimum supported build. Install the newer release from GitHub", + "@settingsUpdateBelowMinimum": { + "description": "Update-check subtitle when this build is below the minimum supported version" + }, + "settingsUpdateAvailable": "A newer build is published on GitHub", + "@settingsUpdateAvailable": { + "description": "Update-check subtitle when a newer build is available" + }, + "settingsUpdateCheckSub": "Asks the release server on launch. It sees your IP address and when you open the app", + "@settingsUpdateCheckSub": { + "description": "Update-check subtitle when the app is up to date" + }, + "settingsGroupAbout": "About", + "@settingsGroupAbout": { + "description": "Settings group heading for version/notices rows" + }, + "settingsVersionRowTitle": "Version", + "@settingsVersionRowTitle": { + "description": "Settings row title showing the app version" + }, + "settingsNoticesLicencesRowTitle": "Notices and licences", + "@settingsNoticesLicencesRowTitle": { + "description": "Settings row title opening open-source notices and licences" + }, + "settingsNoticesLicencesRowSub": "Who this app is not, and whose data it uses", + "@settingsNoticesLicencesRowSub": { + "description": "Settings row subtitle for the notices-and-licences row" + }, + "settingsGroupDeveloper": "Developer", + "@settingsGroupDeveloper": { + "description": "Settings group heading for the hidden developer tools" + }, + "settingsComponentGalleryRowTitle": "Component gallery", + "@settingsComponentGalleryRowTitle": { + "description": "Developer settings row title opening the UI component gallery" + }, + "settingsComponentGalleryRowSub": "Every component, at any text scale, in either theme", + "@settingsComponentGalleryRowSub": { + "description": "Developer settings row subtitle for the component gallery" + }, + "settingsDeveloperModeRowTitle": "Developer mode", + "@settingsDeveloperModeRowTitle": { + "description": "Developer settings row title to leave developer mode" + }, + "settingsResetAllDataRowTitle": "Reset all data", + "@settingsResetAllDataRowTitle": { + "description": "Settings row title for the destructive full data reset" + }, + "settingsNotificationsNavTitle": "Notifications", + "@settingsNotificationsNavTitle": { + "description": "Title of the notification settings screen" + }, + "settingsNotificationsNavSub": "WHAT MAY INTERRUPT YOU", + "@settingsNotificationsNavSub": { + "description": "Subtitle under the notification settings screen title" + }, + "settingsNotificationsOffSystemTitle": "Notifications are off at the system level", + "@settingsNotificationsOffSystemTitle": { + "description": "Status card title when OS-level notification permission is denied" + }, + "settingsNotificationsOffSystemBody": "Nothing below can reach you until the OS lets it.", + "@settingsNotificationsOffSystemBody": { + "description": "Status card body when OS-level notification permission is denied" + }, + "settingsTurnThemOn": "Turn them on", + "@settingsTurnThemOn": { + "description": "Fix button turning on OS notification permission" + }, + "settingsGroupManageNotifications": "Manage notifications", + "@settingsGroupManageNotifications": { + "description": "Settings group heading listing every individual notification toggle" + }, + "settingsHealthExceptionsRowTitle": "Health exceptions", + "@settingsHealthExceptionsRowTitle": { + "description": "Notification row title for the health-exception alert toggle" + }, + "settingsHealthExceptionsRowSub": "One a day at most, and only when something in your own baseline moved", + "@settingsHealthExceptionsRowSub": { + "description": "Notification row subtitle for the health-exception alert toggle" + }, + "settingsBandAlertsRowTitle": "Band alerts", + "@settingsBandAlertsRowTitle": { + "description": "Notification row title for band device alerts" + }, + "settingsBandAlertsRowSub": "Flat battery, on the charger, gone quiet", + "@settingsBandAlertsRowSub": { + "description": "Notification row subtitle for band device alerts" + }, + "settingsAlertMeAtRowTitle": "Alert me at", + "@settingsAlertMeAtRowTitle": { + "description": "Notification row title for the low-battery threshold" + }, + "settingsAlertMeAtRowSub": "Warn when the band drops under this charge level", + "@settingsAlertMeAtRowSub": { + "description": "Notification row subtitle for the low-battery threshold" + }, + "settingsRecoveryReadyRowTitle": "Recovery ready", + "@settingsRecoveryReadyRowTitle": { + "description": "Notification row title for the morning recovery-ready alert" + }, + "settingsRecoveryReadyRowSub": "One note when your morning recovery score lands", + "@settingsRecoveryReadyRowSub": { + "description": "Notification row subtitle for the recovery-ready alert" + }, + "settingsWeeklyLookbackRowTitle": "Weekly lookback", + "@settingsWeeklyLookbackRowTitle": { + "description": "Notification row title for the weekly summary alert" + }, + "settingsWeeklyLookbackRowSub": "Sunday evening, but only for a week that actually found something. Most weeks are quiet", + "@settingsWeeklyLookbackRowSub": { + "description": "Notification row subtitle for the weekly summary alert" + }, + "settingsDetectedWorkoutsRowTitle": "Detected workouts", + "@settingsDetectedWorkoutsRowTitle": { + "description": "Notification row title for auto-detected workout prompts" + }, + "settingsDetectedWorkoutsRowSub": "Ask about efforts the band spotted that you did not start. Off hides the prompt and the review cards; the band goes on measuring either way", + "@settingsDetectedWorkoutsRowSub": { + "description": "Notification row subtitle for auto-detected workout prompts" + }, + "settingsMovementNudgeRowTitle": "Movement nudge", + "@settingsMovementNudgeRowTitle": { + "description": "Notification row title for the sedentary-movement nudge" + }, + "settingsMovementNudgeRowSub": "Nudges you after a still stretch — two hours with no movement at all, or 90 minutes in a desk posture. Phone notification plus a buzz on the band while it is connected", + "@settingsMovementNudgeRowSub": { + "description": "Notification row subtitle for the sedentary-movement nudge" + }, + "settingsWindDownRowTitle": "Wind-down", + "@settingsWindDownRowTitle": { + "description": "Notification row title for the bedtime wind-down nudge" + }, + "settingsWindDownRowSub": "A heads-up about 45 minutes before the bedtime learned from your own nights, kept clear of your quiet hours. Appears after about a week of wear", + "@settingsWindDownRowSub": { + "description": "Notification row subtitle for the bedtime wind-down nudge" + }, + "settingsStepGoalAlertsRowTitle": "Step goal alerts", + "@settingsStepGoalAlertsRowTitle": { + "description": "Notification row title for the step-goal achievement alert" + }, + "settingsStepGoalAlertsRowSub": "Tells you once when today crosses your steps goal", + "@settingsStepGoalAlertsRowSub": { + "description": "Notification row subtitle for the step-goal achievement alert" + }, + "settingsMedicationRemindersRowTitle": "Medication reminders", + "@settingsMedicationRemindersRowTitle": { + "description": "Notification row title for medication-dose reminders" + }, + "settingsMedicationRemindersRowSub": "One notification per scheduled dose, at the times you entered — with a buzz on the band if it is connected. Nothing is sent for a dose already marked taken or skipped", + "@settingsMedicationRemindersRowSub": { + "description": "Notification row subtitle for medication-dose reminders" + }, + "settingsDailyCheckInRowTitle": "Daily check-in", + "@settingsDailyCheckInRowTitle": { + "description": "Notification row title for the evening journal check-in prompt" + }, + "settingsDailyCheckInRowSub": "One prompt in the evening to write the day — mood, energy, stress. Skipped once the day already has a rating in it", + "@settingsDailyCheckInRowSub": { + "description": "Notification row subtitle for the evening journal check-in prompt" + }, + "settingsWaterReminderRowTitle": "Water reminder", + "@settingsWaterReminderRowTitle": { + "description": "Notification row title for the water-logging reminder" + }, + "settingsWaterReminderRowSub": "A buzz on the strap and a notification on your phone through your waking hours, to remind you to log a drink. Nothing is measured either way", + "@settingsWaterReminderRowSub": { + "description": "Notification row subtitle for the water-logging reminder" + }, + "settingsRemindMeEveryRowTitle": "Remind me every", + "@settingsRemindMeEveryRowTitle": { + "description": "Notification row title cycling the water-reminder interval" + }, + "settingsGroupTheStrap": "The strap", + "@settingsGroupTheStrap": { + "description": "Settings group heading for the strap-buzz-on-notification row" + }, + "settingsBuzzOnAppNotificationsRowTitle": "Buzz on app notifications", + "@settingsBuzzOnAppNotificationsRowTitle": { + "description": "Settings row title opening the band-notification relay screen" + }, + "settingsBuzzOnAppNotificationsRowSub": "Pick which phone apps make the strap buzz", + "@settingsBuzzOnAppNotificationsRowSub": { + "description": "Settings row subtitle for the band-notification relay screen entry" + }, + "settingsGroupQuietHours": "Quiet hours", + "@settingsGroupQuietHours": { + "description": "Settings group heading for the quiet-hours rows" + }, + "settingsQuietHoursRowTitle": "Quiet hours", + "@settingsQuietHoursRowTitle": { + "description": "Settings row title toggling quiet hours" + }, + "settingsQuietHoursRowSub": "Nothing buzzes inside this window", + "@settingsQuietHoursRowSub": { + "description": "Settings row subtitle for the quiet-hours toggle" + }, + "settingsQuietHoursStartsRowTitle": "Starts", + "@settingsQuietHoursStartsRowTitle": { + "description": "Settings row title for the quiet-hours start time" + }, + "settingsQuietHoursEndsRowTitle": "Ends", + "@settingsQuietHoursEndsRowTitle": { + "description": "Settings row title for the quiet-hours end time" + }, + "settingsHealthExceptionsBreakThroughRowTitle": "Health exceptions break through", + "@settingsHealthExceptionsBreakThroughRowTitle": { + "description": "Settings row title toggling whether health-exception alerts bypass quiet hours" + }, + "settingsAlarmNotOnListTitle": "The alarm is not on this list", + "@settingsAlarmNotOnListTitle": { + "description": "Status card title clarifying the alarm has its own off switch" + }, + "settingsAlarmNotOnListBody": "Cancel it on the Alarm screen instead.", + "@settingsAlarmNotOnListBody": { + "description": "Status card body directing to the alarm screen to cancel it" + }, + "settingsImportNoPermission": "{store} did not grant those fields. Nothing was read.", + "@settingsImportNoPermission": { + "description": "Result message when the health store denies profile-field read permission", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsImportEmptyWithBirthday": "Nothing came back. {store} holds no height, weight, birthday or sex for you — type them in here instead.", + "@settingsImportEmptyWithBirthday": { + "description": "Result message when the health store (with birthday support) has no profile fields for the user", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsImportEmpty": "Nothing came back. {store} holds no height, weight or sex for you — type them in here instead.", + "@settingsImportEmpty": { + "description": "Result message when the health store (no birthday support) has no profile fields for the user", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsImportNoChange": "Read {fields}. Your profile already says the same thing, so nothing changed.", + "@settingsImportNoChange": { + "description": "Result message when the health store returned fields that already match the stored profile", + "placeholders": { + "fields": { + "type": "String" + } + } + }, + "settingsImportUpdated": "Updated {fields} from {store}.", + "@settingsImportUpdated": { + "description": "Result message when the health store's fields were applied to the profile", + "placeholders": { + "fields": { + "type": "String" + }, + "store": { + "type": "String" + } + } + }, + "settingsImportFailed": "Failed: {error}", + "@settingsImportFailed": { + "description": "Result message shown when the health-profile import throws", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "settingsAgeFieldLabel": "Age", + "@settingsAgeFieldLabel": { + "description": "Label used in the invalid-field list when the age field cannot be read" + }, + "settingsEditProfileNavTitle": "Edit profile", + "@settingsEditProfileNavTitle": { + "description": "Title of the edit-profile screen" + }, + "settingsNameFieldLabel": "NAME", + "@settingsNameFieldLabel": { + "description": "Field label for the profile name text field" + }, + "settingsSexFieldLabel": "SEX", + "@settingsSexFieldLabel": { + "description": "Field label for the profile sex selector" + }, + "settingsSexMale": "Male", + "@settingsSexMale": { + "description": "Sex option: male" + }, + "settingsSexFemale": "Female", + "@settingsSexFemale": { + "description": "Sex option: female" + }, + "settingsSexPreferNotToSay": "Prefer not to say", + "@settingsSexPreferNotToSay": { + "description": "Sex option: prefer not to say" + }, + "settingsAgeYearsFieldLabel": "AGE (YEARS)", + "@settingsAgeYearsFieldLabel": { + "description": "Field label for the profile age text field" + }, + "settingsFourFieldsTitle": "These four change your numbers", + "@settingsFourFieldsTitle": { + "description": "Status card title explaining what the four profile fields affect" + }, + "settingsFourFieldsBody": "They feed heart-rate zones, calorie estimates and training load. Clear one and only the metrics that need it stay unavailable.", + "@settingsFourFieldsBody": { + "description": "Status card body explaining what the four profile fields affect" + }, + "settingsImportBlockAppleHealth": "Height, weight, birthday and sex, straight out of {store}. Height and weight are taken every time; your age and sex only fill a gap, because neither drifts and a value already here was your choice.", + "@settingsImportBlockAppleHealth": { + "description": "Import-block explainer text on Apple Health (birthday-capable stores)", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsImportBlockOther": "Height and weight, straight out of {store}. It has no birthday and no sex to read — no app can — so set those two above yourself.", + "@settingsImportBlockOther": { + "description": "Import-block explainer text on Health Connect (no birthday support)", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "settingsNotSetHint": "Not set", + "@settingsNotSetHint": { + "description": "Hint text shown in an empty profile field" + }, + "settingsAutomationNavTitle": "Automation", + "@settingsAutomationNavTitle": { + "description": "Title of the automation settings screen" + }, + "settingsSyncFinishesSectionTitle": "When a sync finishes", + "@settingsSyncFinishesSectionTitle": { + "description": "Section heading explaining the outbound sync-complete event" + }, + "settingsSyncFinishesAndroidBody": "The app broadcasts an intent your automation app can start a profile on. Filter on the action below; it carries how many records landed and when, at most one a minute.", + "@settingsSyncFinishesAndroidBody": { + "description": "Section body on Android explaining the sync-complete broadcast" + }, + "settingsSyncFinishesIosBody": "iOS cannot do this. A Shortcuts personal automation can only trigger on Apple’s own fixed list of events, and no app can add one — so nothing here can start a shortcut for you. Android gets it; this is a platform limit, not a setting.", + "@settingsSyncFinishesIosBody": { + "description": "Section body on iOS explaining why the sync-complete trigger cannot work" + }, + "settingsSyncFinishesExtras": "Extras: records (int), at (unix seconds)", + "@settingsSyncFinishesExtras": { + "description": "Technical detail line listing the broadcast intent's extras" + }, + "settingsNeverSendSectionTitle": "What it will never send", + "@settingsNeverSendSectionTitle": { + "description": "Section heading listing what the automation event never includes" + }, + "settingsNeverSendBody": "No readiness, no strain, no sleep score — on either platform. A number this app would have shown as absent, with a reason attached, becomes a bare zero the moment it leaves. Facts about the sync go out; measurements do not.", + "@settingsNeverSendBody": { + "description": "Section body listing what the automation event never includes" + }, + "settingsBuzzFromShortcutSectionTitle": "Buzzing the band from a shortcut", + "@settingsBuzzFromShortcutSectionTitle": { + "description": "Section heading explaining the inbound band-buzz endpoint" + }, + "settingsBuzzFromShortcutAndroidBody": "Send wtf.openstrap.openstrap_edge.BUZZ_STRAP with this token as the “token” string extra. Without it any app on the phone could buzz your band.", + "@settingsBuzzFromShortcutAndroidBody": { + "description": "Section body on Android explaining how to trigger the band buzz" + }, + "settingsBuzzFromShortcutIosBody": "This direction works on iOS: a shortcut you run yourself can reach the app. What it cannot do is run itself when the band syncs.", + "@settingsBuzzFromShortcutIosBody": { + "description": "Section body on iOS explaining the inbound band-buzz endpoint" + }, + "settingsNoTokenYet": "No token yet — reopen this screen.", + "@settingsNoTokenYet": { + "description": "Body shown when the automation auth token has not loaded yet" + }, + "settingsCopied": "Copied", + "@settingsCopied": { + "description": "Button label after the automation token was copied" + }, + "settingsCopyTheToken": "Copy the token", + "@settingsCopyTheToken": { + "description": "Button label to copy the automation token" + }, + "bandStatusBluetoothDeniedTitle": "Bluetooth is switched off for this app", + "@bandStatusBluetoothDeniedTitle": { + "description": "BandStatus card title: app-level Bluetooth permission denied" + }, + "bandStatusBluetoothDeniedReason": "The phone is withholding the Bluetooth radio from OpenStrap, so nothing can be scanned or connected. This is not the band — walking closer to it will not help.", + "@bandStatusBluetoothDeniedReason": { + "description": "BandStatus card body: app-level Bluetooth permission denied" + }, + "bandStatusBluetoothDeniedFix": "Open Settings → OpenStrap and allow Bluetooth", + "@bandStatusBluetoothDeniedFix": { + "description": "BandStatus fix action: app-level Bluetooth permission denied" + }, + "bandStatusBluetoothOffTitle": "Bluetooth is turned off", + "@bandStatusBluetoothOffTitle": { + "description": "BandStatus card title: phone Bluetooth radio is off" + }, + "bandStatusBluetoothOffReason": "The phone’s radio is off, so the band cannot be reached by any app. The band keeps recording meanwhile; nothing is lost.", + "@bandStatusBluetoothOffReason": { + "description": "BandStatus card body: phone Bluetooth radio is off" + }, + "bandStatusBluetoothOffFix": "Turn Bluetooth on", + "@bandStatusBluetoothOffFix": { + "description": "BandStatus fix action: phone Bluetooth radio is off" + }, + "bandStatusBluetoothUnsupportedTitle": "This phone has no Bluetooth Low Energy radio", + "@bandStatusBluetoothUnsupportedTitle": { + "description": "BandStatus card title: phone has no BLE radio" + }, + "bandStatusBluetoothUnsupportedReason": "The band can only be reached over Bluetooth Low Energy. Imported data still works; a live link does not.", + "@bandStatusBluetoothUnsupportedReason": { + "description": "BandStatus card body: phone has no BLE radio" + }, + "bandStatusReconnectPausedTitle": "Reconnecting has been paused", + "@bandStatusReconnectPausedTitle": { + "description": "BandStatus card title: auto-reconnect paused after repeated bond refusals" + }, + "bandStatusReconnectPausedReason": "The band refused the pairing key {n, plural, one{{n} time} other{{n} times}} in a row, so the app stopped retrying rather than pin the radio and drain both batteries on a link that will not open. Nothing is reconnecting until you act.", + "@bandStatusReconnectPausedReason": { + "description": "BandStatus card body: auto-reconnect paused, with the refusal count", + "placeholders": { + "n": { + "type": "int" + } + } + }, + "bandStatusRepairNeededTitle": "The band needs to be paired again", + "@bandStatusRepairNeededTitle": { + "description": "BandStatus card title: band needs re-pairing" + }, + "bandStatusRepairNeededReason": "The link comes up, but the band rejects the encryption key the phone holds, so every command is dropped and no data moves. Your recordings are safe on the band.", + "@bandStatusRepairNeededReason": { + "description": "BandStatus card body: band needs re-pairing" + }, + "bandStatusRepairFix": "Forget the band in the phone’s Bluetooth settings, then pair it again here", + "@bandStatusRepairFix": { + "description": "BandStatus fix action: forget and re-pair the band (shared by reconnect-paused and repair-needed states)" + }, + "bandStatusSyncStuckTitle": "One batch of recordings will not finish transferring", + "@bandStatusSyncStuckTitle": { + "description": "BandStatus card title: a sync batch will not finish transferring" + }, + "bandStatusSyncStuckReason": "The band keeps re-sending the same batch because the app cannot get its confirmation through. Everything in it is already saved here — nothing is lost — but the band cannot move on until the confirmation lands.", + "@bandStatusSyncStuckReason": { + "description": "BandStatus card body: a sync batch will not finish transferring" + }, + "bandStatusSyncStuckFix": "Reconnect the band; if it repeats tomorrow, pair it again", + "@bandStatusSyncStuckFix": { + "description": "BandStatus fix action: a sync batch will not finish transferring" + }, + "bandStatusStrapUnresponsiveTitle": "The band has stopped handing over its recordings", + "@bandStatusStrapUnresponsiveTitle": { + "description": "BandStatus card title: band holds newer data it will not hand over" + }, + "bandStatusStrapUnresponsiveReason": "The band reports newer recordings than it will send. Those recordings are still on the band and still safe; it just is not passing them across.", + "@bandStatusStrapUnresponsiveReason": { + "description": "BandStatus card body: band holds newer data it will not hand over" + }, + "bandStatusStrapUnresponsiveFix": "Put the band on its charger for a minute, then reconnect", + "@bandStatusStrapUnresponsiveFix": { + "description": "BandStatus fix action: band holds newer data it will not hand over" + }, + "bandStatusClockLostTitle": "Syncs are finishing with no data in them", + "@bandStatusClockLostTitle": { + "description": "BandStatus card title: syncs complete with no sensor data, clock likely lost" + }, + "bandStatusClockLostReason": "The band completes each sync without handing over a single sensor reading, which almost always means its onboard clock has lost sync. The app keeps resetting it on every connect.", + "@bandStatusClockLostReason": { + "description": "BandStatus card body: syncs complete with no sensor data, clock likely lost" + }, + "bandStatusClockLostFix": "Leave the band connected for a few minutes; if nothing arrives by tomorrow, pair it again", + "@bandStatusClockLostFix": { + "description": "BandStatus fix action: syncs complete with no sensor data, clock likely lost" + }, + "bandStatusConnectedReason": "The band is linked and handing over its recordings.", + "@bandStatusConnectedReason": { + "description": "BandStatus card body: band is connected and syncing normally" + }, + "bandStatusConnectingTitle": "Connecting", + "@bandStatusConnectingTitle": { + "description": "BandStatus card title: link to the band is opening" + }, + "bandStatusConnectingReason": "Opening the link to the band.", + "@bandStatusConnectingReason": { + "description": "BandStatus card body: link to the band is opening" + }, + "bandStatusScanningTitle": "Looking for the band", + "@bandStatusScanningTitle": { + "description": "BandStatus card title: scanning for the band" + }, + "bandStatusScanningReason": "Listening for the band to advertise itself.", + "@bandStatusScanningReason": { + "description": "BandStatus card body: scanning for the band" + }, + "bandStatusDisconnectedReason": "The band is out of range, on its charger, or held by another app. It keeps recording either way.", + "@bandStatusDisconnectedReason": { + "description": "BandStatus card body: band is not connected" + }, + "bandStatusDisconnectedFix": "Bring the band near the phone, and close any other app connected to it", + "@bandStatusDisconnectedFix": { + "description": "BandStatus fix action: band is not connected" + }, + "devicesTierBeatToBeatLabel": "Beat-to-beat intervals", + "@devicesTierBeatToBeatLabel": { + "description": "SourceTier label: electrical beat-to-beat sensor (chest strap)" + }, + "devicesTierBeatToBeatDetail": "Electrical R-peak detection.", + "@devicesTierBeatToBeatDetail": { + "description": "SourceTier detail: electrical beat-to-beat sensor (chest strap)" + }, + "devicesTierWristOpticalLabel": "Wrist optical pulse", + "@devicesTierWristOpticalLabel": { + "description": "SourceTier label: wrist optical pulse (the band)" + }, + "devicesTierWristOpticalDetail": "Continuous 24/7 pulse, sleep and temperature. Beat timing is inferred from a pulse wave, so HRV here is PRV.", + "@devicesTierWristOpticalDetail": { + "description": "SourceTier detail: wrist optical pulse (the band)" + }, + "devicesTierPhoneLabel": "Steps only", + "@devicesTierPhoneLabel": { + "description": "SourceTier label: phone motion coprocessor, steps only" + }, + "devicesTierPhoneDetail": "The phone’s own motion coprocessor. Steps and nothing else.", + "@devicesTierPhoneDetail": { + "description": "SourceTier detail: phone motion coprocessor, steps only" + }, + "deviceActionNoneLabel": "Do nothing", + "@deviceActionNoneLabel": { + "description": "Band double-tap action picker: label for the do-nothing option" + }, + "deviceActionNoneBlurb": "Double-tap does nothing.", + "@deviceActionNoneBlurb": { + "description": "Band double-tap action picker: blurb for the do-nothing option" + }, + "deviceActionMediaPlayPauseLabel": "Play / pause music", + "@deviceActionMediaPlayPauseLabel": { + "description": "Band double-tap action picker: label for play/pause music" + }, + "deviceActionMediaPlayPauseBlurb": "Toggle whatever is playing.", + "@deviceActionMediaPlayPauseBlurb": { + "description": "Band double-tap action picker: blurb for play/pause music" + }, + "deviceActionMediaNextLabel": "Next track", + "@deviceActionMediaNextLabel": { + "description": "Band double-tap action picker: label for next track" + }, + "deviceActionMediaNextBlurb": "Skip to the next track.", + "@deviceActionMediaNextBlurb": { + "description": "Band double-tap action picker: blurb for next track" + }, + "deviceActionMediaPrevLabel": "Previous track", + "@deviceActionMediaPrevLabel": { + "description": "Band double-tap action picker: label for previous track" + }, + "deviceActionMediaPrevBlurb": "Go back a track.", + "@deviceActionMediaPrevBlurb": { + "description": "Band double-tap action picker: blurb for previous track" + }, + "deviceActionVolumeUpLabel": "Volume up", + "@deviceActionVolumeUpLabel": { + "description": "Band double-tap action picker: label for volume up" + }, + "deviceActionVolumeUpBlurb": "Raise media volume a step.", + "@deviceActionVolumeUpBlurb": { + "description": "Band double-tap action picker: blurb for volume up" + }, + "deviceActionVolumeDownLabel": "Volume down", + "@deviceActionVolumeDownLabel": { + "description": "Band double-tap action picker: label for volume down" + }, + "deviceActionVolumeDownBlurb": "Lower media volume a step.", + "@deviceActionVolumeDownBlurb": { + "description": "Band double-tap action picker: blurb for volume down" + }, + "deviceActionRingPhoneLabel": "Ring my phone", + "@deviceActionRingPhoneLabel": { + "description": "Band double-tap action picker: label for ring my phone" + }, + "deviceActionRingPhoneBlurb": "Play a loud sound so you can find your phone.", + "@deviceActionRingPhoneBlurb": { + "description": "Band double-tap action picker: blurb for ring my phone" + }, + "deviceActionTorchLabel": "Flashlight", + "@deviceActionTorchLabel": { + "description": "Band double-tap action picker: label for flashlight toggle" + }, + "deviceActionTorchBlurb": "Toggle your phone's flashlight.", + "@deviceActionTorchBlurb": { + "description": "Band double-tap action picker: blurb for flashlight toggle" + }, + "deviceActionMarkMomentLabel": "Mark a moment", + "@deviceActionMarkMomentLabel": { + "description": "Band double-tap action picker: label for mark a moment" + }, + "deviceActionMarkMomentBlurb": "Tag the current moment in your journal.", + "@deviceActionMarkMomentBlurb": { + "description": "Band double-tap action picker: blurb for mark a moment" + }, + "deviceActionWorkoutToggleLabel": "Start / stop workout", + "@deviceActionWorkoutToggleLabel": { + "description": "Band double-tap action picker: label for start/stop workout" + }, + "deviceActionWorkoutToggleBlurb": "Begin or end a workout from your wrist.", + "@deviceActionWorkoutToggleBlurb": { + "description": "Band double-tap action picker: blurb for start/stop workout" + }, + "deviceActionLogWaterLabel": "Log water", + "@deviceActionLogWaterLabel": { + "description": "Band double-tap action picker: label for log water" + }, + "deviceActionLogWaterBlurb": "Add a glass to today's water, same step as the + on the nutrition screen.", + "@deviceActionLogWaterBlurb": { + "description": "Band double-tap action picker: blurb for log water" + }, + "deviceActionBroadcastToTaskerLabel": "Broadcast to Tasker", + "@deviceActionBroadcastToTaskerLabel": { + "description": "Band double-tap action picker: label for Tasker broadcast (Android only)" + }, + "deviceActionBroadcastToTaskerBlurb": "Fire a broadcast intent so Tasker can trigger any automation.", + "@deviceActionBroadcastToTaskerBlurb": { + "description": "Band double-tap action picker: blurb for Tasker broadcast (Android only)" } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 325d1c84..96804076 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -293,5 +293,2017 @@ "welcomePartOfFileNotUsedTitle": "Parte de ese archivo no se pudo usar", "welcomeStrandedDays": "{n, plural, one{1 día llegó desordenado y solo se usó como contexto para el día siguiente.} other{{n} días llegaron desordenados y solo se usaron como contexto para el día siguiente.}}", "welcomeLateRows": "{n, plural, one{1 fila llegó después de que su día ya se hubiera puntuado y cerrado.} other{{n} filas llegaron después de que su día ya se hubiera puntuado y cerrado.}}", - "welcomeExportAgainInDateOrder": "Exportar de nuevo en orden de fecha" + "welcomeExportAgainInDateOrder": "Exportar de nuevo en orden de fecha", + "scanBarcodeTitle": "Escanea el código de barras", + "scanBarcodeClose": "Cerrar", + "scanBarcodeInstructions": "Mantén el código de barras dentro del recuadro. No se graba nada — solo se leen los dígitos.", + "scanBarcodeNoAccessTitle": "Sin acceso a la cámara", + "scanBarcodeCameraFailedTitle": "La cámara no se pudo iniciar", + "scanBarcodeNoAccessBody": "Escanear necesita la cámara, y no se le ha dado permiso a esta app.", + "scanBarcodeCameraFailedBody": "Este dispositivo no pudo abrir su cámara para el escáner.", + "scanBarcodeTypeInstead": "Escribir los números en su lugar", + "findingsLogTitle": "Observaciones", + "findingsLogEmptyTitle": "Nada ha llamado la atención", + "findingsLogEmptyBody": "Las vigilancias de enfermedad, fisiología nocturna inusual, temperatura de la piel y cambios en tu frecuencia cardíaca en reposo han estado todas tranquilas. Eso es un resultado, no una pantalla vacía.", + "findingsLogDerivedNote": "Se calcula a partir de tus propios días cada vez que se abre, no se guarda cuando ocurrió — así que si un día se vuelve a analizar, lo que dice aquí cambia con él.", + "startCardDefaultSub": "Elige uno y empieza", + "monthGridCoverage": "{have} de {total} días", + "monthGridSemanticsLabel": "{title}: {have} de {total} días tienen un valor. Sombreado según tu propio rango.", + "monthGridDaysAgo": "Hace {days} días", + "monthGridToday": "Hoy", + "monthGridFootnote": "Una celda por día. Más oscuro significa más alto en TU propio rango — el percentil 10 al 90 de cada día que has registrado — y una celda con contorno es un día sin valor, no uno bajo. Más esfuerzo no es mejor esfuerzo, y dormir más no es dormir mejor; esto indica dónde se ubicó un día, no cómo te fue.", + "monthGridNotShadedYetTitle": "{title} aún no tiene sombreado", + "monthGridNotShadedYetBody": "{days, plural, one{{days} día} other{{days} días}} no es un rango — el sombreado indica dónde se ubica un día dentro de tu propio rango. Aparece a partir de {min}.", + "whatChangedTitle": "Qué cambió", + "whatChangedSub": "SEGÚN TU PROPIO HISTORIAL", + "whatChangedNoDataTitle": "Aún no hay datos para este día", + "whatChangedNoDataBody": "El análisis compara un día con los anteriores, y este día no tiene ningún valor con el que comparar. No hay nada inusual porque no se sabe nada.", + "whatChangedLearningTitle": "Todavía aprendiendo tu normalidad", + "whatChangedLearningBody": "Lo inusual solo tiene sentido frente a un rango, y {days, plural, one{hay {days} día} other{hay {days} días}} de historial detrás de este. El análisis comienza a partir de {min}.", + "whatChangedNothingTitle": "Nada llamó la atención", + "whatChangedNothingBody": "Todas las métricas con suficiente historial se mantuvieron dentro del rango que marcan tus propios días. Esa es la respuesta normal, y es una respuesta completa.", + "whatChangedMethodologyNote": "Medido frente a tus propios días anteriores, en tus propias unidades, con la ventana de tiempo indicada — para que puedas cuestionarlo. Nada de esto es una causa ni un diagnóstico.", + "whatChangedDayLinkTitle": "Qué pasó ese día", + "whatChangedDayLinkSub": "Sueño, sesiones, comidas y registros en orden cronológico", + "whatChangedMonthSection": "El mes detrás de esto", + "journalFieldErrorNoName": "Ponle un nombre", + "journalFieldErrorInvalidName": "Usa al menos una letra o un número", + "journalFieldErrorNoUnit": "Indica en qué se mide (mg, ml, tazas…)", + "journalFieldErrorDuplicate": "Ya estás registrando algo con ese nombre", + "journalFieldTitle": "Registrar otra cosa", + "journalFieldNameLabel": "¿Qué quieres registrar?", + "journalFieldNameHint": "Magnesio, tiempo de pantalla, dolor de cabeza…", + "journalFieldKindQuestion": "¿Qué tipo de número es?", + "journalFieldKindRating": "Una calificación de 1 a 5", + "journalFieldKindAmount": "Una cantidad", + "journalFieldKindMinutes": "Minutos", + "journalFieldUnitLabel": "Unidad", + "journalFieldUnitHint": "mg, ml, tazas…", + "journalFieldStepSize": "Tamaño del paso", + "journalFieldMaxPerDay": "Lo máximo que registrarías en un día", + "journalFieldAskLastTime": "Preguntar cuándo fue la última vez", + "journalFieldStartTracking": "Empezar a registrarlo", + "aiBriefingForDay": "PARA {day}", + "aiBriefingNoModelTitle": "No hay ningún modelo configurado", + "aiBriefingNoModelBody": "Un resumen lo escribe un modelo que tú eliges. Hasta que elijas uno, no hay nada que generar y no se ha enviado nada a ningún sitio.", + "aiBriefingChooseModel": "Elegir un modelo", + "aiBriefingNothingTitle": "No se ha escrito nada para hoy", + "aiBriefingNothingBody": "Los resúmenes se generan según un horario, o a petición aquí.", + "aiBriefingWriting": "Escribiendo…", + "aiBriefingWriteNow": "Escribir uno ahora", + "aiBriefingWriteAgain": "Volver a escribirlo", + "aiBriefingFailedTitle": "Eso no se ha podido enviar", + "aiBriefingFailedGeneric": "Ha fallado: {error}", + "aiBriefingSentSection": "Lo que se envió", + "aiBriefingReadSection": "Lo que se leyó", + "aiBriefingNoneBody": "Nada. No hubo ninguna solicitud: la nota anterior se escribió en este teléfono.", + "aiBriefingLocalBody": "Estos números fueron a {host}, en este mismo equipo. Nada salió de él.", + "aiBriefingCloudBody": "Estos números, y nada más, se enviaron a {host} como {model}. Sin grabaciones sin procesar, sin nombre, sin identificador.", + "aiBriefingNoneCardTitle": "Nada llamó la atención, así que no se preguntó nada", + "aiBriefingNoneCardBody": "El barrido se ejecuta en este teléfono. Solo llama a un modelo cuando tiene un hallazgo que entregarle, y hoy no tenía ninguno.", + "aiBriefingEmptyCardTitle": "No había nada disponible para enviar", + "aiBriefingEmptyCardBody": "Ninguna métrica tenía un valor cuando se escribió esto, así que el mensaje no llevaba ninguno.", + "napsFellAsleepHelp": "CUÁNDO TE QUEDASTE DORMIDO", + "napsWokeUpHelp": "CUÁNDO TE DESPERTASTE", + "napsInvalidWindow": "Una siesta dura entre 5 minutos y 6 horas. Cualquier cosa más larga es un sueño nocturno y pertenece a la noche, donde se pueden leer las fases.", + "napsOverlap": "Eso se superpone con una siesta ya registrada este día. Elimina esa primero, en lugar de contar la misma hora dos veces.", + "napsNotReanalysed": "El día no se ha vuelto a analizar: ya había otro análisis en curso. Tu edición está guardada y se aplicará la próxima vez.", + "napsTitle": "Siestas", + "napsNoReadingTitle": "Sin datos de siestas para este día", + "napsNoReadingBody": "Las siestas se calculan a partir del mismo registro de 1 Hz que el resto del día, y este día no tiene suficiente.", + "napsEmptyTitle": "Sin siestas este día", + "napsEmptyBody": "Nada en este día estuvo lo bastante quieto, durante suficiente tiempo, con la caída de frecuencia cardíaca que acompaña al sueño.", + "napsCountsToward": "{mins} de siesta cuentan para tu necesidad de sueño de esta noche.", + "napsNotAppliedTitle": "Eso no se ha aplicado", + "napsWorking": "Procesando…", + "napsLogANap": "Registrar una siesta", + "napsRemovedSection": "Eliminadas", + "napsPutBackSemantic": "Restaurar esta siesta", + "napsPutBackLabel": "Restaurar", + "napsRemovalKept": "Una eliminación se guarda como un intervalo de tiempo, no como un identificador, para que siga aplicándose aunque el detector desplace sus límites.", + "napsYouLoggedThis": "Lo registraste tú", + "napsDetected": "Detectada", + "napsLoggedWithMins": "{mins} · lo registraste tú", + "napsDetectedWithMins": "{mins} dormido · detectada", + "napsDeleteSemantic": "Eliminar esta siesta", + "napsNotANapSemantic": "Esto no fue una siesta", + "napsDeleteLabel": "Eliminar", + "napsNotANapLabel": "No es una siesta", + "readinessDetailTitle": "Preparación", + "readinessDetailNotScoredTitle": "La preparación no tiene puntuación", + "readinessDetailLastNightScored": "La última noche con puntuación fue el {day}.", + "readinessDetailWhatWasMissing": "Qué faltaba", + "readinessDetailWhatWentIntoIt": "Qué se tuvo en cuenta", + "readinessDetailInputsFooter": "{used}/{total} entradas. Cada una se clasifica frente a tu propio historial: una vista paralela de las mismas entradas, no una descomposición del número de arriba.", + "readinessDetailNoBreakdownTitle": "Aún no hay desglose", + "readinessDetailNoBreakdownBody": "Clasificar cada entrada frente a tu propio historial requiere unas dos semanas de noches.", + "readinessDetailHistoryTitle": "Historial", + "readinessDetailLastNDays": "{n, plural, one{Último {n} día} other{Últimos {n} días}}", + "readinessDetailNoHistoryTitle": "Sin historial de preparación", + "readinessDetailNoHistoryBody": "0 días con puntuación.", + "readinessDetailWearOvernight": "Usa la banda durante la noche", + "readinessDetailUnit": "/100", + "readinessDetailDaysAgo": "{n, plural, one{hace {n} día} other{hace {n} días}}", + "readinessDetailToday": "Hoy", + "readinessDetailMeasured": "Medido", + "readinessDetailNotMeasured": "No medido", + "readinessDetailNightsOfHistory": "{n, plural, one{{n} noche de tu propio historial} other{{n} noches de tu propio historial}}", + "readinessDetailNeedSuffix": "{need}. Cada entrada se clasifica frente a tus propias noches, así que la puntuación no puede empezar antes de que haya suficientes.", + "readinessDetailNoNoteFallback": "Todo lo anterior estaba presente y, aun así, no fue posible hacer la comparación con tu propio historial.", + "readinessDetailNotAvailable": "no disponible", + "readinessDetailContributionNotReported": "contribución no notificada", + "readinessDetailRelativeUncalibrated": "relativa, sin calibrar", + "readinessDetailWithinSpread": "dentro de tu rango habitual", + "readinessDetailWeightPercent": "{pct}% de peso", + "dayStepsTitle": "Pasos", + "dayStepsThroughDay": "A lo largo del día", + "dayStepsToday": "hoy", + "dayStepsOnDay": "el {day}", + "dayStepsNoTimesTitle": "Sin horarios detrás del total {when}", + "dayStepsNoStepsTitle": "No se contaron pasos {when}", + "dayStepsStrapCounterBody": "Los {count} pasos contados {when} provienen del contador de pasos propio de la correa, que informa un total del día sin horarios. No hay nada que ubicar en un reloj.", + "dayStepsNothingCounted": "Nada capaz de contar pasos registró datos {when}.", + "dayStepsChartTitle": "CUÁNDO SE CONTARON", + "dayStepsUnit": "pasos", + "dayStepsYourPhone": "Tu teléfono", + "dayStepsYourBand": "Tu correa", + "dayStepsCounted": "Contados", + "dayStepsHonestyMixed": "Contados en tu muñeca y por tu teléfono, y ambos se equivocan de forma distinta: la muñeca subestima una caminata real y puede confundir movimientos rítmicos de la mano con caminar, mientras que el teléfono solo cuenta los pasos que diste llevándolo contigo.", + "dayStepsHonestyStrap": "Contados en tu muñeca, donde una caminata real tiende a subestimarse y el movimiento rítmico de la mano puede leerse como caminar.", + "dayStepsHonestyPhone": "Contados por tu teléfono, así que solo aparecen los pasos que diste llevándolo contigo.", + "roughNightSignRhr": "tu frecuencia cardíaca en reposo subió", + "roughNightSignHrv": "tu VFC bajó", + "roughNightSignDip": "tu frecuencia cardíaca bajó menos por la noche de lo habitual", + "roughNightSignTemp": "tu piel estuvo más caliente", + "roughNightLateTraining": "Entrenaste hasta las {at}, lo que a menudo causa esto por sí solo.", + "roughNightIllness": "El monitor de enfermedad también marcó esta noche: una subida sostenida respecto a tu propia base, no un diagnóstico.", + "roughNightLuteal": "Estás en la fase lútea, que por sí sola eleva la frecuencia cardíaca en reposo y la temperatura de la piel.", + "roughNightWarmRoom": "Tu piel estuvo más caliente de lo habitual — una habitación calurosa también provoca esto.", + "roughNightDismiss": "Descartar", + "roughNightDefaultHeadline": "Una noche más dura de lo habitual", + "roughNightSummary": "{sentence}, en comparación con tus propias noches. Esto es una medición de la noche, no un juicio sobre ti.", + "roughNightTellWhatHappened": "Contar qué pasó", + "roughNightNothingToAnswer": "Nada que responder — esta tarjeta solo informa sobre la noche.", + "roughNightWhatElse": "¿Qué más estaba pasando?", + "roughNightAnythingElse": "¿Algo más?", + "roughNightSaving": "Guardando", + "roughNightLogIt": "Registrarlo para esa noche", + "roughNightAddHowMuch": "Añadir cuánto", + "roughNightDoNotAskAgain": "No volver a preguntar", + "roughNightSeveralMoved": "Varias mediciones nocturnas se movieron juntas", + "driverBreakdownHigherThanUsual": "Más alto que lo habitual", + "driverBreakdownLowerThanUsual": "Más bajo que lo habitual", + "driverBreakdownRightOnUsual": "{now} · justo en tu habitual", + "driverBreakdownAboveUsual": "{now} · {delta} por encima de tu habitual {usual}", + "driverBreakdownBelowUsual": "{now} · {delta} por debajo de tu habitual {usual}", + "driverBreakdownWeightPct": "{pct}% de peso", + "driverBreakdownNotAvailable": "no disponible", + "driverBreakdownContributionNotReported": "contribución no reportada", + "driverBreakdownRelativeUncalibrated": "relativo, sin calibrar", + "driverBreakdownWithinUsualSpread": "dentro de tu variación habitual", + "driverBreakdownBiggerThanNoise": "mayor que el ruido de medición", + "driverBreakdownSmallerThanNoise": "fuera de tu variación habitual, pero lo bastante pequeño como para ser ruido de medición", + "driverBreakdownWhatHelped": "Qué ayudó", + "driverBreakdownWhatHeldYouBack": "Qué te frenó", + "driverBreakdownNeither": "Ninguno", + "driverBreakdownFooter": "Cada dato se compara con tu propio historial: una vista paralela de las mismas entradas, no fragmentos de la puntuación en sí. El «ruido de medición» es cuánto puede moverse una lectura por sí sola sin que nada haya cambiado. Patrones en tus propios registros, no causas.", + "driverBreakdownHideHistory": "{label}, ocultar su historial", + "driverBreakdownShowHistory": "{label}, mostrar su historial", + "driverBreakdownDaysAgo": "{n, plural, one{hace {n} día} other{hace {n} días}}", + "driverBreakdownToday": "Hoy", + "driverBreakdownUsualRange": "Tu rango habitual {lo}–{hi}{unit}", + "driverBreakdownAbsenceTitle": "No hay desglose que mostrar", + "driverBreakdownAbsenceAlgoVersion": "La forma de calcular la preparación cambió con la última actualización y se está reconstruyendo.", + "driverBreakdownAbsenceStale": "El último resumen es demasiado antiguo para respaldarlo.", + "driverBreakdownAbsenceNoVersion": "El resumen guardado no tiene marca de versión.", + "driverBreakdownSyncTheBand": "Sincronizar la banda", + "driverBreakdownAbsenceNoReason": "No hay ningún registro que explique por qué anoche no tiene desglose.", + "coachFiguresCouldNotBeDrawn": "No se pudo dibujar una figura", + "coachFiguresNoType": "El entrenador envió una figura sin tipo.", + "coachFiguresUnsupportedType": "El entrenador pidió una figura de tipo «{type}», que esta aplicación no puede dibujar.", + "coachFiguresFigure": "Figura", + "coachFiguresSeriesN": "Serie {n}", + "coachFiguresLaneN": "Carril {n}", + "coachFiguresNoSleepSegments": "Sin segmentos de sueño", + "coachFiguresNoTimeInZone": "Sin tiempo en zona", + "coachFiguresMinTotal": "{n} min en total", + "coachFiguresGauge": "Medidor", + "coachFiguresGaugeNoValue": "El entrenador envió un medidor sin valor.", + "coachFiguresSummary": "Resumen", + "coachFiguresEmptySummary": "El entrenador envió un resumen vacío.", + "coachFiguresTable": "Tabla", + "coachFiguresTableNoRows": "El entrenador envió una tabla sin filas.", + "circadianDetailTitle": "Reloj corporal", + "circadianDetailNoNightsTitle": "Aún no hay noches que representar", + "circadianDetailNoNightsBody": "0 noches puntuadas.", + "circadianDetailNoNightsFix": "Usa la banda por la noche", + "circadianDetailSleepTitle": "Sueño, noche a noche", + "circadianDetailAsleep": "Dormido", + "circadianDetailSleepFootnote": "{count, plural, one{{count} noche, una columna cada una. Cuanto más oscuro, más tiempo dormido esa hora.} other{{count} noches, una columna cada una. Cuanto más oscuro, más tiempo dormido esa hora.}}", + "circadianDetailYourRhythm": "Tu ritmo", + "circadianDetailWhichNights": "Qué noches", + "circadianDetailHide": "Ocultar", + "circadianDetailShow": "Mostrar", + "circadianDetailTodayPredicted": "Hoy, previsto", + "circadianDetailRhythmStrength": "Fuerza del ritmo", + "circadianDetailWhenStill": "Cuando estás quieto", + "circadianDetailNoStillTitle": "Aún no hay momentos de quietud que leer", + "circadianDetailNoStillBody": "Esto lee el ritmo cardíaco solo en los segundos en que no te moviste, y {days, plural, one{el último {days} día tuvo} other{los últimos {days} días tuvieron}} muy pocos para construir una hora.", + "circadianDetailStillnessTitle": "Variabilidad latido a latido en quietud", + "circadianDetailStillnessFootnote": "Cada hora es el valor medio de {lo}–{hi} tramos de cinco minutos en los que realmente estuviste quieto, durante {days, plural, one{el último {days} día} other{los últimos {days} días}} — nunca solo el de hoy. {drawn} de 24 horas tuvieron al menos tres tramos; el resto queda en blanco. No es una puntuación de estrés: sentarte, una habitación cálida o un café la mueven igual.", + "circadianDetailForecastTitle": "Cómo es probable que vaya hoy", + "circadianDetailForecastFootnote": "Sin escala — la forma es todo el resultado.", + "circadianDetailTroughText": "El tramo más plano cae en {troughLabel}, hacia {start}–{end}.", + "circadianDetailPredictionDisclaimer": "Esto es una predicción, no una medición. Nada en la banda mide cuán alerta estás, y solo conoce la noche pasada y nada más — una siesta, un café o cualquier cosa que ocurra hoy nunca llega a ella.", + "circadianDetailAssumedPhaseNote": "Tu propio pico de reloj aún no está calculado, así que se usa uno promedio.", + "circadianDetailNotADrivingCheck": "No es una prueba de aptitud para conducir ni una herramienta de seguridad en turnos, y no dice que estés incapacitado.", + "circadianDetailChronotype": "Cronotipo", + "circadianDetailMidSleepFree": "Punto medio del sueño, días libres", + "circadianDetailMidSleepWork": "Punto medio del sueño, días laborables", + "circadianDetailSocialJetlag": "Jet lag social", + "circadianDetailLater": "más tarde", + "circadianDetailEarlier": "más temprano", + "circadianDetailNightsCompared": "Noches libres / laborables comparadas", + "circadianDetailRegularityIndex": "Índice de regularidad", + "circadianDetailNightsLeastAlike": "Noches menos parecidas", + "circadianDetailSamePairScale": "Ese par, misma escala", + "circadianDetailRhythmNotEstablished": "Tu ritmo aún no está establecido", + "circadianDetailPairFootnote": "El par que menos coincidió, de {count}. Un fin de semana que se alarga es un horario distinto, no una noche peor. Se excluyen los pares en los que se registró muy poco de alguno de los días.", + "circadianDetailStability": "Estabilidad día a día", + "circadianDetailFragmentation": "Fragmentación hora a hora", + "circadianDetailAmplitude": "Amplitud relativa", + "circadianDetailM10Start": "Inicio de las 10 horas de FC más alta", + "circadianDetailL5Start": "Inicio de las 5 horas de FC más baja", + "circadianDetailRhythmPeak": "Pico del ritmo", + "circadianDetailPeakSwing": "Oscilación pico-media", + "circadianDetailFitCurve": "Ajuste a una curva de 24 h", + "circadianDetailStrengthNotMeasured": "La fuerza del ritmo aún no está medida", + "circadianDetailStrengthWhy": "Necesita días consecutivos con las 24 horas registradas.", + "circadianDetailStrengthFootnoteKnown": "A partir de {used, plural, one{{used} día totalmente registrado} other{{used} días totalmente registrados}} de frecuencia cardíaca. Estas son tus horas de FC más alta y más baja, no las más activas.", + "circadianDetailStrengthFootnoteUnknown": "A partir de una serie de días totalmente registrados de frecuencia cardíaca. Estas son tus horas de FC más alta y más baja, no las más activas.", + "beatsTitle": "Latidos", + "beatsNoNightTitle": "Aún no hay ninguna noche que dibujar", + "beatsNoNightBody": "Este teléfono no ha producido ninguna noche derivada, así que no hay intervalos de latido que representar.", + "beatsNoNightFix": "Usa la banda por la noche y luego sincroniza", + "beatsNightOf": "Noche del {date}", + "beatsPoincareSection": "Cada latido frente al anterior", + "beatsBeatsGoneTitle": "Los latidos de esta noche ya no están en este teléfono", + "beatsBeatsGoneBody": "Los intervalos de latido individuales se conservan unos días después de puntuar la noche y luego se eliminan. Los valores calculados a partir de ellos se conservan para siempre", + "beatsBeatsGoneMeasured": " — esta noche se midió SD1 {sd1} ms, SD2 {sd2} ms", + "beatsScatterTitle": "Cada intervalo, frente al anterior", + "beatsScatterFootnote": "La diagonal es donde un latido duró lo mismo que el anterior. La dispersión a través de esa línea es SD1, latido a latido; a lo largo de ella es SD2, la deriva más lenta.", + "beatsSd1Label": "SD1", + "beatsSd2Label": "SD2", + "beatsIntervalsLabel": "Intervalos", + "beatsIntervalsSurvived": "{count, plural, one{{count} intervalo superó la corrección} other{{count} intervalos superaron la corrección}}", + "beatsDroppedArtifact": " — {count, plural, one{se descartó {count} como artefacto y no está} other{se descartaron {count} como artefacto y no están}} en la nube", + "beatsPulseNotEcg": "Pulso, no ECG — real y tuyo, pero no la imagen que dibuja un ECG.", + "beatsMeasuredOn": "Medido con {device}; las bandas no leen los mismos números entre sí.", + "beatsVariabilitySection": "Variabilidad a lo largo de la noche", + "beatsUnitNights": "noches", + "beatsUnitScreenedNotScreened": "detectado / no detectado", + "beatsVariabilityWhy": "Ningún bloque de media hora de esta noche tuvo suficientes latidos limpios para publicar un RMSSD.", + "beatsNoBinsStored": "No se guardó ningún bloque para esta noche.", + "beatsRmssdTitle": "RMSSD en bloques de media hora", + "beatsStart": "Inicio", + "beatsBandFootnote": "La barra indica cuánta certeza tenemos del bloque, no un rango por el que pasó tu cuerpo. La marca dentro de ella es el valor.", + "beatsHolesFootnote": "{count, plural, one{ {count} bloque tiene muy pocos latidos limpios para publicar uno, y queda vacío en lugar de unirse.} other{ {count} bloques tienen muy pocos latidos limpios para publicar uno, y quedan vacíos en lugar de unirse.}}", + "beatsFirstThird": "Primer tercio", + "beatsLastThird": "Último tercio", + "beatsDcSection": "Capacidad de desaceleración", + "beatsDcWhy": "Ninguna noche guardada ha producido una todavía.", + "beatsDcNoData": "Ninguna noche ha producido una todavía.", + "beatsDcChartTitle": "Tus propias noches, en orden", + "beatsDaysAgo": "hace {count} días", + "beatsTodayLabel": "Hoy", + "beatsAnchorsLastNight": "Anclas la última noche", + "beatsCleanBeats": "Latidos limpios", + "beatsDcNote": "Solo tuyo. Compáralo con tus propias noches y con nada más — no hay una banda de referencia para una muñeca.\n\nPromedia los latidos alrededor de cada momento en que tu corazón se desaceleró. Una línea ascendente puede deberse a una señal más limpia y no a un corazón distinto, así que léela junto al recuento de anclas y la proporción de latidos limpios de arriba. Si cambiaste de banda dentro de esta ventana, las dos mitades no son comparables.", + "beatsRhythmSection": "Cribado de ritmo", + "beatsRhythmChartTitle": "Una celda por día", + "beatsScreenNotFired": "El cribado no saltó", + "beatsScreenFired": "El cribado saltó", + "beatsNotScreened": "Sin cribar", + "beatsNoDayScreened": "Ningún día de esta ventana fue cribado", + "beatsScreenNote": "Un cribado, no una prueba.\n\nUn día en que el cribado no saltó no es un día en que quedás libre de riesgo — no puede descartar nada, y nunca pudo. Los días con contorno no fueron cribados en absoluto: pocos latidos limpios o demasiado movimiento.\n\nEl pulso de muñeca no es un ECG. Si lo que te trajo aquí son síntomas, un profesional clínico puede evaluarlo como corresponde.", + "beatsScreenedSummary": "{screened} de los últimos {win} días fueron cribados", + "beatsFiredSummary": "; el cribado saltó en {fired}", + "beatsNotScreenedNote": " Anoche no se cribó: {note}", + "logWorkoutCouldNotLog": "No se pudo registrar este entrenamiento. Inténtalo de nuevo.", + "logWorkoutCouldNotDismiss": "No se pudo descartar este entrenamiento. Inténtalo de nuevo.", + "logWorkoutAdjustTimes": "Ajustar los horarios", + "logWorkoutDetectedActivityTitle": "Actividad detectada", + "logWorkoutYoursToConfirmSub": "PENDIENTE DE CONFIRMAR", + "logWorkoutReadFailedTitle": "No se pudo leer tu actividad detectada", + "logWorkoutReadFailedBody": "La base de datos no respondió. No se ha registrado ni descartado nada.", + "logWorkoutTryAgain": "Inténtalo de nuevo", + "logWorkoutReadingSpotted": "Leyendo lo que detectó la banda…", + "logWorkoutNothingToReviewTitle": "Nada que revisar", + "logWorkoutNothingToReviewBody": "Es posible que este ya se haya registrado o descartado.", + "logWorkoutHardMinutesTitle": "Estos son los minutos de esfuerzo, no toda la sesión", + "logWorkoutHardMinutesBody": "La detección informa el esfuerzo sostenido que pudo ver, así que un calentamiento y el descanso entre series quedan fuera. Ajusta los horarios antes de registrar si la ventana es corta.", + "logWorkoutMinutesOfEffort": "{mins} min de esfuerzo", + "logWorkoutAvgHr": "FC media", + "logWorkoutPeakHr": "FC máxima", + "logWorkoutLooksLike": "Parece ser", + "logWorkoutLogIt": "Registrarlo", + "logWorkoutNotAWorkout": "No es un entrenamiento", + "logWorkoutToday": "Hoy", + "logWorkoutYesterday": "Ayer", + "logWorkoutDefaultTitle": "Registrar un entrenamiento pasado", + "logWorkoutWindowRescoredSub": "LA VENTANA, RECALCULADA", + "logWorkoutYourOwnTimesSub": "TUS PROPIOS HORARIOS", + "logWorkoutWhenGroup": "Cuándo", + "logWorkoutActivityLabel": "Actividad", + "logWorkoutDateLabel": "Fecha", + "logWorkoutStartedLabel": "Inicio", + "logWorkoutEndedLabel": "Fin", + "logWorkoutLengthLabel": "Duración", + "logWorkoutNextMorningSub": "a la mañana siguiente", + "logWorkoutWindowInvalidTitle": "Esa ventana no se puede guardar", + "logWorkoutTimesUpdatedTitle": "Horarios actualizados", + "logWorkoutLoggedTitle": "Entrenamiento registrado", + "logWorkoutUnscoredSaved": "Guardado. No se registró frecuencia cardíaca en esa ventana, así que no tiene esfuerzo ni calorías: los horarios son todo lo que conserva.", + "logWorkoutCouldNotSave": "No se pudo guardar. Inténtalo de nuevo.", + "logWorkoutScoredTitle": "Calculado a partir de lo que registró la banda", + "logWorkoutScoredBody": "El esfuerzo y las calorías provienen de la frecuencia cardíaca por segundo dentro de estos horarios, con el mismo método que usa el día. Nada se estima a partir de la duración.", + "logWorkoutSaving": "Guardando…", + "logWorkoutSaveNewTimes": "Guardar los nuevos horarios", + "logWorkoutSearchActivities": "Buscar actividades", + "logWorkoutNoActivityByName": "Ninguna actividad con ese nombre", + "logFoodTitle": "Registrar una comida", + "logFoodClose": "Cerrar", + "logFoodIAte": "Comí {meal}", + "logFoodAgain": "De nuevo", + "logFoodAddNumbers": "Añadir los números", + "logFoodScanBarcode": "Escanear un código de barras", + "logFoodScanSubOn": "Consulta el código de barras en openfoodfacts.org y rellena lo que puede confirmar", + "logFoodScanSubOff": "Busca el producto en línea. Pregunta primero", + "logFoodLookingUpTitle": "Buscando", + "logFoodLookingUpBody": "Los campos se rellenan en cuanto llega la respuesta.", + "logFoodWhatLabel": "Qué", + "logFoodWhatHint": "Pollo con arroz", + "logFoodPortionLabel": "Porción", + "logFoodUnknownHint": "desconocido", + "logFoodEnergyLabel": "Energía", + "logFoodProteinLabel": "Proteína", + "logFoodCarbsLabel": "Carbohidratos", + "logFoodFatLabel": "Grasa", + "logFoodFibreLabel": "Fibra", + "logFoodBlankHint": "Un número en blanco se queda en blanco. Solo hace falta \"Qué\".", + "logFoodSayWhatFirst": "Primero indica qué fue.", + "logFoodConsentTitle": "¿Buscar códigos de barras en línea?", + "logFoodConsentBody1": "Un escaneo envía el código de barras a openfoodfacts.org, una base de datos de alimentos gratuita y abierta. Ellos ven el código de barras y tu dirección IP. Nada sobre ti, tus comidas o tu salud sale de este teléfono, y un código que ya has escaneado antes se responde con tu propia copia sin volver a preguntarles.", + "logFoodConsentBody2": "Sus números los escribe el público y bastantes son incorrectos, así que cualquier cosa que no pase una comprobación de coherencia se deja en blanco en vez de rellenarse. Todo lo que sí rellena lo puedes editar antes de guardar.", + "logFoodConsentBody3": "Puedes volver a desactivar esto en Ajustes › Privacidad. Escribir los números del envase funciona de cualquier forma.", + "logFoodAllowLookups": "Permitir búsquedas", + "logFoodNotNow": "Ahora no", + "logFoodBreakfast": "Desayuno", + "logFoodLunch": "Comida", + "logFoodDinner": "Cena", + "logFoodSnack": "Snack", + "logFoodNoNumbersTitle": "Sin números para este producto", + "logFoodNoNumbersBody": "Open Food Facts tiene el producto, pero nada aprovechable en su nutrición, o lo que tenía no pasó una comprobación de coherencia.", + "logFoodNotFoundTitle": "No está en Open Food Facts", + "logFoodNotFoundBody": "Nadie ha añadido todavía este código de barras.", + "logFoodFlaggedTitle": "Este registro está marcado como incorrecto", + "logFoodFlaggedBody": "Open Food Facts marca este producto como que contiene errores, así que no se rellenó ninguno de sus números.", + "logFoodUnreachableTitle": "Sin respuesta de Open Food Facts", + "logFoodUnreachableBody": "No se pudo contactar con openfoodfacts.org.", + "logFoodRefusedTitle": "La búsqueda de códigos de barras está desactivada", + "logFoodRefusedBody": "No se envió nada. Puedes activarla en Ajustes › Privacidad.", + "logFoodPortionNoteBase": "Open Food Facts indica esto por cada 100 g. Cambia la porción y los números se ajustan.", + "logFoodPortionNoteServing": "Open Food Facts indica esto por cada 100 g. Cambia la porción y los números se ajustan. La porción propia del envase es {serving}.", + "logFoodPillOpenFoodFacts": "Open Food Facts", + "logFoodPillYours": "Tuyo", + "logFoodBareOccasion": "REGISTRADO · ENERGÍA NO REGISTRADA", + "logFoodOpensInBrowser": "{label}, se abre en tu navegador", + "dayTimelineChargerOn": "En el cargador", + "dayTimelineChargerOff": "Fuera del cargador", + "dayTimelineDoubleTap": "Tocaste la banda dos veces", + "dayTimelineRestarted": "La banda se reinició", + "dayTimelineBatteryPackAttached": "Batería externa conectada", + "dayTimelineBatteryPackRemoved": "Batería externa desconectada", + "dayTimelineAlarmWentOff": "Sonó la alarma", + "dayTimelineAsleep": "Dormido", + "dayTimelineNap": "Siesta", + "dayTimelineWorkout": "Entrenamiento", + "dayTimelineBandOffWrist": "Banda fuera de la muñeca", + "dayTimelineHighestHr": "Frecuencia cardíaca más alta", + "dayTimelineLowestHr": "Frecuencia cardíaca más baja", + "dayTimelineBpmAt": "{bpm} lpm a las {time}", + "dayTimelineTakenAt": "Tomado a las {time}", + "dayTimelineLastAt": "última a las {time}", + "dayTimelineTaggedTitle": "Etiquetado", + "dayTimelineTitle": "Resumen de tu día", + "dayTimelineSub": "MEDIANOCHE A MEDIANOCHE", + "dayTimelineHeartRateTitle": "Frecuencia cardíaca", + "dayTimelineMidnight": "Medianoche", + "dayTimelineNoon": "Mediodía", + "dayTimelineMoving": "En movimiento", + "dayTimelineNotRecorded": "Sin registrar", + "dayTimelineNothingRecordedTitle": "No se registró nada este día", + "dayTimelineNothingRecordedBody": "Sin sueño, sin sesión, sin registro y sin evento de la banda con hora. Un día sin nada suele ser un día en que no llevaste la banda.", + "dayTimelineNoTimeTitle": "Nada de este día tiene una hora", + "dayTimelineNoTimeBody": "Lo registrado está abajo.", + "dayTimelineWhatHappenedSection": "Lo que pasó", + "dayTimelineAlsoLoggedSection": "También registrado este día", + "dayTimelineNoTimeNote": "Esto se registró para el día pero no tiene una hora concreta, así que no aparece en la línea de tiempo.", + "dayTimelinePatternsNote": "Patrones en tus propios registros, no causas. Que dos cosas aparezcan cerca aquí solo significa que ocurrieron cerca en el tiempo.", + "journalComposeNotReady": "Aún no está listo — abre la app primero.", + "journalComposeSaveFailed": "No se pudo guardar — comprueba el almacenamiento e inténtalo de nuevo.", + "journalComposeWhenWasLastOne": "¿Cuándo fue la última vez?", + "journalComposeTitle": "Diario", + "journalComposeTodaySection": "Hoy", + "journalComposeTrackSomethingElse": "Registrar algo más", + "journalComposeAnythingElseLabel": "Algo más", + "journalComposeAnythingElseHint": "Una línea sobre el día.", + "journalComposeSavingLabel": "Guardando", + "journalComposeHowAreYouFeeling": "¿Cómo te sientes?", + "journalComposeNotAnsweredYet": "Aún sin responder", + "journalComposeMoodOfFive": "Ánimo {value} de 5 · tócalo de nuevo para borrarlo", + "journalComposeMoodOfFiveSelected": "Ánimo {n} de 5, seleccionado. Actívalo para borrarlo.", + "journalComposeMoodOfFiveLabel": "Ánimo {n} de 5", + "journalComposeNotLogged": "Sin registrar", + "journalComposeWhenWasLastField": "Cuándo fue el último {field}", + "journalComposeAddTimeOfLastOne": "Añade la hora de la última vez", + "journalComposeLastAt": "Última a las {time}", + "journalComposeIncrease": "Aumentar", + "journalComposeDecrease": "Disminuir", + "journalComposeWeightLabel": "Peso", + "journalComposeNotEntered": "Sin registrar", + "journalComposeEnteredNotMeasured": "{value} · introducido, no medido", + "journalComposeEnterWeight": "Introducir peso", + "journalComposeEnter": "Introducir", + "journalComposeChange": "Cambiar", + "journalComposeSeeWeightTrend": "Ver la tendencia de peso", + "journalComposeSeeTheTrend": "Ver la tendencia", + "journalComposeWeightToday": "Peso de hoy", + "journalComposeWeightKgLabel": "Peso (kg)", + "journalComposeWeightScaleNote": "Lo que tú o tu báscula midan. La banda no mide esto.", + "journalComposeClear": "Borrar", + "journalComposeNotEnoughEntriesTitle": "No hay suficientes registros para una tendencia", + "journalComposeNotEnoughEntriesBody": "La línea es un promedio de siete días con lo que introdujiste, así que hacen falta al menos dos días. No se rellena nada entre ellos.", + "journalComposeSevenDayTrend": "Tendencia de siete días", + "journalComposeTrendFootnote": "Introducido por ti. Los días sin registro quedan vacíos.", + "journalComposeWeightTrendExplainer": "Introducido por ti o por tu báscula — la banda no mide el peso. Lo que se dibuja es un promedio de siete días, porque una báscula puede variar uno o dos kilos solo por agua y comida, y las lecturas sin procesar mostrarían eso como algo que le sucede a tu cuerpo. {count, plural, one{Se registró {count} día.} other{Se registraron {count} días.}}", + "nutritionTabToday": "Hoy", + "nutritionTabWeek": "Semana", + "nutritionTabGoals": "Objetivos", + "nutritionLogFood": "Registrar comida", + "nutritionTitle": "Nutrición", + "nutritionEmptyTodayTitle": "Nada registrado hoy", + "nutritionEmptyTodayBody": "Un toque es un registro completo.", + "nutritionLogOccasionFix": "Registrar una comida", + "nutritionOccasionsSection": "Comidas", + "nutritionAddAction": "Añadir", + "nutritionFloorTitle": "La energía de hoy es un mínimo, no un total", + "nutritionFloorBody": "{unknown} de {total} comidas se registraron sin una cifra de energía, así que el número de arriba es lo mínimo que comiste, no lo que comiste.", + "nutritionAddNumbersFix": "Añadir las cifras a una comida", + "nutritionDaysNotCounted": "{excluded} de los últimos {span} días no se pudieron contar", + "nutritionDayCountsRule": "Un día cuenta cuando cada comida lleva una cifra de energía.", + "nutritionDaysLoggedLabel": "Días con algo registrado", + "nutritionPartialExcluded": "{partial} registrados pero parciales, así que se excluyen de cada media de abajo", + "nutritionEnergyByDay": "Energía, día a día", + "nutritionSevenDayAvg": "Media de siete días", + "nutritionNoCompleteDayTitle": "Aún no hay un día completo que promediar", + "nutritionNoCompleteDayBody": "No tienes ninguno.", + "nutritionLabelEnergy": "Energía", + "nutritionLabelProtein": "Proteína", + "nutritionLabelCarbs": "Carbohidratos", + "nutritionLabelFat": "Grasa", + "nutritionLabelFibre": "Fibra", + "nutritionEnergyBalance": "Balance energético", + "nutritionLabelEaten": "COMIDO", + "nutritionLabelBurned": "QUEMADO", + "nutritionLabelBalance": "BALANCE", + "nutritionEatenMeanNote": "Lo comido es la media de {days} días completos. Lo quemado es solo de hoy.", + "nutritionEnergyLoggedTitle": "Energía registrada", + "nutritionPartialFootnote": "{n} parciales, excluidos de las medias de abajo.", + "nutritionNothingLoggedYet": "Nada registrado aún", + "nutritionNoEnergyFiguresYet": "Aún no hay cifras de energía", + "nutritionDailyEnergy": "Energía diaria", + "nutritionDailyProtein": "Proteína diaria", + "nutritionEnergyWord": "energía", + "nutritionProteinWord": "proteína", + "nutritionYourTargetsSection": "Tus objetivos", + "nutritionHintNone": "ninguno", + "nutritionNoTargetsTitle": "Sin objetivos establecidos", + "nutritionNoTargetsBody": "Un objetivo aquí es uno que escribes tú.", + "nutritionSetTargetFix": "Establecer un objetivo", + "nutritionEditAction": "Editar", + "nutritionBodySpentToday": "Lo que tu cuerpo gastó hoy", + "nutritionEstimatedExpenditure": "Gasto estimado", + "nutritionNotMeasured": "No medido", + "nutritionExpenditureSub": "HOY, SEGÚN LA FRECUENCIA CARDÍACA Y TU PERFIL", + "nutritionRemoveTitle": "¿Eliminar {label}?", + "nutritionRemoveBody": "Elimina el día y toda media que lo contara. No se puede deshacer.", + "nutritionNothingToMeasure": "Nada que medir aún frente a {label}", + "nutritionFloorAverageBody": "Todos los días completos tuvieron una comida registrada sin cifra de {nutrient}, así que la media solo sería un límite inferior.", + "nutritionCountedNoFigureBody": "{count} de los últimos {span} días contaron, pero ninguno llevaba una cifra de {nutrient}.", + "nutritionDayCountsRuleFull": "Un día cuenta cuando cada comida lleva una cifra y el registro llega hasta la noche. Ninguno de los últimos {span} días lo ha hecho.", + "nutritionOnTarget": "En el objetivo", + "nutritionRateAbove": "{amount} {unit}/día por encima", + "nutritionRateBelow": "{amount} {unit}/día por debajo", + "nutritionMeanOfDays": "{n, plural, one{media de {n} día completo} other{media de {n} días completos}}", + "nutritionLoggedToday": "REGISTRADO HOY", + "nutritionEatenToday": "COMIDO HOY", + "nutritionAtLeast": "Al menos", + "nutritionOccasionsUnit": "{n, plural, one{comida} other{comidas}}", + "nutritionOccasionsCount": "{n, plural, one{{n} comida} other{{n} comidas}}", + "nutritionLabelBalanceAtLeast": "BALANCE MÍNIMO", + "nutritionNotLogged": "Sin registrar", + "nutritionLoggedNoEnergy": "{n} registradas · energía no consignada", + "nutritionAtLeastPrefix": "al menos ", + "nutritionMealBreakfast": "Desayuno", + "nutritionMealLunch": "Almuerzo", + "nutritionMealDinner": "Cena", + "nutritionMealSnacks": "Snacks", + "nutritionNotCounted": "No contado", + "nutritionNotRecorded": "No consignado", + "nutritionEveryDayNoFigure": "TODOS LOS DÍAS COMPLETOS TUVIERON UNA COMIDA SIN CIFRA DE {label}", + "nutritionNoDayRecorded": "NINGÚN DÍA COMPLETO CONSIGNÓ {label}", + "nutritionMeanOfCompleteDaysCaps": "{n, plural, one{MEDIA DE {n} DÍA COMPLETO} other{MEDIA DE {n} DÍAS COMPLETOS}}", + "nutritionLeftOutAsFloor": " · {n} EXCLUIDOS POR SER UN MÍNIMO", + "nutritionWaterLabel": "Agua", + "nutritionTapToChange": "Toca − o + para cambiar", + "nutritionNoneYet": "Nada aún", + "nutritionAddWater": "Añadir agua", + "nutritionRemoveWater": "Quitar agua", + "coachApiKeyLabel": "Clave de API", + "coachApiKeyLocalLabel": "Clave de API (no necesaria en local)", + "coachAsking": "Preguntando…", + "coachAskLabel": "Preguntar al coach", + "coachBaseUrlLabel": "URL base", + "coachBriefingMenuSub": "El resumen exacto que salió de este dispositivo", + "coachBriefingMenuTitle": "Informe, y qué se envió", + "coachChooseModelFix": "Elegir un modelo", + "coachCloudDataNote": "Tus preguntas y los datos que lee el coach se envían a este endpoint. Consulta exactamente qué es eso en «Qué se envió».", + "coachDeleteChat": "Eliminar {title}", + "coachDeleteIt": "Eliminarlo", + "coachDestructiveWarning": "Esto elimina datos de este dispositivo y no se puede deshacer.", + "coachEndpointUnreachable": "No se pudo contactar con ese endpoint: {error}", + "coachErrorTitle": "Eso no se pudo completar", + "coachInputHint": "Pregunta sobre tu salud…", + "coachIntroBody": "Pregunta sobre cualquier cosa que mida la app, y puede registrar comida, agua, entrenamientos, dosis y cómo te sentiste — siempre preguntando antes.", + "coachKeychainRefused": "El llavero rechazó la clave: {error}", + "coachKeyStillSavedBody": "No se pudo leer del llavero esta vez, algo que ocurre cuando la app se despierta con el teléfono bloqueado.", + "coachKeyStillSavedTitle": "Tu clave sigue guardada", + "coachListModels": "Listar modelos", + "coachLocalDataNote": "Tus preguntas y los datos que lee el coach se quedan en tu propia máquina.", + "coachLocalSub": "En esta red. Nada sale de tu máquina.", + "coachMenuSemantic": "Chats y ajustes de IA", + "coachModelHint": "buscar, o escribir un id", + "coachModelLabel": "Modelo", + "coachModelsFound": "{n, plural, one{{n} modelo. Toca uno.} other{{n} modelos. Toca uno.}}", + "coachNavTitle": "Coach", + "coachNewChat": "Nuevo chat", + "coachNoChatsYet": "Nada aún — esta es tu primera conversación.", + "coachNoDataBody": "El coach responde a partir de tus propios días derivados, y aún no hay ninguno en este dispositivo.", + "coachNoDataTitle": "Aún no hay datos que leer", + "coachNoModelsListed": "Ese endpoint no listó ningún modelo. Escribe uno abajo.", + "coachNotSetUp": "Sin configurar", + "coachNotSetUpBody": "Funciona con un modelo que tú eliges — uno en tu propia máquina, o cualquier proveedor compatible con OpenAI con tu propia clave. Nada pasa por OpenStrap en ningún caso.", + "coachNotSetUpTitle": "El coach no está configurado", + "coachPastChats": "CHATS ANTERIORES", + "coachPickModelFirst": "Elige o escribe un modelo primero.", + "coachSafeWarning": "No se escribe nada hasta que toques abajo.", + "coachSaveIt": "Guardarlo", + "coachSendLabel": "Enviar", + "coachSetupNavSub": "Trae tu propio modelo", + "coachSetupNavTitle": "Ajustes de IA", + "coachSomethingWrong": "Algo salió mal: {error}", + "coachStarterAteYesterday": "¿Qué comí ayer?", + "coachStarterHrvChart": "Grafica mi VFC del último mes", + "coachStarterLogRun": "Corrí 40 minutos esta mañana — regístralo", + "coachStarterLogWater": "Registra 500 ml de agua para hoy", + "coachStarterRecovery": "¿Cómo de recuperado estoy hoy, y por qué?", + "coachStarterSleep": "¿Cómo ha sido mi sueño esta semana?", + "coachTryAgainFix": "Intentar de nuevo", + "coachTryAsking": "Prueba a preguntar", + "coachUntitledChat": "Chat sin título", + "coachWhereModelRuns": "Dónde se ejecuta el modelo", + "coachYourDataYourModel": "TUS DATOS, TU MODELO", + "investigateNerdStatsLabel": "ESTADÍSTICAS TÉCNICAS", + "investigateProvenanceLabel": "Procedencia", + "investigateDayLabel": "Día", + "investigateCoverageLabel": "Cobertura", + "investigateSleepWindowLabel": "Ventana de sueño", + "investigateSourceLabel": "Origen", + "investigateSourceOnDevice": "Registros de la banda · calculado en este teléfono", + "investigateSourceImported": "Importado · {source}", + "investigateAlgoVersionLabel": "Versión del algoritmo", + "investigateWhatHappenedTitle": "Qué pasó ese día", + "investigateWhatHappenedSub": "Sueño, sesiones, comidas y registros en orden cronológico", + "investigateWhichSensorCounted": "Qué sensor contó", + "investigateStrapPedometer": "banda · podómetro de 100 Hz", + "investigateStrapOnChipCounter": "banda · contador integrado", + "investigatePhonePedometer": "teléfono · podómetro", + "investigateDayTotal": "total del día", + "investigateStrapChipReported": "chip de la banda reportó", + "investigateTimeDomain": "Dominio del tiempo", + "investigateRmssd": "RMSSD", + "investigateSdnn": "SDNN", + "investigateSdann": "SDANN", + "investigateSdnnIndex": "Índice SDNN", + "investigatePnn50": "pNN50", + "investigateLnRmssd": "ln RMSSD", + "investigateBaselineRmssd": "Tu RMSSD de referencia", + "investigateStabilityCv": "Estabilidad (CV)", + "investigateFrequencyDomain": "Dominio de la frecuencia", + "investigateUlfPower": "Potencia ULF", + "investigateVlfPower": "Potencia VLF", + "investigateLfPower": "Potencia LF", + "investigateHfPower": "Potencia HF", + "investigateTotalPower": "Potencia total", + "investigateLfHf": "LF / HF", + "investigateLfNormalised": "LF, normalizada", + "investigateHfNormalised": "HF, normalizada", + "investigateHfGated": "HF filtrada", + "investigateYes": "sí", + "investigateNo": "no", + "investigateNoFrequencySpectrum": "Sin espectro de frecuencia para esta noche", + "investigateRecordingTooShort": "La grabación fue demasiado corta para resolver las bandas.", + "investigateNonLinear": "No lineal", + "investigateSd1Sleep": "SD1, sueño", + "investigateSd2Sleep": "SD2, sueño", + "investigateSd124h": "SD1, 24 h", + "investigateSd224h": "SD2, 24 h", + "investigateSd1Sd224h": "SD1 / SD2, 24 h", + "investigateSuccessiveIntervalsOver70ms": "Intervalos sucesivos superiores a 70 ms", + "investigateIrregularRhythmFlagSleep": "Aviso de ritmo irregular, sueño", + "investigateIrregularRhythmFlag24h": "Aviso de ritmo irregular, 24 h", + "investigateFlagRaised": "activado", + "investigateFlagClear": "sin activar", + "investigateDecelerationCapacity": "Capacidad de desaceleración", + "investigateAccelerationCapacity": "Capacidad de aceleración", + "investigateDcAnchors": "Anclas de DC", + "investigateSignalQuality": "Calidad de la señal", + "investigateBeatsAnalysed": "Latidos analizados", + "investigateBeatsAnalysed24h": "Latidos analizados, 24 h", + "investigateNoShapeForNight": "Sin forma para esta noche", + "investigateTooFewBeatsToBin": "La noche tuvo muy pocos latidos limpios para agrupar.", + "investigateShapeOfTheNight": "Forma de la noche", + "investigateBinRmssd": "RMSSD por bloque", + "investigateSamplingRange": "Rango de muestreo", + "investigateShapeFootnote": "{drawn} de {total} bloques tuvieron suficientes latidos para leerse; el resto son huecos, no ceros. El par exterior es la dispersión de muestreo propia del estimador, no un rango en el que estuviste. Esto describe la noche y no puede explicarla — un primer tercio bajo es igualmente consistente con alcohol, una comida tardía, entrenamiento tardío, una habitación cálida, el inicio de una enfermedad o nada en absoluto.", + "investigateNightShape": "Forma de la noche", + "investigateBinWidth": "Ancho del bloque", + "investigateBinsRead": "Bloques leídos", + "investigateFirstThird": "Primer tercio", + "investigateLastThird": "Último tercio", + "investigateLastThirdOverFirst": "Último tercio ÷ primero", + "investigate29DaysAgo": "Hace 29 días", + "investigateToday": "Hoy", + "investigateDcFootnoteWithBeats": "Solo tus propias noches — no existe un rango de referencia para los latidos de pulso. La calidad de la señal cambia esta línea de una noche a otra, y anoche fueron {beats} latidos.", + "investigateDcFootnote": "Solo tus propias noches — no existe un rango de referencia para los latidos de pulso. La calidad de la señal cambia esta línea de una noche a otra.", + "investigateIrregularRhythmScreen": "Detección de ritmo irregular", + "investigateOneSquarePerDay": "un cuadrado por día", + "investigate12WeeksAgo": "Hace 12 semanas", + "investigateThisWeek": "Esta semana", + "investigateScreenRan": "Detección realizada", + "investigateRhythmStripFootnote": "Se ejecutó en {ran} {ran, plural, one{día} other{días}}, activó su aviso en {raised}. Un cuadrado con contorno es un día en que no se ejecutó. Una franja limpia no es un resultado negativo: es una detección basada en el ritmo del pulso, y no puede distinguir un latido ectópico de uno perdido ni de un movimiento de la banda en tu muñeca.", + "investigateNoRestingBreathingRate": "Sin frecuencia respiratoria en reposo fuera del sueño", + "investigateNoRestingBreathingRateBody": "Esto solo lee la respiración en tramos de tres minutos en los que la banda te vio casi completamente quieto, fuera de la ventana de sueño. La mayoría de los días no tienen ninguno — un día sin ninguno es un día en que te moviste, no un día en que algo salió mal.", + "investigateBreathingAtRestAwake": "Respiración en reposo, despierto", + "investigateStillStretchesOutsideSleep": "Tramos quietos fuera del sueño", + "investigateLowest": "Más bajo", + "investigateNextLowest": "Siguiente más bajo", + "investigateHighestOfThem": "El más alto de ellos", + "investigateFloorNotRateBody": "Un mínimo, no una tasa para el día. Solo pueden leerse los tramos en que estuviste casi completamente quieto, así que estos son los minutos más quietos que la banda vio fuera de tu sueño — nada aquí describe el resto de tu día, y la respiración mientras te mueves no puede recuperarse a partir del ritmo de los latidos.", + "investigateCycleScreenDidNotRun": "La detección de ciclos no se ejecutó esta noche", + "investigateNotEnoughCleanBeats": "No hay suficientes latidos limpios para ejecutarla.", + "investigateHeartRateCycles": "Ciclos de frecuencia cardíaca", + "investigateCyclesCounted": "Ciclos contados", + "investigateObservedHoursAnalysed": "Horas observadas analizadas", + "investigateCyclesPerObservedHour": "Ciclos por hora observada", + "investigateMeanCycleLength": "Duración media del ciclo", + "investigateMeanDipDepth": "Profundidad media de la caída", + "investigateCycleLengthQuartiles": "Duración del ciclo, cuartiles", + "investigateDipDepthQuartiles": "Profundidad de la caída, cuartiles", + "investigateNotEnoughNightsAcross": "No hay noches suficientes para la vista comparativa", + "investigateNeedsSeveralNights": "Esto requiere varias noches con algunas horas observadas cada una.", + "investigateDroppedIrregular": "{count, plural, one{# noche excluida porque la detección de ritmo irregular la marcó} other{# noches excluidas porque la detección de ritmo irregular las marcó}}", + "investigateDroppedThin": "{count, plural, one{# noche excluida por muy pocas horas observadas} other{# noches excluidas por muy pocas horas observadas}}", + "investigateAcrossNOwnNights": "EN {n} DE TUS PROPIAS NOCHES", + "investigateCvhrAboveUsual": "En tus noches más recientes, el ciclo de frecuencia cardíaca que mide esta detección ha estado más alto que en las {n} noches que lo respaldan.", + "investigateCvhrInsideUsual": "En tus noches más recientes, el ciclo de frecuencia cardíaca que mide esta detección se ha mantenido dentro del rango de las {n} noches que lo respaldan.", + "investigateCvhrExplainer": "Es un patrón en tu pulso, no una medición de tu respiración, y no es una prueba de nada. El mismo ciclo se produce por un ritmo irregular, por estar en altitud y por cualquier noche entrecortada — y los betabloqueantes, la diabetes y los trastornos nerviosos lo atenúan, por lo que una respiración realmente alterada muchas veces no deja ninguna huella aquí.", + "investigateCvhrNotNegativeResult": "Así que nada de esto es un resultado negativo ni descarta nada, y ninguno de estos datos dice nada sobre una noche en particular — el conteo de una sola noche cambia por docenas de razones por sí solo.", + "investigateCvhrSeeClinicianIfSymptoms": "Si roncas, te despiertas sin sentirte descansado, o alguien te ha visto dejar de respirar mientras duermes, un médico puede hacerte una prueba adecuada.", + "investigateStageMinutesAsCounted": "Minutos por etapa, tal como se contaron", + "investigateLight": "Ligero", + "investigateDeep": "Profundo", + "investigateRem": "REM", + "investigateAwake": "Despierto", + "investigateTotalSleep": "Sueño total", + "investigateSegmentationConfidence": "Confianza de segmentación", + "investigateNotPublished": "no publicada", + "investigateNothingComputedForKey": "No se calculó nada para esta clave", + "investigateNoStoredSeries": "Sin serie almacenada", + "investigateNothingStoredYet": "Aún no hay nada guardado para {metric}.", + "investigateSeries": "Serie", + "investigateDaysDerived": "Días calculados", + "investigateLatest": "Más reciente", + "investigateMean": "Media", + "investigateMedian": "Mediana", + "investigateSd": "DE", + "investigateMin": "Mín", + "investigateMax": "Máx", + "investigateUnit": "Unidad", + "investigateUnitless": "sin unidad", + "investigateStorage": "Almacenamiento", + "investigateOneValuePerDerivedDay": "un valor por día calculado", + "investigateMethodLabel": "MÉTODO", + "investigateNotDocumented": "No documentado.", + "calmBreathingResonanceLabel": "Resonancia", + "calmBreathingResonanceDescription": "Inhala y exhala uniformemente a unas {rate} respiraciones por minuto. La única con puntuación de coherencia.", + "calmBreathingCloseBreathing": "Cerrar respiración", + "calmBreathingFinishNow": "Terminar ahora", + "calmBreathingStop": "Detener", + "calmBreathingEndSession": "Finalizar sesión", + "calmBreathingBegin": "Comenzar", + "calmBreathingTakeABreath": "Respira hondo.", + "calmBreathingRingLeads": "El anillo marca el ritmo. Deja el teléfono.", + "calmBreathingScoredPill": "Con puntuación", + "calmBreathingHowLong": "Duración", + "calmBreathingMinutesSemantic": "{m, plural, one{# minuto} other{# minutos}}", + "calmBreathingMinutesAbbrev": "{m} min", + "calmBreathingYourOwnPace": "Tu propio ritmo", + "calmBreathingWindowRowSemantic": "Medir antes y después, añade cuatro minutos", + "calmBreathingMeasureBeforeAfter": "Medir antes y después · añade 4 min", + "calmBreathingNeedsBandBeatTiming": "Necesita la banda puesta — la comparación se hace a partir del ritmo de los latidos.", + "calmBreathingFindYourPace": "Encuentra el ritmo que sigue tu corazón", + "calmBreathingSweepIntro": "Seis minutos: {rates} respiraciones por minuto, dos minutos cada una. Se necesitan dos sesiones que coincidan antes de que algo cambie.", + "calmBreathingSweepAgreed": "Dos sesiones coincidieron en {rate} respiraciones por minuto, y Resonancia está ajustada a ese ritmo. Repítelo para comprobarlo.", + "calmBreathingPaceOfRate": "RITMO {block} DE {total} · {rate} RESPIRACIONES POR MINUTO", + "calmBreathingOfClock": "de {clock}", + "calmBreathingNoScoreForSession": "Sin puntuación de coherencia para esta sesión", + "calmBreathingScoringNeedsBand": "La puntuación necesita el ritmo de los latidos de la banda. No está conectada, así que esta sesión marca el ritmo pero no se guarda.", + "calmBreathingBeforeLabel": "ANTES", + "calmBreathingAfterLabel": "DESPUÉS", + "calmBreathingSitStill": "Siéntate quieto un momento.", + "calmBreathingStaySitting": "Sigue sentado.", + "calmBreathingNothingPacingScored": "Respira como lo harías normalmente. Nada está marcando el ritmo y nada se está puntuando.", + "calmBreathingPatternNotScored": "{pattern} no tiene puntuación. Resonancia es la única ajustada al ritmo para el que se creó la puntuación.", + "calmBreathingTooFewBeatTimings": "Muy pocos tiempos de latido limpios durante la sesión para puntuarla.", + "calmBreathingThatIsDone": "Listo.", + "calmBreathingCardiacCoherence": "Coherencia cardíaca", + "calmBreathingHowStronglyFollowedPace": "CON QUÉ INTENSIDAD TU FRECUENCIA CARDÍACA SIGUIÓ EL RITMO", + "calmBreathingStoppedThere": "Detenido ahí.", + "calmBreathingHowStronglyEachPace": "CON QUÉ INTENSIDAD TU FRECUENCIA CARDÍACA SIGUIÓ CADA RITMO", + "calmBreathingBreathsAMinute": "{rate} respiraciones por minuto", + "calmBreathingNotReached": "no alcanzado", + "calmBreathingTooFewCleanBeats": "muy pocos latidos limpios", + "calmBreathingRankingExplainer": "Una clasificación de tres ritmos en una sola sesión. Los bloques se ejecutan uno tras otro, así que cada ritmo se mide mientras aún te estás asentando del anterior. Indica qué ritmo siguió tu corazón con más fuerza, y nada más.", + "calmBreathingVerdictAborted": "Te detuviste a mitad de camino, así que no había nada que comparar. Nada ha cambiado.", + "calmBreathingVerdictCouldNotScore": "Al menos un ritmo no pudo puntuarse, así que no hay nada que clasificar. Nada ha cambiado.", + "calmBreathingVerdictTied": "Dos de los ritmos obtuvieron la misma puntuación, así que esta sesión no puede distinguirlos. Nada ha cambiado.", + "calmBreathingVerdictConfirmed": "De los ritmos probados, {w} dio tu respuesta más fuerte — y ya son dos sesiones seguidas. Resonancia está ajustada a ese ritmo.", + "calmBreathingVerdictFirstWin": "De los ritmos probados, {w} dio tu respuesta más fuerte. Nada está fijado todavía: el ritmo solo cambia cuando dos sesiones eligen el mismo.", + "breathPatternBoxName": "Caja", + "breathPatternBoxDesc": "Cuatro tiempos en cada dirección, con pausas incluidas. Estabiliza cuando tu mente va a mil.", + "breathPattern478Name": "4-7-8", + "breathPattern478Desc": "Una pausa larga y una exhalación aún más larga. Se usa normalmente para conciliar el sueño.", + "breathPatternExtendedExhaleName": "Exhalación larga", + "breathPatternExtendedExhaleDesc": "Exhala el doble de tiempo que inhalas. Sin pausas, así que es fácil mantenerlo un rato.", + "breathPhaseInhale": "Inhala", + "breathPhaseHold": "Mantén", + "breathPhaseExhale": "Exhala", + "breathPhaseWork": "Trabajo", + "breathPhaseRest": "Descanso", + "metricDetailToday": "Hoy", + "metricDetailRange7Days": "7 días", + "metricDetailRange30Days": "30 días", + "metricDetailRange6Months": "6 meses", + "metricDetailRangeYear": "Año", + "metricDetailLockedNote": "{label} necesita {needed} días de historial. Tienes {have}.", + "metricDetailNotShownTitle": "No se muestra como tendencia", + "metricDetailNothingRecordedToday": "Nada registrado hoy", + "metricDetailNoHistoryYet": "Aún no hay historial de {metric}", + "metricDetailNoValueYet": "Hoy aún no ha producido un valor.", + "metricDetailNoValueYetWiderRanges": "Hoy aún no ha producido un valor. Los rangos más amplios de arriba tienen los días que sí lo hicieron.", + "metricDetailNoValueInWindow": "Ningún día de este período produjo un valor.", + "metricDetailWearBandFix": "Usa la banda toda la noche para iniciar la serie", + "metricDetailBeatsLinkTitle": "Latidos", + "metricDetailBeatsLinkSub": "Los intervalos de los que está hecha una noche, dibujados", + "metricDetailBreakdownLinkTitle": "Desglose", + "metricDetailBreakdownLinkSub": "Cada tramo de hoy, y qué lo contó", + "metricDetailNerdStatsTitle": "Estadísticas técnicas", + "metricDetailNerdStatsSub": "Las cifras detrás de la imagen", + "metricDetailDailyAverage": "Promedio diario · {count} de {win} días", + "metricDetailLatestReading": "Último {value} {unit} · {asOf}", + "metricDetailAlgoBreakFootnote": "{n, plural, one{La línea punteada es un cambio en cómo se calcularon estos días. Las lecturas a cada lado de ella provienen de versiones diferentes.} other{Las líneas punteadas son cambios en cómo se calcularon estos días. Las lecturas a cada lado de una provienen de versiones diferentes.}}", + "metricDetailDaysAgoLabel": "{n, plural, one{Hace {n} día} other{Hace {n} días}}", + "metricDetailWornChartTitle": "Puesta", + "metricDetailHoursADayUnit": "h al día", + "metricDetailWearFootnote": "{have} de estos {win} días tienen un registro de uso. El resto son huecos en ambos gráficos — la línea de arriba no se traza a través de ellos.", + "metricDetailSlotNoRecord": "{day}, sin registro", + "metricDetailSlotWithValue": "{day}, {value} {unit}", + "metricDetailOpenDay": "Abrir {day}", + "metricDetailNoRecordLabel": "Sin registro", + "metricDetailLowest": "Mínimo", + "metricDetailTypical": "Típico", + "metricDetailHighest": "Máximo", + "metricDetailFromDaysCount": "De {n} de tus propios días.", + "metricDetailPercentileTodayNoBand": "Hoy se ubica en el percentil {ordinal} de tu propio historial.", + "metricDetailPercentileTodayBand": "Hoy se ubica en el percentil {ordinal} de tu propio historial — {band}.", + "metricDetailPercentileFromNoBand": "Tu lectura del {date} se ubica en el percentil {ordinal} de tu propio historial.", + "metricDetailPercentileFromBand": "Tu lectura del {date} se ubica en el percentil {ordinal} de tu propio historial — {band}.", + "metricDetailDaysWithWithout": "{withCount} días con · {withoutCount} sin", + "metricDetailPatternsNotCauses": "Patrones en tus propios registros, no causas.", + "metricDetailChooseDayHelp": "Elegir un día", + "metricDetailPreviousDay": "Día anterior", + "metricDetailNextDay": "Día siguiente", + "metricDetailChooseDayShowing": "Elegir un día. Mostrando {day}", + "metricDetailNormalRangeSection": "Tu rango normal", + "metricDetailWhatMovesItSection": "Qué lo influye", + "cycleRemoveLogTitle": "¿Eliminar {date}?", + "cycleRemoveLogBody": "El día del ciclo, la fase y la próxima fecha prevista se calculan a partir de los días que registras. Solo puedes registrar el día de hoy, así que este no se podrá recuperar.", + "cycleWhatAppliesToYou": "Lo que se aplica a ti", + "cyclePreferNotToSay": "Prefiero no decirlo", + "cyclePreferNotToSayWhy": "La app mantiene la fase desactivada.", + "cycleReproCyclingLabel": "Tengo ciclos naturales", + "cycleReproCyclingWhy": "Cuenta una fase a partir de tus inicios registrados.", + "cycleReproContraceptionLabel": "Anticoncepción hormonal", + "cycleReproContraceptionWhy": "No hay ovulación que contar, así que no hay fase. Los sangrados se siguen registrando.", + "cycleReproNoneLabel": "Embarazada, posparto o sin ciclos", + "cycleReproNoneWhy": "No hay fase ni próxima fecha prevista. Tus datos biométricos se siguen mostrando.", + "cycleReproNotSet": "Sin definir", + "cycleTrackingOffTitle": "El seguimiento del ciclo está desactivado", + "cycleTrackingOffBody": "Permanece en este teléfono.", + "cycleTurnOnTracking": "Activar el seguimiento del ciclo", + "cycleNoPeriodTitle": "Aún no hay ningún período registrado", + "cycleNoPeriodBody": "Se calcula a partir de los días que registras.", + "cycleLogPeriodButton": "Registrar hoy el inicio del período", + "cycleLogKindStart": "INICIO", + "cycleLogKindEnd": "FIN", + "cycleAcrossCyclesTitle": "A lo largo de tus ciclos", + "cycleUnitCompleteCycle": "ciclo completo", + "cycleUnitCompleteCycles": "ciclos completos", + "cycleOpenAction": "Abrir", + "cycleWhatYouNoticedToday": "Lo que notaste hoy", + "cycleLoggedDays": "Días registrados", + "cycleReproOptionalHint": "Opcional. Hasta que lo indiques, la app mantiene la fase desactivada.", + "cycleReproPrivateHint": "Solo tú y este teléfono. Nunca se exporta.", + "cycleTurnOffTracking": "Desactivar el seguimiento del ciclo", + "cycleDayInThisCycle": "DÍA DE ESTE CICLO", + "cycleCountedFromLastStart": "contado desde tu último inicio registrado", + "cycleOfAboutDays": "de unos {days}", + "cyclePhaseMenstrual": "Menstrual", + "cyclePhaseFollicular": "Folicular", + "cyclePhaseOvulation": "Ventana de ovulación", + "cyclePhaseLuteal": "Lútea", + "cycleNextPeriodBetween": "PRÓXIMO PERÍODO, PREVISTO ENTRE", + "cycleNextPeriodAround": "PRÓXIMO PERÍODO, PREVISTO ALREDEDOR DE", + "cycleFromOneMeasuredGap": "a partir de tu único intervalo medido, que no puede mostrar cuánto varía tu propio ciclo", + "cyclePastEndOfIt": "{days} días después del final · ", + "cycleInsideItNow": "ahora estás dentro de él · ", + "cycleInDaysRange": "en {lo}–{hi} días · ", + "cycleHalfOfMeasuredGaps": "la mitad de tus {n} intervalos medidos cayó dentro de un rango tan amplio como este", + "cycleLeadDaysLate": "{days} días de retraso · ", + "cycleLeadToday": "hoy · ", + "cycleLeadInDays": "en {days} días · ", + "cycleWhatYouUsuallyNotice": "Lo que sueles notar", + "cycleSymptomShapeSummary": "Cuatro números, uno por cada semana del ciclo, contados a partir de tus propios inicios registrados. Registraste algo en {daysByWeek} días de cada semana a lo largo de {cycles} ciclos: esos son los únicos días que se tienen en cuenta aquí.", + "cycleRemoveLoggedDay": "Eliminar {date}", + "cycleSymptomCramps": "cólicos", + "cycleSymptomHeadache": "dolor de cabeza", + "cycleSymptomBloating": "hinchazón", + "cycleSymptomFatigue": "fatiga", + "cycleSymptomLowMood": "ánimo bajo", + "cycleSymptomAcne": "acné", + "cycleSymptomTenderBreasts": "sensibilidad en los senos", + "cycleSymptomNausea": "náuseas", + "cycleThisCycle": "Este ciclo", + "cycleByDayOfYourCycle": "Por día de tu ciclo", + "cycleHowLongCyclesBeen": "La duración de tus ciclos", + "cycleRestingHeartRate": "Frecuencia cardíaca en reposo", + "cycleUnitBpm": "lpm", + "cycleHrvRmssdTitle": "VFC (RMSSD)", + "cycleUnitMs": "ms", + "cycleNotEnoughDescribeDayTitle": "Aún no hay suficientes ciclos para describir un día del ciclo", + "cycleNotEnoughDescribeDayBody": "Cada punto aquí es el valor medio del mismo día en dos o más de tus propios ciclos. Ninguno tiene aún dos ciclos detrás.", + "cycleOwnPastCyclesDescribed": "Tus propios ciclos pasados, descritos. Los días que solo alcanzó un ciclo se dejan vacíos en lugar de dibujarse: una sola noche no es una media. Describe lo que ocurrió, no lo que ocurrirá.", + "cycleDayOneLabel": "Día 1", + "cycleDayNLabel": "Día {n}", + "cycleMiddleOfNCycles": "Valor medio de {n} ciclos en cada día.", + "cycleMiddleOfRangeCycles": "Valor medio de entre {lo} y {hi} ciclos en cada día.", + "cycleNotEnoughCompareTitle": "Aún no hay suficientes ciclos para comparar un día consigo mismo", + "cycleCompareBodyGeneric": "Esto pone el día de hoy junto al mismo día de tus ciclos anteriores. Se necesitan tres que hayan llegado tan lejos.", + "cycleCompareBodyWithDay": "Esto pone el día de hoy junto al mismo día de tus ciclos anteriores. Se necesitan tres que hayan llegado al día {day}.", + "cycleNightOfLabel": "NOCHE DEL {date}", + "cycleComparisonNotCorrection": "Una comparación, no una corrección. Nada en tu preparación física se ha reescalado por esto, y nada aquí es una instrucción de entrenamiento.", + "cycleCompareHrvLabel": "VFC", + "cycleCompareLine": "{label} {z1} frente a tus últimas 3 semanas, {z2} frente a tus últimos {n} días como el {cycleDay}.", + "cycleLengthsTitle": "La duración de tus ciclos frente a un rango publicado", + "cycleLengthsBody": "Desactivado a menos que lo solicites. Muestra los días entre tus propios inicios registrados junto al rango publicado para un ciclo adulto, y no dice nada más sobre ellos.", + "cycleShowIt": "Mostrarlo", + "cycleNotEnoughLoggedTitle": "Aún no hay suficientes ciclos registrados", + "cycleNotEnoughLoggedBody": "Esto necesita un largo recorrido: llevas {n} de {total} intervalos, lo que equivale a más o menos un año registrando cada inicio.", + "cycleGapTitle": "Hay un hueco en tus inicios registrados", + "cycleGapBody": "Uno de ellos ocurre más de {days} días después del anterior. Un inicio que nunca registraste y un ciclo que realmente duró tanto se ven igual desde aquí, así que no se dibuja nada.", + "cycleDaysBetweenStarts": "Días entre tus inicios registrados", + "cycleUnitDays": "días", + "cycleLegendYourCycles": "Tus ciclos", + "cycleLegendPublishedRange": "Rango publicado", + "cycleTwoLinesFootnote": "Las dos líneas están en {low} y {high} días.", + "cycleLengthChangesReasons": "La duración del ciclo cambia por muchas razones — tiroides, estrés, cambios de peso, anticoncepción, SOP y otras. Estos son tus propios datos registrados junto a un rango publicado. Es un motivo para consultar a un médico, no una respuesta de uno.", + "cycleHideLengths": "Ocultar la duración de los ciclos", + "cycleDescriptiveOnly": "Solo descriptivo.", + "cycleNotEnoughDerivedNights": "Aún no hay suficientes noches derivadas en este ciclo", + "cycleMdcNoteInsideSpread": " Todos los días mostrados aquí están dentro de tu propia variación de noche a noche: la mayor diferencia entre dos de ellos es {s}, y {n} es el cambio más pequeño que esto puede distinguir del ruido. Una forma, no un cambio.", + "cycleMdcNoteVaries": " Tus noches varían por {n} por sí solas, así que los días más próximos entre sí que eso no se distinguen. La mayor diferencia aquí es {s}.", + "healthTabOverview": "Resumen", + "healthTabExplore": "Explorar", + "healthTabTrends": "Tendencias", + "healthTabVitals": "Vitales", + "healthTabLabs": "Laboratorio", + "healthTitle": "Salud", + "healthCouldNotRead": "No se pudieron leer tus {what}", + "healthReadFailedBody": "No se pudieron cargar los registros guardados. No se eliminó nada: fue un error de lectura.", + "healthTryAgain": "Intentar de nuevo", + "healthWhatVitals": "vitales", + "healthWhatLabResults": "resultados de laboratorio", + "healthMeasuresUnit": "medidas", + "healthRowRestingHr": "Frecuencia cardíaca en reposo", + "healthRowHrv": "VFC", + "healthRowSleep": "Sueño", + "healthRowStress": "Estrés", + "healthRowRespRate": "Frecuencia respiratoria", + "healthSubOvernight": "Durante la noche", + "healthSubRmssdAsleep": "RMSSD, dormido", + "healthSubLastNight": "Anoche", + "healthSubAsleep": "Dormido", + "healthNoMetric": "Sin {name}", + "healthWhyReadFromSleep": "Se obtiene del sueño, y no se puntuó ninguna noche.", + "healthWhyReadOnlyFromSleep": "Solo se obtiene del sueño, y no se puntuó ninguna noche.", + "healthWhySleepNotLongEnough": "No se registró un período de sueño lo bastante largo para puntuar.", + "healthWhyReadFromNight": "Se obtiene de la noche, y no se puntuó ninguna noche.", + "healthWhyNoReadingLastNight": "No hay lectura de anoche.", + "healthIllnessRedTitle": "Varias noches seguidas se apartan de tu valor normal", + "healthIllnessLastNightTitle": "Anoche estuvo fuera de tu rango normal", + "healthIllnessDayTitle": "{day} estuvo fuera de tu rango normal", + "healthIllnessBodyNoZ": "Tu frecuencia cardíaca en reposo nocturna se ha mantenido por encima de tu propia línea base. Esto vigila una sola señal. Nombra un patrón, no una causa.", + "healthIllnessBodyWithZ": "Tu frecuencia cardíaca en reposo nocturna se ha mantenido por encima de tu propia línea base; esa noche estuvo {z} desviaciones estándar por {direction} de ella. Esto vigila una sola señal. Nombra un patrón, no una causa.", + "healthDirectionAbove": "encima", + "healthDirectionBelow": "debajo", + "healthIllnessAdvice": "Vale la pena tenerlo en cuenta si continúa más de un par de días.", + "healthObservationsTitle": "Observaciones", + "healthSeeAll": "Ver todo", + "healthNapsTitle": "Siestas", + "healthNoNapReading": "Sin lectura de siesta", + "healthNoNapReadingFor": "Sin lectura de siesta para {day}", + "healthNapsBody": "Las siestas se obtienen de la misma grabación segundo a segundo que el resto del día, y este día no tiene suficiente de ella.", + "healthDaytimeSleep": "Sueño diurno", + "healthValueNone": "Ninguna", + "healthNoneDetectedOn": "No se detectó ninguna · {day}", + "healthNapCountLabel": "{n, plural, one{{n} siesta} other{{n} siestas}}", + "healthAddOrCorrect": "Añadir o corregir", + "healthNoTrendYet": "Aún no hay tendencia de {label}", + "healthZeroDaysStored": "0 días guardados.", + "healthVsDayAverage": "frente a tu media de {days} días", + "healthAsOf": " · a partir del {date}", + "healthNoBaseline": "sin línea base", + "healthFirstReadings": "primeras lecturas", + "healthTimeAsleep": "Tiempo dormido", + "healthVsNeed": "frente a tu necesidad de {need}", + "healthBodyClockTitle": "Reloj biológico", + "healthChronotypeJetlagRegularity": "Cronotipo, jet lag y regularidad", + "healthChronotypeLabel": "CRONOTIPO", + "healthSocialJetlagLabel": "JET LAG SOCIAL", + "healthRegularityLabel": "REGULARIDAD", + "healthConsistencyTitle": "Constancia", + "healthDaysWithRecord": "Días con un registro calculado en los últimos 30 días", + "healthToday": "Hoy", + "healthRowHeartRate": "Frecuencia cardíaca", + "healthRowSkinTemp": "Temperatura de la piel", + "healthVsOwnNights": "frente a tus propias noches", + "healthVsOwnNightsOn": "frente a tus propias noches · {day}", + "healthRowWearTime": "Tiempo de uso", + "healthTheDay": "el día", + "healthCoverageOf": "{pct}% de {day}", + "healthNothingMeasuredDay": "No se midió nada este día", + "healthNoBandRecordings": "No llegaron grabaciones de la banda para este día.", + "healthSyncTheBand": "Sincronizar la banda", + "healthDeepDivesTitle": "Análisis en profundidad", + "healthHeartRateVariability": "Variabilidad de la frecuencia cardíaca", + "healthTimeFrequencyNonLinear": "Tiempo, frecuencia y no lineal", + "healthRmssdOfLastNights": "RMSSD, {have} de las últimas {days} noches", + "healthNightsAgo": "hace {n} noches", + "healthOneNightNotTrend": "Una noche todavía no es una tendencia", + "healthMeasuresWithHistory": "Medidas con historial guardado en este dispositivo", + "healthEachOneOpens": "Cada una abre su gráfico, tu propio rango y cómo se calcula.", + "healthCatHeartRhythm": "Corazón y ritmo", + "healthCatBreathing": "Respiración", + "healthCatMovementLoad": "Movimiento y carga", + "healthCatBodyWear": "Cuerpo y uso", + "healthBlurbRestingHr": "El ritmo sostenido más bajo de la noche", + "healthBlurbHrv": "RMSSD durante la ventana de sueño más limpia", + "healthBlurbHrvCv": "Cuánto varía eso de una noche a otra", + "healthBlurbLfHf": "Dónde se concentra la potencia del ritmo cardíaco entre frecuencias", + "healthBlurbDip": "Cuánto baja tu frecuencia cardíaca mientras duermes", + "healthBlurbHrr": "Cuán rápido baja en el minuto después de un esfuerzo", + "healthBlurbSleep": "Tiempo dormido, calculado a partir del movimiento y el ritmo cardíaco", + "healthBlurbEfficiency": "Tiempo dormido como parte del tiempo en la cama", + "healthBlurbDeep": "Estabilidad de la frecuencia cardíaca dentro del sueño NREM", + "healthBlurbRem": "Determinado a partir de la variabilidad del ritmo y el movimiento", + "healthBlurbNapMin": "Sueño detectado fuera de la noche principal", + "healthBlurbRespRate": "Respiraciones por minuto, obtenidas del ritmo cardíaco", + "healthBlurbBrv": "Cuánto varía ese ritmo a lo largo de la noche", + "healthBlurbSteps": "Contados por un podómetro, nunca estimados", + "healthBlurbActiveMin": "Minutos de volumen de movimiento, no de desplazamiento", + "healthBlurbCalories": "Energía activa calculada a partir de la frecuencia cardíaca y tu perfil", + "healthBlurbStrain": "Carga cardiovascular a lo largo del día, en una escala de 0 a 21", + "healthBlurbTrimp": "Tiempo en cada zona, ponderado por su costo", + "healthBlurbSkinTemp": "Distancia respecto a tus propias noches recientes", + "healthBlurbWear": "Minutos con un registro de la banda presente", + "healthNothingMeasuredHere": "Todavía no se ha medido nada aquí", + "healthNotMeasuredYet": "Aún no medido", + "healthNoDayProduced": "Ningún día en este dispositivo ha producido uno todavía.", + "healthNoLabResults": "Sin resultados de laboratorio", + "healthNoLabResultsBody": "Nada registrado. Todo lo que añadas aquí permanece en este dispositivo, y todo lo que elimines desaparece de él.", + "healthLastPanel": "Último panel {date} · registrado manualmente", + "healthMarkersYouNamed": "Marcadores que nombraste", + "healthAddAResult": "Añadir un resultado", + "healthRangesDifferByLab": "Los rangos varían según el laboratorio. Usa el de tu informe.", + "healthRemoveMarkerFrom": "Eliminar {marker} del {date}", + "healthNoReferenceInterval": "Sin intervalo de referencia · {date}", + "healthTypicalRange": "Habitual {low}–{high} · {date}", + "healthRemoveLabelFrom": "¿Eliminar {label} del {date}?", + "healthRemoveLabBody": "Los {value} {unit} que registraste para esa extracción. Salen de este dispositivo y no hay forma de deshacerlo.", + "healthRemoveLabOlderNote": " Tu extracción del {date} permanece, y se muestra aquí en su lugar.", + "healthRemovedNoneLeft": "Se eliminó {label} del {date}. No quedan resultados de {label}.", + "healthRemovedShowingOlder": "Se eliminó {label} del {date}. Ahora se muestra tu extracción del {older}.", + "healthRemoveTheMarker": "Eliminar el marcador {label}", + "healthNothingLoggedUnderIt": "Nada registrado bajo este", + "healthResultsCount": "{n, plural, one{{n} resultado · {unit}} other{{n} resultados · {unit}}}", + "healthStillHoldsResults": "{count, plural, one{{label} todavía contiene {count} resultado. Elimínalo primero — el marcador es lo que lo etiqueta.} other{{label} todavía contiene {count} resultados. Elimínalos primero — el marcador es lo que los etiqueta.}}", + "healthRemoveMarkerQ": "¿Eliminar {label}?", + "healthRemoveMarkerBody": "Sale de la lista de marcadores, así que ya no podrás registrarlo. No se lleva ninguna medición consigo — no tienes resultados bajo este.", + "healthMarkerLabel": "Marcador", + "healthValueUnit": "Valor ({unit})", + "healthDateDrawn": "Fecha de extracción (AAAA-MM-DD)", + "healthValueMustBeNumber": "El valor debe ser un número por sí solo, sin la unidad. No se guardó nada.", + "healthDateFormatError": "La fecha debe tener el formato AAAA-MM-DD. No se guardó nada.", + "healthCouldNotSaveIt": "No se pudo guardar: {error}", + "homeStepSensorStrapPhone": "Correa + teléfono", + "homeStepSensorStrap": "Correa", + "homeStepSensorPhone": "Teléfono", + "homeOvernightBuilding": "Los datos de anoche aún se están procesando.", + "homeOvernightNothingYet": "Todavía no ha llegado nada de anoche a la app.", + "homeMonthJanuary": "enero", + "homeMonthFebruary": "febrero", + "homeMonthMarch": "marzo", + "homeMonthApril": "abril", + "homeMonthMay": "mayo", + "homeMonthJune": "junio", + "homeMonthJuly": "julio", + "homeMonthAugust": "agosto", + "homeMonthSeptember": "septiembre", + "homeMonthOctober": "octubre", + "homeMonthNovember": "noviembre", + "homeMonthDecember": "diciembre", + "homeMonthJanuaryShort": "ene", + "homeMonthFebruaryShort": "feb", + "homeMonthMarchShort": "mar", + "homeMonthAprilShort": "abr", + "homeMonthMayShort": "may", + "homeMonthJuneShort": "jun", + "homeMonthJulyShort": "jul", + "homeMonthAugustShort": "ago", + "homeMonthSeptemberShort": "sep", + "homeMonthOctoberShort": "oct", + "homeMonthNovemberShort": "nov", + "homeMonthDecemberShort": "dic", + "homeWeekdayMonday": "lunes", + "homeWeekdayTuesday": "martes", + "homeWeekdayWednesday": "miércoles", + "homeWeekdayThursday": "jueves", + "homeWeekdayFriday": "viernes", + "homeWeekdaySaturday": "sábado", + "homeWeekdaySunday": "domingo", + "homeReadinessNotScored": "Sin puntuación", + "homeReadinessGoodToGo": "Listo para hoy", + "homeReadinessSteady": "Estable", + "homeReadinessTakeItEasy": "Tómatelo con calma", + "homeReadinessRestToday": "Descansa hoy", + "homeDriverHrv": "VFC", + "homeDriverRhr": "Frecuencia cardíaca en reposo", + "homeDriverResp": "Frecuencia respiratoria", + "homeDriverTemp": "Temperatura de la piel", + "homeDbRebuiltTitle": "Tu base de datos se reconstruyó para iniciar la app", + "homeDbRebuiltNothingRecovered": "No se pudo recuperar nada.", + "homeDbRebuiltRecovered": "Recuperado: {list}.", + "homeDbRebuiltEmpty": "Vacío: {list}.", + "homeDbRebuiltKept": "El archivo original se conserva en {path}; no se eliminó nada.", + "homeWorkoutHoldTitle": "Todavía hay un entrenamiento en curso", + "homeWorkoutHoldBody": "Hoy está en espera mientras un entrenamiento está activo: la correa sigue grabando, pero los números se calculan al terminar la sesión. Finaliza el entrenamiento con la barra de abajo y hoy se completará; sincronizar no lo hará.", + "homeInsightsRebuildingTitle": "Tus datos entre días se están reconstruyendo", + "homeInsightsRebuildingAlgoVersion": "La forma de calcular esto cambió con la última actualización.", + "homeInsightsStaleOverWeek": "El último resumen se generó hace más de una semana, lo cual es demasiado antiguo para confiar en él.", + "homeInsightsStaleOnDay": "El último resumen se generó el {day}, lo cual es demasiado antiguo para confiar en él.", + "homeInsightsNoVersionStamp": "El resumen guardado no tiene marca de versión.", + "homeSyncBand": "Sincronizar la correa", + "homeWhyLabel": "¿Por qué?", + "homeCalibrating": "Calibrando", + "homeCalibratingNights": "{have} de {need} noches", + "homeCalibratingDays": "{have} de {need} días", + "homeGapNoReason": "No hay ningún dato registrado que explique por qué falta esto.", + "homeRingRecovery": "Recuperación", + "homeRingStrain": "Esfuerzo", + "homeRingSleep": "Sueño", + "homeRingNoStrain": "Sin esfuerzo", + "homeRingNoSleep": "Sin sueño", + "homeSleepGapFallback": "No se registró ninguna noche lo bastante larga para puntuar.", + "homeStrainOf21": "de 21", + "homeSleepNoTarget": "Aún sin objetivo", + "homeOfSpan": "de {duration}", + "homeLoadFailedTitle": "No se pudo leer el día de hoy", + "homeLoadFailedBody": "No se pudo cargar el día guardado. No se eliminó nada; fue un error de lectura, no datos perdidos.", + "homeTryAgain": "Intentar de nuevo", + "homeNothingDerivedTitle": "Aún no se ha calculado nada", + "homeNothingDerivedBody": "Todavía no se ha procesado ninguna grabación de la correa.", + "homeAskCoach": "Preguntar al entrenador", + "homeProfileSettings": "Perfil y ajustes", + "homeNothingTodayTitle": "Nada registrado hoy", + "homeNothingTodayBody": "La última noche que la app puntuó fue el {day}. No ha llegado nada desde entonces.", + "homeReadinessNotScoredTitle": "La recuperación no tiene puntuación hoy", + "homeReadinessNeedBody": "{need} para saber qué es lo normal en tu caso.", + "homeReadinessNoReason": "No hay ningún dato registrado que lo explique.", + "homeSeeWhatWasMissing": "Ver qué faltaba", + "homeAtAGlance": "De un vistazo", + "homeTodaysPlan": "Plan de hoy", + "homeBreakdownTitle": "Desglose de tu día", + "homeBreakdownSubtitle": "Hora por hora", + "homeIllnessRedTitle": "Varias noches seguidas se alejan de tu valor normal", + "homeIllnessAmberSameNight": "Anoche estuvo fuera de tu rango normal", + "homeIllnessAmberOtherNight": "El {day} estuvo fuera de tu rango normal", + "homeIllnessBodyNoZ": "Tu frecuencia cardíaca nocturna en reposo ha estado por encima de tu propio valor de referencia. Esto refleja una sola señal. Indica un patrón, pero no indica una causa.", + "homeIllnessBodyAbove": "Tu frecuencia cardíaca nocturna en reposo ha estado por encima de tu propio valor de referencia; esa noche se situó {z} desviaciones estandarizadas por encima. Esto refleja una sola señal. Indica un patrón, pero no indica una causa.", + "homeIllnessBodyBelow": "Tu frecuencia cardíaca nocturna en reposo ha estado por encima de tu propio valor de referencia; esa noche se situó {z} desviaciones estandarizadas por debajo. Esto refleja una sola señal. Indica un patrón, pero no indica una causa.", + "homeIllnessAdvice": "Vale la pena prestarle atención si continúa varios días.", + "homeHeartRate": "Frecuencia cardíaca", + "homeRestingSub": "En reposo", + "homeNoRestingHr": "Sin frecuencia cardíaca en reposo", + "homeNoRestingHrWhy": "La frecuencia cardíaca en reposo se obtiene del sueño, y no se registró ningún sueño.", + "homeSteps": "Pasos", + "homeStepsNone": "Ninguno", + "homeStepsNotRecorded": "NO REGISTRADO", + "homeStepsPercentGoal": "{pct} % del objetivo", + "homeActiveEnergy": "Energía activa", + "homeCaloriesEstimated": "Estimado", + "homeCaloriesTotal": "{total} en total", + "homeNoEnergyEstimate": "Sin estimación de energía", + "homeStepsLeft": "{left} pasos restantes", + "homeMovement": "Movimiento", + "homeGoalSteps": "Objetivo {goal}", + "homeStepGoalMet": "Objetivo de pasos cumplido", + "homeStrainTargetMet": "Objetivo de esfuerzo cumplido", + "homeAimForStrain": "Apunta a un esfuerzo de {aim}", + "homeTraining": "Entrenamiento", + "homeSleepNeedRow": "{duration} de sueño", + "homeTonight": "Esta noche", + "homeNeed": "Falta calcular", + "homeBedTime": "Acostarse a las {time}", + "homeNoPlanTitle": "Todavía no hay plan para hoy", + "homeNoPlanWhyStale": "Los datos entre días de los que provienen se están reconstruyendo.", + "homeNoPlanWhyNone": "Todavía no se ha establecido ninguno.", + "homeGreetingStillUp": "Aún despierto", + "homeGreetingMorning": "Buenos días", + "homeGreetingAfternoon": "Buenas tardes", + "homeGreetingEvening": "Buenas noches", + "wellnessTitle": "Bienestar", + "wellnessTabMind": "Mente", + "wellnessTabRecovery": "Recuperación", + "wellnessTabHabits": "Hábitos", + "wellnessTabMedication": "Medicación", + "wellnessTabCycle": "Ciclo", + "wellnessStartASitting": "COMENZAR UNA SESIÓN", + "wellnessExercisesNoun": "ejercicios", + "wellnessPickOneAndGo": "Elige uno y empieza", + "wellnessLastMinutes": "Última: {count} min", + "wellnessWriteTheDayDown": "Anota tu día", + "wellnessOpen": "Abrir", + "wellnessStressLastNight": "Estrés anoche", + "wellnessNoStressTitle": "Sin lectura de estrés de anoche", + "wellnessNoStressBody": "El estrés se calcula a partir del ritmo cardíaco mientras descansabas por la noche, y anoche no se obtuvo ninguna lectura.", + "wellnessAutonomicTension": "Tensión autonómica", + "wellnessStressLevelLow": "bajo", + "wellnessStressLevelNormal": "normal", + "wellnessStressLevelElevated": "elevado", + "wellnessStressLevelHigh": "alto", + "wellnessJournalDefaultSubtitle": "Cualquier cosa que quieras recordar sobre hoy", + "wellnessJournalSubtitleShort": "{fields} y una nota", + "wellnessJournalSubtitleLong": "{fields} y {more} más, además de una nota", + "wellnessTurnInBy": "Acuéstate antes de las {time}", + "wellnessDebtBody": "Tienes un déficit de {debt} respecto a tu propia necesidad, y esta noche es de {need}.", + "wellnessSeeWhatLastNightCost": "Mira lo que te costó anoche", + "wellnessWhatChargedAndDrained": "Qué te cargó y qué te agotó", + "wellnessNoDriversTitle": "Aún no hay factores de disposición", + "wellnessNoDriversBody": "Se necesitan suficientes noches para saber qué es lo normal para ti.", + "wellnessSleepNeedTonight": "Necesidad de sueño esta noche", + "wellnessNoSleepNeedTitle": "Aún no hay necesidad de sueño", + "wellnessNoSleepNeedBody": "No hay ningún dato que explique por qué no hay necesidad de sueño para esta noche.", + "wellnessTonightsNeed": "Necesidad de esta noche", + "wellnessSleepDebt": "Deuda de sueño", + "wellnessAddedForStrain": "Añadido por esfuerzo", + "wellnessCreditedFromNaps": "Acreditado por siestas", + "wellnessTargetBedtime": "Hora de acostarse objetivo", + "wellnessTargetWake": "Hora de despertar objetivo", + "wellnessRemoveHabitSemantic": "Eliminar {label}", + "wellnessDaysYouDidIt": "Días que lo cumpliste", + "wellnessAddAHabit": "Añadir un hábito", + "wellnessWhatYouLogTitle": "Lo que registras, frente a tus cifras", + "wellnessWhatYouLogSubtitle": "Dosis, diferencia por hábito y día de la semana", + "wellnessRemoveHabitConfirmTitle": "¿Eliminar {label}?", + "wellnessRemoveHabitConfirmBody": "Dejará de preguntarse. Los días que ya registraste se conservan.", + "wellnessHabitHint": "Caminar después de comer", + "wellnessAlreadyTrack": "Ya estás registrando \"{name}\".", + "wellnessNothingScheduledTitle": "Nada programado", + "wellnessNothingScheduledBody": "Añade lo que tomas y cuándo.", + "wellnessAddAMedication": "Añadir una medicación", + "wellnessNothingDueTodayTitle": "Nada pendiente hoy", + "wellnessNothingDueTodayBody": "Lo que tomas está programado para otros días u horas.", + "wellnessAdherence": "Adherencia", + "wellnessNothingToScoreTitle": "Aún nada que evaluar", + "wellnessNothingToScoreBody": "Todavía no ha vencido ninguna dosis programada.", + "wellnessTakenOfScheduled": "Tomadas, de las programadas en los últimos siete días.", + "wellnessDosesUnit": "dosis", + "wellnessUndoSkipped": "Deshacer omitida", + "wellnessSkippedOnPurpose": "Omitida a propósito", + "wellnessBackToNotTaken": "Vuelve a no tomada.", + "wellnessRecordedAsDecision": "Registrada como una decisión, no un olvido.", + "wellnessWhichDaysDue": "En qué días vence", + "wellnessRemoveMedTitle": "Eliminar {label}", + "wellnessRemoveMedBody": "Deja de estar programada. Las dosis marcadas se conservan.", + "wellnessRemoveMedConfirmTitle": "¿Eliminar {label}?", + "wellnessRemoveMedConfirmBody": "Deja de estar programada y de contar para la adherencia. Las dosis que ya marcaste se conservan.", + "wellnessMedHint": "Vitamina D", + "wellnessNameLabel": "Nombre", + "wellnessAdd": "Añadir", + "wellnessEveryDay": "Todos los días", + "wellnessWeekdays": "Días laborables", + "wellnessWeekends": "Fines de semana", + "wellnessMon": "lun", + "wellnessTue": "mar", + "wellnessWed": "mié", + "wellnessThu": "jue", + "wellnessFri": "vie", + "wellnessSat": "sáb", + "wellnessSun": "dom", + "wellnessWhenYouTakeIt": "Cuándo lo tomas", + "wellnessChangeTheTime": "Cambiar la hora", + "wellnessWhichDays": "QUÉ DÍAS", + "wellnessPickAtLeastOneDay": "Elige al menos un día.", + "wellnessDueDays": "Vence {days}.", + "wellnessMoreForMed": "Más opciones para {label}", + "wellnessMedAtTime": "{label} a las {time}", + "wellnessStateTaken": "tomada", + "wellnessStateSkipped": "omitida", + "wellnessStateNotTaken": "no tomada", + "wellnessStateDueLater": "pendiente", + "wellnessMarkDone": "Marcar como hecho", + "wellnessWhatYouLogScreenTitle": "Lo que registras", + "wellnessNothingSeparatedTitle": "Nada se ha diferenciado aún", + "wellnessNothingSeparatedBody": "Todo lo que registras se compara con tu recuperación, VFC, frecuencia cardíaca en reposo y eficiencia del sueño. Nada ha superado el umbral todavía.", + "wellnessTheDaysYouDidIt": "Los días que lo cumpliste", + "wellnessHowMuchAndWhatFollowed": "Cuánto, y qué siguió", + "wellnessLinkNeverCause": "Una relación en tus propios días, nunca una causa. Los días en que haces algo ya eran, de por sí, ese tipo de día.", + "wellnessWhichDayOfWeek": "Qué día de la semana", + "wellnessHigher": "más alto", + "wellnessLower": "más bajo", + "wellnessHeadlineBinary": "En los {n} días que registraste {field}, {outcome} fue {amount} {direction}", + "wellnessHeadlineNoSlope": "En los {n} días que registraste {field}, más de eso se asoció con un {outcome} {direction}", + "wellnessHeadlineSlope": "En los {n} días que registraste {field}, {outcome} fue {amount} {direction} por {step}", + "wellnessMatchedSameDay": "Comparado con los datos del mismo día.", + "wellnessMatchedNightFollowed": "Comparado con la noche siguiente.", + "wellnessMatchedNightEnded": "Comparado con la noche que terminó esa mañana.", + "wellnessAgainstDaysYouDidNot": "Frente a los {n} días que no lo hiciste", + "wellnessRangeTo": "{lo} a {hi}", + "wellnessRankCorrelation": "Correlación de rangos {rho}{ci}. ", + "wellnessCaffeineCaveat": "Esta es solo tu última cafeína del día: dos tazas y cinco se ven igual aquí, así que \"más tarde\" puede en realidad significar \"más\". Un día largo y estresante produce tanto el café tardío como la mala noche.", + "wellnessHourLater": "hora más tarde", + "wellnessPointUnit": "punto", + "wellnessNotEnoughWeeksTitle": "Aún no hay suficientes semanas", + "wellnessNotEnoughWeeksBody": "Comparar los siete días de la semana requiere al menos ocho semanas de datos, con cinco de cada día de la semana en ellas.", + "wellnessNoDayStandsOutTitle": "Ningún día de la semana destaca", + "wellnessNoDayStandsOutBody": "Ningún día se distingue de los otros seis una vez que se tiene en cuenta que se compararon los siete.", + "wellnessWeekdayHeadline": "{weekday}: la disposición es {delta} {direction} que tu mediana general", + "wellnessWeekdayDetail": "De {n} de ellos. Un día de la semana no es una causa; es un contenedor de lo que haces en él. Nada aquí es un consejo.", + "wellnessPluralMonday": "los lunes", + "wellnessPluralTuesday": "los martes", + "wellnessPluralWednesday": "los miércoles", + "wellnessPluralThursday": "los jueves", + "wellnessPluralFriday": "los viernes", + "wellnessPluralSaturday": "los sábados", + "wellnessPluralSunday": "los domingos", + "sleepDetailNavTitle": "Sueño", + "sleepDetailNoNightTitle": "No hay noche que mostrar", + "sleepDetailNoNightBody": "No hay un tramo de grabaciones de la banda lo bastante largo para puntuar.", + "sleepDetailNoNightFix": "Usa la banda toda la noche y sincroniza por la mañana", + "sleepDetailStagesSection": "Fases", + "sleepDetailVersusUsualSection": "Frente a lo habitual", + "sleepDetailUnusualLastNight": "Algo inusual anoche", + "sleepDetailUnusualOnDay": "Algo inusual el {day}", + "sleepDetailOvernightSection": "Señales nocturnas", + "sleepDetailTonightSection": "Esta noche", + "sleepDetailTotalSleep": "Sueño total", + "sleepDetailInBed": "EN CAMA", + "sleepDetailWatched": "OBSERVADO", + "sleepDetailAsleepOfThat": "DORMIDO DE ESO", + "sleepDetailAsleep": "DORMIDO", + "sleepDetailWatchedExplain": "Observamos {watched} de tus {inBed} en cama; el resto no es una medición. El sueño, y los porcentajes de fases de abajo, se calculan sobre el tiempo que observamos.", + "sleepDetailWindowMine": "Tú estableciste esta ventana", + "sleepDetailWindowFallback": "Esta ventana se infirió a partir de la frecuencia cardíaca", + "sleepDetailWindowAuto": "Esta ventana se calculó a partir de las señales", + "sleepDetailWindowFallbackBody": "El sistema no pudo encontrar los límites, así que las horas son una estimación.", + "sleepDetailWindowSol": "Desde el inicio de tu ventana hasta dormirte: {band}.", + "sleepDetailConfirmTimes": "Estas horas son correctas", + "sleepDetailChangeTimes": "Cambiar las horas", + "sleepDetailSetTimesMyself": "Establecer las horas yo mismo", + "sleepDetailBackToAutomatic": "Volver a automático", + "sleepDetailReanalysing": "Reanalizando la noche…", + "sleepDetailCorrectionFailedTitle": "Esa corrección no se ha aplicado", + "sleepDetailBedTimeHelp": "CUÁNDO TE ACOSTASTE", + "sleepDetailWakeTimeHelp": "CUÁNDO TE LEVANTASTE", + "sleepDetailReanalyseFailed": "La noche no se ha vuelto a analizar: ya había otro reanálisis en curso, o falló. Las horas que estableciste están guardadas; \"Reanalizar todo\" en Tus datos las aplicará.", + "sleepDetailNoHypnogramTitle": "No hay hipnograma para esta noche", + "sleepDetailNoHypnogramBody": "El cálculo de fases necesita movimiento y ritmo cardíaco. Faltaba uno de los dos.", + "sleepDetailThroughTheNight": "Durante la noche", + "sleepDetailUnitStage": "fase", + "sleepDetailTapDragCycles": "{n, plural, one{Toca o arrastra el gráfico para ver cualquier momento. {n} ciclo.} other{Toca o arrastra el gráfico para ver cualquier momento. {n} ciclos.}}", + "sleepDetailTapDragCyclesAvg": "{n, plural, one{Toca o arrastra el gráfico para ver cualquier momento. {n} ciclo, {avg} de media.} other{Toca o arrastra el gráfico para ver cualquier momento. {n} ciclos, {avg} de media.}}", + "sleepDetailTapDragNone": "Toca o arrastra el gráfico para ver cualquier momento de la noche.", + "sleepDetailNoWakeups": "Sin despertares de 5 minutos o más; los más breves son invisibles para una muñequera.", + "sleepDetailAtLeastWakeups": "{n, plural, one{Al menos {n} despertar de 5 minutos o más; los más breves son invisibles para una muñequera.} other{Al menos {n} despertares de 5 minutos o más; los más breves son invisibles para una muñequera.}}", + "sleepDetailLongestStretch": "Tramo continuo más largo: {longest}.", + "sleepDetailHypnogramLabel": "Hipnograma", + "sleepDetailPercentThroughNight": "{pct}% de la noche", + "sleepDetailNotMeasured": "no medido", + "sleepDetailScrubAt": "{at}, {stage}", + "sleepDetailHeartRate": "Frecuencia cardíaca", + "sleepDetailHrv": "VFC", + "sleepDetailBreathing": "Respiración", + "sleepDetailTemp": "Temp.", + "sleepDetailNotMeasuredCap": "No medido", + "sleepDetailNoSignalAtMoment": "No se registró ninguna señal en este momento.", + "sleepDetailStageAwake": "Despierto", + "sleepDetailStageRem": "REM", + "sleepDetailStageLight": "Sueño ligero", + "sleepDetailStageDeep": "Sueño profundo", + "sleepDetailDeep": "Profundo", + "sleepDetailLight": "Ligero", + "sleepDetailNoStageSplitTitle": "No hay desglose de fases para esta noche", + "sleepDetailNoStageSplitBody": "No hubo ritmo cardíaco medido en toda la ventana.", + "sleepDetailStageRangeExplain": "Cada fase es un rango, no un recuento: cuanto mejor viéramos la noche, más estrecho es. El sueño profundo es el más amplio. El sueño despierto se mantiene como una sola cifra. Estadísticas avanzadas tiene los recuentos exactos.", + "sleepDetailTimeAsleep": "Tiempo dormido", + "sleepDetailShorterThanUsual": "más corto de lo habitual", + "sleepDetailLongerThanUsual": "más largo de lo habitual", + "sleepDetailLessThanUsual": "menos de lo habitual", + "sleepDetailMoreThanUsual": "más de lo habitual", + "sleepDetailAsleepWhileInBed": "Dormido mientras estabas en cama", + "sleepDetailLowerThanUsual": "más bajo de lo habitual", + "sleepDetailHigherThanUsual": "más alto de lo habitual", + "sleepDetailFellAsleep": "Te dormiste", + "sleepDetailEarlierThanUsual": "más temprano de lo habitual", + "sleepDetailLaterThanUsual": "más tarde de lo habitual", + "sleepDetailNotEnoughNightsTitle": "No hay noches suficientes para comparar", + "sleepDetailNightsSoFar": "{have} de {min} noches hasta ahora", + "sleepDetailBarExplain": "La barra es la mitad central de tus propias noches.", + "sleepDetailLessThanAny": "{noun} {value}: menos que cualquiera de tus últimas {count} noches, la más baja de las cuales fue {lowest}.", + "sleepDetailMoreThanAny": "{noun} {value}: más que cualquiera de tus últimas {count} noches, la más alta de las cuales fue {highest}.", + "sleepDetailYouSlept": "Dormiste", + "sleepDetailShortestNightLately": "Tu noche más corta últimamente", + "sleepDetailLongestNightLately": "Tu noche más larga últimamente", + "sleepDetailSleepingHrHighTitle": "La frecuencia cardíaca al dormir fue alta", + "sleepDetailSleepingHrHighBody": "{bpm} lpm por encima de tu propia línea base. Es habitual tras el alcohol, una comida tardía, una sesión exigente o el inicio de una infección; esto es una medición, no un diagnóstico.", + "sleepDetailNothingStoodOut": "Nada llamó la atención.", + "sleepDetailSleepingHr": "FC AL DORMIR", + "sleepDetailLowest": "MÍNIMA", + "sleepDetailBreathingCaps": "RESPIRACIÓN", + "sleepDetailSkinTemp": "Temp. cutánea", + "sleepDetailSleepNeedNotEstablished": "Necesidad de sueño aún no establecida", + "sleepDetailYourNeedIs": "Tu necesidad es {need}", + "sleepDetailYouAreDown": "vas {debt} por debajo", + "sleepDetailLightsOut": "hora de apagar la luz", + "sleepDetailToAimFor": "objetivo", + "sleepDetailNoPersonalRangeYet": "Aún no hay rango personal: {count} de {min} noches.", + "sleepDetailNotFarEnoughToCall": "No hay diferencia suficiente para saberlo", + "sleepDetailTypicalForYou": "Típico en ti", + "sleepDetailVerdictSummary": "{verdict} · habitual {lo}–{hi} en {n} noches", + "sleepDetailNoOvernightTitle": "Sin líneas de señal nocturna", + "sleepDetailNoOvernightBody": "Ninguna grabación nocturna llegó este día.", + "sleepDetailSolUnder15": "menos de 15 minutos", + "sleepDetailSolOverHour": "más de una hora", + "sleepDetailSolRange": "{lo}–{hi} minutos", + "workoutTabForYou": "Para ti", + "workoutTabActivities": "Actividades", + "workoutTabHistory": "Historial", + "workoutScreenTitle": "Entrenamiento", + "workoutStartSessionLabel": "EMPEZAR UNA SESIÓN", + "workoutActivitiesNoun": "actividades", + "workoutThisWeek": "Esta semana", + "workoutTrainingLoad": "Carga de entrenamiento", + "workoutTodaysStrainAction": "Esfuerzo de hoy", + "workoutMechanicalLoadTitle": "CARGA MECÁNICA", + "workoutKgLiftedUnit": "kg levantados", + "workoutTonnageFootnoteIntro": "Repeticiones × carga en las series que registraste con peso. ", + "workoutTonnageFootnotePartial": "Las series registradas sin peso no están incluidas, así que esto es un mínimo y no un total. ", + "workoutTonnageFootnoteOutro": "Exacto para lo que escribiste e inútil entre ejercicios distintos; por eso se excluye de la tensión y la recuperación.", + "workoutOverreachHeadline": "Tus últimos 7 días de carga son {ratio}× tus últimas seis semanas habituales, y tu frecuencia cardiaca en reposo estuvo por encima de lo habitual en {nightsElevated} de {nightsConsidered} noches.", + "workoutOverreachBody": "Dos mediciones que apuntan en la misma dirección. Una enfermedad, un viaje, la altitud, el alcohol o varias noches de mal sueño pueden producir el mismo patrón, y nada aquí puede distinguirlos.", + "workoutNoLoadTitle": "Todavía no hay carga de entrenamiento", + "workoutNoLoadBody": "La forma física y la fatiga son promedios de 42 y 7 días. Necesitan unas dos semanas de sesiones.", + "workoutFitnessLabel": "forma física", + "workoutDailyLoadTitle": "CARGA DIARIA", + "workoutTrimpUnit": "TRIMP", + "workoutDailyLoadFootnoteIntro": "Impulso de entrenamiento de Banister: minutos ponderados por la reserva de frecuencia cardiaca. ", + "workoutDailyLoadAllDays": "Los últimos siete días.", + "workoutDailyLoadPartialDays": "{days} de los últimos siete días arrojaron una cifra.", + "workoutFatigueLabel": "Fatiga", + "workoutFormLabel": "Forma", + "workoutNotYet": "Aún no", + "workoutFormFresh": "Fresco", + "workoutFormSteady": "Estable", + "workoutFormBuilding": "En aumento", + "workoutFormOverreaching": "Sobreentrenamiento", + "workoutSearchActivitiesLabel": "Buscar actividades", + "workoutSearchActivitiesCount": "{count, plural, one{Buscar {count} actividad} other{Buscar {count} actividades}}", + "workoutQuickStartHeader": "INICIO RÁPIDO", + "workoutCalorieNeedWeightTitle": "Las estimaciones de calorías necesitan tu peso", + "workoutAddWeightFix": "Añadir peso en el perfil", + "workoutCalorieEstimatesTitle": "Las cifras de calorías son estimaciones", + "workoutSuggestionsTitle": "{n, plural, one{{n} esfuerzo detectado sin registrar} other{{n} esfuerzos detectados sin registrar}}", + "workoutSuggestionsBody": "La banda detectó actividad sostenida y no se inició ninguna sesión. No se registra nada hasta que tú lo decidas.", + "workoutReviewFix": "{n, plural, one{Revisarlo} other{Revisarlos}}", + "workoutLogPastTitle": "¿Hiciste algo que la banda no detectó?", + "workoutLogPastBody": "Introduce tú mismo los horarios y se puntuará a partir de la frecuencia cardiaca registrada en ese intervalo, como cualquier otra sesión.", + "workoutLogPastFix": "Registrar un entrenamiento pasado", + "workoutNoSessionsTitle": "Aún no hay sesiones registradas", + "workoutNoSessionsBody": "Las sesiones aparecen aquí en cuanto empieces una.", + "workoutStartWorkoutFix": "Empezar un entrenamiento", + "workoutTrackedLabel": "Registrados", + "workoutWeeklyLoadLabel": "Carga semanal", + "workoutNoneLabel": "Ninguna", + "workoutImportedThisWeekNote": "{count} de las sesiones de esta semana vinieron de {storeName}. Cuentan aquí, pero quedan fuera de la carga semanal: un entrenamiento importado llega sin traza de frecuencia cardiaca, y una cifra de carga sin ella sería inventada.", + "workoutAutoImportOnLabel": "Importación automática activada. Toca para desactivarla.", + "workoutAutoImportOffLabel": "Importación automática desactivada. Toca para activarla.", + "workoutImportFromStore": "Importar desde {storeName}", + "workoutFetchNowLabel": "Buscar entrenamientos ahora", + "workoutImportDenied": "{storeName} no concedió acceso a los entrenamientos. No se leyó nada.", + "workoutImportEmpty": "No llegó nada. {storeName} no tiene entrenamientos dentro de la ventana que comparte.", + "workoutImportNoRoutes": " {storeName} no compartirá rutas, así que ninguna tiene coordenadas.", + "workoutImportNoneWithRoute": " Ninguno tenía una ruta registrada.", + "workoutImportSomeWithRoute": " {count} llegaron con ruta.", + "workoutImportBroughtIn": "{count, plural, one{Se importó {count} entrenamiento.} other{Se importaron {count} entrenamientos.}}", + "workoutImportFailed": "Error: {error}", + "workoutMorningAfterTitle": "La mañana siguiente", + "workoutMorningAfterBody": "Tu propio historial, no una regla sobre la actividad: estas mañanas también tuvieron la noche que las precedió. Nada aquí es una razón para saltarte una sesión.", + "workoutAfterActivity": "Después de {name}", + "workoutUnchangedLabel": "Sin cambios", + "workoutRestingHeartRateLabel": "Frecuencia cardiaca en reposo", + "workoutHrvLabel": "VFC", + "workoutMorningCount": "{n, plural, one{{n} mañana} other{{n} mañanas}}", + "workoutInsideRangeSuffix": " · dentro de tu rango habitual noche a noche", + "workoutDeleteSessionLabel": "Eliminar esta sesión", + "workoutStrainLabel": "tensión", + "workoutTimeInZonesTitle": "TIEMPO EN ZONAS", + "workoutMinutesUnit": "minutos", + "workoutFixTimesOnSessionLabel": "Corregir los horarios de esta sesión", + "workoutFixTimes": "Corregir los horarios", + "workoutTimeStatLabel": "Tiempo", + "workoutDistanceStatLabel": "Distancia", + "workoutCaloriesStatLabel": "Calorías", + "workoutNotCostedValue": "Sin calcular", + "workoutMaxHrStatLabel": "FC máx.", + "workoutNoReadingValue": "Sin lectura", + "workoutConfirmDeleteTitle": "¿Eliminar esta sesión de {activity}?", + "workoutDeleteBodyOwn": "Desaparece de OpenStrap. Una copia en {storeName}, si existe, se queda donde está.", + "workoutDeleteBodyImported": "Desaparece de OpenStrap y no se volverá a importar. El original en {storeName} se queda donde está.", + "workoutWhenToday": "Hoy, {time}", + "workoutWhenYesterday": "Ayer, {time}", + "workoutWeekdayLetterMon": "L", + "workoutWeekdayLetterTue": "M", + "workoutWeekdayLetterWed": "X", + "workoutWeekdayLetterThu": "J", + "workoutWeekdayLetterFri": "V", + "workoutWeekdayLetterSat": "S", + "workoutWeekdayLetterSun": "D", + "workoutWeekdayAbbrMon": "lun", + "workoutWeekdayAbbrTue": "mar", + "workoutWeekdayAbbrWed": "mié", + "workoutWeekdayAbbrThu": "jue", + "workoutWeekdayAbbrFri": "vie", + "workoutWeekdayAbbrSat": "sáb", + "workoutWeekdayAbbrSun": "dom", + "activitySetupRouteLabel": "Ruta", + "activitySetupRouteDetail": "Se graba si hay ubicación disponible, y se guarda en este teléfono", + "activitySetupHeartRateLabel": "Frecuencia cardíaca", + "activitySetupBandConnected": "Banda conectada", + "activitySetupNoBandConnected": "Ninguna banda conectada", + "activitySetupPrivateLabel": "Sesión privada", + "activitySetupPrivateDetail": "Oculta en resúmenes y exportaciones", + "activitySetupCaloriesNeedWeight": "Las calorías necesitan tu peso.", + "activitySetupCalorieEstimate": "Unas {est} kcal cada {minutes} min, a partir de {met} MET y tu peso.", + "activitySetupTrackSets": "Series, repeticiones y carga: registradas por ti", + "activitySetupTrackDistanceGps": "Distancia, ritmo y frecuencia cardíaca", + "activitySetupTrackTime": "Tiempo y frecuencia cardíaca", + "activitySetupTrackInterval": "Rondas y frecuencia cardíaca", + "activitySetupTrackStillness": "Tiempo, respiración y quietud", + "activitySetupSessionRunningTitle": "Ya hay una sesión en curso", + "activitySetupSessionRunningBody": "Solo puede haber una sesión activa a la vez.", + "activitySetupOpenRunningSession": "Abrir la sesión en curso", + "activitySetupStart": "Empezar", + "activityPickerTitle": "Elegir actividad", + "activityPickerSearchLabel": "Buscar actividades", + "activityPickerSearchHint": "Buscar entre {count} actividades", + "activityPickerNoMatchTitle": "Ninguna actividad coincide", + "activityPickerNoMatchBody": "El catálogo cubre unas setenta actividades con un coste energético publicado. Elige la más parecida.", + "activityPickerQuickStart": "INICIO RÁPIDO", + "activityPickerRecent": "RECIENTES", + "activityPickerCalorieEstimatesTitle": "Las calorías son estimaciones", + "activityPickerMetValue": "{met} MET", + "activityPickerKcalPer30": "{kcal} kcal / 30 min", + "dayStrainToday": "HOY", + "dayStrainTitle": "Esfuerzo diario", + "dayStrainNoTraceTitle": "Sin registro de esfuerzo para este día", + "dayStrainNoMinuteTraceTitle": "Sin registro minuto a minuto para este día", + "dayStrainNoReasonBody": "No hay ningún registro que explique por qué este día no generó esfuerzo.", + "dayStrainScoredNoTraceBody": "El esfuerzo diario es {strain}. Los minutos despierto con los que se calculó no están guardados para este día.", + "dayStrainWearBandFix": "Usa la banda durante todo el día", + "dayStrainChartTitle": "ESFUERZO A LO LARGO DEL DÍA", + "dayStrainChartFootnote": "Es acumulativo, así que solo puede subir: las partes EMPINADAS son donde estuvo el esfuerzo. Calculado a partir de {drawn} minutos despierto registrados.", + "dayStrainPeakHr": "FC máxima", + "dayStrainWorn": "Uso", + "dayStrainLowCoverageTitle": "La banda registró el {pct}% de este día", + "dayStrainLowCoverageBody": "El esfuerzo es un total sobre los minutos registrados, así que un día usado solo en parte se ve más bajo que uno completo y no son comparables.", + "dayStrainTimeInZonesSection": "Tiempo en zonas", + "dayStrainZonesChartTitle": "TIEMPO EN ZONAS", + "dayStrainZoneFootnoteKarvonen": "Los límites de zona abarcan la diferencia entre tu frecuencia cardíaca en reposo medida y la más alta registrada ({maxHr} lpm). Ambas medidas en ti.", + "dayStrainZoneFootnoteObserved": "Los límites de zona son porcentajes de la frecuencia cardíaca más alta registrada ({maxHr} lpm): medida, no estimada.", + "dayStrainHowSet": "Cómo se establecen", + "dayStrainInputsSection": "De qué se compone", + "dayStrainInputsBase": "TRIMP de Banister sobre tu frecuencia cardíaca despierto, escalado de 0 a 21.", + "dayStrainInputsMaxHr": "Se calculó contra un máximo asumido de {maxHr} lpm, estimado a partir de tu edad y tu correa, no medido.", + "dayStrainInputsMeasuredCeilingNote": "La barra de zonas de arriba usa en cambio el límite medido; el esfuerzo no se ha movido a ese límite, porque eso reescribiría todas las puntuaciones de esfuerzo que hayas visto.", + "dayStrainInputsRhrAnchor": "El otro ancla es tu frecuencia cardíaca en reposo de la noche anterior, así que una noche que la banda no registró afecta a todo el día.", + "activityZonesTitle": "Zonas de frecuencia cardíaca", + "activityZonesYourZonesSection": "Tus zonas", + "activityZonesIntensitySection": "En qué se fue tu intensidad", + "activityZonesNoCeilingTitle": "Aún sin límite medido", + "activityZonesNoCeilingTanakaTail": " Hasta que se mida uno, las zonas de abajo se basan en tu edad.", + "activityZonesNoCeilingDefaultBody": "Solo contamos una lectura alta que la banda mantuvo durante 15 segundos mientras te movías. Un pico de un segundo no es una frecuencia cardíaca.", + "activityZonesWearBandFix": "Usa la banda en tus sesiones intensas habituales", + "activityZonesHighestSeenLabel": "MÁXIMO REGISTRADO", + "activityZonesBpmUnit": "lpm", + "activityZonesCeilingOnDate": "el {date}", + "activityZonesCeilingDuringSession": "durante {session}", + "activityZonesHighestSeenFootnote": "Es lo más alto que hemos medido, no un límite: sube poco a poco a medida que la banda ve esfuerzos más duros. No intentes ponerlo a prueba.", + "activityZonesNoZonesTitle": "Aún sin zonas", + "catalogueZonesWhy": "Los límites de zona son porcentajes de una frecuencia cardíaca máxima estimada a partir de tu edad, no medida en ti.", + "activityZonesNoAgeBody": "Los límites de zona son porcentajes de una frecuencia cardíaca máxima, y sin tu edad no hay nada de qué sacar un porcentaje.", + "activityZonesNoZonesDefaultBody": "No hay ningún registro que explique por qué aún no hay límites de zona.", + "activityZonesAddAgeFix": "Añade tu edad en Perfil", + "activityZonesAnchorKarvonen": "Se calcula a partir de dos números que la banda midió en ti: tu frecuencia en reposo ({restingHr}, la mediana de tus últimas {restingDays} noches) y la más alta registrada ({maxHr}). Una frecuencia en reposo baja hace que la zona 1 sea amplia. Son las bandas habituales, no tus propios umbrales medidos.", + "activityZonesAnchorObserved": "Se calcula a partir de la frecuencia cardíaca más alta registrada ({maxHr}). Tras {restingMinDays} noches de frecuencia en reposo (tienes {restingDays}) tu frecuencia en reposo se sumará, lo que se ajusta mejor a ti. Son las bandas habituales, no tus propios umbrales medidos.", + "activityZonesAnchorTanaka": "Se calcula a partir de {maxHr} lpm, estimados según tu edad en lugar de medidos en ti; puede variar hasta 20 lpm en cualquier dirección. Los límites pasarán a un límite medido en cuanto la banda registre una sesión suficientemente intensa.", + "activityZonesAnchorDefault": "Los límites de zona son porcentajes de una frecuencia cardíaca máxima.", + "activityZonesNotShownTitle": "Aún no disponible", + "activityZonesNeedsMonthBody": "Necesita alrededor de un mes de sesiones registradas, cada una con frecuencia cardíaca minuto a minuto.", + "activityZonesAgeEstimateBody": "Las barras solo mostrarían la estimación por edad, no tu entrenamiento. Aparecerán en cuanto los límites de zona de arriba estén medidos.", + "activityZonesSessionMinutesChartTitle": "MINUTOS DE SESIÓN, ÚLTIMOS 28 DÍAS", + "activityZonesShapePyramidal": "La mayoría de tus minutos son suaves, menos en el medio y menos aún intensos: una pirámide.", + "activityZonesShapePolarised": "La mayoría de tus minutos son suaves y el resto son intensos, con muy poco en el medio.", + "activityZonesShapeMiddleHeavy": "La mayoría de tus minutos están en el medio en lugar de ser suaves o intensos.", + "activityZonesShapeSummary": "{easy} min suaves, {moderate} moderados, {hard} intensos, en {sessions} sesiones registradas. Una descripción, no un objetivo.", + "activityShareTitle": "Compartir", + "activityShareOpenFailed": "No se pudo abrir el menú para compartir.", + "activitySharePhotoHeader": "TU FOTO", + "activityShareAddPhoto": "Añadir una foto", + "activityShareChangePhoto": "Cambiar foto", + "activitySharePhotoHint": "Desde este teléfono. No se sube a ningún sitio", + "activityShareRemovePhoto": "Quitar la foto", + "activityShareBasemapHeader": "MAPA BASE", + "activityShareDrawMap": "Mostrar el mapa real", + "activityShareMapHint": "Pide a openstreetmap.org los mosaicos que cubren esta ruta. Si está desactivado, la ruta se dibuja por sí sola", + "activityShareFetchingMapTitle": "Obteniendo el mapa", + "activityShareFetchingMapBody": "La tarjeta se dibuja en cuanto llega cada mosaico.", + "activityShareNoMapTitle": "No hay mapa para esta tarjeta", + "activityShareNoMapBody": "No se pudieron obtener los mosaicos del mapa, así que la ruta se dibuja por sí sola. El resto de la tarjeta no cambia.", + "activityShareStatusPrivateTitle": "Esta sesión es privada", + "activityShareStatusPrivateBody": "Oculta en los resúmenes y las exportaciones.", + "activityPosterFormatPost": "Publicación", + "activityPosterFormatStory": "Historia", + "activitySummaryRpeHeadline": "¿QUÉ TAN DURO SE SINTIÓ?", + "activitySummaryRpeBody": "Tu propia valoración del esfuerzo. Es una sensación, no una medición — y esa es la idea, porque puede no coincidir con los números de arriba.", + "activitySummaryRateEffort": "Calificar este esfuerzo {n} de 10", + "activitySummaryRpeVeryEasy": "1 · muy fácil", + "activitySummaryRpeMaximal": "10 · máximo", + "activitySummaryNotNow": "Ahora no", + "activitySummaryShareThis": "Compartir esta actividad de {name}", + "activitySummaryChangeType": "Cambiar el tipo de actividad", + "activitySummaryUnsavedTitle": "Esta sesión aún no se ha guardado", + "activitySummaryUnsavedBody": "No se pudo escribir en este teléfono.", + "activitySummarySaving": "Guardando", + "activitySummaryTryAgain": "Reintentar", + "activitySummaryPrivate": "Privada", + "activitySummaryStepsBasis": "Los pasos provienen del propio sensor de movimiento de la banda, que solo los cuenta caminando.", + "activitySummaryCaloriesNeedWeight": "Las calorías necesitan tu peso.", + "activitySummaryNoCalorieNoStrain": "No hay cifra de calorías para esta sesión. Una estimación de energía a partir del ritmo cardíaco necesita tu frecuencia máxima y en reposo, y una de ellas no está configurada.", + "activitySummaryNoCalorieWithStrain": "No hay cifra de calorías para esta sesión — una estimación de energía a partir del ritmo cardíaco necesita tu frecuencia máxima y en reposo, y una de ellas no está configurada. El esfuerzo de arriba es lo que sí se midió, en su propia escala de 0–21.", + "activitySummaryCalorieNoHr": "Estimado a partir de {met} MET y tu peso. No se registró ritmo cardíaco en esta sesión, así que no forma parte de la cifra.", + "activitySummaryCalorieWithHr": "Estimado a partir de {met} MET, tu peso y el ritmo cardíaco.", + "activitySummaryNothingLoggedWithLoad": "No se registró nada con carga", + "activitySummarySetUnit": "{n, plural, one{serie} other{series}}", + "activitySummaryVolumeLoadedSets": "Volumen de las series con carga", + "activitySummaryTotalVolume": "Volumen total", + "activitySummaryElapsedTime": "Tiempo transcurrido", + "activitySummaryClimbed": "+{m} m de ascenso", + "activitySummaryLapsCaption": "{n, plural, one{{n} largo} other{{n} largos}}", + "activitySummaryNoRouteTitle": "No hay ruta para esta sesión", + "activitySummaryNoRouteBody": "La ubicación estaba desactivada, o esta actividad no se registró con GPS.", + "activitySummaryRouteTitle": "RUTA", + "activitySummarySlower": "Más lento", + "activitySummaryFaster": "Más rápido", + "activitySummaryStartFinishPinned": "El inicio y el final están marcados.", + "activitySummaryRouteFootnote": "{distance} {unit}, inicio y final marcados.", + "activitySummaryNoSetsTitle": "No se registraron series", + "activitySummaryNoSetsBody": "No se ingresó nada en esta sesión, así que no hay carga ni volumen que sumar.", + "activitySummaryNoRoundsTitle": "No se registraron rondas", + "activitySummaryNoRoundsBody": "0 rondas registradas.", + "activitySummaryIntervalLadderTitle": "ESCALERA DE INTERVALOS", + "activitySummaryWork": "Trabajo", + "activitySummaryRest": "Descanso", + "activitySummaryRoundLabel": "Ronda {n}", + "activitySummaryLongestBlock": "Bloque más largo {time}.", + "activitySummaryPosesCount": "{n, plural, one{{n} postura} other{{n} posturas}}", + "activitySummaryNoLapsTitle": "No se contaron largos", + "activitySummaryNoLapsBody": "0 largos marcados.", + "activitySummaryLapsTitle": "LARGOS", + "activitySummarySecondsPerLap": "segundos por largo", + "activitySummaryLapLabel": "Largo {n}", + "activitySummaryPoolLength": "Piscina de {m} m", + "activitySummaryFastest": "más rápido {time}", + "activitySummarySlowest": "más lento {time}", + "activitySummaryNoElevationTitle": "No hay perfil de elevación", + "activitySummaryNoElevationBody": "No hay ruta, o la ruta no registró altitud.", + "activitySummaryElevationTitle": "ELEVACIÓN", + "activitySummaryStart": "Inicio", + "activitySummaryFinish": "Final", + "activitySummaryGain": "Ascenso", + "activitySummaryLoss": "Descenso", + "activitySummaryPeak": "Punto más alto", + "activitySummaryColdPlungeWhy": "El frío cierra los vasos sanguíneos que lee el sensor. No encontrar nada aquí es lo esperado, no una falla.", + "activitySummaryHeatWhy": "El calor, el sudor y una correa que se afloja al calentarte impiden que el sensor detecte el pulso. No encontrar nada aquí es normal, no una falla.", + "activitySummaryNoPulseTitle": "Sin lectura de pulso para esta sesión de {activity}", + "activitySummaryOneMinutePulse": "Un minuto de pulso, y nada más", + "activitySummaryPulseGapNote": "La banda encontró pulso en {have} de {total} minutos. Los huecos son esperados, así que lo dibujado es la parte que pudo ver.", + "activitySummaryTooShortTitle": "Demasiado corta para graficar", + "activitySummaryTooShortBody": "Un minuto de ritmo cardíaco es un punto, no una línea.", + "activitySummaryNoHrTitle": "Sin ritmo cardíaco para esta sesión", + "activitySummaryNoHrBody": "La banda no reportó nada mientras esto ocurría.", + "activitySummaryCheckBandConnection": "Revisar la conexión de la banda", + "activitySummaryPartialTrace": "Trazo parcial — la banda entregó el {pct}% de estos minutos.", + "activitySummaryHeartRateTitle": "RITMO CARDÍACO", + "activitySummaryHardMinutesNote": "{min} min por encima del 80% de tu máximo.", + "activitySummaryTimeInZonesTitle": "TIEMPO EN ZONAS", + "activitySummaryTopSet": "Mejor serie", + "activitySummaryOneRepMax": "1RM estimado {kg} kg", + "activitySummarySomeSetsNoLoadTitle": "Algunas series no tenían carga", + "activitySummarySomeSetsNoLoadBody": "Contadas en series y repeticiones, pero excluidas del volumen.", + "activitySummaryScore": "Marcador", + "activitySummaryGameSetLabel": "Set {n}", + "activitySummaryNoSplitsTitle": "No hay parciales para esta sesión", + "activitySummaryNoSplitsBody": "Los parciales necesitan una distancia registrada.", + "activitySummaryKm": "KM", + "activitySummaryPace": "RITMO", + "activitySummaryHr": "FC", + "activitySummarySetsLoggedZero": "0 series registradas.", + "activitySummaryRoundHeader": "R", + "activitySummaryWorkHeader": "TRABAJO", + "activitySummaryRestHeader": "DESCANSO", + "activitySummaryAvgBpm": "FC PROM", + "activitySummaryLapHeader": "LARGO", + "activitySummaryTimeHeader": "TIEMPO", + "activitySummarySpeedVsFastest": "VELOCIDAD vs MÁS RÁPIDO", + "activitySummaryBodyweightReps": "{n, plural, one{{n} repetición · peso corporal} other{{n} repeticiones · peso corporal}}", + "activitySummaryRpeValue": "RPE {v}", + "activitySummaryNothingToPlot": "Nada para graficar en esta sesión de {activity}", + "activitySummaryNoSeriesTitle": "No hay series para graficar", + "activitySummaryNoSeriesBody": "Esta sesión no registró flujos por minuto.", + "activitySummaryHeartRateZones": "Zonas de ritmo cardíaco", + "activitySummaryTabOverview": "Resumen", + "activitySummaryTabSplits": "Parciales", + "activitySummaryTabGraphs": "Gráficos", + "activityLiveAddALap": "Añadir un largo", + "activityLiveAddExerciseTitle": "Añadir ejercicio", + "activityLiveAllowLocation": "Permitir ubicación", + "activityLiveBestLabel": "Mejor", + "activityLiveBodyweightExcludedNote": "Peso corporal — excluido del volumen", + "activityLiveBodyweightOnly": "solo peso corporal", + "activityLiveBpmUnit": "ppm", + "activityLiveBwAbbrev": "PC", + "activityLiveChangeStroke": "Cambiar estilo", + "activityLiveDecrease": "Bajar {label}", + "activityLiveDeniedForeverBody": "La ubicación está denegada para esta app, y solo Ajustes puede cambiarlo.", + "activityLiveDurationHeader": "DURACIÓN", + "activityLiveEffortRpeHeader": "ESFUERZO (RPE)", + "activityLiveEndSet": "Terminar set", + "activityLiveExerciseOf": "EJERCICIO {index} DE {total}", + "activityLiveFinishSessionLabel": "Finalizar sesión", + "activityLiveHoldTime": "Mantén · {time}", + "activityLiveIncrease": "Subir {label}", + "activityLiveIntervalSubtitle": "{workSec} S TRABAJO · {restSec} S DESCANSO", + "activityLiveKcalEstUnit": "kcal · est.", + "activityLiveKgVolumeUnit": "kg de volumen", + "activityLiveLapButtonLabel": "LARGO", + "activityLiveLapsChartTitle": "LARGOS", + "activityLiveLapsCount": "{count, plural, one{{count} largo} other{{count} largos}} · {stroke}", + "activityLiveLapsFootnote": "Más rápido {time} · el largo de la barra es la velocidad respecto a él.", + "activityLiveLapXLabel": "Largo {n}", + "activityLiveLogAsBodyweight": "Registrar como peso corporal", + "activityLiveMatchSetSubtitle": "SET {n}", + "activityLiveMetrePoolLabel": "piscina de {len} metros", + "activityLiveMinimiseLabel": "Minimizar", + "activityLiveNextExercise": "Siguiente ejercicio", + "activityLiveNextLabel": "SIGUIENTE", + "activityLiveNextPose": "Siguiente postura", + "activityLiveNextRest": "Descanso · {time}", + "activityLiveNextWork": "Trabajo · {time}", + "activityLiveNoHrBody": "El band no está conectado, así que no llega nada para esta sesión.", + "activityLiveNoHrTitle": "Sin frecuencia cardíaca", + "activityLiveNoHrYetBody": "El band está conectado pero aún no ha registrado un latido, así que necesita ir ajustado, a un dedo por encima del hueso de la muñeca.", + "activityLiveNoHrYetTitle": "Aún sin frecuencia cardíaca", + "activityLiveNoneYet": "Todavía ninguno", + "activityLiveNoRouteFailedBody": "El teléfono devolvió un error al pedir una ubicación.", + "activityLiveNoRouteFailedTitle": "Sin ruta: fallo de ubicación", + "activityLiveNoRouteNotAllowedTitle": "Sin ruta: ubicación no permitida", + "activityLiveNoRouteOffBody": "Los servicios de ubicación están desactivados en este teléfono, así que no llegan ubicaciones.", + "activityLiveNoRouteOffTitle": "Sin ruta: ubicación desactivada", + "activityLiveOneLapFewer": "Un largo menos", + "activityLiveOpenSettings": "Abrir Ajustes", + "activityLiveOpponentLabel": "RIVAL", + "activityLivePauseLabel": "Pausar", + "activityLivePerLapUnit": "por largo", + "activityLivePointLabel": "punto de {side}", + "activityLivePoolSubtitle": "PISCINA DE {len}M · {stroke}", + "activityLivePoseBridge": "Puente", + "activityLivePoseChair": "Silla", + "activityLivePoseChildsPose": "Postura del niño", + "activityLivePoseForwardFold": "Flexión hacia delante", + "activityLivePoseMountain": "Montaña", + "activityLivePoseOf": "POSTURA {index} DE {total}", + "activityLivePosePigeon": "Paloma", + "activityLivePosePlank": "Plancha", + "activityLivePoseSavasana": "Savasana", + "activityLivePoseTriangle": "Triángulo", + "activityLivePoseWarriorTwo": "Guerrero II", + "activityLivePreviousExercise": "Ejercicio anterior", + "activityLivePreviousLabel": "Anterior", + "activityLivePrivateSession": "Sesión privada", + "activityLiveRecordingRoute": "Grabando ruta", + "activityLiveRepsBodyweightRow": "{n, plural, one{{n} repetición · peso corporal} other{{n} repeticiones · peso corporal}}", + "activityLiveRepsLabel": "REPETICIONES", + "activityLiveRepsLoggedBodyweight": "{n, plural, one{{n} repetición registrada} other{{n} repeticiones registradas}}", + "activityLiveRepsOnly": "{n, plural, one{{n} repetición} other{{n} repeticiones}}", + "activityLiveRepsUnit": "repeticiones", + "activityLiveRestingHeader": "DESCANSANDO", + "activityLiveRestWord": "Descanso", + "activityLiveResumeLabel": "Reanudar", + "activityLiveRoundLabel": "RONDA {n}", + "activityLiveRouteFootnoteNoDistance": "Punto de inicio fijado; la distancia aparece cuando las ubicaciones se estabilizan.", + "activityLiveRouteFootnoteWithDistance": "{distance} según las ubicaciones registradas hasta ahora.", + "activityLiveRouteSoFarTitle": "RUTA HASTA AHORA", + "activityLiveSetNumber": "Set {n}", + "activityLiveSetsCountSubtitle": "{n, plural, one{{n} SET} other{{n} SETS}}", + "activityLiveSetsListHeader": "SETS", + "activityLiveSetsUnit": "sets", + "activityLiveStepsUnit": "pasos", + "activityLiveStrainUnit": "esfuerzo", + "activityLiveStrokeBack": "Espalda", + "activityLiveStrokeBreast": "Braza", + "activityLiveStrokeFly": "Mariposa", + "activityLiveStrokeFree": "Crol", + "activityLiveThisExerciseLabel": "ESTE EJERCICIO", + "activityLiveTimeInZonesTitle": "TIEMPO EN ZONAS", + "activityLiveTimeUnit": "tiempo", + "activityLiveTryAgain": "Reintentar", + "activityLiveTurnOnLocation": "Activar ubicación", + "activityLiveVolumeSetsSubtitle": "{kg} KG · {n, plural, one{{n} SET} other{{n} SETS}}", + "activityLiveWeightLabel": "PESO", + "activityLiveWeightRepsLogged": "{kg} kg × {n} registrado", + "activityLiveWorkWord": "Trabajo", + "activityLiveYouLabel": "TÚ", + "activityLiveZoneLabel": "Zona {z}", + "activityLiveLogSet": "Registrar set", + "activityLiveRestOverAnnounce": "Fin del descanso", + "activityLiveSkipRest": "Saltar descanso", + "gesturesNavTitle": "Doble toque", + "gesturesSectionTitle": "Toca el band dos veces", + "gesturesSectionBody": "Solo mientras la app está conectada y activa. Un toque que el band guardó mientras tu teléfono no estaba disponible llega más tarde con una marca de tiempo antigua, y se ignora en lugar de activarse horas después de lo que querías.", + "gesturesItDoesTitle": "Hace esto", + "gesturesNoPhoneActionsTitle": "¿Nada en el teléfono?", + "gesturesNoPhoneActionsBody": "Hacer sonar tu teléfono y la linterna no aparecen porque la app no pudo consultar al sistema qué permite este dispositivo. Vuelve a abrir la app y regresa; las acciones dentro de la app de arriba funcionan de todos modos.", + "settingsBarcodeSaveFailed": "Eso no se pudo guardar; puede volver a activarse la próxima vez que abras la app.", + "settingsIconRowTitle": "Icono", + "settingsIconRowConfirmHint": "El iPhone te pedirá que confirmes", + "settingsIconChoiceLabel": "Icono {label}.", + "settingsSelectedSuffix": " Seleccionado.", + "settingsHealthSyncOff": "Desactivado. No se escribe nada en {store}", + "settingsHealthSyncReady": "Escribe el sueño, la frecuencia cardíaca en reposo, la VFC, la frecuencia respiratoria, la energía y los entrenamientos de cada día en {store} una vez que quedan definitivos", + "settingsHealthSyncNeedsPermission": "{store} no ha concedido acceso de escritura. Toca para abrirlo", + "settingsHealthSyncNotInstalled": "Health Connect no está instalado. Toca para obtenerlo", + "settingsHealthSyncNeedsUpdate": "Health Connect es demasiado antiguo para escribir en él. Toca para actualizarlo", + "settingsHealthSyncUnsupported": "Este dispositivo no tiene un almacén de salud en el que escribir", + "settingsHealthSyncChecking": "Comprobando {store}…", + "settingsWriteToHealthStoreRowTitle": "Escribir en {store}", + "settingsHealthShareOffTitle": "Contribución desactivada", + "settingsHealthShareOffNeverUploaded": "Nunca se subió nada. No se subirá nada.", + "settingsHealthShareOffDetail": "No se subirá nada más.\n\nSe subió una copia de tu base de datos el {date}. El servidor conserva solo la copia más reciente por dispositivo. Intentamos avisarle que tu consentimiento fue retirado — ese mensaje se envía una sola vez y no se reintenta, así que si este teléfono está sin conexión no habrá llegado, y tampoco podemos mostrarte que la copia ya no existe.", + "settingsOk": "Aceptar", + "settingsHealthShareOnTitle": "¿Contribuir con tus datos de salud?", + "settingsHealthShareOnBody": "Una vez al día, con Wi-Fi y mientras se carga, se sube una copia comprimida de TODA tu base de datos — cada día derivado y cada fila de sensor sin procesar que el band ha enviado. Se usa para mejorar los algoritmos.\n\nNo es anónimo en ningún sentido real: es todo tu historial de salud. Puedes desactivarlo en cualquier momento, y a partir de ese momento no se envía nada más.", + "settingsNo": "No", + "settingsContribute": "Contribuir", + "settingsResetTitle": "¿Eliminar todo?", + "settingsResetBody": "Esto elimina, de forma permanente y sin copia en ningún otro lugar:\n\n· cada día medido, sueño, entrenamiento y ruta\n· cada resultado de laboratorio, comida, dosis de medicación, hábito, sesión de respiración y serie registrada\n· tu diario, registro de ciclo y líneas base continuas\n· tu perfil, cada preferencia y cualquier clave de IA guardada\n· el widget de pantalla de inicio y cada recordatorio programado\n\nEl band queda desvinculado, y no puede reenviar el historial que ya entregó. Exporta desde Tus datos primero si quieres una copia.", + "settingsResetKeepData": "Conservar mis datos", + "settingsResetDeleteEverything": "Eliminar todo", + "settingsNavTitle": "Ajustes", + "settingsGroupTheBand": "El band", + "settingsAlarmRowTitle": "Alarma", + "settingsAlarmRowSub": "Vibra en tu muñeca, según el reloj propio del band", + "settingsGroupThisPhone": "Este teléfono", + "settingsStepsRowTitle": "Pasos", + "settingsStepsRowSub": "El propio contador de pasos de este teléfono, para las horas que el band no cubre. Nada sale del dispositivo", + "settingsGroupNotifications": "Notificaciones", + "settingsManageNotificationsRowTitle": "Gestionar notificaciones", + "settingsManageNotificationsRowSub": "Qué puede interrumpirte, horas silenciosas, e interruptores para desactivarlas todas", + "settingsGroupPreferences": "Preferencias", + "settingsUnitsRowTitle": "Unidades", + "settingsAppearanceRowTitle": "Apariencia", + "settingsCycleTrackingRowTitle": "Seguimiento del ciclo", + "settingsCycleTrackingRowSub": "Agrega la pestaña Ciclo a Bienestar. Desactivado la oculta y conserva todo lo ya registrado", + "settingsGroupYourData": "Tus datos", + "settingsExportBackupImportRowTitle": "Exportar, respaldar, importar", + "settingsExportBackupImportRowSub": "Hojas de cálculo, una copia completa, e importar historial", + "settingsGroupAutomation": "Automatización", + "settingsDoubleTapRowTitle": "Doble toque", + "settingsDoubleTapRowSub": "Qué hace un doble toque en el band", + "settingsTaskerShortcutsRowTitle": "Tasker y Atajos", + "settingsTaskerShortcutsRowSub": "Solo Android para eventos salientes. iOS puede hacer vibrar el band pero no puede ser activado por él", + "settingsGroupPrivacy": "Privacidad", + "settingsCrashReportsRowTitle": "Informes de fallos", + "settingsCrashReportsRowSub": "No se envía nada hasta que tú lo autorices", + "settingsBarcodeLookupRowTitle": "Buscar códigos de barras en línea", + "settingsBarcodeLookupRowSub": "Envía un código de barras escaneado a openfoodfacts.org. Nada sobre ti va con él", + "settingsContributeHealthDataRowTitle": "Contribuir con mis datos de salud", + "settingsContributeHealthDataRowSub": "Sube toda tu base de datos una vez al día, con Wi-Fi y cargando, para mejorar los algoritmos", + "settingsCheckForUpdatesRowTitle": "Buscar actualizaciones", + "settingsUpdateBelowMinimum": "Esta versión está por debajo de la mínima admitida. Instala la versión más reciente desde GitHub", + "settingsUpdateAvailable": "Hay una versión más reciente publicada en GitHub", + "settingsUpdateCheckSub": "Consulta al servidor de versiones al iniciar. Ve tu dirección IP y cuándo abres la app", + "settingsGroupAbout": "Acerca de", + "settingsVersionRowTitle": "Versión", + "settingsNoticesLicencesRowTitle": "Avisos y licencias", + "settingsNoticesLicencesRowSub": "Quién no es esta app, y de quién son los datos que usa", + "settingsGroupDeveloper": "Desarrollador", + "settingsComponentGalleryRowTitle": "Galería de componentes", + "settingsComponentGalleryRowSub": "Todos los componentes, en cualquier escala de texto, en cualquiera de los dos temas", + "settingsDeveloperModeRowTitle": "Modo desarrollador", + "settingsResetAllDataRowTitle": "Restablecer todos los datos", + "settingsNotificationsNavTitle": "Notificaciones", + "settingsNotificationsNavSub": "QUÉ PUEDE INTERRUMPIRTE", + "settingsNotificationsOffSystemTitle": "Las notificaciones están desactivadas a nivel del sistema", + "settingsNotificationsOffSystemBody": "Nada de lo siguiente puede llegar a ti hasta que el sistema lo permita.", + "settingsTurnThemOn": "Activarlas", + "settingsGroupManageNotifications": "Gestionar notificaciones", + "settingsHealthExceptionsRowTitle": "Excepciones de salud", + "settingsHealthExceptionsRowSub": "Como máximo una al día, y solo cuando algo en tu propia línea base cambió", + "settingsBandAlertsRowTitle": "Alertas del band", + "settingsBandAlertsRowSub": "Batería agotada, en el cargador, dejó de responder", + "settingsAlertMeAtRowTitle": "Avisarme al", + "settingsAlertMeAtRowSub": "Avisa cuando el band baje de este nivel de carga", + "settingsRecoveryReadyRowTitle": "Recuperación lista", + "settingsRecoveryReadyRowSub": "Un aviso cuando llega tu puntuación de recuperación matutina", + "settingsWeeklyLookbackRowTitle": "Resumen semanal", + "settingsWeeklyLookbackRowSub": "El domingo por la noche, pero solo para una semana en la que realmente se encontró algo. La mayoría de las semanas son silenciosas", + "settingsDetectedWorkoutsRowTitle": "Entrenamientos detectados", + "settingsDetectedWorkoutsRowSub": "Pregunta sobre esfuerzos que el band detectó que tú no iniciaste. Desactivado oculta el aviso y las tarjetas de revisión; el band sigue midiendo de todas formas", + "settingsMovementNudgeRowTitle": "Aviso de movimiento", + "settingsMovementNudgeRowSub": "Te avisa después de un tramo sin moverte — dos horas sin ningún movimiento, o 90 minutos en postura de escritorio. Notificación en el teléfono más una vibración en el band mientras está conectado", + "settingsWindDownRowTitle": "Relajación previa", + "settingsWindDownRowSub": "Un aviso unos 45 minutos antes de la hora de dormir aprendida de tus propias noches, sin interferir con tus horas silenciosas. Aparece tras aproximadamente una semana de uso", + "settingsStepGoalAlertsRowTitle": "Alertas de meta de pasos", + "settingsStepGoalAlertsRowSub": "Te avisa una vez cuando hoy superas tu meta de pasos", + "settingsMedicationRemindersRowTitle": "Recordatorios de medicación", + "settingsMedicationRemindersRowSub": "Una notificación por dosis programada, en los horarios que ingresaste — con vibración en el band si está conectado. No se envía nada para una dosis ya marcada como tomada u omitida", + "settingsDailyCheckInRowTitle": "Registro diario", + "settingsDailyCheckInRowSub": "Un aviso por la noche para escribir el día — ánimo, energía, estrés. Se omite si el día ya tiene una calificación", + "settingsWaterReminderRowTitle": "Recordatorio de agua", + "settingsWaterReminderRowSub": "Una vibración en la correa y una notificación en tu teléfono durante tus horas de vigilia, para recordarte registrar una bebida. No se mide nada de todos modos", + "settingsRemindMeEveryRowTitle": "Recordarme cada", + "settingsGroupTheStrap": "La correa", + "settingsBuzzOnAppNotificationsRowTitle": "Vibrar con notificaciones de apps", + "settingsBuzzOnAppNotificationsRowSub": "Elige qué apps del teléfono hacen vibrar la correa", + "settingsGroupQuietHours": "Horas silenciosas", + "settingsQuietHoursRowTitle": "Horas silenciosas", + "settingsQuietHoursRowSub": "Nada vibra dentro de esta ventana", + "settingsQuietHoursStartsRowTitle": "Comienza", + "settingsQuietHoursEndsRowTitle": "Termina", + "settingsHealthExceptionsBreakThroughRowTitle": "Las excepciones de salud rompen el silencio", + "settingsAlarmNotOnListTitle": "La alarma no está en esta lista", + "settingsAlarmNotOnListBody": "Cancélala en la pantalla de Alarma.", + "settingsImportNoPermission": "{store} no concedió esos campos. No se leyó nada.", + "settingsImportEmptyWithBirthday": "No llegó nada. {store} no tiene altura, peso, fecha de nacimiento ni sexo para ti — escríbelos aquí.", + "settingsImportEmpty": "No llegó nada. {store} no tiene altura, peso ni sexo para ti — escríbelos aquí.", + "settingsImportNoChange": "Se leyó {fields}. Tu perfil ya dice lo mismo, así que nada cambió.", + "settingsImportUpdated": "Se actualizó {fields} desde {store}.", + "settingsImportFailed": "Error: {error}", + "settingsAgeFieldLabel": "Edad", + "settingsEditProfileNavTitle": "Editar perfil", + "settingsNameFieldLabel": "NOMBRE", + "settingsSexFieldLabel": "SEXO", + "settingsSexMale": "Masculino", + "settingsSexFemale": "Femenino", + "settingsSexPreferNotToSay": "Prefiero no decirlo", + "settingsAgeYearsFieldLabel": "EDAD (AÑOS)", + "settingsFourFieldsTitle": "Estos cuatro cambian tus números", + "settingsFourFieldsBody": "Alimentan las zonas de frecuencia cardíaca, las estimaciones de calorías y la carga de entrenamiento. Borra uno y solo las métricas que lo necesitan quedan sin disponibilidad.", + "settingsImportBlockAppleHealth": "Altura, peso, fecha de nacimiento y sexo, directamente de {store}. La altura y el peso se toman siempre; tu edad y sexo solo llenan un vacío, porque ninguno cambia y un valor ya presente fue tu elección.", + "settingsImportBlockOther": "Altura y peso, directamente de {store}. No tiene fecha de nacimiento ni sexo para leer — ninguna app puede — así que configura esos dos arriba tú mismo.", + "settingsNotSetHint": "Sin definir", + "settingsAutomationNavTitle": "Automatización", + "settingsSyncFinishesSectionTitle": "Cuando termina una sincronización", + "settingsSyncFinishesAndroidBody": "La app emite un intent sobre el que tu app de automatización puede iniciar un perfil. Filtra por la acción de abajo; lleva cuántos registros llegaron y cuándo, como máximo uno por minuto.", + "settingsSyncFinishesIosBody": "iOS no puede hacer esto. Una automatización personal de Atajos solo puede activarse con la lista fija de eventos propia de Apple, y ninguna app puede añadir uno — así que nada aquí puede iniciar un atajo por ti. Android sí lo tiene; esto es un límite de la plataforma, no un ajuste.", + "settingsSyncFinishesExtras": "Extras: records (int), at (segundos unix)", + "settingsNeverSendSectionTitle": "Lo que nunca enviará", + "settingsNeverSendBody": "Sin preparación, sin esfuerzo, sin puntuación de sueño — en ninguna de las dos plataformas. Un número que esta app habría mostrado como ausente, con un motivo adjunto, se convierte en un simple cero en el momento en que sale. Salen datos sobre la sincronización; las mediciones no.", + "settingsBuzzFromShortcutSectionTitle": "Hacer vibrar el band desde un atajo", + "settingsBuzzFromShortcutAndroidBody": "Envía wtf.openstrap.openstrap_edge.BUZZ_STRAP con este token como el extra de cadena “token”. Sin él, cualquier app del teléfono podría hacer vibrar tu band.", + "settingsBuzzFromShortcutIosBody": "Esta dirección funciona en iOS: un atajo que tú mismo ejecutas puede llegar a la app. Lo que no puede hacer es ejecutarse solo cuando el band se sincroniza.", + "settingsNoTokenYet": "Aún no hay token — vuelve a abrir esta pantalla.", + "settingsCopied": "Copiado", + "settingsCopyTheToken": "Copiar el token", + "bandStatusBluetoothDeniedTitle": "El Bluetooth está desactivado para esta app", + "bandStatusBluetoothDeniedReason": "El teléfono no le concede la radio Bluetooth a OpenStrap, así que no se puede escanear ni conectar nada. Esto no es la banda — acercarse más no ayudará.", + "bandStatusBluetoothDeniedFix": "Abre Ajustes → OpenStrap y permite el Bluetooth", + "bandStatusBluetoothOffTitle": "El Bluetooth está apagado", + "bandStatusBluetoothOffReason": "La radio del teléfono está apagada, así que ninguna app puede alcanzar la banda. Mientras tanto la banda sigue registrando; no se pierde nada.", + "bandStatusBluetoothOffFix": "Activa el Bluetooth", + "bandStatusBluetoothUnsupportedTitle": "Este teléfono no tiene radio Bluetooth de baja energía", + "bandStatusBluetoothUnsupportedReason": "La banda solo se puede alcanzar por Bluetooth de baja energía (BLE). Los datos importados siguen funcionando; un enlace en vivo no.", + "bandStatusReconnectPausedTitle": "La reconexión se ha pausado", + "bandStatusReconnectPausedReason": "{n, plural, one{La banda rechazó la clave de emparejamiento {n} vez seguida, así que la app dejó de reintentar en lugar de acaparar la radio y agotar ambas baterías con un enlace que no se abrirá. Nada se está reconectando hasta que actúes.} other{La banda rechazó la clave de emparejamiento {n} veces seguidas, así que la app dejó de reintentar en lugar de acaparar la radio y agotar ambas baterías con un enlace que no se abrirá. Nada se está reconectando hasta que actúes.}}", + "bandStatusRepairNeededTitle": "La banda necesita emparejarse de nuevo", + "bandStatusRepairNeededReason": "El enlace se establece, pero la banda rechaza la clave de cifrado que tiene el teléfono, así que cada comando se descarta y no se mueve ningún dato. Tus registros están a salvo en la banda.", + "bandStatusRepairFix": "Olvida la banda en los ajustes de Bluetooth del teléfono y luego empareja de nuevo aquí", + "bandStatusSyncStuckTitle": "Un lote de registros no terminará de transferirse", + "bandStatusSyncStuckReason": "La banda sigue reenviando el mismo lote porque la app no logra hacerle llegar la confirmación. Todo lo que contiene ya está guardado aquí — no se pierde nada — pero la banda no puede continuar hasta que llegue la confirmación.", + "bandStatusSyncStuckFix": "Reconecta la banda; si se repite mañana, empareja de nuevo", + "bandStatusStrapUnresponsiveTitle": "La banda ha dejado de entregar sus registros", + "bandStatusStrapUnresponsiveReason": "La banda informa de registros más recientes que no está enviando. Esos registros siguen a salvo en la banda; simplemente no los está transfiriendo.", + "bandStatusStrapUnresponsiveFix": "Pon la banda en su cargador un minuto y luego reconéctala", + "bandStatusClockLostTitle": "Las sincronizaciones terminan sin ningún dato", + "bandStatusClockLostReason": "La banda completa cada sincronización sin entregar ni una sola lectura de sensor, lo que casi siempre significa que su reloj interno perdió la sincronía. La app lo reajusta en cada conexión.", + "bandStatusClockLostFix": "Deja la banda conectada unos minutos; si no llega nada para mañana, empareja de nuevo", + "bandStatusConnectedReason": "La banda está enlazada y entregando sus registros.", + "bandStatusConnectingTitle": "Conectando", + "bandStatusConnectingReason": "Abriendo el enlace con la banda.", + "bandStatusScanningTitle": "Buscando la banda", + "bandStatusScanningReason": "Escuchando a que la banda se anuncie.", + "bandStatusDisconnectedReason": "La banda está fuera de alcance, en su cargador, o conectada a otra app. Sigue registrando de todos modos.", + "bandStatusDisconnectedFix": "Acerca la banda al teléfono y cierra cualquier otra app conectada a ella", + "devicesTierBeatToBeatLabel": "Intervalos latido a latido", + "devicesTierBeatToBeatDetail": "Detección eléctrica del pico R.", + "devicesTierWristOpticalLabel": "Pulso óptico de muñeca", + "devicesTierWristOpticalDetail": "Pulso continuo 24/7, sueño y temperatura. El ritmo de los latidos se infiere de una onda de pulso, así que la VFC aquí es en realidad VFP.", + "devicesTierPhoneLabel": "Solo pasos", + "devicesTierPhoneDetail": "El propio coprocesador de movimiento del teléfono. Pasos y nada más.", + "deviceActionNoneLabel": "No hacer nada", + "deviceActionNoneBlurb": "El doble toque no hace nada.", + "deviceActionMediaPlayPauseLabel": "Reproducir / pausar música", + "deviceActionMediaPlayPauseBlurb": "Alternar lo que se esté reproduciendo.", + "deviceActionMediaNextLabel": "Pista siguiente", + "deviceActionMediaNextBlurb": "Saltar a la siguiente pista.", + "deviceActionMediaPrevLabel": "Pista anterior", + "deviceActionMediaPrevBlurb": "Volver a la pista anterior.", + "deviceActionVolumeUpLabel": "Subir volumen", + "deviceActionVolumeUpBlurb": "Subir el volumen multimedia un nivel.", + "deviceActionVolumeDownLabel": "Bajar volumen", + "deviceActionVolumeDownBlurb": "Bajar el volumen multimedia un nivel.", + "deviceActionRingPhoneLabel": "Hacer sonar mi teléfono", + "deviceActionRingPhoneBlurb": "Reproducir un sonido fuerte para encontrar tu teléfono.", + "deviceActionTorchLabel": "Linterna", + "deviceActionTorchBlurb": "Encender o apagar la linterna del teléfono.", + "deviceActionMarkMomentLabel": "Marcar un momento", + "deviceActionMarkMomentBlurb": "Etiquetar el momento actual en tu diario.", + "deviceActionWorkoutToggleLabel": "Iniciar / detener entrenamiento", + "deviceActionWorkoutToggleBlurb": "Empezar o terminar un entrenamiento desde tu muñeca.", + "deviceActionLogWaterLabel": "Registrar agua", + "deviceActionLogWaterBlurb": "Añade un vaso al agua de hoy, el mismo paso que el + de la pantalla de nutrición.", + "deviceActionBroadcastToTaskerLabel": "Emitir a Tasker", + "deviceActionBroadcastToTaskerBlurb": "Enviar un intent de difusión para que Tasker pueda disparar cualquier automatización." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0ee6b645..c50b688f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -293,5 +293,2017 @@ "welcomePartOfFileNotUsedTitle": "Une partie de ce fichier n’a pas pu être utilisée", "welcomeStrandedDays": "{n, plural, one{{n} jour est arrivé dans le désordre et n’a servi que de contexte pour le jour suivant.} other{{n} jours sont arrivés dans le désordre et n’ont servi que de contexte pour le jour suivant.}}", "welcomeLateRows": "{n, plural, one{{n} ligne est arrivée après que son jour avait déjà été noté et clôturé.} other{{n} lignes sont arrivées après que leur jour avait déjà été noté et clôturé.}}", - "welcomeExportAgainInDateOrder": "Exporter à nouveau dans l’ordre chronologique" + "welcomeExportAgainInDateOrder": "Exporter à nouveau dans l’ordre chronologique", + "scanBarcodeTitle": "Scannez le code-barres", + "scanBarcodeClose": "Fermer", + "scanBarcodeInstructions": "Maintenez le code-barres dans le cadre. Rien n'est enregistré — seuls les chiffres sont lus.", + "scanBarcodeNoAccessTitle": "Pas d'accès à la caméra", + "scanBarcodeCameraFailedTitle": "La caméra n'a pas démarré", + "scanBarcodeNoAccessBody": "La numérisation nécessite la caméra, et l'autorisation n'a pas été accordée à cette application.", + "scanBarcodeCameraFailedBody": "Cet appareil n'a pas pu ouvrir sa caméra pour le scanner.", + "scanBarcodeTypeInstead": "Saisir les chiffres à la place", + "findingsLogTitle": "Observations", + "findingsLogEmptyTitle": "Rien à signaler", + "findingsLogEmptyBody": "Les surveillances de la maladie, de la physiologie nocturne inhabituelle, de la température cutanée et d'un changement de votre fréquence cardiaque au repos sont toutes restées silencieuses. C'est un résultat, pas un écran vide.", + "findingsLogDerivedNote": "Calculé à partir de vos propres journées à chaque ouverture, non enregistré au moment où c'est arrivé — donc si une journée est réanalysée, ce qui est affiché ici change avec elle.", + "startCardDefaultSub": "Choisissez-en une et lancez-vous", + "monthGridCoverage": "{have} sur {total} jours", + "monthGridSemanticsLabel": "{title} : {have} sur {total} jours ont une valeur. Nuancé selon votre propre plage.", + "monthGridDaysAgo": "Il y a {days} jours", + "monthGridToday": "Aujourd'hui", + "monthGridFootnote": "Une cellule par jour. Plus la teinte est foncée, plus la journée se situe haut dans VOTRE propre plage — du 10ᵉ au 90ᵉ centile de chaque jour enregistré — et une cellule au contour vide est un jour sans valeur, pas un jour bas. Plus d'effort n'est pas un meilleur effort, et dormir plus longtemps n'est pas mieux dormir ; ceci indique où se situait une journée, pas comment elle s'est passée.", + "monthGridNotShadedYetTitle": "{title} n'est pas encore nuancé", + "monthGridNotShadedYetBody": "{days, plural, one{{days} jour} other{{days} jours}} ne constitue pas une plage — la nuance indique où se situe une journée dans votre propre plage. Elle apparaît à partir de {min}.", + "whatChangedTitle": "Ce qui a changé", + "whatChangedSub": "SELON VOTRE PROPRE HISTORIQUE", + "whatChangedNoDataTitle": "Rien n'est encore arrivé pour ce jour", + "whatChangedNoDataBody": "L'analyse compare un jour à ceux qui le précèdent, et ce jour n'a aucune valeur à comparer. Rien n'est inhabituel car rien n'est connu.", + "whatChangedLearningTitle": "Encore en train d'apprendre vos habitudes", + "whatChangedLearningBody": "L'inhabituel n'a de sens que face à une plage, et il y a {days, plural, one{{days} jour} other{{days} jours}} d'historique derrière celui-ci. L'analyse commence à partir de {min}.", + "whatChangedNothingTitle": "Rien ne ressort", + "whatChangedNothingBody": "Chaque indicateur disposant d'un historique suffisant est resté dans la plage définie par vos propres journées. C'est la réponse normale, et c'est une réponse complète.", + "whatChangedMethodologyNote": "Mesuré par rapport à vos propres jours précédents, dans vos propres unités, avec la fenêtre indiquée — pour que vous puissiez le remettre en question. Rien ici n'est une cause ni un diagnostic.", + "whatChangedDayLinkTitle": "Ce qui s'est passé ce jour-là", + "whatChangedDayLinkSub": "Sommeil, séances, repas et notes dans l'ordre chronologique", + "whatChangedMonthSection": "Le mois qui l'entoure", + "journalFieldErrorNoName": "Donnez-lui un nom", + "journalFieldErrorInvalidName": "Utilisez au moins une lettre ou un chiffre", + "journalFieldErrorNoUnit": "Précisez l'unité de mesure (mg, ml, tasses…)", + "journalFieldErrorDuplicate": "Vous suivez déjà quelque chose sous ce nom", + "journalFieldTitle": "Suivre autre chose", + "journalFieldNameLabel": "Que voulez-vous suivre ?", + "journalFieldNameHint": "Magnésium, temps d'écran, mal de tête…", + "journalFieldKindQuestion": "Quel type de nombre est-ce ?", + "journalFieldKindRating": "Une note de 1 à 5", + "journalFieldKindAmount": "Une quantité", + "journalFieldKindMinutes": "Minutes", + "journalFieldUnitLabel": "Unité", + "journalFieldUnitHint": "mg, ml, tasses…", + "journalFieldStepSize": "Pas d'incrément", + "journalFieldMaxPerDay": "Le maximum que vous consigneriez en une journée", + "journalFieldAskLastTime": "Demander quand c'était la dernière fois", + "journalFieldStartTracking": "Commencer le suivi", + "aiBriefingForDay": "POUR {day}", + "aiBriefingNoModelTitle": "Aucun modèle n'est configuré", + "aiBriefingNoModelBody": "Un briefing est rédigé par un modèle que vous choisissez. Tant que vous n'en avez pas choisi un, il n'y a rien à générer et rien n'a été envoyé où que ce soit.", + "aiBriefingChooseModel": "Choisir un modèle", + "aiBriefingNothingTitle": "Rien n'a été rédigé pour aujourd'hui", + "aiBriefingNothingBody": "Les briefings sont générés selon un planning, ou à la demande ici.", + "aiBriefingWriting": "Rédaction…", + "aiBriefingWriteNow": "En rédiger un maintenant", + "aiBriefingWriteAgain": "Le rédiger à nouveau", + "aiBriefingFailedTitle": "Cela n'a pas abouti", + "aiBriefingFailedGeneric": "Échec : {error}", + "aiBriefingSentSection": "Ce qui a été envoyé", + "aiBriefingReadSection": "Ce qui a été lu", + "aiBriefingNoneBody": "Rien. Il n'y a eu aucune requête — la note ci-dessus a été rédigée sur ce téléphone.", + "aiBriefingLocalBody": "Ces chiffres sont allés vers {host}, sur cette machine. Rien n'en est sorti.", + "aiBriefingCloudBody": "Ces chiffres, et rien d'autre, ont été envoyés à {host} en tant que {model}. Aucun enregistrement brut, aucun nom, aucun identifiant.", + "aiBriefingNoneCardTitle": "Rien ne s'est démarqué, donc rien n'a été demandé", + "aiBriefingNoneCardBody": "Le balayage s'exécute sur ce téléphone. Il n'appelle un modèle que lorsqu'il a un constat à lui transmettre, et aujourd'hui il n'en avait aucun.", + "aiBriefingEmptyCardTitle": "Il n'y avait rien à envoyer", + "aiBriefingEmptyCardBody": "Aucune métrique n'avait de valeur au moment de la rédaction, donc l'invite n'en contenait aucune.", + "napsFellAsleepHelp": "QUAND VOUS VOUS ÊTES ENDORMI", + "napsWokeUpHelp": "QUAND VOUS VOUS ÊTES RÉVEILLÉ", + "napsInvalidWindow": "Une sieste dure entre 5 minutes et 6 heures. Au-delà, il s'agit d'un sommeil, et cela appartient à la nuit, où les phases peuvent être lues.", + "napsOverlap": "Cela chevauche une sieste déjà enregistrée ce jour-là. Supprimez d'abord celle-ci plutôt que de compter la même heure deux fois.", + "napsNotReanalysed": "La journée n'a pas été réanalysée — une autre analyse était déjà en cours. Votre modification est enregistrée et s'appliquera la prochaine fois.", + "napsTitle": "Siestes", + "napsNoReadingTitle": "Aucune donnée de sieste pour cette journée", + "napsNoReadingBody": "Les siestes sont calculées à partir du même enregistrement à 1 Hz que le reste de la journée, et cette journée n'en compte pas assez.", + "napsEmptyTitle": "Aucune sieste ce jour-là", + "napsEmptyBody": "Rien, ce jour-là, n'est resté assez immobile, assez longtemps, avec la baisse de fréquence cardiaque qui accompagne le sommeil.", + "napsCountsToward": "{mins} de sieste comptent dans votre besoin de sommeil de ce soir.", + "napsNotAppliedTitle": "Cela n'a pas été appliqué", + "napsWorking": "Traitement en cours…", + "napsLogANap": "Enregistrer une sieste", + "napsRemovedSection": "Supprimées", + "napsPutBackSemantic": "Remettre cette sieste", + "napsPutBackLabel": "Remettre", + "napsRemovalKept": "Une suppression est conservée sous forme de plage horaire plutôt que d'identifiant, afin qu'elle reste valable même si les limites du détecteur se déplacent.", + "napsYouLoggedThis": "Vous l'avez enregistrée", + "napsDetected": "Détectée", + "napsLoggedWithMins": "{mins} · vous l'avez enregistrée", + "napsDetectedWithMins": "{mins} de sommeil · détectée", + "napsDeleteSemantic": "Supprimer cette sieste", + "napsNotANapSemantic": "Ce n'était pas une sieste", + "napsDeleteLabel": "Supprimer", + "napsNotANapLabel": "Pas une sieste", + "readinessDetailTitle": "Forme", + "readinessDetailNotScoredTitle": "La forme n'a pas de score", + "readinessDetailLastNightScored": "La dernière nuit notée était le {day}.", + "readinessDetailWhatWasMissing": "Ce qui manquait", + "readinessDetailWhatWentIntoIt": "Ce qui a été pris en compte", + "readinessDetailInputsFooter": "{used}/{total} entrées. Chacune est classée par rapport à votre propre historique — une vue parallèle des mêmes entrées, pas une décomposition du chiffre ci-dessus.", + "readinessDetailNoBreakdownTitle": "Pas encore de détail", + "readinessDetailNoBreakdownBody": "Classer chaque entrée par rapport à votre propre historique nécessite environ deux semaines de nuits.", + "readinessDetailHistoryTitle": "Historique", + "readinessDetailLastNDays": "{n, plural, one{Dernier {n} jour} other{Derniers {n} jours}}", + "readinessDetailNoHistoryTitle": "Aucun historique de forme", + "readinessDetailNoHistoryBody": "0 jour noté.", + "readinessDetailWearOvernight": "Portez le bracelet pendant la nuit", + "readinessDetailUnit": "/100", + "readinessDetailDaysAgo": "{n, plural, one{il y a {n} jour} other{il y a {n} jours}}", + "readinessDetailToday": "Aujourd'hui", + "readinessDetailMeasured": "Mesuré", + "readinessDetailNotMeasured": "Non mesuré", + "readinessDetailNightsOfHistory": "{n, plural, one{{n} nuit de votre propre historique} other{{n} nuits de votre propre historique}}", + "readinessDetailNeedSuffix": "{need}. Chaque entrée est classée par rapport à vos propres nuits, donc le score ne peut pas commencer avant qu'il y en ait suffisamment.", + "readinessDetailNoNoteFallback": "Tout ce qui précède était présent, et pourtant la comparaison avec votre propre historique n'a pas pu être faite.", + "readinessDetailNotAvailable": "non disponible", + "readinessDetailContributionNotReported": "contribution non indiquée", + "readinessDetailRelativeUncalibrated": "relative, non calibrée", + "readinessDetailWithinSpread": "dans votre écart habituel", + "readinessDetailWeightPercent": "{pct} % de poids", + "dayStepsTitle": "Pas", + "dayStepsThroughDay": "Au fil de la journée", + "dayStepsToday": "aujourd'hui", + "dayStepsOnDay": "le {day}", + "dayStepsNoTimesTitle": "Aucun horaire derrière ce total {when}", + "dayStepsNoStepsTitle": "Aucun pas compté {when}", + "dayStepsStrapCounterBody": "Les {count} pas comptés {when} proviennent du compteur de pas propre au bracelet, qui indique un total journalier sans horaires. Il n'y a rien à placer sur une horloge.", + "dayStepsNothingCounted": "Rien de capable de compter les pas n'a enregistré de données {when}.", + "dayStepsChartTitle": "QUAND ILS ONT ÉTÉ COMPTÉS", + "dayStepsUnit": "pas", + "dayStepsYourPhone": "Votre téléphone", + "dayStepsYourBand": "Votre bracelet", + "dayStepsCounted": "Comptés", + "dayStepsHonestyMixed": "Comptés au poignet et par votre téléphone, et les deux se trompent différemment : le poignet sous-estime une vraie marche et peut confondre un mouvement rythmique de la main avec de la marche, tandis que le téléphone ne compte que les pas faits en le portant sur vous.", + "dayStepsHonestyStrap": "Comptés au poignet, où une vraie marche a tendance à être sous-estimée et où un mouvement rythmique de la main peut être lu comme de la marche.", + "dayStepsHonestyPhone": "Comptés par votre téléphone, donc seuls les pas faits en le portant sur vous apparaissent ici.", + "roughNightSignRhr": "votre fréquence cardiaque au repos a augmenté", + "roughNightSignHrv": "votre VFC a diminué", + "roughNightSignDip": "votre fréquence cardiaque a moins baissé pendant la nuit que d'habitude", + "roughNightSignTemp": "votre peau était plus chaude", + "roughNightLateTraining": "Vous vous êtes entraîné jusqu'à {at}, ce qui provoque souvent cela à soi seul.", + "roughNightIllness": "Le suivi de maladie a lui aussi signalé cette nuit — une hausse soutenue par rapport à votre propre base, pas un diagnostic.", + "roughNightLuteal": "Vous êtes en phase lutéale, ce qui augmente à soi seul la fréquence cardiaque au repos et la température cutanée.", + "roughNightWarmRoom": "Votre peau était plus chaude que d'habitude — une pièce chaude provoque aussi cela.", + "roughNightDismiss": "Ignorer", + "roughNightDefaultHeadline": "Une nuit plus difficile que d'habitude", + "roughNightSummary": "{sentence}, par rapport à vos propres nuits. Ceci est une mesure de la nuit, pas un jugement sur vous.", + "roughNightTellWhatHappened": "Dire ce qui s'est passé", + "roughNightNothingToAnswer": "Rien à répondre — cette carte ne fait que rapporter la nuit.", + "roughNightWhatElse": "Que se passait-il d'autre ?", + "roughNightAnythingElse": "Autre chose ?", + "roughNightSaving": "Enregistrement", + "roughNightLogIt": "Enregistrer pour cette nuit", + "roughNightAddHowMuch": "Ajouter la quantité", + "roughNightDoNotAskAgain": "Ne plus demander", + "roughNightSeveralMoved": "Plusieurs mesures nocturnes ont évolué ensemble", + "driverBreakdownHigherThanUsual": "Plus élevé que d’habitude", + "driverBreakdownLowerThanUsual": "Plus bas que d’habitude", + "driverBreakdownRightOnUsual": "{now} · exactement comme d’habitude", + "driverBreakdownAboveUsual": "{now} · {delta} au-dessus de votre habitude de {usual}", + "driverBreakdownBelowUsual": "{now} · {delta} en dessous de votre habitude de {usual}", + "driverBreakdownWeightPct": "{pct} % de poids", + "driverBreakdownNotAvailable": "non disponible", + "driverBreakdownContributionNotReported": "contribution non signalée", + "driverBreakdownRelativeUncalibrated": "relatif, non calibré", + "driverBreakdownWithinUsualSpread": "dans votre écart habituel", + "driverBreakdownBiggerThanNoise": "supérieur au bruit de mesure", + "driverBreakdownSmallerThanNoise": "hors de votre écart habituel, mais assez faible pour être du bruit de mesure", + "driverBreakdownWhatHelped": "Ce qui a aidé", + "driverBreakdownWhatHeldYouBack": "Ce qui vous a freiné", + "driverBreakdownNeither": "Ni l’un ni l’autre", + "driverBreakdownFooter": "Chaque donnée est comparée à votre propre historique — une vue parallèle des mêmes entrées, pas des fragments du score lui-même. Le « bruit de mesure » désigne l’écart qu’une mesure peut avoir à elle seule sans que rien n’ait changé. Des tendances dans vos propres relevés, pas des causes.", + "driverBreakdownHideHistory": "{label}, masquer son historique", + "driverBreakdownShowHistory": "{label}, afficher son historique", + "driverBreakdownDaysAgo": "{n, plural, one{il y a {n} jour} other{il y a {n} jours}}", + "driverBreakdownToday": "Aujourd’hui", + "driverBreakdownUsualRange": "Votre plage habituelle {lo}–{hi}{unit}", + "driverBreakdownAbsenceTitle": "Aucune décomposition à afficher", + "driverBreakdownAbsenceAlgoVersion": "La façon de calculer la disposition a changé avec la dernière mise à jour, et elle est en cours de reconstruction.", + "driverBreakdownAbsenceStale": "Le dernier récapitulatif est trop ancien pour être garanti.", + "driverBreakdownAbsenceNoVersion": "Le récapitulatif enregistré ne porte aucune marque de version.", + "driverBreakdownSyncTheBand": "Synchroniser le bracelet", + "driverBreakdownAbsenceNoReason": "Aucun enregistrement n’explique pourquoi la nuit dernière n’a pas de décomposition.", + "coachFiguresCouldNotBeDrawn": "Impossible de tracer une figure", + "coachFiguresNoType": "Le coach a envoyé une figure sans type.", + "coachFiguresUnsupportedType": "Le coach a demandé une figure de type « {type} », que cette application ne sait pas tracer.", + "coachFiguresFigure": "Figure", + "coachFiguresSeriesN": "Série {n}", + "coachFiguresLaneN": "Voie {n}", + "coachFiguresNoSleepSegments": "Aucun segment de sommeil", + "coachFiguresNoTimeInZone": "Aucun temps en zone", + "coachFiguresMinTotal": "{n} min au total", + "coachFiguresGauge": "Jauge", + "coachFiguresGaugeNoValue": "Le coach a envoyé une jauge sans valeur.", + "coachFiguresSummary": "Résumé", + "coachFiguresEmptySummary": "Le coach a envoyé un résumé vide.", + "coachFiguresTable": "Tableau", + "coachFiguresTableNoRows": "Le coach a envoyé un tableau sans lignes.", + "circadianDetailTitle": "Horloge biologique", + "circadianDetailNoNightsTitle": "Aucune nuit à représenter pour l'instant", + "circadianDetailNoNightsBody": "0 nuit notée.", + "circadianDetailNoNightsFix": "Portez la bande pendant la nuit", + "circadianDetailSleepTitle": "Sommeil, nuit après nuit", + "circadianDetailAsleep": "Endormi", + "circadianDetailSleepFootnote": "{count, plural, one{{count} nuit, une colonne chacune. Plus c'est foncé, plus vous étiez endormi cette heure-là.} other{{count} nuits, une colonne chacune. Plus c'est foncé, plus vous étiez endormi cette heure-là.}}", + "circadianDetailYourRhythm": "Votre rythme", + "circadianDetailWhichNights": "Quelles nuits", + "circadianDetailHide": "Masquer", + "circadianDetailShow": "Afficher", + "circadianDetailTodayPredicted": "Aujourd'hui, prévu", + "circadianDetailRhythmStrength": "Force du rythme", + "circadianDetailWhenStill": "Quand vous êtes immobile", + "circadianDetailNoStillTitle": "Pas encore de moments d'immobilité à lire", + "circadianDetailNoStillBody": "Ceci lit le rythme cardiaque uniquement pendant les secondes où vous étiez immobile, et {days, plural, one{le dernier jour en a eu} other{les {days} derniers jours en ont eu}} trop peu pour construire une heure.", + "circadianDetailStillnessTitle": "Variabilité battement à battement au repos", + "circadianDetailStillnessFootnote": "Chaque heure est la valeur médiane de {lo}–{hi} périodes de cinq minutes où vous étiez vraiment immobile, sur {days, plural, one{le dernier jour} other{les {days} derniers jours}} — jamais uniquement aujourd'hui. {drawn} heures sur 24 ont eu au moins trois périodes ; le reste est vide. Ce n'est pas un score de stress — s'asseoir, une pièce chaude ou un café le font varier tout autant.", + "circadianDetailForecastTitle": "Comment la journée devrait probablement se dérouler", + "circadianDetailForecastFootnote": "Pas d'échelle — la forme est le seul résultat.", + "circadianDetailTroughText": "Le passage le plus plat se situe vers {troughLabel}, autour de {start}–{end}.", + "circadianDetailPredictionDisclaimer": "C'est une prédiction, pas une mesure. Rien sur la bande ne mesure votre niveau d'éveil, et elle ne connaît que la nuit dernière et rien d'autre — une sieste, un café ou tout ce qui se passe aujourd'hui ne l'atteint jamais.", + "circadianDetailAssumedPhaseNote": "Votre propre pic d'horloge n'est pas encore déterminé, donc une moyenne est utilisée.", + "circadianDetailNotADrivingCheck": "Ce n'est ni un contrôle d'aptitude à la conduite ni un outil de sécurité au travail posté, et cela ne signifie pas que vous êtes diminué.", + "circadianDetailChronotype": "Chronotype", + "circadianDetailMidSleepFree": "Milieu de sommeil, jours libres", + "circadianDetailMidSleepWork": "Milieu de sommeil, jours travaillés", + "circadianDetailSocialJetlag": "Jetlag social", + "circadianDetailLater": "plus tard", + "circadianDetailEarlier": "plus tôt", + "circadianDetailNightsCompared": "Nuits libres / travaillées comparées", + "circadianDetailRegularityIndex": "Indice de régularité", + "circadianDetailNightsLeastAlike": "Nuits les moins semblables", + "circadianDetailSamePairScale": "Cette paire, même échelle", + "circadianDetailRhythmNotEstablished": "Votre rythme n'est pas encore établi", + "circadianDetailPairFootnote": "La paire la moins concordante, sur {count}. Un week-end qui s'étire correspond à un horaire différent, pas à une nuit moins bonne. Les paires où trop peu de l'un des deux jours a été enregistré sont exclues.", + "circadianDetailStability": "Stabilité d'un jour à l'autre", + "circadianDetailFragmentation": "Fragmentation d'une heure à l'autre", + "circadianDetailAmplitude": "Amplitude relative", + "circadianDetailM10Start": "Début des 10 heures de FC la plus élevée", + "circadianDetailL5Start": "Début des 5 heures de FC la plus basse", + "circadianDetailRhythmPeak": "Pic du rythme", + "circadianDetailPeakSwing": "Amplitude pic-moyenne", + "circadianDetailFitCurve": "Ajustement à une courbe de 24 h", + "circadianDetailStrengthNotMeasured": "La force du rythme n'est pas encore mesurée", + "circadianDetailStrengthWhy": "Nécessite des jours consécutifs avec les 24 heures enregistrées.", + "circadianDetailStrengthFootnoteKnown": "À partir de {used, plural, one{{used} jour entièrement enregistré} other{{used} jours entièrement enregistrés}} de fréquence cardiaque. Ce sont vos heures de FC les plus hautes et les plus basses, pas les plus actives.", + "circadianDetailStrengthFootnoteUnknown": "À partir d'une série de jours entièrement enregistrés de fréquence cardiaque. Ce sont vos heures de FC les plus hautes et les plus basses, pas les plus actives.", + "beatsTitle": "Battements", + "beatsNoNightTitle": "Aucune nuit à tracer pour l'instant", + "beatsNoNightBody": "Aucune nuit dérivée n'a été produite sur ce téléphone, donc il n'y a aucun intervalle de battement à tracer.", + "beatsNoNightFix": "Portez la bande pendant la nuit, puis synchronisez", + "beatsNightOf": "Nuit du {date}", + "beatsPoincareSection": "Chaque battement face au précédent", + "beatsBeatsGoneTitle": "Les battements de cette nuit ne sont plus sur ce téléphone", + "beatsBeatsGoneBody": "Les intervalles de battement individuels sont conservés quelques jours après la notation de la nuit, puis supprimés. Les valeurs qui en sont tirées sont conservées définitivement", + "beatsBeatsGoneMeasured": " — cette nuit a mesuré SD1 {sd1} ms, SD2 {sd2} ms", + "beatsScatterTitle": "Chaque intervalle, tracé contre le précédent", + "beatsScatterFootnote": "La diagonale correspond à un battement de même durée que le précédent. La dispersion perpendiculaire à cette ligne est le SD1, battement par battement ; le long de la ligne, c'est le SD2, la dérive plus lente.", + "beatsSd1Label": "SD1", + "beatsSd2Label": "SD2", + "beatsIntervalsLabel": "Intervalles", + "beatsIntervalsSurvived": "{count, plural, one{{count} intervalle a survécu à la correction} other{{count} intervalles ont survécu à la correction}}", + "beatsDroppedArtifact": " — {count, plural, one{{count} a été rejeté comme artefact et n'est pas} other{{count} ont été rejetés comme artefacts et ne sont pas}} dans le cloud", + "beatsPulseNotEcg": "Pouls, pas ECG — réel et vous appartenant, mais pas l'image que trace un ECG.", + "beatsMeasuredOn": "Mesuré avec {device} ; les bandes ne lisent pas les mêmes chiffres entre elles.", + "beatsVariabilitySection": "Variabilité au fil de la nuit", + "beatsUnitNights": "nuits", + "beatsUnitScreenedNotScreened": "dépisté / non dépisté", + "beatsVariabilityWhy": "Aucun bloc de trente minutes de cette nuit n'a eu assez de battements propres pour publier un RMSSD.", + "beatsNoBinsStored": "Aucun bloc n'a été enregistré pour cette nuit.", + "beatsRmssdTitle": "RMSSD par blocs de trente minutes", + "beatsStart": "Début", + "beatsBandFootnote": "La barre indique notre niveau de confiance dans le bloc, pas une plage traversée par votre corps. Le repère à l'intérieur est la valeur.", + "beatsHolesFootnote": "{count, plural, one{ {count} bloc a trop peu de battements propres pour en publier un, et reste vide plutôt que relié.} other{ {count} blocs ont trop peu de battements propres pour en publier un, et restent vides plutôt que reliés.}}", + "beatsFirstThird": "Premier tiers", + "beatsLastThird": "Dernier tiers", + "beatsDcSection": "Capacité de décélération", + "beatsDcWhy": "Aucune nuit enregistrée n'en a encore produit une.", + "beatsDcNoData": "Aucune nuit n'en a encore produit une.", + "beatsDcChartTitle": "Vos propres nuits, dans l'ordre", + "beatsDaysAgo": "il y a {count} jours", + "beatsTodayLabel": "Aujourd'hui", + "beatsAnchorsLastNight": "Ancres la nuit dernière", + "beatsCleanBeats": "Battements propres", + "beatsDcNote": "Uniquement le vôtre. Comparez-le à vos propres autres nuits et à rien d'autre — il n'existe pas de plage de référence pour un poignet.\n\nCela fait la moyenne des battements autour de chaque moment où votre cœur a ralenti. Une ligne montante peut refléter un signal plus propre plutôt qu'un cœur différent, alors lisez-la avec le nombre d'ancres et la part de battements propres ci-dessus. Si vous avez changé de bande pendant cette fenêtre, les deux moitiés ne sont pas comparables.", + "beatsRhythmSection": "Dépistage du rythme", + "beatsRhythmChartTitle": "Une cellule par jour", + "beatsScreenNotFired": "Le dépistage ne s'est pas déclenché", + "beatsScreenFired": "Le dépistage s'est déclenché", + "beatsNotScreened": "Non dépisté", + "beatsNoDayScreened": "Aucun jour de cette fenêtre n'a été dépisté", + "beatsScreenNote": "Un dépistage, pas un test.\n\nUn jour où le dépistage ne s'est pas déclenché n'est pas un jour où vous êtes blanchi — il ne peut rien exclure, et ne l'a jamais pu. Les jours en contour n'ont pas été du tout dépistés : trop peu de battements propres, ou trop de mouvement.\n\nLe pouls au poignet n'est pas un ECG. Si ce sont des symptômes qui vous amènent ici, un professionnel de santé peut faire le test approprié.", + "beatsScreenedSummary": "{screened} des {win} derniers jours ont été dépistés", + "beatsFiredSummary": " ; le dépistage s'est déclenché {fired} fois", + "beatsNotScreenedNote": " La nuit dernière n'a pas été dépistée : {note}", + "logWorkoutCouldNotLog": "Impossible d’enregistrer cette séance — réessayez.", + "logWorkoutCouldNotDismiss": "Impossible d’ignorer cette séance — réessayez.", + "logWorkoutAdjustTimes": "Ajuster les horaires", + "logWorkoutDetectedActivityTitle": "Activité détectée", + "logWorkoutYoursToConfirmSub": "À VOUS DE CONFIRMER", + "logWorkoutReadFailedTitle": "Impossible de lire votre activité détectée", + "logWorkoutReadFailedBody": "La base de données n’a pas répondu. Rien n’a été enregistré ni ignoré.", + "logWorkoutTryAgain": "Réessayer", + "logWorkoutReadingSpotted": "Lecture de ce que le bracelet a repéré…", + "logWorkoutNothingToReviewTitle": "Rien à examiner", + "logWorkoutNothingToReviewBody": "Celle-ci a peut-être déjà été enregistrée ou ignorée.", + "logWorkoutHardMinutesTitle": "Ce sont les minutes d’effort intense, pas toute la séance", + "logWorkoutHardMinutesBody": "La détection ne rapporte que l’effort soutenu qu’elle a pu observer, donc un échauffement et les temps de repos entre les séries en sont exclus. Ajustez les horaires avant d’enregistrer si la plage est courte.", + "logWorkoutMinutesOfEffort": "{mins} min d’effort", + "logWorkoutAvgHr": "FC moy.", + "logWorkoutPeakHr": "FC max.", + "logWorkoutLooksLike": "On dirait", + "logWorkoutLogIt": "Enregistrer", + "logWorkoutNotAWorkout": "Ce n’est pas une séance", + "logWorkoutToday": "Aujourd’hui", + "logWorkoutYesterday": "Hier", + "logWorkoutDefaultTitle": "Enregistrer une séance passée", + "logWorkoutWindowRescoredSub": "LA PLAGE, RECALCULÉE", + "logWorkoutYourOwnTimesSub": "VOS PROPRES HORAIRES", + "logWorkoutWhenGroup": "Quand", + "logWorkoutActivityLabel": "Activité", + "logWorkoutDateLabel": "Date", + "logWorkoutStartedLabel": "Début", + "logWorkoutEndedLabel": "Fin", + "logWorkoutLengthLabel": "Durée", + "logWorkoutNextMorningSub": "le lendemain matin", + "logWorkoutWindowInvalidTitle": "Cette plage ne pourra pas être enregistrée", + "logWorkoutTimesUpdatedTitle": "Horaires mis à jour", + "logWorkoutLoggedTitle": "Séance enregistrée", + "logWorkoutUnscoredSaved": "Enregistré. Aucune fréquence cardiaque n’a été relevée sur cette plage, donc elle n’a ni effort ni calories — seuls les horaires sont conservés.", + "logWorkoutCouldNotSave": "Impossible d’enregistrer — réessayez.", + "logWorkoutScoredTitle": "Calculé à partir de ce que le bracelet a enregistré", + "logWorkoutScoredBody": "L’effort et les calories proviennent de la fréquence cardiaque seconde par seconde sur ces horaires, avec la même méthode que pour la journée. Rien n’est estimé à partir de la durée.", + "logWorkoutSaving": "Enregistrement…", + "logWorkoutSaveNewTimes": "Enregistrer les nouveaux horaires", + "logWorkoutSearchActivities": "Rechercher une activité", + "logWorkoutNoActivityByName": "Aucune activité portant ce nom", + "logFoodTitle": "Enregistrer un repas", + "logFoodClose": "Fermer", + "logFoodIAte": "J’ai mangé : {meal}", + "logFoodAgain": "À nouveau", + "logFoodAddNumbers": "Ajouter les chiffres", + "logFoodScanBarcode": "Scanner un code-barres", + "logFoodScanSubOn": "Interroge openfoodfacts.org sur le code-barres et remplit ce qu’il peut garantir", + "logFoodScanSubOff": "Recherche le produit en ligne. Demande d’abord", + "logFoodLookingUpTitle": "Recherche en cours", + "logFoodLookingUpBody": "Les champs se remplissent dès que la réponse arrive.", + "logFoodWhatLabel": "Quoi", + "logFoodWhatHint": "Poulet et riz", + "logFoodPortionLabel": "Portion", + "logFoodUnknownHint": "inconnu", + "logFoodEnergyLabel": "Énergie", + "logFoodProteinLabel": "Protéines", + "logFoodCarbsLabel": "Glucides", + "logFoodFatLabel": "Lipides", + "logFoodFibreLabel": "Fibres", + "logFoodBlankHint": "Un nombre laissé vide reste vide. Seul « Quoi » est nécessaire.", + "logFoodSayWhatFirst": "Indiquez d’abord de quoi il s’agit.", + "logFoodConsentTitle": "Rechercher les codes-barres en ligne ?", + "logFoodConsentBody1": "Un scan envoie le code-barres à openfoodfacts.org, une base de données alimentaire libre et gratuite. Ce site voit le code-barres et votre adresse IP. Rien sur vous, vos repas ou votre santé ne quitte ce téléphone, et un code-barres déjà scanné reçoit une réponse à partir de votre propre copie sans nouvelle requête.", + "logFoodConsentBody2": "Les chiffres sont saisis par le public et pas mal d’entre eux sont erronés, donc tout ce qui échoue à un contrôle de cohérence est laissé vide plutôt que rempli. Tout ce qui est rempli reste modifiable avant d’enregistrer.", + "logFoodConsentBody3": "Vous pouvez désactiver cela dans Réglages › Confidentialité. Saisir les chiffres depuis l’emballage fonctionne dans tous les cas.", + "logFoodAllowLookups": "Autoriser les recherches", + "logFoodNotNow": "Pas maintenant", + "logFoodBreakfast": "Petit-déjeuner", + "logFoodLunch": "Déjeuner", + "logFoodDinner": "Dîner", + "logFoodSnack": "Encas", + "logFoodNoNumbersTitle": "Aucun chiffre pour ce produit", + "logFoodNoNumbersBody": "Open Food Facts connaît le produit, mais rien d’exploitable côté nutrition — ou ce qui existait n’a pas passé le contrôle de cohérence.", + "logFoodNotFoundTitle": "Absent d’Open Food Facts", + "logFoodNotFoundBody": "Personne n’a encore ajouté ce code-barres.", + "logFoodFlaggedTitle": "Cette fiche est signalée comme erronée", + "logFoodFlaggedBody": "Open Food Facts signale que ce produit contient des erreurs, donc aucun de ses chiffres n’a été rempli.", + "logFoodUnreachableTitle": "Aucune réponse d’Open Food Facts", + "logFoodUnreachableBody": "Impossible de joindre openfoodfacts.org.", + "logFoodRefusedTitle": "La recherche de codes-barres est désactivée", + "logFoodRefusedBody": "Rien n’a été envoyé. Vous pouvez l’activer dans Réglages › Confidentialité.", + "logFoodPortionNoteBase": "Open Food Facts indique ces valeurs pour 100 g. Changez la portion et les chiffres suivent.", + "logFoodPortionNoteServing": "Open Food Facts indique ces valeurs pour 100 g. Changez la portion et les chiffres suivent. La portion propre à l’emballage est {serving}.", + "logFoodPillOpenFoodFacts": "Open Food Facts", + "logFoodPillYours": "À vous", + "logFoodBareOccasion": "ENREGISTRÉ · ÉNERGIE NON RENSEIGNÉE", + "logFoodOpensInBrowser": "{label}, s’ouvre dans votre navigateur", + "dayTimelineChargerOn": "Sur le chargeur", + "dayTimelineChargerOff": "Retiré du chargeur", + "dayTimelineDoubleTap": "Vous avez tapé deux fois sur le bracelet", + "dayTimelineRestarted": "Le bracelet a redémarré", + "dayTimelineBatteryPackAttached": "Batterie externe fixée", + "dayTimelineBatteryPackRemoved": "Batterie externe retirée", + "dayTimelineAlarmWentOff": "L'alarme a sonné", + "dayTimelineAsleep": "Endormi", + "dayTimelineNap": "Sieste", + "dayTimelineWorkout": "Entraînement", + "dayTimelineBandOffWrist": "Bracelet retiré du poignet", + "dayTimelineHighestHr": "Fréquence cardiaque la plus élevée", + "dayTimelineLowestHr": "Fréquence cardiaque la plus basse", + "dayTimelineBpmAt": "{bpm} bpm à {time}", + "dayTimelineTakenAt": "Pris à {time}", + "dayTimelineLastAt": "dernier à {time}", + "dayTimelineTaggedTitle": "Étiqueté", + "dayTimelineTitle": "Résumé de votre journée", + "dayTimelineSub": "DE MINUIT À MINUIT", + "dayTimelineHeartRateTitle": "Fréquence cardiaque", + "dayTimelineMidnight": "Minuit", + "dayTimelineNoon": "Midi", + "dayTimelineMoving": "En mouvement", + "dayTimelineNotRecorded": "Non enregistré", + "dayTimelineNothingRecordedTitle": "Rien n'a été enregistré ce jour-là", + "dayTimelineNothingRecordedBody": "Pas de sommeil, pas de séance, pas de note, pas d'événement du bracelet horodaté. Une journée vide comme celle-ci correspond en général à un jour sans le bracelet.", + "dayTimelineNoTimeTitle": "Rien ce jour-là n'a d'heure associée", + "dayTimelineNoTimeBody": "Ce qui a été noté figure ci-dessous.", + "dayTimelineWhatHappenedSection": "Ce qui s'est passé", + "dayTimelineAlsoLoggedSection": "Aussi noté ce jour-là", + "dayTimelineNoTimeNote": "Ceci a été enregistré pour la journée mais sans heure précise, donc ce n'est pas placé sur la frise.", + "dayTimelinePatternsNote": "Des motifs dans vos propres données, pas des causes. Deux éléments proches ici se sont simplement produits près l'un de l'autre dans le temps, c'est tout ce que cette page affirme.", + "journalComposeNotReady": "Pas encore prêt — ouvrez d'abord l'application.", + "journalComposeSaveFailed": "Impossible d'enregistrer — vérifiez le stockage et réessayez.", + "journalComposeWhenWasLastOne": "Quand était la dernière fois ?", + "journalComposeTitle": "Journal", + "journalComposeTodaySection": "Aujourd'hui", + "journalComposeTrackSomethingElse": "Suivre autre chose", + "journalComposeAnythingElseLabel": "Autre chose", + "journalComposeAnythingElseHint": "Une ligne sur la journée.", + "journalComposeSavingLabel": "Enregistrement", + "journalComposeHowAreYouFeeling": "Comment vous sentez-vous ?", + "journalComposeNotAnsweredYet": "Pas encore répondu", + "journalComposeMoodOfFive": "Humeur {value} sur 5 · appuyez à nouveau pour effacer", + "journalComposeMoodOfFiveSelected": "Humeur {n} sur 5, sélectionnée. Appuyez pour effacer.", + "journalComposeMoodOfFiveLabel": "Humeur {n} sur 5", + "journalComposeNotLogged": "Non renseigné", + "journalComposeWhenWasLastField": "Quand était le dernier {field}", + "journalComposeAddTimeOfLastOne": "Ajouter l'heure de la dernière fois", + "journalComposeLastAt": "Dernier à {time}", + "journalComposeIncrease": "Augmenter", + "journalComposeDecrease": "Diminuer", + "journalComposeWeightLabel": "Poids", + "journalComposeNotEntered": "Non renseigné", + "journalComposeEnteredNotMeasured": "{value} · saisi, non mesuré", + "journalComposeEnterWeight": "Saisir le poids", + "journalComposeEnter": "Saisir", + "journalComposeChange": "Modifier", + "journalComposeSeeWeightTrend": "Voir la tendance de poids", + "journalComposeSeeTheTrend": "Voir la tendance", + "journalComposeWeightToday": "Poids du jour", + "journalComposeWeightKgLabel": "Poids (kg)", + "journalComposeWeightScaleNote": "Ce que vous ou votre balance mesurez. Le bracelet ne mesure pas ceci.", + "journalComposeClear": "Effacer", + "journalComposeNotEnoughEntriesTitle": "Pas assez de données pour une tendance", + "journalComposeNotEnoughEntriesBody": "La courbe est une moyenne sur sept jours de ce que vous avez saisi, il faut donc au moins deux jours. Rien n'est comblé entre eux.", + "journalComposeSevenDayTrend": "Tendance sur sept jours", + "journalComposeTrendFootnote": "Saisi par vous. Les jours sans donnée restent vides.", + "journalComposeWeightTrendExplainer": "Saisi par vous ou votre balance — le bracelet ne mesure pas le poids. Ce qui est tracé est une moyenne sur sept jours, car une balance peut varier d'un à deux kilos rien qu'à cause de l'eau et de la nourriture, et les mesures brutes montreraient cela comme un changement du corps. {count, plural, one{{count} jour saisi.} other{{count} jours saisis.}}", + "nutritionTabToday": "Aujourd'hui", + "nutritionTabWeek": "Semaine", + "nutritionTabGoals": "Objectifs", + "nutritionLogFood": "Enregistrer un repas", + "nutritionTitle": "Nutrition", + "nutritionEmptyTodayTitle": "Rien d'enregistré aujourd'hui", + "nutritionEmptyTodayBody": "Un tap suffit pour un enregistrement complet.", + "nutritionLogOccasionFix": "Enregistrer un repas", + "nutritionOccasionsSection": "Repas", + "nutritionAddAction": "Ajouter", + "nutritionFloorTitle": "L'énergie d'aujourd'hui est un minimum, pas un total", + "nutritionFloorBody": "{unknown} repas sur {total} ont été enregistrés sans valeur énergétique, donc le chiffre ci-dessus est le minimum que vous avez mangé, pas ce que vous avez mangé.", + "nutritionAddNumbersFix": "Ajouter les chiffres à un repas", + "nutritionDaysNotCounted": "{excluded} des {span} derniers jours n'ont pas pu être comptés", + "nutritionDayCountsRule": "Un jour compte dès que chaque repas porte une valeur énergétique.", + "nutritionDaysLoggedLabel": "Jours avec quelque chose d'enregistré", + "nutritionPartialExcluded": "{partial} enregistré(s) mais partiel(s), donc exclus de chaque moyenne ci-dessous", + "nutritionEnergyByDay": "Énergie, jour par jour", + "nutritionSevenDayAvg": "Moyenne sur sept jours", + "nutritionNoCompleteDayTitle": "Aucun jour complet à moyenner pour l'instant", + "nutritionNoCompleteDayBody": "Vous n'en avez aucun.", + "nutritionLabelEnergy": "Énergie", + "nutritionLabelProtein": "Protéines", + "nutritionLabelCarbs": "Glucides", + "nutritionLabelFat": "Lipides", + "nutritionLabelFibre": "Fibres", + "nutritionEnergyBalance": "Bilan énergétique", + "nutritionLabelEaten": "MANGÉ", + "nutritionLabelBurned": "BRÛLÉ", + "nutritionLabelBalance": "BILAN", + "nutritionEatenMeanNote": "Le mangé est la moyenne sur {days} jours complets. Le brûlé est celui d'aujourd'hui seulement.", + "nutritionEnergyLoggedTitle": "Énergie enregistrée", + "nutritionPartialFootnote": "{n} partiel(s), exclu(s) des moyennes ci-dessous.", + "nutritionNothingLoggedYet": "Rien d'enregistré pour l'instant", + "nutritionNoEnergyFiguresYet": "Aucune valeur énergétique pour l'instant", + "nutritionDailyEnergy": "Énergie quotidienne", + "nutritionDailyProtein": "Protéines quotidiennes", + "nutritionEnergyWord": "l'énergie", + "nutritionProteinWord": "protéines", + "nutritionYourTargetsSection": "Vos objectifs", + "nutritionHintNone": "aucun", + "nutritionNoTargetsTitle": "Aucun objectif défini", + "nutritionNoTargetsBody": "Un objectif ici est un chiffre que vous saisissez vous-même.", + "nutritionSetTargetFix": "Définir un objectif", + "nutritionEditAction": "Modifier", + "nutritionBodySpentToday": "Ce que votre corps a dépensé aujourd'hui", + "nutritionEstimatedExpenditure": "Dépense estimée", + "nutritionNotMeasured": "Non mesuré", + "nutritionExpenditureSub": "AUJOURD'HUI, D'APRÈS LA FRÉQUENCE CARDIAQUE ET VOTRE PROFIL", + "nutritionRemoveTitle": "Supprimer {label} ?", + "nutritionRemoveBody": "Cela retire ce repas du jour et de toute moyenne qui le comptait. Aucun retour en arrière possible.", + "nutritionNothingToMeasure": "Rien à comparer pour {label} pour l'instant", + "nutritionFloorAverageBody": "Chaque jour complet avait un repas enregistré sans valeur de {nutrient}, donc la moyenne ne serait qu'un minimum.", + "nutritionCountedNoFigureBody": "{count} des {span} derniers jours ont compté, mais aucun ne portait de valeur de {nutrient}.", + "nutritionDayCountsRuleFull": "Un jour compte dès que chaque repas porte une valeur et que le journal va jusqu'au soir. Aucun des {span} derniers jours n'y est parvenu.", + "nutritionOnTarget": "Dans l'objectif", + "nutritionRateAbove": "{amount} {unit}/jour au-dessus", + "nutritionRateBelow": "{amount} {unit}/jour en dessous", + "nutritionMeanOfDays": "{n, plural, one{moyenne sur {n} jour complet} other{moyenne sur {n} jours complets}}", + "nutritionLoggedToday": "ENREGISTRÉ AUJOURD'HUI", + "nutritionEatenToday": "MANGÉ AUJOURD'HUI", + "nutritionAtLeast": "Au moins", + "nutritionOccasionsUnit": "{n, plural, one{repas} other{repas}}", + "nutritionOccasionsCount": "{n, plural, one{{n} repas} other{{n} repas}}", + "nutritionLabelBalanceAtLeast": "BILAN AU MOINS", + "nutritionNotLogged": "Non enregistré", + "nutritionLoggedNoEnergy": "{n} enregistré(s) · énergie non consignée", + "nutritionAtLeastPrefix": "au moins ", + "nutritionMealBreakfast": "Petit-déjeuner", + "nutritionMealLunch": "Déjeuner", + "nutritionMealDinner": "Dîner", + "nutritionMealSnacks": "Collations", + "nutritionNotCounted": "Non compté", + "nutritionNotRecorded": "Non consigné", + "nutritionEveryDayNoFigure": "CHAQUE JOUR COMPLET AVAIT UN REPAS SANS VALEUR DE {label}", + "nutritionNoDayRecorded": "AUCUN JOUR COMPLET N'A CONSIGNÉ {label}", + "nutritionMeanOfCompleteDaysCaps": "{n, plural, one{MOYENNE SUR {n} JOUR COMPLET} other{MOYENNE SUR {n} JOURS COMPLETS}}", + "nutritionLeftOutAsFloor": " · {n} exclu(s) comme minimum", + "nutritionWaterLabel": "Eau", + "nutritionTapToChange": "Touchez − ou + pour changer", + "nutritionNoneYet": "Rien pour l'instant", + "nutritionAddWater": "Ajouter de l'eau", + "nutritionRemoveWater": "Retirer de l'eau", + "coachApiKeyLabel": "Clé API", + "coachApiKeyLocalLabel": "Clé API (inutile en local)", + "coachAsking": "Interrogation…", + "coachAskLabel": "Demander au coach", + "coachBaseUrlLabel": "URL de base", + "coachBriefingMenuSub": "L'instantané exact qui a quitté cet appareil", + "coachBriefingMenuTitle": "Rapport, et ce qui a été envoyé", + "coachChooseModelFix": "Choisir un modèle", + "coachCloudDataNote": "Vos questions et les données que le coach lit sont envoyées à ce point de terminaison. Voyez exactement ce que c'est dans « Ce qui a été envoyé ».", + "coachDeleteChat": "Supprimer {title}", + "coachDeleteIt": "Le supprimer", + "coachDestructiveWarning": "Cela retire des données de cet appareil et c'est irréversible.", + "coachEndpointUnreachable": "Impossible de joindre ce point de terminaison : {error}", + "coachErrorTitle": "Ça n'est pas passé", + "coachInputHint": "Posez une question sur votre santé…", + "coachIntroBody": "Posez une question sur tout ce que l'app mesure, et il peut enregistrer repas, eau, entraînements, doses et votre ressenti — en demandant toujours d'abord.", + "coachKeychainRefused": "Le trousseau a refusé la clé : {error}", + "coachKeyStillSavedBody": "Elle n'a pas pu être lue dans le trousseau cette fois, ce qui arrive quand l'app se réveille alors que le téléphone est verrouillé.", + "coachKeyStillSavedTitle": "Votre clé est toujours enregistrée", + "coachListModels": "Lister les modèles", + "coachLocalDataNote": "Vos questions et les données que le coach lit restent sur votre propre machine.", + "coachLocalSub": "Sur ce réseau. Rien ne quitte votre machine.", + "coachMenuSemantic": "Discussions et réglages IA", + "coachModelHint": "chercher, ou saisir un identifiant", + "coachModelLabel": "Modèle", + "coachModelsFound": "{n, plural, one{{n} modèle. Touchez-en un.} other{{n} modèles. Touchez-en un.}}", + "coachNavTitle": "Coach", + "coachNewChat": "Nouvelle discussion", + "coachNoChatsYet": "Rien pour l'instant — voici votre première conversation.", + "coachNoDataBody": "Le coach répond à partir de vos propres jours dérivés, et il n'y en a encore aucun sur cet appareil.", + "coachNoDataTitle": "Aucune donnée à lire pour l'instant", + "coachNoModelsListed": "Ce point de terminaison n'a listé aucun modèle. Saisissez-en un ci-dessous.", + "coachNotSetUp": "Non configuré", + "coachNotSetUpBody": "Il fonctionne avec un modèle que vous choisissez — un sur votre propre machine, ou tout fournisseur compatible OpenAI avec votre propre clé. Rien ne transite par OpenStrap dans les deux cas.", + "coachNotSetUpTitle": "Le coach n'est pas configuré", + "coachPastChats": "DISCUSSIONS PRÉCÉDENTES", + "coachPickModelFirst": "Choisissez ou saisissez d'abord un modèle.", + "coachSafeWarning": "Rien n'est écrit tant que vous n'appuyez pas ci-dessous.", + "coachSaveIt": "L'enregistrer", + "coachSendLabel": "Envoyer", + "coachSetupNavSub": "Apportez votre propre modèle", + "coachSetupNavTitle": "Réglages IA", + "coachSomethingWrong": "Quelque chose a mal tourné : {error}", + "coachStarterAteYesterday": "Qu'ai-je mangé hier ?", + "coachStarterHrvChart": "Trace ma VFC du dernier mois", + "coachStarterLogRun": "J'ai couru 40 minutes ce matin — enregistre-le", + "coachStarterLogWater": "Enregistre 500 ml d'eau pour aujourd'hui", + "coachStarterRecovery": "À quel point suis-je récupéré aujourd'hui, et pourquoi ?", + "coachStarterSleep": "Comment a été mon sommeil cette semaine ?", + "coachTryAgainFix": "Réessayer", + "coachTryAsking": "Essayez de demander", + "coachUntitledChat": "Discussion sans titre", + "coachWhereModelRuns": "Où le modèle s'exécute", + "coachYourDataYourModel": "VOS DONNÉES, VOTRE MODÈLE", + "investigateNerdStatsLabel": "STATS TECHNIQUES", + "investigateProvenanceLabel": "Provenance", + "investigateDayLabel": "Jour", + "investigateCoverageLabel": "Couverture", + "investigateSleepWindowLabel": "Fenêtre de sommeil", + "investigateSourceLabel": "Source", + "investigateSourceOnDevice": "Données du bracelet · calculées sur ce téléphone", + "investigateSourceImported": "Importé · {source}", + "investigateAlgoVersionLabel": "Version de l'algorithme", + "investigateWhatHappenedTitle": "Ce qui s'est passé ce jour-là", + "investigateWhatHappenedSub": "Sommeil, séances, repas et journaux dans l'ordre chronologique", + "investigateWhichSensorCounted": "Quel capteur a compté", + "investigateStrapPedometer": "bracelet · podomètre 100 Hz", + "investigateStrapOnChipCounter": "bracelet · compteur intégré", + "investigatePhonePedometer": "téléphone · podomètre", + "investigateDayTotal": "total du jour", + "investigateStrapChipReported": "puce du bracelet a indiqué", + "investigateTimeDomain": "Domaine temporel", + "investigateRmssd": "RMSSD", + "investigateSdnn": "SDNN", + "investigateSdann": "SDANN", + "investigateSdnnIndex": "Indice SDNN", + "investigatePnn50": "pNN50", + "investigateLnRmssd": "ln RMSSD", + "investigateBaselineRmssd": "Votre RMSSD de référence", + "investigateStabilityCv": "Stabilité (CV)", + "investigateFrequencyDomain": "Domaine fréquentiel", + "investigateUlfPower": "Puissance ULF", + "investigateVlfPower": "Puissance VLF", + "investigateLfPower": "Puissance LF", + "investigateHfPower": "Puissance HF", + "investigateTotalPower": "Puissance totale", + "investigateLfHf": "LF / HF", + "investigateLfNormalised": "LF, normalisée", + "investigateHfNormalised": "HF, normalisée", + "investigateHfGated": "HF filtrée", + "investigateYes": "oui", + "investigateNo": "non", + "investigateNoFrequencySpectrum": "Aucun spectre fréquentiel pour cette nuit", + "investigateRecordingTooShort": "L'enregistrement était trop court pour résoudre les bandes.", + "investigateNonLinear": "Non linéaire", + "investigateSd1Sleep": "SD1, sommeil", + "investigateSd2Sleep": "SD2, sommeil", + "investigateSd124h": "SD1, 24 h", + "investigateSd224h": "SD2, 24 h", + "investigateSd1Sd224h": "SD1 / SD2, 24 h", + "investigateSuccessiveIntervalsOver70ms": "Intervalles successifs supérieurs à 70 ms", + "investigateIrregularRhythmFlagSleep": "Indicateur de rythme irrégulier, sommeil", + "investigateIrregularRhythmFlag24h": "Indicateur de rythme irrégulier, 24 h", + "investigateFlagRaised": "déclenché", + "investigateFlagClear": "non déclenché", + "investigateDecelerationCapacity": "Capacité de décélération", + "investigateAccelerationCapacity": "Capacité d'accélération", + "investigateDcAnchors": "Ancres DC", + "investigateSignalQuality": "Qualité du signal", + "investigateBeatsAnalysed": "Battements analysés", + "investigateBeatsAnalysed24h": "Battements analysés, 24 h", + "investigateNoShapeForNight": "Aucune forme pour cette nuit", + "investigateTooFewBeatsToBin": "La nuit comptait trop peu de battements propres pour être regroupée.", + "investigateShapeOfTheNight": "Forme de la nuit", + "investigateBinRmssd": "RMSSD par bloc", + "investigateSamplingRange": "Plage d'échantillonnage", + "investigateShapeFootnote": "{drawn} blocs sur {total} avaient assez de battements pour être lus ; le reste, ce sont des vides, pas des zéros. La paire extérieure est la dispersion d'échantillonnage propre à l'estimateur, pas une plage dans laquelle vous vous trouviez. Cela décrit la nuit et ne peut pas l'expliquer — un premier tiers faible est tout aussi compatible avec de l'alcool, un repas tardif, un entraînement tardif, une pièce chaude, le début d'une maladie, ou rien du tout.", + "investigateNightShape": "Forme de la nuit", + "investigateBinWidth": "Largeur du bloc", + "investigateBinsRead": "Blocs lus", + "investigateFirstThird": "Premier tiers", + "investigateLastThird": "Dernier tiers", + "investigateLastThirdOverFirst": "Dernier tiers ÷ premier", + "investigate29DaysAgo": "Il y a 29 jours", + "investigateToday": "Aujourd'hui", + "investigateDcFootnoteWithBeats": "Uniquement vos propres nuits — aucune plage de référence n'existe pour les battements de pouls. La qualité du signal fait bouger cette ligne d'une nuit à l'autre, et la nuit dernière comptait {beats} battements.", + "investigateDcFootnote": "Uniquement vos propres nuits — aucune plage de référence n'existe pour les battements de pouls. La qualité du signal fait bouger cette ligne d'une nuit à l'autre.", + "investigateIrregularRhythmScreen": "Dépistage de rythme irrégulier", + "investigateOneSquarePerDay": "un carré par jour", + "investigate12WeeksAgo": "Il y a 12 semaines", + "investigateThisWeek": "Cette semaine", + "investigateScreenRan": "Dépistage effectué", + "investigateRhythmStripFootnote": "Effectué {ran} {ran, plural, one{jour} other{jours}}, a déclenché son indicateur {raised} fois. Un carré avec contour est un jour où il ne s'est pas exécuté. Une bande vierge n'est pas un résultat négatif : c'est un dépistage basé sur le rythme du pouls, qui ne peut pas distinguer un battement ectopique d'un battement manqué ou d'un mouvement du bracelet sur votre poignet.", + "investigateNoRestingBreathingRate": "Aucune fréquence respiratoire au repos hors sommeil", + "investigateNoRestingBreathingRateBody": "Ceci ne lit la respiration que sur des plages de trois minutes où le bracelet vous a vu presque parfaitement immobile, en dehors de la fenêtre de sommeil. La plupart des jours n'en ont aucune — un jour sans aucune est un jour où vous avez bougé, pas un jour où quelque chose n'allait pas.", + "investigateBreathingAtRestAwake": "Respiration au repos, éveillé", + "investigateStillStretchesOutsideSleep": "Plages immobiles hors sommeil", + "investigateLowest": "Le plus bas", + "investigateNextLowest": "Le suivant plus bas", + "investigateHighestOfThem": "Le plus élevé d'entre eux", + "investigateFloorNotRateBody": "Un plancher, pas un taux pour la journée. Seules les plages où vous étiez presque parfaitement immobile peuvent être lues, ce sont donc les minutes les plus calmes que le bracelet a vues en dehors de votre sommeil — rien ici ne décrit le reste de votre journée, et la respiration pendant le mouvement ne peut pas être retrouvée à partir du rythme des battements.", + "investigateCycleScreenDidNotRun": "Le dépistage des cycles ne s'est pas exécuté cette nuit", + "investigateNotEnoughCleanBeats": "Pas assez de battements propres pour l'exécuter.", + "investigateHeartRateCycles": "Cycles de fréquence cardiaque", + "investigateCyclesCounted": "Cycles comptés", + "investigateObservedHoursAnalysed": "Heures observées analysées", + "investigateCyclesPerObservedHour": "Cycles par heure observée", + "investigateMeanCycleLength": "Durée moyenne du cycle", + "investigateMeanDipDepth": "Profondeur moyenne du creux", + "investigateCycleLengthQuartiles": "Durée du cycle, quartiles", + "investigateDipDepthQuartiles": "Profondeur du creux, quartiles", + "investigateNotEnoughNightsAcross": "Pas assez de nuits pour la vue comparative", + "investigateNeedsSeveralNights": "Cela nécessite plusieurs nuits avec quelques heures observées chacune.", + "investigateDroppedIrregular": "{count, plural, one{# nuit exclue car le dépistage de rythme irrégulier l'a signalée} other{# nuits exclues car le dépistage de rythme irrégulier les a signalées}}", + "investigateDroppedThin": "{count, plural, one{# nuit exclue pour trop peu d'heures observées} other{# nuits exclues pour trop peu d'heures observées}}", + "investigateAcrossNOwnNights": "SUR {n} DE VOS PROPRES NUITS", + "investigateCvhrAboveUsual": "Au cours de vos nuits les plus récentes, le cycle de fréquence cardiaque mesuré par ce dépistage a été plus élevé que sur les {n} nuits qui le composent.", + "investigateCvhrInsideUsual": "Au cours de vos nuits les plus récentes, le cycle de fréquence cardiaque mesuré par ce dépistage est resté dans la plage des {n} nuits qui le composent.", + "investigateCvhrExplainer": "C'est un motif dans votre pouls, pas une mesure de votre respiration, et ce n'est un test de rien. Le même cycle provient d'un rythme irrégulier, de l'altitude, ou de toute nuit entrecoupée — et les bêtabloquants, le diabète et les troubles nerveux l'atténuent, si bien qu'une respiration réellement perturbée ne laisse souvent aucune trace ici.", + "investigateCvhrNotNegativeResult": "Rien ici n'est donc un résultat négatif et rien ici ne blanchit quoi que ce soit, et rien de tout cela ne dit quoi que ce soit sur une nuit en particulier — le compte d'une seule nuit varie pour une dizaine de raisons à lui seul.", + "investigateCvhrSeeClinicianIfSymptoms": "Si vous ronflez, vous réveillez sans être reposé, ou si quelqu'un vous a vu arrêter de respirer pendant votre sommeil, un médecin peut faire un test approprié.", + "investigateStageMinutesAsCounted": "Minutes par phase, telles que comptées", + "investigateLight": "Léger", + "investigateDeep": "Profond", + "investigateRem": "REM", + "investigateAwake": "Éveillé", + "investigateTotalSleep": "Sommeil total", + "investigateSegmentationConfidence": "Confiance de segmentation", + "investigateNotPublished": "non publiée", + "investigateNothingComputedForKey": "Rien calculé pour cette clé", + "investigateNoStoredSeries": "Aucune série enregistrée", + "investigateNothingStoredYet": "Rien n'est encore enregistré pour {metric}.", + "investigateSeries": "Série", + "investigateDaysDerived": "Jours calculés", + "investigateLatest": "Dernière", + "investigateMean": "Moyenne", + "investigateMedian": "Médiane", + "investigateSd": "ÉT", + "investigateMin": "Min", + "investigateMax": "Max", + "investigateUnit": "Unité", + "investigateUnitless": "sans unité", + "investigateStorage": "Stockage", + "investigateOneValuePerDerivedDay": "une valeur par jour calculé", + "investigateMethodLabel": "MÉTHODE", + "investigateNotDocumented": "Non documenté.", + "calmBreathingResonanceLabel": "Résonance", + "calmBreathingResonanceDescription": "Inspiration et expiration régulières à environ {rate} respirations par minute. Celle avec un score de cohérence.", + "calmBreathingCloseBreathing": "Fermer la respiration", + "calmBreathingFinishNow": "Terminer maintenant", + "calmBreathingStop": "Arrêter", + "calmBreathingEndSession": "Terminer la séance", + "calmBreathingBegin": "Commencer", + "calmBreathingTakeABreath": "Respirez profondément.", + "calmBreathingRingLeads": "L'anneau donne le rythme. Posez le téléphone.", + "calmBreathingScoredPill": "Notée", + "calmBreathingHowLong": "Durée", + "calmBreathingMinutesSemantic": "{m, plural, one{# minute} other{# minutes}}", + "calmBreathingMinutesAbbrev": "{m} min", + "calmBreathingYourOwnPace": "Votre propre rythme", + "calmBreathingWindowRowSemantic": "Mesurer avant et après, ajoute quatre minutes", + "calmBreathingMeasureBeforeAfter": "Mesurer avant et après · ajoute 4 min", + "calmBreathingNeedsBandBeatTiming": "Nécessite le bracelet porté — la comparaison se fait à partir du rythme des battements.", + "calmBreathingFindYourPace": "Trouvez le rythme que suit votre cœur", + "calmBreathingSweepIntro": "Six minutes : {rates} respirations par minute, deux minutes chacune. Il faut deux séances qui concordent avant que quoi que ce soit change.", + "calmBreathingSweepAgreed": "Deux séances se sont accordées sur {rate} respirations par minute, et Résonance est calée sur ce rythme. Refaites-le pour vérifier.", + "calmBreathingPaceOfRate": "RYTHME {block} SUR {total} · {rate} RESPIRATIONS PAR MINUTE", + "calmBreathingOfClock": "sur {clock}", + "calmBreathingNoScoreForSession": "Aucun score de cohérence pour cette séance", + "calmBreathingScoringNeedsBand": "La notation nécessite le rythme des battements du bracelet. Non connecté, cette séance vous guide donc mais n'est pas enregistrée.", + "calmBreathingBeforeLabel": "AVANT", + "calmBreathingAfterLabel": "APRÈS", + "calmBreathingSitStill": "Restez assis immobile un instant.", + "calmBreathingStaySitting": "Restez assis.", + "calmBreathingNothingPacingScored": "Respirez comme vous le feriez normalement. Rien ne vous guide et rien n'est noté.", + "calmBreathingPatternNotScored": "{pattern} n'est pas noté. Résonance est le seul calé sur le rythme pour lequel le score a été conçu.", + "calmBreathingTooFewBeatTimings": "Trop peu de battements propres pendant la séance pour la noter.", + "calmBreathingThatIsDone": "C'est terminé.", + "calmBreathingCardiacCoherence": "Cohérence cardiaque", + "calmBreathingHowStronglyFollowedPace": "À QUEL POINT VOTRE FRÉQUENCE CARDIAQUE A SUIVI LE RYTHME", + "calmBreathingStoppedThere": "Arrêté là.", + "calmBreathingHowStronglyEachPace": "À QUEL POINT VOTRE FRÉQUENCE CARDIAQUE A SUIVI CHAQUE RYTHME", + "calmBreathingBreathsAMinute": "{rate} respirations par minute", + "calmBreathingNotReached": "non atteint", + "calmBreathingTooFewCleanBeats": "trop peu de battements propres", + "calmBreathingRankingExplainer": "Un classement de trois rythmes en une seule séance. Les blocs s'enchaînent, donc chaque rythme est mesuré pendant que vous vous stabilisez encore du précédent. Cela indique quel rythme votre cœur a suivi le plus fortement, et rien d'autre.", + "calmBreathingVerdictAborted": "Vous vous êtes arrêté en cours de route, il n'y avait donc rien à comparer. Rien n'a changé.", + "calmBreathingVerdictCouldNotScore": "Au moins un rythme n'a pas pu être noté, il n'y a donc rien à classer. Rien n'a changé.", + "calmBreathingVerdictTied": "Deux des rythmes ont obtenu le même score, cette séance ne peut donc pas les départager. Rien n'a changé.", + "calmBreathingVerdictConfirmed": "Parmi les rythmes testés, {w} a donné votre réponse la plus forte — et c'est désormais deux séances d'affilée. Résonance est calée sur ce rythme.", + "calmBreathingVerdictFirstWin": "Parmi les rythmes testés, {w} a donné votre réponse la plus forte. Rien n'est encore fixé : le rythme ne change que lorsque deux séances choisissent le même.", + "breathPatternBoxName": "Carrée", + "breathPatternBoxDesc": "Quatre temps dans chaque sens, pauses incluses. Apaisant quand vos pensées s'emballent.", + "breathPattern478Name": "4-7-8", + "breathPattern478Desc": "Une longue pause et une expiration encore plus longue. Généralement utilisée pour s'endormir.", + "breathPatternExtendedExhaleName": "Expiration longue", + "breathPatternExtendedExhaleDesc": "Expirez deux fois plus longtemps que vous inspirez. Pas de pause, donc facile à tenir un moment.", + "breathPhaseInhale": "Inspirez", + "breathPhaseHold": "Retenez", + "breathPhaseExhale": "Expirez", + "breathPhaseWork": "Effort", + "breathPhaseRest": "Repos", + "metricDetailToday": "Aujourd’hui", + "metricDetailRange7Days": "7 jours", + "metricDetailRange30Days": "30 jours", + "metricDetailRange6Months": "6 mois", + "metricDetailRangeYear": "Année", + "metricDetailLockedNote": "{label} nécessite {needed} jours d’historique. Vous en avez {have}.", + "metricDetailNotShownTitle": "Non affiché sous forme de tendance", + "metricDetailNothingRecordedToday": "Rien d’enregistré aujourd’hui", + "metricDetailNoHistoryYet": "Pas encore d’historique pour {metric}", + "metricDetailNoValueYet": "Aujourd’hui n’a pas encore produit de valeur.", + "metricDetailNoValueYetWiderRanges": "Aujourd’hui n’a pas encore produit de valeur. Les plages plus larges ci-dessus contiennent les jours qui en ont produit une.", + "metricDetailNoValueInWindow": "Aucun jour de cette plage n’a produit de valeur.", + "metricDetailWearBandFix": "Portez la bande toute la nuit pour démarrer la série", + "metricDetailBeatsLinkTitle": "Battements", + "metricDetailBeatsLinkSub": "Les intervalles qui composent une nuit, tracés", + "metricDetailBreakdownLinkTitle": "Détail", + "metricDetailBreakdownLinkSub": "Chaque plage de la journée, et ce qui l’a comptée", + "metricDetailNerdStatsTitle": "Statistiques avancées", + "metricDetailNerdStatsSub": "Les chiffres derrière l’image", + "metricDetailDailyAverage": "Moyenne quotidienne · {count} sur {win} jours", + "metricDetailLatestReading": "Dernier {value} {unit} · {asOf}", + "metricDetailAlgoBreakFootnote": "{n, plural, one{La ligne pointillée est un changement dans la façon dont ces jours ont été calculés. Les mesures de chaque côté proviennent de versions différentes.} other{Les lignes pointillées sont des changements dans la façon dont ces jours ont été calculés. Les mesures de chaque côté de l’une d’elles proviennent de versions différentes.}}", + "metricDetailDaysAgoLabel": "{n, plural, one{Il y a {n} jour} other{Il y a {n} jours}}", + "metricDetailWornChartTitle": "Porté", + "metricDetailHoursADayUnit": "h par jour", + "metricDetailWearFootnote": "{have} de ces {win} jours ont un enregistrement de port. Le reste correspond à des trous dans les deux graphiques — la ligne ci-dessus n’est pas prolongée au travers.", + "metricDetailSlotNoRecord": "{day}, aucun enregistrement", + "metricDetailSlotWithValue": "{day}, {value} {unit}", + "metricDetailOpenDay": "Ouvrir {day}", + "metricDetailNoRecordLabel": "Aucun enregistrement", + "metricDetailLowest": "Minimum", + "metricDetailTypical": "Typique", + "metricDetailHighest": "Maximum", + "metricDetailFromDaysCount": "Sur {n} de vos propres jours.", + "metricDetailPercentileTodayNoBand": "Aujourd’hui se situe au {ordinal} percentile de votre propre historique.", + "metricDetailPercentileTodayBand": "Aujourd’hui se situe au {ordinal} percentile de votre propre historique — {band}.", + "metricDetailPercentileFromNoBand": "Votre mesure du {date} se situe au {ordinal} percentile de votre propre historique.", + "metricDetailPercentileFromBand": "Votre mesure du {date} se situe au {ordinal} percentile de votre propre historique — {band}.", + "metricDetailDaysWithWithout": "{withCount} jours avec · {withoutCount} sans", + "metricDetailPatternsNotCauses": "Des tendances dans vos propres journaux, pas des causes.", + "metricDetailChooseDayHelp": "Choisir un jour", + "metricDetailPreviousDay": "Jour précédent", + "metricDetailNextDay": "Jour suivant", + "metricDetailChooseDayShowing": "Choisir un jour. Affichage de {day}", + "metricDetailNormalRangeSection": "Votre plage normale", + "metricDetailWhatMovesItSection": "Ce qui l’influence", + "cycleRemoveLogTitle": "Supprimer le {date} ?", + "cycleRemoveLogBody": "Le jour du cycle, la phase et la prochaine date prévue sont tous calculés à partir des jours que vous enregistrez. Seul aujourd'hui peut être enregistré, donc celui-ci ne pourra pas être restauré.", + "cycleWhatAppliesToYou": "Ce qui s'applique à vous", + "cyclePreferNotToSay": "Je préfère ne pas le dire", + "cyclePreferNotToSayWhy": "L'application laisse la phase désactivée.", + "cycleReproCyclingLabel": "J'ai des cycles naturels", + "cycleReproCyclingWhy": "Calcule une phase à partir de vos débuts enregistrés.", + "cycleReproContraceptionLabel": "Contraception hormonale", + "cycleReproContraceptionWhy": "Pas d'ovulation à compter, donc pas de phase. Les saignements continuent d'être enregistrés.", + "cycleReproNoneLabel": "Enceinte, post-partum, ou sans cycles", + "cycleReproNoneWhy": "Pas de phase ni de prochaine date prévue. Vos données biométriques restent affichées.", + "cycleReproNotSet": "Non défini", + "cycleTrackingOffTitle": "Le suivi du cycle est désactivé", + "cycleTrackingOffBody": "Il reste sur ce téléphone.", + "cycleTurnOnTracking": "Activer le suivi du cycle", + "cycleNoPeriodTitle": "Aucune règle enregistrée pour le moment", + "cycleNoPeriodBody": "Calculé à partir des jours que vous enregistrez.", + "cycleLogPeriodButton": "Enregistrer le début des règles aujourd'hui", + "cycleLogKindStart": "DÉBUT", + "cycleLogKindEnd": "FIN", + "cycleAcrossCyclesTitle": "Au fil de vos cycles", + "cycleUnitCompleteCycle": "cycle complet", + "cycleUnitCompleteCycles": "cycles complets", + "cycleOpenAction": "Ouvrir", + "cycleWhatYouNoticedToday": "Ce que vous avez remarqué aujourd'hui", + "cycleLoggedDays": "Jours enregistrés", + "cycleReproOptionalHint": "Facultatif. Tant que vous ne le précisez pas, l'application laisse la phase désactivée.", + "cycleReproPrivateHint": "Seulement vous et ce téléphone. Jamais exporté.", + "cycleTurnOffTracking": "Désactiver le suivi du cycle", + "cycleDayInThisCycle": "JOUR DE CE CYCLE", + "cycleCountedFromLastStart": "compté depuis votre dernier début enregistré", + "cycleOfAboutDays": "d'environ {days}", + "cyclePhaseMenstrual": "Menstruelle", + "cyclePhaseFollicular": "Folliculaire", + "cyclePhaseOvulation": "Fenêtre d'ovulation", + "cyclePhaseLuteal": "Lutéale", + "cycleNextPeriodBetween": "PROCHAINES RÈGLES, PRÉVUES ENTRE", + "cycleNextPeriodAround": "PROCHAINES RÈGLES, PRÉVUES VERS", + "cycleFromOneMeasuredGap": "d'après votre seul intervalle mesuré, qui ne peut pas indiquer à quel point votre cycle varie", + "cyclePastEndOfIt": "{days} jours après sa fin · ", + "cycleInsideItNow": "vous êtes actuellement dedans · ", + "cycleInDaysRange": "dans {lo} à {hi} jours · ", + "cycleHalfOfMeasuredGaps": "la moitié de vos {n} intervalles mesurés se situait dans une fourchette aussi large que celle-ci", + "cycleLeadDaysLate": "{days} jours de retard · ", + "cycleLeadToday": "aujourd'hui · ", + "cycleLeadInDays": "dans {days} jours · ", + "cycleWhatYouUsuallyNotice": "Ce que vous remarquez habituellement", + "cycleSymptomShapeSummary": "Quatre chiffres, un par semaine du cycle, comptés depuis vos propres débuts enregistrés. Vous avez enregistré quelque chose {daysByWeek} jours de chaque semaine sur {cycles} cycles — ce sont les seuls jours pris en compte ici.", + "cycleRemoveLoggedDay": "Supprimer {date}", + "cycleSymptomCramps": "crampes", + "cycleSymptomHeadache": "maux de tête", + "cycleSymptomBloating": "ballonnements", + "cycleSymptomFatigue": "fatigue", + "cycleSymptomLowMood": "humeur basse", + "cycleSymptomAcne": "acné", + "cycleSymptomTenderBreasts": "seins sensibles", + "cycleSymptomNausea": "nausées", + "cycleThisCycle": "Ce cycle", + "cycleByDayOfYourCycle": "Par jour de votre cycle", + "cycleHowLongCyclesBeen": "La durée de vos cycles", + "cycleRestingHeartRate": "Fréquence cardiaque au repos", + "cycleUnitBpm": "bpm", + "cycleHrvRmssdTitle": "VFC (RMSSD)", + "cycleUnitMs": "ms", + "cycleNotEnoughDescribeDayTitle": "Pas encore assez de cycles pour décrire un jour du cycle", + "cycleNotEnoughDescribeDayBody": "Chaque point ici est la valeur médiane du même jour sur deux ou plusieurs de vos propres cycles. Aucun n'en a encore deux derrière lui.", + "cycleOwnPastCyclesDescribed": "Vos propres cycles passés, décrits. Les jours atteints par un seul cycle sont laissés vides plutôt que tracés — une seule nuit n'est pas une médiane. Cela décrit ce qui s'est passé, pas ce qui se passera.", + "cycleDayOneLabel": "Jour 1", + "cycleDayNLabel": "Jour {n}", + "cycleMiddleOfNCycles": "Valeur médiane de {n} cycles à chaque jour.", + "cycleMiddleOfRangeCycles": "Valeur médiane de {lo} à {hi} cycles à chaque jour.", + "cycleNotEnoughCompareTitle": "Pas encore assez de cycles pour comparer un jour à lui-même", + "cycleCompareBodyGeneric": "Cela place aujourd'hui à côté du même jour de vos cycles précédents. Il en faut trois qui soient allés aussi loin.", + "cycleCompareBodyWithDay": "Cela place aujourd'hui à côté du même jour de vos cycles précédents. Il en faut trois qui aient atteint le jour {day}.", + "cycleNightOfLabel": "NUIT DU {date}", + "cycleComparisonNotCorrection": "Une comparaison, pas une correction. Rien dans votre indice de forme n'a été réajusté par cela, et rien ici n'est une consigne d'entraînement.", + "cycleCompareHrvLabel": "VFC", + "cycleCompareLine": "{label} {z1} par rapport à vos 3 dernières semaines, {z2} par rapport à vos {n} derniers jours {cycleDay} de votre cycle.", + "cycleLengthsTitle": "La durée de vos cycles comparée à une fourchette publiée", + "cycleLengthsBody": "Désactivé sauf si vous le demandez. Affiche les jours entre vos propres débuts enregistrés à côté de la fourchette publiée pour un cycle adulte, sans rien dire de plus à leur sujet.", + "cycleShowIt": "Afficher", + "cycleNotEnoughLoggedTitle": "Pas encore assez de cycles enregistrés", + "cycleNotEnoughLoggedBody": "Cela nécessite un long historique : {n} intervalles sur {total} jusqu'à présent, soit environ un an d'enregistrement de chaque début.", + "cycleGapTitle": "Il y a un trou dans vos débuts enregistrés", + "cycleGapBody": "L'un d'eux survient plus de {days} jours après le précédent. Un début que vous n'avez jamais enregistré et un cycle qui a vraiment duré aussi longtemps se ressemblent depuis ce point de vue, donc rien n'est tracé.", + "cycleDaysBetweenStarts": "Jours entre vos débuts enregistrés", + "cycleUnitDays": "jours", + "cycleLegendYourCycles": "Vos cycles", + "cycleLegendPublishedRange": "Fourchette publiée", + "cycleTwoLinesFootnote": "Les deux lignes se situent à {low} et {high} jours.", + "cycleLengthChangesReasons": "La durée du cycle change pour de nombreuses raisons — thyroïde, stress, changement de poids, contraception, SOPK et autres. Ce sont vos propres données enregistrées à côté d'une fourchette publiée. C'est une raison de consulter un médecin, pas une réponse venant d'un médecin.", + "cycleHideLengths": "Masquer la durée des cycles", + "cycleDescriptiveOnly": "Descriptif uniquement.", + "cycleNotEnoughDerivedNights": "Pas encore assez de nuits dérivées pour ce cycle", + "cycleMdcNoteInsideSpread": " Chaque jour affiché ici se situe dans votre propre variation nuit après nuit : le plus grand écart entre deux d'entre eux est de {s}, et {n} est le plus petit changement que ceci peut distinguer du bruit. Une forme, pas un changement.", + "cycleMdcNoteVaries": " Vos nuits varient de {n} à elles seules, donc les jours plus proches que cela ne sont pas distingués. Le plus grand écart ici est de {s}.", + "healthTabOverview": "Aperçu", + "healthTabExplore": "Explorer", + "healthTabTrends": "Tendances", + "healthTabVitals": "Vitaux", + "healthTabLabs": "Labo", + "healthTitle": "Santé", + "healthCouldNotRead": "Impossible de lire vos {what}", + "healthReadFailedBody": "Le chargement des lignes enregistrées a échoué. Rien n'a été supprimé — c'est une lecture qui a mal tourné.", + "healthTryAgain": "Réessayer", + "healthWhatVitals": "vitaux", + "healthWhatLabResults": "résultats de laboratoire", + "healthMeasuresUnit": "mesures", + "healthRowRestingHr": "Fréquence cardiaque au repos", + "healthRowHrv": "VFC", + "healthRowSleep": "Sommeil", + "healthRowStress": "Stress", + "healthRowRespRate": "Fréquence respiratoire", + "healthSubOvernight": "Pendant la nuit", + "healthSubRmssdAsleep": "RMSSD, endormi", + "healthSubLastNight": "Cette nuit", + "healthSubAsleep": "Endormi", + "healthNoMetric": "Pas de {name}", + "healthWhyReadFromSleep": "Mesuré pendant le sommeil, et aucune nuit n'a été évaluée.", + "healthWhyReadOnlyFromSleep": "Mesuré uniquement pendant le sommeil, et aucune nuit n'a été évaluée.", + "healthWhySleepNotLongEnough": "Aucune période de sommeil assez longue pour être évaluée n'a été enregistrée.", + "healthWhyReadFromNight": "Mesuré pendant la nuit, et aucune nuit n'a été évaluée.", + "healthWhyNoReadingLastNight": "Aucune mesure la nuit dernière.", + "healthIllnessRedTitle": "Plusieurs nuits d'affilée s'écartent de votre normale", + "healthIllnessLastNightTitle": "Cette nuit se situait hors de votre plage normale", + "healthIllnessDayTitle": "{day} se situait hors de votre plage normale", + "healthIllnessBodyNoZ": "Votre fréquence cardiaque au repos nocturne se maintient au-dessus de votre propre référence. Ceci ne surveille qu'un seul signal. Cela nomme un schéma, pas une cause.", + "healthIllnessBodyWithZ": "Votre fréquence cardiaque au repos nocturne se maintient au-dessus de votre propre référence ; cette nuit-là se situait à {z} écarts-types {direction} de votre référence. Ceci ne surveille qu'un seul signal. Cela nomme un schéma, pas une cause.", + "healthDirectionAbove": "au-dessus", + "healthDirectionBelow": "en dessous", + "healthIllnessAdvice": "À surveiller si cela se poursuit au-delà de quelques jours.", + "healthObservationsTitle": "Observations", + "healthSeeAll": "Tout voir", + "healthNapsTitle": "Siestes", + "healthNoNapReading": "Aucune sieste mesurée", + "healthNoNapReadingFor": "Aucune sieste mesurée pour {day}", + "healthNapsBody": "Les siestes proviennent du même enregistrement seconde par seconde que le reste de la journée, et cette journée n'en a pas assez.", + "healthDaytimeSleep": "Sommeil diurne", + "healthValueNone": "Aucune", + "healthNoneDetectedOn": "Aucune détectée · {day}", + "healthNapCountLabel": "{n, plural, one{{n} sieste} other{{n} siestes}}", + "healthAddOrCorrect": "Ajouter ou corriger", + "healthNoTrendYet": "Pas encore de tendance pour {label}", + "healthZeroDaysStored": "0 jour enregistré.", + "healthVsDayAverage": "par rapport à votre moyenne sur {days} jours", + "healthAsOf": " · au {date}", + "healthNoBaseline": "aucune référence", + "healthFirstReadings": "premières mesures", + "healthTimeAsleep": "Temps de sommeil", + "healthVsNeed": "par rapport à votre besoin de {need}", + "healthBodyClockTitle": "Horloge biologique", + "healthChronotypeJetlagRegularity": "Chronotype, décalage horaire et régularité", + "healthChronotypeLabel": "CHRONOTYPE", + "healthSocialJetlagLabel": "DÉCALAGE SOCIAL", + "healthRegularityLabel": "RÉGULARITÉ", + "healthConsistencyTitle": "Constance", + "healthDaysWithRecord": "Jours avec un enregistrement calculé au cours des 30 derniers jours", + "healthToday": "Aujourd'hui", + "healthRowHeartRate": "Fréquence cardiaque", + "healthRowSkinTemp": "Température cutanée", + "healthVsOwnNights": "par rapport à vos propres nuits", + "healthVsOwnNightsOn": "par rapport à vos propres nuits · {day}", + "healthRowWearTime": "Temps de port", + "healthTheDay": "la journée", + "healthCoverageOf": "{pct} % de {day}", + "healthNothingMeasuredDay": "Rien n'a été mesuré ce jour-là", + "healthNoBandRecordings": "Aucun enregistrement du bracelet n'a atteint ce jour-là.", + "healthSyncTheBand": "Synchroniser le bracelet", + "healthDeepDivesTitle": "Analyses approfondies", + "healthHeartRateVariability": "Variabilité de la fréquence cardiaque", + "healthTimeFrequencyNonLinear": "Temporel, fréquentiel et non linéaire", + "healthRmssdOfLastNights": "RMSSD, {have} des {days} dernières nuits", + "healthNightsAgo": "il y a {n} nuits", + "healthOneNightNotTrend": "Une seule nuit ne fait pas encore une tendance", + "healthMeasuresWithHistory": "Mesures avec un historique enregistré sur cet appareil", + "healthEachOneOpens": "Chacune ouvre son graphique, votre propre plage et la façon dont elle est calculée.", + "healthCatHeartRhythm": "Cœur et rythme", + "healthCatBreathing": "Respiration", + "healthCatMovementLoad": "Mouvement et charge", + "healthCatBodyWear": "Corps et port", + "healthBlurbRestingHr": "Le rythme soutenu le plus bas de la nuit", + "healthBlurbHrv": "RMSSD sur la fenêtre de sommeil la plus propre", + "healthBlurbHrvCv": "À quel point cela varie d'une nuit à l'autre", + "healthBlurbLfHf": "Où se situe la puissance du rythme cardiaque selon les fréquences", + "healthBlurbDip": "De combien votre fréquence cardiaque baisse pendant votre sommeil", + "healthBlurbHrr": "À quelle vitesse elle baisse dans la minute suivant un effort", + "healthBlurbSleep": "Temps de sommeil, calculé à partir du mouvement et du rythme cardiaque", + "healthBlurbEfficiency": "Le sommeil comme part du temps passé au lit", + "healthBlurbDeep": "Stabilité de la fréquence cardiaque pendant le sommeil NREM", + "healthBlurbRem": "Déterminé à partir de la variabilité du rythme et du mouvement", + "healthBlurbNapMin": "Sommeil détecté en dehors de la nuit principale", + "healthBlurbRespRate": "Respirations par minute, déduites du rythme cardiaque", + "healthBlurbBrv": "À quel point ce rythme varie au cours de la nuit", + "healthBlurbSteps": "Comptés par un podomètre, jamais modélisés", + "healthBlurbActiveMin": "Minutes de volume de mouvement, pas de déplacement", + "healthBlurbCalories": "Énergie active calculée à partir de la fréquence cardiaque et de votre profil", + "healthBlurbStrain": "Charge cardiovasculaire sur la journée, sur une échelle de 0 à 21", + "healthBlurbTrimp": "Temps passé dans chaque zone, pondéré selon son coût", + "healthBlurbSkinTemp": "Écart par rapport à vos propres nuits récentes", + "healthBlurbWear": "Minutes avec un enregistrement du bracelet présent", + "healthNothingMeasuredHere": "Rien n'a encore été mesuré ici", + "healthNotMeasuredYet": "Pas encore mesuré", + "healthNoDayProduced": "Aucun jour sur cet appareil n'en a encore produit un.", + "healthNoLabResults": "Aucun résultat de laboratoire", + "healthNoLabResultsBody": "Rien n'est enregistré. Tout ce que vous ajoutez ici reste sur cet appareil, et tout ce que vous supprimez en disparaît.", + "healthLastPanel": "Dernier bilan {date} · saisi manuellement", + "healthMarkersYouNamed": "Marqueurs que vous avez nommés", + "healthAddAResult": "Ajouter un résultat", + "healthRangesDifferByLab": "Les plages varient selon le laboratoire. Utilisez celle de votre rapport.", + "healthRemoveMarkerFrom": "Supprimer {marker} du {date}", + "healthNoReferenceInterval": "Aucun intervalle de référence · {date}", + "healthTypicalRange": "Habituel {low}–{high} · {date}", + "healthRemoveLabelFrom": "Supprimer {label} du {date} ?", + "healthRemoveLabBody": "Les {value} {unit} que vous avez enregistrés pour ce prélèvement. Ils quittent cet appareil et il n'y a pas de retour en arrière possible.", + "healthRemoveLabOlderNote": " Votre prélèvement du {date} reste, et s'affiche ici à la place.", + "healthRemovedNoneLeft": "{label} supprimé du {date}. Il ne reste aucun résultat de {label}.", + "healthRemovedShowingOlder": "{label} supprimé du {date}. Votre prélèvement du {older} s'affiche maintenant.", + "healthRemoveTheMarker": "Supprimer le marqueur {label}", + "healthNothingLoggedUnderIt": "Rien n'est enregistré sous celui-ci", + "healthResultsCount": "{n, plural, one{{n} résultat · {unit}} other{{n} résultats · {unit}}}", + "healthStillHoldsResults": "{count, plural, one{{label} contient encore {count} résultat. Supprimez-le d'abord — c'est le marqueur qui l'identifie.} other{{label} contient encore {count} résultats. Supprimez-les d'abord — c'est le marqueur qui les identifie.}}", + "healthRemoveMarkerQ": "Supprimer {label} ?", + "healthRemoveMarkerBody": "Il quitte la liste des marqueurs, vous ne pourrez donc plus l'enregistrer. Rien de mesuré ne part avec lui — vous n'avez aucun résultat sous ce marqueur.", + "healthMarkerLabel": "Marqueur", + "healthValueUnit": "Valeur ({unit})", + "healthDateDrawn": "Date du prélèvement (AAAA-MM-JJ)", + "healthValueMustBeNumber": "La valeur doit être un nombre seul, sans l'unité. Rien n'a été enregistré.", + "healthDateFormatError": "La date doit être au format AAAA-MM-JJ. Rien n'a été enregistré.", + "healthCouldNotSaveIt": "Impossible d'enregistrer : {error}", + "homeStepSensorStrapPhone": "Bracelet + téléphone", + "homeStepSensorStrap": "Bracelet", + "homeStepSensorPhone": "Téléphone", + "homeOvernightBuilding": "Les données de cette nuit sont encore en cours de traitement.", + "homeOvernightNothingYet": "Rien de cette nuit n'est encore arrivé dans l'application.", + "homeMonthJanuary": "janvier", + "homeMonthFebruary": "février", + "homeMonthMarch": "mars", + "homeMonthApril": "avril", + "homeMonthMay": "mai", + "homeMonthJune": "juin", + "homeMonthJuly": "juillet", + "homeMonthAugust": "août", + "homeMonthSeptember": "septembre", + "homeMonthOctober": "octobre", + "homeMonthNovember": "novembre", + "homeMonthDecember": "décembre", + "homeMonthJanuaryShort": "janv.", + "homeMonthFebruaryShort": "févr.", + "homeMonthMarchShort": "mars", + "homeMonthAprilShort": "avr.", + "homeMonthMayShort": "mai", + "homeMonthJuneShort": "juin", + "homeMonthJulyShort": "juil.", + "homeMonthAugustShort": "août", + "homeMonthSeptemberShort": "sept.", + "homeMonthOctoberShort": "oct.", + "homeMonthNovemberShort": "nov.", + "homeMonthDecemberShort": "déc.", + "homeWeekdayMonday": "lundi", + "homeWeekdayTuesday": "mardi", + "homeWeekdayWednesday": "mercredi", + "homeWeekdayThursday": "jeudi", + "homeWeekdayFriday": "vendredi", + "homeWeekdaySaturday": "samedi", + "homeWeekdaySunday": "dimanche", + "homeReadinessNotScored": "Non évalué", + "homeReadinessGoodToGo": "Prêt à y aller", + "homeReadinessSteady": "Stable", + "homeReadinessTakeItEasy": "Vas-y doucement", + "homeReadinessRestToday": "Repose-toi aujourd'hui", + "homeDriverHrv": "VFC", + "homeDriverRhr": "Fréquence cardiaque au repos", + "homeDriverResp": "Fréquence respiratoire", + "homeDriverTemp": "Température cutanée", + "homeDbRebuiltTitle": "Ta base de données a été reconstruite pour démarrer l'application", + "homeDbRebuiltNothingRecovered": "Rien n'a pu être récupéré.", + "homeDbRebuiltRecovered": "Récupéré : {list}.", + "homeDbRebuiltEmpty": "Vide : {list}.", + "homeDbRebuiltKept": "Le fichier d'origine est conservé à {path} ; rien n'a été supprimé.", + "homeWorkoutHoldTitle": "Un entraînement est encore en cours", + "homeWorkoutHoldBody": "Aujourd'hui est en attente pendant qu'un entraînement est en cours : le bracelet continue d'enregistrer, mais les chiffres sont calculés une fois la séance terminée. Termine l'entraînement depuis la barre ci-dessous et la journée se complètera ; synchroniser ne suffira pas.", + "homeInsightsRebuildingTitle": "Tes données inter-journalières sont en cours de reconstruction", + "homeInsightsRebuildingAlgoVersion": "La façon dont c'est calculé a changé avec la dernière mise à jour.", + "homeInsightsStaleOverWeek": "Le dernier récapitulatif date de plus d'une semaine, ce qui est trop ancien pour s'y fier.", + "homeInsightsStaleOnDay": "Le dernier récapitulatif a été généré le {day}, ce qui est trop ancien pour s'y fier.", + "homeInsightsNoVersionStamp": "Le récapitulatif enregistré ne porte aucune marque de version.", + "homeSyncBand": "Synchroniser le bracelet", + "homeWhyLabel": "Pourquoi ?", + "homeCalibrating": "Étalonnage", + "homeCalibratingNights": "{have, plural, one{{have} nuit sur {need}} other{{have} nuits sur {need}}}", + "homeCalibratingDays": "{have, plural, one{{have} jour sur {need}} other{{have} jours sur {need}}}", + "homeGapNoReason": "Rien n'a été enregistré pour expliquer cette absence.", + "homeRingRecovery": "Récupération", + "homeRingStrain": "Effort", + "homeRingSleep": "Sommeil", + "homeRingNoStrain": "Pas d'effort", + "homeRingNoSleep": "Pas de sommeil", + "homeSleepGapFallback": "Aucune nuit assez longue pour être évaluée n'a été enregistrée.", + "homeStrainOf21": "sur 21", + "homeSleepNoTarget": "Pas encore d'objectif", + "homeOfSpan": "sur {duration}", + "homeLoadFailedTitle": "Impossible de lire les données d'aujourd'hui", + "homeLoadFailedBody": "La journée enregistrée n'a pas pu être chargée. Rien n'a été supprimé : c'est une erreur de lecture, pas une perte de données.", + "homeTryAgain": "Réessayer", + "homeNothingDerivedTitle": "Rien n'a encore été calculé", + "homeNothingDerivedBody": "Aucun enregistrement du bracelet n'a encore été traité.", + "homeAskCoach": "Demander au coach", + "homeProfileSettings": "Profil et réglages", + "homeNothingTodayTitle": "Rien d'enregistré aujourd'hui", + "homeNothingTodayBody": "La dernière nuit évaluée par l'application est le {day}. Rien n'est arrivé depuis.", + "homeReadinessNotScoredTitle": "La récupération n'est pas évaluée aujourd'hui", + "homeReadinessNeedBody": "{need} pour savoir ce qui est normal pour toi.", + "homeReadinessNoReason": "Rien n'a été enregistré pour l'expliquer.", + "homeSeeWhatWasMissing": "Voir ce qui manquait", + "homeAtAGlance": "En un coup d'œil", + "homeTodaysPlan": "Plan du jour", + "homeBreakdownTitle": "Détail de ta journée", + "homeBreakdownSubtitle": "Heure par heure", + "homeIllnessRedTitle": "Plusieurs nuits d'affilée s'écartent de ta normale", + "homeIllnessAmberSameNight": "Cette nuit se situait hors de ta plage normale", + "homeIllnessAmberOtherNight": "Le {day} se situait hors de ta plage normale", + "homeIllnessBodyNoZ": "Ta fréquence cardiaque nocturne au repos est restée au-dessus de ta propre référence. Ceci reflète un seul signal. Cela indique une tendance, pas une cause.", + "homeIllnessBodyAbove": "Ta fréquence cardiaque nocturne au repos est restée au-dessus de ta propre référence ; cette nuit-là, elle se situait {z} écarts-types au-dessus. Ceci reflète un seul signal. Cela indique une tendance, pas une cause.", + "homeIllnessBodyBelow": "Ta fréquence cardiaque nocturne au repos est restée au-dessus de ta propre référence ; cette nuit-là, elle se situait {z} écarts-types en dessous. Ceci reflète un seul signal. Cela indique une tendance, pas une cause.", + "homeIllnessAdvice": "À surveiller si cela continue plusieurs jours.", + "homeHeartRate": "Fréquence cardiaque", + "homeRestingSub": "Au repos", + "homeNoRestingHr": "Pas de fréquence cardiaque au repos", + "homeNoRestingHrWhy": "La fréquence cardiaque au repos est lue depuis le sommeil, et aucun sommeil n'a été enregistré.", + "homeSteps": "Pas", + "homeStepsNone": "Aucun", + "homeStepsNotRecorded": "NON ENREGISTRÉ", + "homeStepsPercentGoal": "{pct} % de l'objectif", + "homeActiveEnergy": "Énergie active", + "homeCaloriesEstimated": "Estimé", + "homeCaloriesTotal": "{total} au total", + "homeNoEnergyEstimate": "Pas d'estimation d'énergie", + "homeStepsLeft": "{left} pas restants", + "homeMovement": "Mouvement", + "homeGoalSteps": "Objectif {goal}", + "homeStepGoalMet": "Objectif de pas atteint", + "homeStrainTargetMet": "Objectif d'effort atteint", + "homeAimForStrain": "Vise un effort de {aim}", + "homeTraining": "Entraînement", + "homeSleepNeedRow": "{duration} de sommeil", + "homeTonight": "Ce soir", + "homeNeed": "À calculer", + "homeBedTime": "Coucher à {time}", + "homeNoPlanTitle": "Pas encore de plan pour aujourd'hui", + "homeNoPlanWhyStale": "Les données inter-journalières dont il proviendrait sont en cours de reconstruction.", + "homeNoPlanWhyNone": "Aucune n'est encore établie.", + "homeGreetingStillUp": "Encore debout", + "homeGreetingMorning": "Bonjour", + "homeGreetingAfternoon": "Bon après-midi", + "homeGreetingEvening": "Bonsoir", + "wellnessTitle": "Bien-être", + "wellnessTabMind": "Esprit", + "wellnessTabRecovery": "Récupération", + "wellnessTabHabits": "Habitudes", + "wellnessTabMedication": "Médicaments", + "wellnessTabCycle": "Cycle", + "wellnessStartASitting": "COMMENCER UNE SÉANCE", + "wellnessExercisesNoun": "exercices", + "wellnessPickOneAndGo": "Choisissez-en une et lancez-vous", + "wellnessLastMinutes": "Dernière : {count} min", + "wellnessWriteTheDayDown": "Notez votre journée", + "wellnessOpen": "Ouvrir", + "wellnessStressLastNight": "Stress cette nuit", + "wellnessNoStressTitle": "Aucune mesure de stress cette nuit", + "wellnessNoStressBody": "Le stress est mesuré à partir du rythme cardiaque pendant votre repos nocturne, et cette nuit n'a produit aucune mesure.", + "wellnessAutonomicTension": "Tension autonome", + "wellnessStressLevelLow": "faible", + "wellnessStressLevelNormal": "normal", + "wellnessStressLevelElevated": "élevé", + "wellnessStressLevelHigh": "haut", + "wellnessJournalDefaultSubtitle": "Tout ce que vous voulez retenir de cette journée", + "wellnessJournalSubtitleShort": "{fields} et une note", + "wellnessJournalSubtitleLong": "{fields} et {more} de plus, plus une note", + "wellnessTurnInBy": "Couchez-vous avant {time}", + "wellnessDebtBody": "Vous accusez un déficit de {debt} par rapport à votre propre besoin, et celui de ce soir est de {need}.", + "wellnessSeeWhatLastNightCost": "Voir ce que cette nuit vous a coûté", + "wellnessWhatChargedAndDrained": "Ce qui vous a chargé et épuisé", + "wellnessNoDriversTitle": "Pas encore de facteurs de disposition", + "wellnessNoDriversBody": "Il faut suffisamment de nuits pour savoir ce qui est normal pour vous.", + "wellnessSleepNeedTonight": "Besoin de sommeil ce soir", + "wellnessNoSleepNeedTitle": "Pas encore de besoin de sommeil", + "wellnessNoSleepNeedBody": "Rien n'indique pourquoi il n'y a pas de besoin établi pour ce soir.", + "wellnessTonightsNeed": "Besoin de ce soir", + "wellnessSleepDebt": "Dette de sommeil", + "wellnessAddedForStrain": "Ajouté pour l'effort", + "wellnessCreditedFromNaps": "Crédité par les siestes", + "wellnessTargetBedtime": "Heure de coucher visée", + "wellnessTargetWake": "Heure de réveil visée", + "wellnessRemoveHabitSemantic": "Supprimer {label}", + "wellnessDaysYouDidIt": "Jours où vous l'avez fait", + "wellnessAddAHabit": "Ajouter une habitude", + "wellnessWhatYouLogTitle": "Ce que vous notez, comparé à vos chiffres", + "wellnessWhatYouLogSubtitle": "Dose, différence par habitude, et jour de la semaine", + "wellnessRemoveHabitConfirmTitle": "Supprimer {label} ?", + "wellnessRemoveHabitConfirmBody": "Elle ne sera plus demandée. Les jours déjà enregistrés sont conservés.", + "wellnessHabitHint": "Marcher après le déjeuner", + "wellnessAlreadyTrack": "Vous suivez déjà « {name} ».", + "wellnessNothingScheduledTitle": "Rien de programmé", + "wellnessNothingScheduledBody": "Ajoutez ce que vous prenez et quand.", + "wellnessAddAMedication": "Ajouter un médicament", + "wellnessNothingDueTodayTitle": "Rien à prendre aujourd'hui", + "wellnessNothingDueTodayBody": "Ce que vous prenez est programmé pour d'autres jours ou heures.", + "wellnessAdherence": "Observance", + "wellnessNothingToScoreTitle": "Rien à évaluer pour l'instant", + "wellnessNothingToScoreBody": "Aucune dose programmée n'est encore arrivée à échéance.", + "wellnessTakenOfScheduled": "Prises, parmi celles programmées ces sept derniers jours.", + "wellnessDosesUnit": "doses", + "wellnessUndoSkipped": "Annuler l'omission", + "wellnessSkippedOnPurpose": "Omise volontairement", + "wellnessBackToNotTaken": "Retour à non prise.", + "wellnessRecordedAsDecision": "Enregistrée comme une décision, pas un oubli.", + "wellnessWhichDaysDue": "Les jours où elle est due", + "wellnessRemoveMedTitle": "Supprimer {label}", + "wellnessRemoveMedBody": "Elle ne sera plus programmée. Les doses marquées sont conservées.", + "wellnessRemoveMedConfirmTitle": "Supprimer {label} ?", + "wellnessRemoveMedConfirmBody": "Elle ne sera plus programmée et ne comptera plus dans l'observance. Les doses déjà marquées sont conservées.", + "wellnessMedHint": "Vitamine D", + "wellnessNameLabel": "Nom", + "wellnessAdd": "Ajouter", + "wellnessEveryDay": "Tous les jours", + "wellnessWeekdays": "Jours de semaine", + "wellnessWeekends": "Week-ends", + "wellnessMon": "lun.", + "wellnessTue": "mar.", + "wellnessWed": "mer.", + "wellnessThu": "jeu.", + "wellnessFri": "ven.", + "wellnessSat": "sam.", + "wellnessSun": "dim.", + "wellnessWhenYouTakeIt": "Quand vous le prenez", + "wellnessChangeTheTime": "Changer l'heure", + "wellnessWhichDays": "QUELS JOURS", + "wellnessPickAtLeastOneDay": "Choisissez au moins un jour.", + "wellnessDueDays": "À prendre {days}.", + "wellnessMoreForMed": "Plus d'options pour {label}", + "wellnessMedAtTime": "{label} à {time}", + "wellnessStateTaken": "prise", + "wellnessStateSkipped": "omise", + "wellnessStateNotTaken": "non prise", + "wellnessStateDueLater": "à venir", + "wellnessMarkDone": "Marquer comme fait", + "wellnessWhatYouLogScreenTitle": "Ce que vous notez", + "wellnessNothingSeparatedTitle": "Rien ne s'est encore démarqué", + "wellnessNothingSeparatedBody": "Tout ce que vous notez est comparé à votre récupération, votre VFC, votre fréquence cardiaque au repos et votre efficacité de sommeil. Rien n'a encore franchi le seuil.", + "wellnessTheDaysYouDidIt": "Les jours où vous l'avez fait", + "wellnessHowMuchAndWhatFollowed": "Combien, et ce qui a suivi", + "wellnessLinkNeverCause": "Un lien sur vos propres journées, jamais une cause. Les jours où vous faites quelque chose sont déjà des jours de ce genre-là.", + "wellnessWhichDayOfWeek": "Quel jour de la semaine", + "wellnessHigher": "plus élevé", + "wellnessLower": "plus bas", + "wellnessHeadlineBinary": "Les {n} jours où vous avez noté {field}, {outcome} était {amount} {direction}", + "wellnessHeadlineNoSlope": "Les {n} jours où vous avez noté {field}, plus il y en avait, plus {outcome} était {direction}", + "wellnessHeadlineSlope": "Les {n} jours où vous avez noté {field}, {outcome} était {amount} {direction} par {step}", + "wellnessMatchedSameDay": "Comparé aux chiffres du même jour.", + "wellnessMatchedNightFollowed": "Comparé à la nuit suivante.", + "wellnessMatchedNightEnded": "Comparé à la nuit qui s'est terminée ce matin-là.", + "wellnessAgainstDaysYouDidNot": "Par rapport aux {n} jours où vous ne l'avez pas fait", + "wellnessRangeTo": "{lo} à {hi}", + "wellnessRankCorrelation": "Corrélation de rang {rho}{ci}. ", + "wellnessCaffeineCaveat": "Il ne s'agit que de votre dernière prise de caféine de la journée : deux tasses et cinq ont l'air identiques ici, donc « plus tard » peut en réalité vouloir dire « plus ». Une journée longue et stressante produit à la fois le café tardif et la mauvaise nuit.", + "wellnessHourLater": "heure plus tard", + "wellnessPointUnit": "point", + "wellnessNotEnoughWeeksTitle": "Pas encore assez de semaines", + "wellnessNotEnoughWeeksBody": "Comparer les sept jours de la semaine nécessite au moins huit semaines de données, avec cinq occurrences de chaque jour.", + "wellnessNoDayStandsOutTitle": "Aucun jour de la semaine ne se démarque", + "wellnessNoDayStandsOutBody": "Aucun jour ne se distingue des six autres une fois qu'on tient compte du fait que les sept ont été vérifiés.", + "wellnessWeekdayHeadline": "{weekday} : la disposition est {delta} {direction} que votre médiane globale", + "wellnessWeekdayDetail": "Sur {n} d'entre eux. Un jour de la semaine n'est pas une cause : c'est un contenant pour ce que vous y faites. Rien ici n'est un conseil.", + "wellnessPluralMonday": "les lundis", + "wellnessPluralTuesday": "les mardis", + "wellnessPluralWednesday": "les mercredis", + "wellnessPluralThursday": "les jeudis", + "wellnessPluralFriday": "les vendredis", + "wellnessPluralSaturday": "les samedis", + "wellnessPluralSunday": "les dimanches", + "sleepDetailNavTitle": "Sommeil", + "sleepDetailNoNightTitle": "Aucune nuit à afficher", + "sleepDetailNoNightBody": "Aucune plage d'enregistrements du bracelet assez longue pour être notée.", + "sleepDetailNoNightFix": "Portez le bracelet toute la nuit et synchronisez le matin", + "sleepDetailStagesSection": "Phases", + "sleepDetailVersusUsualSection": "Par rapport à d'habitude", + "sleepDetailUnusualLastNight": "Chose inhabituelle cette nuit", + "sleepDetailUnusualOnDay": "Chose inhabituelle le {day}", + "sleepDetailOvernightSection": "Signaux nocturnes", + "sleepDetailTonightSection": "Ce soir", + "sleepDetailTotalSleep": "Sommeil total", + "sleepDetailInBed": "AU LIT", + "sleepDetailWatched": "OBSERVÉ", + "sleepDetailAsleepOfThat": "ENDORMI SUR CE TEMPS", + "sleepDetailAsleep": "ENDORMI", + "sleepDetailWatchedExplain": "Nous avons observé {watched} sur vos {inBed} passées au lit ; le reste n'est pas une mesure. Le sommeil, ainsi que les parts de phases ci-dessous, sont calculés sur le temps observé.", + "sleepDetailWindowMine": "Vous avez défini cette fenêtre", + "sleepDetailWindowFallback": "Cette fenêtre a été déduite de la fréquence cardiaque", + "sleepDetailWindowAuto": "Cette fenêtre a été estimée à partir des signaux", + "sleepDetailWindowFallbackBody": "L'analyse n'a pas pu trouver les limites, donc ces horaires sont une estimation.", + "sleepDetailWindowSol": "Du début de votre fenêtre jusqu'à l'endormissement : {band}.", + "sleepDetailConfirmTimes": "Ces horaires sont corrects", + "sleepDetailChangeTimes": "Modifier les horaires", + "sleepDetailSetTimesMyself": "Définir les horaires moi-même", + "sleepDetailBackToAutomatic": "Revenir à l'automatique", + "sleepDetailReanalysing": "Réanalyse de la nuit…", + "sleepDetailCorrectionFailedTitle": "Cette correction n'a pas été appliquée", + "sleepDetailBedTimeHelp": "HEURE DU COUCHER", + "sleepDetailWakeTimeHelp": "HEURE DU LEVER", + "sleepDetailReanalyseFailed": "La nuit n'a pas été réanalysée : une autre réanalyse était déjà en cours, ou elle a échoué. Les horaires que vous avez définis sont enregistrés ; « Tout réanalyser » dans Vos données les appliquera.", + "sleepDetailNoHypnogramTitle": "Aucun hypnogramme pour cette nuit", + "sleepDetailNoHypnogramBody": "L'analyse des phases nécessite le mouvement et le rythme cardiaque. L'un des deux manquait.", + "sleepDetailThroughTheNight": "Au fil de la nuit", + "sleepDetailUnitStage": "phase", + "sleepDetailTapDragCycles": "{n, plural, one{Touchez ou faites glisser le graphique pour n'importe quel instant. {n} cycle.} other{Touchez ou faites glisser le graphique pour n'importe quel instant. {n} cycles.}}", + "sleepDetailTapDragCyclesAvg": "{n, plural, one{Touchez ou faites glisser le graphique pour n'importe quel instant. {n} cycle, {avg} en moyenne.} other{Touchez ou faites glisser le graphique pour n'importe quel instant. {n} cycles, {avg} en moyenne.}}", + "sleepDetailTapDragNone": "Touchez ou faites glisser le graphique pour n'importe quel instant de la nuit.", + "sleepDetailNoWakeups": "Aucun réveil de 5 minutes ou plus ; les plus courts sont invisibles pour un bracelet.", + "sleepDetailAtLeastWakeups": "{n, plural, one{Au moins {n} réveil de 5 minutes ou plus ; les plus courts sont invisibles pour un bracelet.} other{Au moins {n} réveils de 5 minutes ou plus ; les plus courts sont invisibles pour un bracelet.}}", + "sleepDetailLongestStretch": "Plus longue période ininterrompue : {longest}.", + "sleepDetailHypnogramLabel": "Hypnogramme", + "sleepDetailPercentThroughNight": "{pct} % de la nuit", + "sleepDetailNotMeasured": "non mesuré", + "sleepDetailScrubAt": "{at}, {stage}", + "sleepDetailHeartRate": "Fréquence cardiaque", + "sleepDetailHrv": "VFC", + "sleepDetailBreathing": "Respiration", + "sleepDetailTemp": "Temp.", + "sleepDetailNotMeasuredCap": "Non mesuré", + "sleepDetailNoSignalAtMoment": "Aucun signal enregistré à cet instant.", + "sleepDetailStageAwake": "Éveil", + "sleepDetailStageRem": "Paradoxal", + "sleepDetailStageLight": "Sommeil léger", + "sleepDetailStageDeep": "Sommeil profond", + "sleepDetailDeep": "Profond", + "sleepDetailLight": "Léger", + "sleepDetailNoStageSplitTitle": "Aucune répartition des phases pour cette nuit", + "sleepDetailNoStageSplitBody": "Aucun rythme cardiaque mesuré sur toute la fenêtre.", + "sleepDetailStageRangeExplain": "Chaque phase est une fourchette, pas un décompte : mieux la nuit a été observée, plus elle est étroite. Le sommeil profond est le plus large. L'éveil reste un chiffre unique. Les statistiques avancées donnent les décomptes exacts.", + "sleepDetailTimeAsleep": "Temps de sommeil", + "sleepDetailShorterThanUsual": "plus court que d'habitude", + "sleepDetailLongerThanUsual": "plus long que d'habitude", + "sleepDetailLessThanUsual": "moins que d'habitude", + "sleepDetailMoreThanUsual": "plus que d'habitude", + "sleepDetailAsleepWhileInBed": "Endormi pendant le temps au lit", + "sleepDetailLowerThanUsual": "plus bas que d'habitude", + "sleepDetailHigherThanUsual": "plus haut que d'habitude", + "sleepDetailFellAsleep": "Endormissement", + "sleepDetailEarlierThanUsual": "plus tôt que d'habitude", + "sleepDetailLaterThanUsual": "plus tard que d'habitude", + "sleepDetailNotEnoughNightsTitle": "Pas assez de nuits pour comparer", + "sleepDetailNightsSoFar": "{have} nuit(s) sur {min} pour l'instant", + "sleepDetailBarExplain": "La barre représente la moitié centrale de vos propres nuits.", + "sleepDetailLessThanAny": "{noun} {value} — moins que n'importe laquelle de vos {count} dernières nuits, dont la plus basse était {lowest}.", + "sleepDetailMoreThanAny": "{noun} {value} — plus que n'importe laquelle de vos {count} dernières nuits, dont la plus haute était {highest}.", + "sleepDetailYouSlept": "Vous avez dormi", + "sleepDetailShortestNightLately": "Votre nuit la plus courte récemment", + "sleepDetailLongestNightLately": "Votre nuit la plus longue récemment", + "sleepDetailSleepingHrHighTitle": "La fréquence cardiaque au sommeil était élevée", + "sleepDetailSleepingHrHighBody": "{bpm} bpm au-dessus de votre propre référence. Courant après de l'alcool, un repas tardif, une séance intense ou le début d'une infection — ceci est une mesure, pas un diagnostic.", + "sleepDetailNothingStoodOut": "Rien de particulier à signaler.", + "sleepDetailSleepingHr": "FC AU SOMMEIL", + "sleepDetailLowest": "MINIMUM", + "sleepDetailBreathingCaps": "RESPIRATION", + "sleepDetailSkinTemp": "Temp. cutanée", + "sleepDetailSleepNeedNotEstablished": "Besoin de sommeil pas encore établi", + "sleepDetailYourNeedIs": "Votre besoin est de {need}", + "sleepDetailYouAreDown": "vous êtes en déficit de {debt}", + "sleepDetailLightsOut": "extinction des feux", + "sleepDetailToAimFor": "à viser", + "sleepDetailNoPersonalRangeYet": "Pas encore de plage personnelle — {count} nuit(s) sur {min}.", + "sleepDetailNotFarEnoughToCall": "Pas assez éloigné de d'habitude pour trancher", + "sleepDetailTypicalForYou": "Typique pour vous", + "sleepDetailVerdictSummary": "{verdict} · habituel {lo}–{hi} sur {n} nuits", + "sleepDetailNoOvernightTitle": "Aucune ligne de signal nocturne", + "sleepDetailNoOvernightBody": "Aucun enregistrement nocturne n'est parvenu ce jour-là.", + "sleepDetailSolUnder15": "moins de 15 minutes", + "sleepDetailSolOverHour": "plus d'une heure", + "sleepDetailSolRange": "{lo}–{hi} minutes", + "workoutTabForYou": "Pour vous", + "workoutTabActivities": "Activités", + "workoutTabHistory": "Historique", + "workoutScreenTitle": "Entraînement", + "workoutStartSessionLabel": "DÉMARRER UNE SÉANCE", + "workoutActivitiesNoun": "activités", + "workoutThisWeek": "Cette semaine", + "workoutTrainingLoad": "Charge d'entraînement", + "workoutTodaysStrainAction": "Effort du jour", + "workoutMechanicalLoadTitle": "CHARGE MÉCANIQUE", + "workoutKgLiftedUnit": "kg soulevés", + "workoutTonnageFootnoteIntro": "Répétitions × charge sur les séries enregistrées avec un poids. ", + "workoutTonnageFootnotePartial": "Les séries enregistrées sans poids n'y figurent pas, donc c'est un minimum et non un total. ", + "workoutTonnageFootnoteOutro": "Exact pour ce que vous avez saisi et sans valeur entre exercices différents — c'est pourquoi c'est exclu de la tension et de la récupération.", + "workoutOverreachHeadline": "Vos 7 derniers jours de charge sont {ratio}× vos six dernières semaines habituelles, et votre fréquence cardiaque au repos était au-dessus de la normale {nightsElevated} nuits sur {nightsConsidered}.", + "workoutOverreachBody": "Deux mesures qui pointent par coïncidence dans la même direction. Une maladie, un voyage, l'altitude, l'alcool ou plusieurs nuits de mauvais sommeil produisent le même schéma, et rien ici ne permet de les distinguer.", + "workoutNoLoadTitle": "Pas encore de charge d'entraînement", + "workoutNoLoadBody": "La forme et la fatigue sont des moyennes sur 42 et 7 jours. Il faut environ deux semaines de séances.", + "workoutFitnessLabel": "forme", + "workoutDailyLoadTitle": "CHARGE QUOTIDIENNE", + "workoutTrimpUnit": "TRIMP", + "workoutDailyLoadFootnoteIntro": "Impulsion d'entraînement de Banister — minutes pondérées par la réserve de fréquence cardiaque. ", + "workoutDailyLoadAllDays": "Les sept derniers jours.", + "workoutDailyLoadPartialDays": "{days} des sept derniers jours ont produit un chiffre.", + "workoutFatigueLabel": "Fatigue", + "workoutFormLabel": "Forme", + "workoutNotYet": "Pas encore", + "workoutFormFresh": "Frais", + "workoutFormSteady": "Stable", + "workoutFormBuilding": "En hausse", + "workoutFormOverreaching": "Surentraînement", + "workoutSearchActivitiesLabel": "Rechercher des activités", + "workoutSearchActivitiesCount": "{count, plural, one{Rechercher {count} activité} other{Rechercher {count} activités}}", + "workoutQuickStartHeader": "DÉMARRAGE RAPIDE", + "workoutCalorieNeedWeightTitle": "Les estimations de calories ont besoin de votre poids", + "workoutAddWeightFix": "Ajouter le poids dans le profil", + "workoutCalorieEstimatesTitle": "Les calories affichées sont des estimations", + "workoutSuggestionsTitle": "{n, plural, one{{n} effort repéré mais non enregistré} other{{n} efforts repérés mais non enregistrés}}", + "workoutSuggestionsBody": "Le bracelet a détecté un effort soutenu et rien n'a été démarré. Rien n'est enregistré tant que vous ne le décidez pas.", + "workoutReviewFix": "{n, plural, one{L'examiner} other{Les examiner}}", + "workoutLogPastTitle": "Avez-vous fait quelque chose que le bracelet a manqué ?", + "workoutLogPastBody": "Saisissez vous-même les horaires : la séance sera notée à partir de la fréquence cardiaque enregistrée sur cette période, comme toute autre séance.", + "workoutLogPastFix": "Enregistrer une séance passée", + "workoutNoSessionsTitle": "Aucune séance enregistrée pour l'instant", + "workoutNoSessionsBody": "Les séances apparaissent ici dès que vous en commencez une.", + "workoutStartWorkoutFix": "Démarrer une séance", + "workoutTrackedLabel": "Suivies", + "workoutWeeklyLoadLabel": "Charge hebdomadaire", + "workoutNoneLabel": "Aucune", + "workoutImportedThisWeekNote": "{count} des séances de cette semaine viennent de {storeName}. Elles comptent ici, mais sont exclues de la charge hebdomadaire — une séance importée arrive sans trace de fréquence cardiaque, et un chiffre de charge sans cela serait inventé.", + "workoutAutoImportOnLabel": "Importation automatique activée. Touchez pour la désactiver.", + "workoutAutoImportOffLabel": "Importation automatique désactivée. Touchez pour l'activer.", + "workoutImportFromStore": "Importer depuis {storeName}", + "workoutFetchNowLabel": "Récupérer les séances maintenant", + "workoutImportDenied": "{storeName} n'a pas autorisé l'accès aux séances. Rien n'a été lu.", + "workoutImportEmpty": "Rien n'est revenu. {storeName} ne contient aucune séance dans la période partagée.", + "workoutImportNoRoutes": " {storeName} ne partagera pas les itinéraires, aucun n'aura donc de coordonnées.", + "workoutImportNoneWithRoute": " Aucun n'avait d'itinéraire enregistré.", + "workoutImportSomeWithRoute": " {count} sont arrivés avec un itinéraire.", + "workoutImportBroughtIn": "{count, plural, one{{count} séance importée.} other{{count} séances importées.}}", + "workoutImportFailed": "Échec : {error}", + "workoutMorningAfterTitle": "Le lendemain matin", + "workoutMorningAfterBody": "Votre propre historique, pas une règle sur l'activité — ces matins ont aussi eu la soirée qui les a précédés. Rien ici n'est une raison de sauter une séance.", + "workoutAfterActivity": "Après {name}", + "workoutUnchangedLabel": "Inchangé", + "workoutRestingHeartRateLabel": "Fréquence cardiaque au repos", + "workoutHrvLabel": "VFC", + "workoutMorningCount": "{n, plural, one{{n} matin} other{{n} matins}}", + "workoutInsideRangeSuffix": " · dans votre plage habituelle nuit après nuit", + "workoutDeleteSessionLabel": "Supprimer cette séance", + "workoutStrainLabel": "tension", + "workoutTimeInZonesTitle": "TEMPS PAR ZONE", + "workoutMinutesUnit": "minutes", + "workoutFixTimesOnSessionLabel": "Corriger les horaires de cette séance", + "workoutFixTimes": "Corriger les horaires", + "workoutTimeStatLabel": "Durée", + "workoutDistanceStatLabel": "Distance", + "workoutCaloriesStatLabel": "Calories", + "workoutNotCostedValue": "Non chiffrées", + "workoutMaxHrStatLabel": "FC max", + "workoutNoReadingValue": "Aucune mesure", + "workoutConfirmDeleteTitle": "Supprimer cette séance de {activity} ?", + "workoutDeleteBodyOwn": "Elle disparaît d'OpenStrap. Une copie dans {storeName}, si elle existe, reste où elle est.", + "workoutDeleteBodyImported": "Elle disparaît d'OpenStrap et ne sera pas réimportée. L'original dans {storeName} reste en place.", + "workoutWhenToday": "Aujourd'hui, {time}", + "workoutWhenYesterday": "Hier, {time}", + "workoutWeekdayLetterMon": "L", + "workoutWeekdayLetterTue": "M", + "workoutWeekdayLetterWed": "M", + "workoutWeekdayLetterThu": "J", + "workoutWeekdayLetterFri": "V", + "workoutWeekdayLetterSat": "S", + "workoutWeekdayLetterSun": "D", + "workoutWeekdayAbbrMon": "lun", + "workoutWeekdayAbbrTue": "mar", + "workoutWeekdayAbbrWed": "mer", + "workoutWeekdayAbbrThu": "jeu", + "workoutWeekdayAbbrFri": "ven", + "workoutWeekdayAbbrSat": "sam", + "workoutWeekdayAbbrSun": "dim", + "activitySetupRouteLabel": "Itinéraire", + "activitySetupRouteDetail": "Enregistré si la position est disponible, et conservé sur ce téléphone", + "activitySetupHeartRateLabel": "Fréquence cardiaque", + "activitySetupBandConnected": "Bracelet connecté", + "activitySetupNoBandConnected": "Aucun bracelet connecté", + "activitySetupPrivateLabel": "Séance privée", + "activitySetupPrivateDetail": "Masquée des résumés et des exports", + "activitySetupCaloriesNeedWeight": "Les calories nécessitent votre poids.", + "activitySetupCalorieEstimate": "Environ {est} kcal par {minutes} min, à partir de {met} MET et de votre poids.", + "activitySetupTrackSets": "Séries, répétitions et charge — saisies par vous", + "activitySetupTrackDistanceGps": "Distance, allure et fréquence cardiaque", + "activitySetupTrackTime": "Durée et fréquence cardiaque", + "activitySetupTrackInterval": "Rounds et fréquence cardiaque", + "activitySetupTrackStillness": "Durée, respiration et immobilité", + "activitySetupSessionRunningTitle": "Une séance est déjà en cours", + "activitySetupSessionRunningBody": "Une seule séance peut être active à la fois.", + "activitySetupOpenRunningSession": "Ouvrir la séance en cours", + "activitySetupStart": "Démarrer", + "activityPickerTitle": "Choisir une activité", + "activityPickerSearchLabel": "Rechercher des activités", + "activityPickerSearchHint": "Rechercher parmi {count} activités", + "activityPickerNoMatchTitle": "Aucune activité ne correspond", + "activityPickerNoMatchBody": "Le catalogue couvre environ soixante-dix activités avec un coût énergétique publié. Choisissez la plus proche.", + "activityPickerQuickStart": "DÉMARRAGE RAPIDE", + "activityPickerRecent": "RÉCENTES", + "activityPickerCalorieEstimatesTitle": "Les calories sont des estimations", + "activityPickerMetValue": "{met} MET", + "activityPickerKcalPer30": "{kcal} kcal / 30 min", + "dayStrainToday": "AUJOURD’HUI", + "dayStrainTitle": "Effort du jour", + "dayStrainNoTraceTitle": "Aucune courbe d’effort pour ce jour", + "dayStrainNoMinuteTraceTitle": "Aucune courbe minute par minute pour ce jour", + "dayStrainNoReasonBody": "Aucune donnée n’explique pourquoi ce jour n’a produit aucun effort.", + "dayStrainScoredNoTraceBody": "L’effort du jour est {strain}. Les minutes d’éveil à partir desquelles il a été calculé ne sont pas conservées pour ce jour.", + "dayStrainWearBandFix": "Portez le bracelet toute la journée", + "dayStrainChartTitle": "EFFORT AU FIL DE LA JOURNÉE", + "dayStrainChartFootnote": "C’est cumulatif, donc la courbe ne fait que monter — les portions RAIDES indiquent où était l’effort. Calculée à partir de {drawn} minutes d’éveil enregistrées.", + "dayStrainPeakHr": "FC max", + "dayStrainWorn": "Porté", + "dayStrainLowCoverageTitle": "Le bracelet a vu {pct} % de cette journée", + "dayStrainLowCoverageBody": "L’effort est un total calculé sur les minutes enregistrées ; une journée portée partiellement affiche donc une valeur plus basse qu’une journée complète, et les deux ne sont pas comparables.", + "dayStrainTimeInZonesSection": "Temps dans les zones", + "dayStrainZonesChartTitle": "TEMPS DANS LES ZONES", + "dayStrainZoneFootnoteKarvonen": "Les limites des zones couvrent l’écart entre votre fréquence cardiaque au repos mesurée et la plus élevée observée ({maxHr} bpm). Les deux ont été mesurées sur vous.", + "dayStrainZoneFootnoteObserved": "Les limites des zones sont des pourcentages de la fréquence cardiaque la plus élevée observée ({maxHr} bpm) — mesurée, non estimée.", + "dayStrainHowSet": "Comment elles sont définies", + "dayStrainInputsSection": "De quoi il est composé", + "dayStrainInputsBase": "TRIMP de Banister calculé sur votre fréquence cardiaque à l’état éveillé, ramené sur une échelle de 0 à 21.", + "dayStrainInputsMaxHr": "Il a été calculé par rapport à un maximum supposé de {maxHr} bpm — estimé à partir de votre âge et de votre bracelet, non mesuré.", + "dayStrainInputsMeasuredCeilingNote": "La barre des zones ci-dessus utilise plutôt le plafond mesuré ; l’effort n’a pas été basculé dessus, car cela réécrirait tous les scores d’effort que vous avez déjà vus.", + "dayStrainInputsRhrAnchor": "L’autre point d’ancrage est votre fréquence cardiaque au repos de la nuit précédente ; une nuit manquée par le bracelet affecte donc toute la journée.", + "activityZonesTitle": "Zones de fréquence cardiaque", + "activityZonesYourZonesSection": "Vos zones", + "activityZonesIntensitySection": "Où est passée votre intensité", + "activityZonesNoCeilingTitle": "Pas encore de plafond mesuré", + "activityZonesNoCeilingTanakaTail": " Tant qu’aucun n’est mesuré, les zones ci-dessous sont basées sur votre âge.", + "activityZonesNoCeilingDefaultBody": "Nous ne comptons qu’une lecture élevée maintenue par le bracelet pendant 15 secondes pendant que vous bougiez. Un pic d’une seconde n’est pas une fréquence cardiaque.", + "activityZonesWearBandFix": "Portez le bracelet pendant vos séances intenses habituelles", + "activityZonesHighestSeenLabel": "PLUS ÉLEVÉ OBSERVÉ", + "activityZonesBpmUnit": "bpm", + "activityZonesCeilingOnDate": "le {date}", + "activityZonesCeilingDuringSession": "pendant {session}", + "activityZonesHighestSeenFootnote": "C’est la valeur la plus élevée que nous ayons mesurée, pas une limite — elle augmente progressivement à mesure que le bracelet observe des efforts plus intenses. N’allez pas la tester exprès.", + "activityZonesNoZonesTitle": "Pas encore de zones", + "catalogueZonesWhy": "Les limites de zone sont des pourcentages d'une fréquence cardiaque maximale estimée à partir de votre âge — non mesurée sur vous.", + "activityZonesNoAgeBody": "Les limites des zones sont des pourcentages d’une fréquence cardiaque maximale, et sans votre âge, il n’y a rien dont calculer un pourcentage.", + "activityZonesNoZonesDefaultBody": "Aucune donnée n’explique pourquoi il n’y a pas encore de limites de zones.", + "activityZonesAddAgeFix": "Ajoutez votre âge dans le profil", + "activityZonesAnchorKarvonen": "Calculées à partir de deux valeurs mesurées par le bracelet sur vous : votre fréquence au repos ({restingHr}, la médiane de vos {restingDays} dernières nuits) et la plus élevée observée ({maxHr}). Une fréquence au repos basse élargit la zone 1. Ce sont les bandes habituelles, pas vos propres seuils mesurés.", + "activityZonesAnchorObserved": "Calculées à partir de la fréquence cardiaque la plus élevée observée ({maxHr}). Après {restingMinDays} nuits de fréquence au repos (vous en avez {restingDays}), votre fréquence au repos s’y ajoutera, ce qui vous correspondra mieux. Ce sont les bandes habituelles, pas vos propres seuils mesurés.", + "activityZonesAnchorTanaka": "Calculées à partir de {maxHr} bpm, estimés d’après votre âge plutôt que mesurés sur vous — l’écart peut atteindre 20 bpm dans un sens ou dans l’autre. Les limites passeront à un plafond mesuré dès que le bracelet observera une séance suffisamment intense.", + "activityZonesAnchorDefault": "Les limites des zones sont des pourcentages d’une fréquence cardiaque maximale.", + "activityZonesNotShownTitle": "Pas encore affiché", + "activityZonesNeedsMonthBody": "Nécessite environ un mois de séances enregistrées, chacune avec une fréquence cardiaque minute par minute.", + "activityZonesAgeEstimateBody": "Les barres ne refléteraient que l’estimation basée sur l’âge, pas votre entraînement. Elles apparaîtront une fois les limites des zones ci-dessus mesurées.", + "activityZonesSessionMinutesChartTitle": "MINUTES DE SÉANCE, 28 DERNIERS JOURS", + "activityZonesShapePyramidal": "La plupart de vos minutes sont faciles, moins au milieu, le moins en intense — une pyramide.", + "activityZonesShapePolarised": "La plupart de vos minutes sont faciles et le reste est intense, avec peu d’entre-deux.", + "activityZonesShapeMiddleHeavy": "La plupart de vos minutes se situent dans la zone intermédiaire plutôt que faciles ou intenses.", + "activityZonesShapeSummary": "{easy} min faciles, {moderate} modérées, {hard} intenses, sur {sessions} séances enregistrées. Une description, pas un objectif.", + "activityShareTitle": "Partager", + "activityShareOpenFailed": "Impossible d'ouvrir le menu de partage.", + "activitySharePhotoHeader": "VOTRE PHOTO", + "activityShareAddPhoto": "Ajouter une photo", + "activityShareChangePhoto": "Changer de photo", + "activitySharePhotoHint": "Depuis ce téléphone. Rien n'est mis en ligne", + "activityShareRemovePhoto": "Retirer la photo", + "activityShareBasemapHeader": "FOND DE CARTE", + "activityShareDrawMap": "Afficher la vraie carte", + "activityShareMapHint": "Demande à openstreetmap.org les tuiles qui couvrent cet itinéraire. Désactivé, l'itinéraire se dessine seul", + "activityShareFetchingMapTitle": "Récupération de la carte", + "activityShareFetchingMapBody": "Le visuel s'affiche dès que chaque tuile est arrivée.", + "activityShareNoMapTitle": "Pas de carte pour ce visuel", + "activityShareNoMapBody": "Les tuiles de la carte n'ont pas pu être récupérées, donc l'itinéraire se dessine seul. Le reste du visuel ne change pas.", + "activityShareStatusPrivateTitle": "Cette séance est privée", + "activityShareStatusPrivateBody": "Masquée des résumés et des exports.", + "activityPosterFormatPost": "Publication", + "activityPosterFormatStory": "Story", + "activitySummaryRpeHeadline": "QUEL A ÉTÉ LE NIVEAU D'EFFORT ?", + "activitySummaryRpeBody": "Votre propre évaluation de l'effort. C'est un ressenti, pas une mesure — c'est justement le but, car elle peut ne pas correspondre aux chiffres ci-dessus.", + "activitySummaryRateEffort": "Noter cet effort {n} sur 10", + "activitySummaryRpeVeryEasy": "1 · très facile", + "activitySummaryRpeMaximal": "10 · maximal", + "activitySummaryNotNow": "Pas maintenant", + "activitySummaryShareThis": "Partager cette séance de {name}", + "activitySummaryChangeType": "Changer le type d'activité", + "activitySummaryUnsavedTitle": "Cette séance n'est pas encore enregistrée", + "activitySummaryUnsavedBody": "L'écriture sur ce téléphone a échoué.", + "activitySummarySaving": "Enregistrement", + "activitySummaryTryAgain": "Réessayer", + "activitySummaryPrivate": "Privée", + "activitySummaryStepsBasis": "Les pas proviennent du propre capteur de mouvement du bracelet, qui ne les compte qu'à la marche.", + "activitySummaryCaloriesNeedWeight": "Les calories nécessitent votre poids.", + "activitySummaryNoCalorieNoStrain": "Aucun chiffre de calories pour cette séance. Une estimation d'énergie à partir de la fréquence cardiaque nécessite vos fréquences maximale et au repos, et l'une d'elles n'est pas définie.", + "activitySummaryNoCalorieWithStrain": "Aucun chiffre de calories pour cette séance — une estimation d'énergie à partir de la fréquence cardiaque nécessite vos fréquences maximale et au repos, et l'une d'elles n'est pas définie. L'effort ci-dessus est ce qui a été mesuré, sur sa propre échelle de 0 à 21.", + "activitySummaryCalorieNoHr": "Estimé à partir de {met} MET et de votre poids. Aucune fréquence cardiaque n'a été enregistrée pendant cette séance, elle n'entre donc pas dans le chiffre.", + "activitySummaryCalorieWithHr": "Estimé à partir de {met} MET, de votre poids et de votre fréquence cardiaque.", + "activitySummaryNothingLoggedWithLoad": "Rien n'a été enregistré avec une charge", + "activitySummarySetUnit": "{n, plural, one{série} other{séries}}", + "activitySummaryVolumeLoadedSets": "Volume des séries chargées", + "activitySummaryTotalVolume": "Volume total", + "activitySummaryElapsedTime": "Temps écoulé", + "activitySummaryClimbed": "+{m} m de dénivelé", + "activitySummaryLapsCaption": "{n, plural, one{{n} longueur} other{{n} longueurs}}", + "activitySummaryNoRouteTitle": "Aucun itinéraire pour cette séance", + "activitySummaryNoRouteBody": "La localisation était désactivée, ou cette activité n'a pas été enregistrée avec le GPS.", + "activitySummaryRouteTitle": "ITINÉRAIRE", + "activitySummarySlower": "Plus lent", + "activitySummaryFaster": "Plus rapide", + "activitySummaryStartFinishPinned": "Le départ et l'arrivée sont épinglés.", + "activitySummaryRouteFootnote": "{distance} {unit}, départ et arrivée épinglés.", + "activitySummaryNoSetsTitle": "Aucune série enregistrée", + "activitySummaryNoSetsBody": "Rien n'a été saisi pour cette séance, il n'y a donc ni charge ni volume à totaliser.", + "activitySummaryNoRoundsTitle": "Aucune reprise enregistrée", + "activitySummaryNoRoundsBody": "0 reprise enregistrée.", + "activitySummaryIntervalLadderTitle": "ÉCHELLE D'INTERVALLES", + "activitySummaryWork": "Effort", + "activitySummaryRest": "Repos", + "activitySummaryRoundLabel": "Reprise {n}", + "activitySummaryLongestBlock": "Bloc le plus long {time}.", + "activitySummaryPosesCount": "{n, plural, one{{n} posture} other{{n} postures}}", + "activitySummaryNoLapsTitle": "Aucune longueur comptée", + "activitySummaryNoLapsBody": "0 longueur comptée.", + "activitySummaryLapsTitle": "LONGUEURS", + "activitySummarySecondsPerLap": "secondes par longueur", + "activitySummaryLapLabel": "Longueur {n}", + "activitySummaryPoolLength": "Bassin de {m} m", + "activitySummaryFastest": "plus rapide {time}", + "activitySummarySlowest": "plus lent {time}", + "activitySummaryNoElevationTitle": "Aucun profil d'élévation", + "activitySummaryNoElevationBody": "Aucun itinéraire, ou l'itinéraire ne comportait aucune altitude.", + "activitySummaryElevationTitle": "ÉLÉVATION", + "activitySummaryStart": "Départ", + "activitySummaryFinish": "Arrivée", + "activitySummaryGain": "Dénivelé positif", + "activitySummaryLoss": "Dénivelé négatif", + "activitySummaryPeak": "Point culminant", + "activitySummaryColdPlungeWhy": "Le froid resserre les vaisseaux sanguins que lit le capteur. Ne rien trouver ici est attendu, pas un défaut.", + "activitySummaryHeatWhy": "La chaleur, la transpiration et un bracelet qui se desserre en chauffant empêchent tous le capteur de détecter un pouls. Ne rien trouver ici est normal, pas un défaut.", + "activitySummaryNoPulseTitle": "Aucune lecture de pouls pour cette séance de {activity}", + "activitySummaryOneMinutePulse": "Une minute de pouls, et rien de plus", + "activitySummaryPulseGapNote": "Le bracelet a détecté un pouls pendant {have} des {total} minutes. Les interruptions sont attendues, ce qui est tracé est donc la partie qu'il a pu voir.", + "activitySummaryTooShortTitle": "Trop courte pour être tracée", + "activitySummaryTooShortBody": "Une minute de fréquence cardiaque est un point, pas une ligne.", + "activitySummaryNoHrTitle": "Aucune fréquence cardiaque pour cette séance", + "activitySummaryNoHrBody": "Le bracelet n'a rien signalé pendant cette séance.", + "activitySummaryCheckBandConnection": "Vérifier la connexion du bracelet", + "activitySummaryPartialTrace": "Trace partielle — le bracelet a transmis {pct}% de ces minutes.", + "activitySummaryHeartRateTitle": "FRÉQUENCE CARDIAQUE", + "activitySummaryHardMinutesNote": "{min} min au-dessus de 80% de votre maximum.", + "activitySummaryTimeInZonesTitle": "TEMPS PAR ZONE", + "activitySummaryTopSet": "Meilleure série", + "activitySummaryOneRepMax": "1RM estimé {kg} kg", + "activitySummarySomeSetsNoLoadTitle": "Certaines séries n'avaient pas de charge", + "activitySummarySomeSetsNoLoadBody": "Comptées dans les séries et répétitions, mais exclues du volume.", + "activitySummaryScore": "Score", + "activitySummaryGameSetLabel": "Set {n}", + "activitySummaryNoSplitsTitle": "Aucun temps intermédiaire pour cette séance", + "activitySummaryNoSplitsBody": "Les temps intermédiaires nécessitent une distance enregistrée.", + "activitySummaryKm": "KM", + "activitySummaryPace": "ALLURE", + "activitySummaryHr": "FC", + "activitySummarySetsLoggedZero": "0 série enregistrée.", + "activitySummaryRoundHeader": "R", + "activitySummaryWorkHeader": "EFFORT", + "activitySummaryRestHeader": "REPOS", + "activitySummaryAvgBpm": "FC MOY", + "activitySummaryLapHeader": "LONG.", + "activitySummaryTimeHeader": "TEMPS", + "activitySummarySpeedVsFastest": "VITESSE vs PLUS RAPIDE", + "activitySummaryBodyweightReps": "{n, plural, one{{n} répétition · poids du corps} other{{n} répétitions · poids du corps}}", + "activitySummaryRpeValue": "RPE {v}", + "activitySummaryNothingToPlot": "Rien à tracer pour cette séance de {activity}", + "activitySummaryNoSeriesTitle": "Aucune série à tracer", + "activitySummaryNoSeriesBody": "Cette séance n'a enregistré aucun flux par minute.", + "activitySummaryHeartRateZones": "Zones de fréquence cardiaque", + "activitySummaryTabOverview": "Aperçu", + "activitySummaryTabSplits": "Temps intermédiaires", + "activitySummaryTabGraphs": "Graphiques", + "activityLiveAddALap": "Ajouter une longueur", + "activityLiveAddExerciseTitle": "Ajouter un exercice", + "activityLiveAllowLocation": "Autoriser la localisation", + "activityLiveBestLabel": "Meilleur", + "activityLiveBodyweightExcludedNote": "Poids du corps — exclu du volume", + "activityLiveBodyweightOnly": "poids du corps uniquement", + "activityLiveBpmUnit": "bpm", + "activityLiveBwAbbrev": "PDC", + "activityLiveChangeStroke": "Changer de nage", + "activityLiveDecrease": "Diminuer {label}", + "activityLiveDeniedForeverBody": "La localisation est refusée pour cette appli, et seuls les Réglages peuvent changer cela.", + "activityLiveDurationHeader": "DURÉE", + "activityLiveEffortRpeHeader": "EFFORT (RPE)", + "activityLiveEndSet": "Terminer le set", + "activityLiveExerciseOf": "EXERCICE {index} SUR {total}", + "activityLiveFinishSessionLabel": "Terminer la séance", + "activityLiveHoldTime": "Maintenir · {time}", + "activityLiveIncrease": "Augmenter {label}", + "activityLiveIntervalSubtitle": "{workSec} S TRAVAIL · {restSec} S REPOS", + "activityLiveKcalEstUnit": "kcal · est.", + "activityLiveKgVolumeUnit": "kg de volume", + "activityLiveLapButtonLabel": "LONGUEUR", + "activityLiveLapsChartTitle": "LONGUEURS", + "activityLiveLapsCount": "{count, plural, one{{count} longueur} other{{count} longueurs}} · {stroke}", + "activityLiveLapsFootnote": "Plus rapide {time} · la longueur de la barre indique la vitesse par rapport à elle.", + "activityLiveLapXLabel": "Longueur {n}", + "activityLiveLogAsBodyweight": "Enregistrer au poids du corps", + "activityLiveMatchSetSubtitle": "SET {n}", + "activityLiveMetrePoolLabel": "bassin de {len} mètres", + "activityLiveMinimiseLabel": "Réduire", + "activityLiveNextExercise": "Exercice suivant", + "activityLiveNextLabel": "SUIVANT", + "activityLiveNextPose": "Posture suivante", + "activityLiveNextRest": "Repos · {time}", + "activityLiveNextWork": "Travail · {time}", + "activityLiveNoHrBody": "Le bracelet n'est pas connecté, donc rien n'arrive pour cette séance.", + "activityLiveNoHrTitle": "Pas de fréquence cardiaque", + "activityLiveNoHrYetBody": "Le bracelet est connecté mais n'a pas encore détecté de battement ; il doit être ajusté, à la largeur d'un doigt au-dessus de l'os du poignet.", + "activityLiveNoHrYetTitle": "Pas encore de fréquence cardiaque", + "activityLiveNoneYet": "Aucun pour l'instant", + "activityLiveNoRouteFailedBody": "Le téléphone a renvoyé une erreur lors de la demande de position.", + "activityLiveNoRouteFailedTitle": "Pas d'itinéraire : échec de localisation", + "activityLiveNoRouteNotAllowedTitle": "Pas d'itinéraire : localisation non autorisée", + "activityLiveNoRouteOffBody": "Les services de localisation sont désactivés sur ce téléphone, donc aucune position n'arrive.", + "activityLiveNoRouteOffTitle": "Pas d'itinéraire : localisation désactivée", + "activityLiveOneLapFewer": "Une longueur de moins", + "activityLiveOpenSettings": "Ouvrir les Réglages", + "activityLiveOpponentLabel": "ADVERSAIRE", + "activityLivePauseLabel": "Mettre en pause", + "activityLivePerLapUnit": "par longueur", + "activityLivePointLabel": "point pour {side}", + "activityLivePoolSubtitle": "BASSIN DE {len}M · {stroke}", + "activityLivePoseBridge": "Pont", + "activityLivePoseChair": "Chaise", + "activityLivePoseChildsPose": "Posture de l'enfant", + "activityLivePoseForwardFold": "Flexion avant", + "activityLivePoseMountain": "Montagne", + "activityLivePoseOf": "POSTURE {index} SUR {total}", + "activityLivePosePigeon": "Pigeon", + "activityLivePosePlank": "Planche", + "activityLivePoseSavasana": "Savasana", + "activityLivePoseTriangle": "Triangle", + "activityLivePoseWarriorTwo": "Guerrier II", + "activityLivePreviousExercise": "Exercice précédent", + "activityLivePreviousLabel": "Précédent", + "activityLivePrivateSession": "Séance privée", + "activityLiveRecordingRoute": "Enregistrement de l'itinéraire", + "activityLiveRepsBodyweightRow": "{n, plural, one{{n} répétition · poids du corps} other{{n} répétitions · poids du corps}}", + "activityLiveRepsLabel": "RÉPÉTITIONS", + "activityLiveRepsLoggedBodyweight": "{n, plural, one{{n} répétition enregistrée} other{{n} répétitions enregistrées}}", + "activityLiveRepsOnly": "{n, plural, one{{n} répétition} other{{n} répétitions}}", + "activityLiveRepsUnit": "répétitions", + "activityLiveRestingHeader": "REPOS", + "activityLiveRestWord": "Repos", + "activityLiveResumeLabel": "Reprendre", + "activityLiveRoundLabel": "ROUND {n}", + "activityLiveRouteFootnoteNoDistance": "Point de départ épinglé ; la distance apparaît une fois les positions stabilisées.", + "activityLiveRouteFootnoteWithDistance": "{distance} d'après les positions enregistrées jusqu'ici.", + "activityLiveRouteSoFarTitle": "ITINÉRAIRE JUSQU'ICI", + "activityLiveSetNumber": "Set {n}", + "activityLiveSetsCountSubtitle": "{n, plural, one{{n} SET} other{{n} SETS}}", + "activityLiveSetsListHeader": "SETS", + "activityLiveSetsUnit": "sets", + "activityLiveStepsUnit": "pas", + "activityLiveStrainUnit": "effort", + "activityLiveStrokeBack": "Dos", + "activityLiveStrokeBreast": "Brasse", + "activityLiveStrokeFly": "Papillon", + "activityLiveStrokeFree": "Nage libre", + "activityLiveThisExerciseLabel": "CET EXERCICE", + "activityLiveTimeInZonesTitle": "TEMPS PAR ZONE", + "activityLiveTimeUnit": "temps", + "activityLiveTryAgain": "Réessayer", + "activityLiveTurnOnLocation": "Activer la localisation", + "activityLiveVolumeSetsSubtitle": "{kg} KG · {n, plural, one{{n} SET} other{{n} SETS}}", + "activityLiveWeightLabel": "POIDS", + "activityLiveWeightRepsLogged": "{kg} kg × {n} enregistré", + "activityLiveWorkWord": "Travail", + "activityLiveYouLabel": "VOUS", + "activityLiveZoneLabel": "Zone {z}", + "activityLiveLogSet": "Enregistrer le set", + "activityLiveRestOverAnnounce": "Fin du repos", + "activityLiveSkipRest": "Passer le repos", + "gesturesNavTitle": "Double appui", + "gesturesSectionTitle": "Appuyez deux fois sur le bracelet", + "gesturesSectionBody": "Uniquement lorsque l’app est connectée et éveillée. Un appui que le bracelet a enregistré pendant l’absence de votre téléphone arrive plus tard avec un horodatage ancien, et il est ignoré plutôt que déclenché des heures après coup.", + "gesturesItDoesTitle": "Cela fait", + "gesturesNoPhoneActionsTitle": "Rien sur le téléphone ?", + "gesturesNoPhoneActionsBody": "Faire sonner votre téléphone et la lampe torche sont absents car l’app n’a pas pu demander au système ce que cet appareil autorise. Rouvrez l’app et revenez ; les actions internes ci-dessus fonctionnent de toute façon.", + "settingsBarcodeSaveFailed": "Cela n’a pas pu être enregistré — il se peut que ce soit de retour à la prochaine ouverture de l’app.", + "settingsIconRowTitle": "Icône", + "settingsIconRowConfirmHint": "l’iPhone vous demandera de confirmer", + "settingsIconChoiceLabel": "Icône {label}.", + "settingsSelectedSuffix": " Sélectionné.", + "settingsHealthSyncOff": "Désactivé. Rien n’est écrit dans {store}", + "settingsHealthSyncReady": "Écrit le sommeil, la fréquence cardiaque au repos, la VFC, la fréquence respiratoire, l’énergie et les séances d’entraînement de chaque jour dans {store} une fois finalisés", + "settingsHealthSyncNeedsPermission": "{store} n’a pas accordé l’accès en écriture. Appuyez pour l’ouvrir", + "settingsHealthSyncNotInstalled": "Health Connect n’est pas installé. Appuyez pour l’obtenir", + "settingsHealthSyncNeedsUpdate": "Health Connect est trop ancien pour y écrire. Appuyez pour le mettre à jour", + "settingsHealthSyncUnsupported": "Cet appareil n’a aucun magasin de santé où écrire", + "settingsHealthSyncChecking": "Vérification de {store}…", + "settingsWriteToHealthStoreRowTitle": "Écrire dans {store}", + "settingsHealthShareOffTitle": "Contribution désactivée", + "settingsHealthShareOffNeverUploaded": "Rien n’a jamais été téléversé. Rien ne le sera.", + "settingsHealthShareOffDetail": "Rien de plus ne sera téléversé.\n\nUne copie de votre base de données a été téléversée le {date}. Le serveur ne conserve que la copie la plus récente par appareil. Nous avons essayé de lui signaler que votre consentement est retiré — ce message n’est envoyé qu’une fois et n’est pas retenté, donc si ce téléphone est hors ligne il ne sera pas arrivé, et nous ne pouvons pas non plus vous montrer que la copie a disparu.", + "settingsOk": "OK", + "settingsHealthShareOnTitle": "Contribuer vos données de santé ?", + "settingsHealthShareOnBody": "Une fois par jour, en Wi-Fi et en charge, une copie compressée de TOUTE votre base de données est téléversée — chaque jour dérivé et chaque ligne de capteur brute envoyée par le bracelet. Elle sert à améliorer les algorithmes.\n\nCe n’est anonyme en aucun sens réel : c’est tout votre historique de santé. Vous pouvez désactiver ceci à tout moment, et rien de plus n’est envoyé à partir de ce moment-là.", + "settingsNo": "Non", + "settingsContribute": "Contribuer", + "settingsResetTitle": "Tout supprimer ?", + "settingsResetBody": "Ceci supprime, définitivement et sans copie ailleurs :\n\n· chaque jour mesuré, sommeil, séance et itinéraire\n· chaque résultat de labo, repas, dose de médicament, habitude, séance de respiration et série enregistrée\n· votre journal, suivi de cycle et lignes de base glissantes\n· votre profil, chaque préférence et toute clé IA stockée\n· le widget d’écran d’accueil et chaque rappel programmé\n\nLe bracelet est dissocié et ne peut pas renvoyer l’historique déjà transmis. Exportez depuis Vos données d’abord si vous voulez une copie.", + "settingsResetKeepData": "Conserver mes données", + "settingsResetDeleteEverything": "Tout supprimer", + "settingsNavTitle": "Réglages", + "settingsGroupTheBand": "Le bracelet", + "settingsAlarmRowTitle": "Alarme", + "settingsAlarmRowSub": "Vibre à votre poignet, sur l’horloge propre du bracelet", + "settingsGroupThisPhone": "Ce téléphone", + "settingsStepsRowTitle": "Pas", + "settingsStepsRowSub": "Le compteur de pas propre à ce téléphone, pour les heures que le bracelet ne couvre pas. Rien ne quitte l’appareil", + "settingsGroupNotifications": "Notifications", + "settingsManageNotificationsRowTitle": "Gérer les notifications", + "settingsManageNotificationsRowSub": "Ce qui peut vous interrompre, les heures calmes, et les interrupteurs pour toutes les désactiver", + "settingsGroupPreferences": "Préférences", + "settingsUnitsRowTitle": "Unités", + "settingsAppearanceRowTitle": "Apparence", + "settingsCycleTrackingRowTitle": "Suivi du cycle", + "settingsCycleTrackingRowSub": "Ajoute l’onglet Cycle à Bien-être. Désactivé le masque et conserve tout ce qui est déjà enregistré", + "settingsGroupYourData": "Vos données", + "settingsExportBackupImportRowTitle": "Exporter, sauvegarder, importer", + "settingsExportBackupImportRowSub": "Feuilles de calcul, une copie complète, et importer l’historique", + "settingsGroupAutomation": "Automatisation", + "settingsDoubleTapRowTitle": "Double appui", + "settingsDoubleTapRowSub": "Ce que fait un double appui sur le bracelet", + "settingsTaskerShortcutsRowTitle": "Tasker et Raccourcis", + "settingsTaskerShortcutsRowSub": "Android uniquement pour les événements sortants. iOS peut faire vibrer le bracelet mais ne peut pas être déclenché par lui", + "settingsGroupPrivacy": "Confidentialité", + "settingsCrashReportsRowTitle": "Rapports de plantage", + "settingsCrashReportsRowSub": "Rien n’est envoyé tant que vous ne l’avez pas autorisé", + "settingsBarcodeLookupRowTitle": "Rechercher les codes-barres en ligne", + "settingsBarcodeLookupRowSub": "Envoie un code-barres scanné à openfoodfacts.org. Rien à votre sujet ne l’accompagne", + "settingsContributeHealthDataRowTitle": "Contribuer mes données de santé", + "settingsContributeHealthDataRowSub": "Téléverse toute votre base de données une fois par jour, en Wi-Fi et en charge, pour améliorer les algorithmes", + "settingsCheckForUpdatesRowTitle": "Vérifier les mises à jour", + "settingsUpdateBelowMinimum": "Cette version est en dessous de la version minimale prise en charge. Installez la nouvelle version depuis GitHub", + "settingsUpdateAvailable": "Une version plus récente est publiée sur GitHub", + "settingsUpdateCheckSub": "Interroge le serveur de versions au lancement. Il voit votre adresse IP et l’heure d’ouverture de l’app", + "settingsGroupAbout": "À propos", + "settingsVersionRowTitle": "Version", + "settingsNoticesLicencesRowTitle": "Avis et licences", + "settingsNoticesLicencesRowSub": "Ce que cette app n’est pas, et à qui sont les données qu’elle utilise", + "settingsGroupDeveloper": "Développeur", + "settingsComponentGalleryRowTitle": "Galerie de composants", + "settingsComponentGalleryRowSub": "Chaque composant, à n’importe quelle échelle de texte, dans les deux thèmes", + "settingsDeveloperModeRowTitle": "Mode développeur", + "settingsResetAllDataRowTitle": "Réinitialiser toutes les données", + "settingsNotificationsNavTitle": "Notifications", + "settingsNotificationsNavSub": "CE QUI PEUT VOUS INTERROMPRE", + "settingsNotificationsOffSystemTitle": "Les notifications sont désactivées au niveau système", + "settingsNotificationsOffSystemBody": "Rien ci-dessous ne peut vous atteindre tant que le système d’exploitation ne l’autorise pas.", + "settingsTurnThemOn": "Les activer", + "settingsGroupManageNotifications": "Gérer les notifications", + "settingsHealthExceptionsRowTitle": "Exceptions de santé", + "settingsHealthExceptionsRowSub": "Une par jour au maximum, et seulement quand quelque chose dans votre propre référence a bougé", + "settingsBandAlertsRowTitle": "Alertes du bracelet", + "settingsBandAlertsRowSub": "Batterie à plat, en charge, silence radio", + "settingsAlertMeAtRowTitle": "M’alerter à", + "settingsAlertMeAtRowSub": "Avertit quand le bracelet passe sous ce niveau de charge", + "settingsRecoveryReadyRowTitle": "Récupération prête", + "settingsRecoveryReadyRowSub": "Une note lorsque votre score de récupération matinal arrive", + "settingsWeeklyLookbackRowTitle": "Rétrospective hebdomadaire", + "settingsWeeklyLookbackRowSub": "Le dimanche soir, mais seulement pour une semaine où quelque chose a réellement été trouvé. La plupart des semaines sont calmes", + "settingsDetectedWorkoutsRowTitle": "Séances détectées", + "settingsDetectedWorkoutsRowSub": "Demande à propos des efforts détectés par le bracelet que vous n’avez pas démarrés. Désactivé masque l’invite et les cartes de révision ; le bracelet continue de mesurer quoi qu’il en soit", + "settingsMovementNudgeRowTitle": "Rappel de mouvement", + "settingsMovementNudgeRowSub": "Vous rappelle après une période d’immobilité — deux heures sans aucun mouvement, ou 90 minutes en posture assise. Notification sur le téléphone plus une vibration sur le bracelet tant qu’il est connecté", + "settingsWindDownRowTitle": "Détente avant le coucher", + "settingsWindDownRowSub": "Un signal environ 45 minutes avant l’heure du coucher apprise de vos propres nuits, en dehors de vos heures calmes. Apparaît après environ une semaine de port", + "settingsStepGoalAlertsRowTitle": "Alertes objectif de pas", + "settingsStepGoalAlertsRowSub": "Vous informe une fois lorsque vous atteignez votre objectif de pas du jour", + "settingsMedicationRemindersRowTitle": "Rappels de médicaments", + "settingsMedicationRemindersRowSub": "Une notification par dose programmée, aux horaires que vous avez saisis — avec une vibration sur le bracelet s’il est connecté. Rien n’est envoyé pour une dose déjà marquée prise ou ignorée", + "settingsDailyCheckInRowTitle": "Bilan quotidien", + "settingsDailyCheckInRowSub": "Une invite le soir pour écrire la journée — humeur, énergie, stress. Ignorée si le jour a déjà une note", + "settingsWaterReminderRowTitle": "Rappel d’eau", + "settingsWaterReminderRowSub": "Une vibration sur le bracelet et une notification sur votre téléphone pendant vos heures d’éveil, pour vous rappeler de consigner une boisson. Rien n’est mesuré dans un cas comme dans l’autre", + "settingsRemindMeEveryRowTitle": "Me rappeler toutes les", + "settingsGroupTheStrap": "Le bracelet", + "settingsBuzzOnAppNotificationsRowTitle": "Vibrer sur les notifications d’apps", + "settingsBuzzOnAppNotificationsRowSub": "Choisissez quelles applications du téléphone font vibrer le bracelet", + "settingsGroupQuietHours": "Heures calmes", + "settingsQuietHoursRowTitle": "Heures calmes", + "settingsQuietHoursRowSub": "Rien ne vibre pendant cette période", + "settingsQuietHoursStartsRowTitle": "Début", + "settingsQuietHoursEndsRowTitle": "Fin", + "settingsHealthExceptionsBreakThroughRowTitle": "Les exceptions de santé passent outre", + "settingsAlarmNotOnListTitle": "L’alarme n’est pas dans cette liste", + "settingsAlarmNotOnListBody": "Annulez-la depuis l’écran Alarme.", + "settingsImportNoPermission": "{store} n’a pas accordé ces champs. Rien n’a été lu.", + "settingsImportEmptyWithBirthday": "Rien n’est revenu. {store} ne contient ni taille, ni poids, ni date de naissance, ni sexe pour vous — saisissez-les ici à la place.", + "settingsImportEmpty": "Rien n’est revenu. {store} ne contient ni taille, ni poids, ni sexe pour vous — saisissez-les ici à la place.", + "settingsImportNoChange": "Lu : {fields}. Votre profil dit déjà la même chose, donc rien n’a changé.", + "settingsImportUpdated": "{fields} mis à jour depuis {store}.", + "settingsImportFailed": "Échec : {error}", + "settingsAgeFieldLabel": "Âge", + "settingsEditProfileNavTitle": "Modifier le profil", + "settingsNameFieldLabel": "NOM", + "settingsSexFieldLabel": "SEXE", + "settingsSexMale": "Homme", + "settingsSexFemale": "Femme", + "settingsSexPreferNotToSay": "Je préfère ne pas répondre", + "settingsAgeYearsFieldLabel": "ÂGE (ANNÉES)", + "settingsFourFieldsTitle": "Ces quatre changent vos chiffres", + "settingsFourFieldsBody": "Elles alimentent les zones de fréquence cardiaque, les estimations caloriques et la charge d’entraînement. Effacez-en un et seules les métriques qui en ont besoin restent indisponibles.", + "settingsImportBlockAppleHealth": "Taille, poids, date de naissance et sexe, directement depuis {store}. La taille et le poids sont pris à chaque fois ; votre âge et votre sexe ne comblent qu’un vide, car ni l’un ni l’autre ne dérive et une valeur déjà présente était votre choix.", + "settingsImportBlockOther": "Taille et poids, directement depuis {store}. Il n’a ni date de naissance ni sexe à lire — aucune app ne le peut — donc réglez ces deux-là vous-même ci-dessus.", + "settingsNotSetHint": "Non défini", + "settingsAutomationNavTitle": "Automatisation", + "settingsSyncFinishesSectionTitle": "Quand une synchronisation se termine", + "settingsSyncFinishesAndroidBody": "L’app diffuse une intention sur laquelle votre app d’automatisation peut démarrer un profil. Filtrez sur l’action ci-dessous ; elle porte le nombre d’enregistrements arrivés et quand, au plus une fois par minute.", + "settingsSyncFinishesIosBody": "iOS ne peut pas faire cela. Une automatisation personnelle Raccourcis ne peut se déclencher que sur la liste fixe d’événements propre à Apple, et aucune app ne peut en ajouter — donc rien ici ne peut démarrer un raccourci pour vous. Android l’a ; c’est une limite de plateforme, pas un réglage.", + "settingsSyncFinishesExtras": "Extras : records (int), at (secondes unix)", + "settingsNeverSendSectionTitle": "Ce qui ne sera jamais envoyé", + "settingsNeverSendBody": "Pas de préparation, pas de tension, pas de score de sommeil — sur aucune des deux plateformes. Un nombre que cette app aurait affiché comme absent, avec une raison attachée, devient un simple zéro dès qu’il sort. Les faits sur la synchronisation sortent ; les mesures non.", + "settingsBuzzFromShortcutSectionTitle": "Faire vibrer le bracelet depuis un raccourci", + "settingsBuzzFromShortcutAndroidBody": "Envoyez wtf.openstrap.openstrap_edge.BUZZ_STRAP avec ce jeton comme extra de chaîne « token ». Sans lui, n’importe quelle app du téléphone pourrait faire vibrer votre bracelet.", + "settingsBuzzFromShortcutIosBody": "Ce sens fonctionne sur iOS : un raccourci que vous exécutez vous-même peut atteindre l’app. Ce qu’il ne peut pas faire, c’est s’exécuter tout seul quand le bracelet se synchronise.", + "settingsNoTokenYet": "Pas encore de jeton — rouvrez cet écran.", + "settingsCopied": "Copié", + "settingsCopyTheToken": "Copier le jeton", + "bandStatusBluetoothDeniedTitle": "Le Bluetooth est désactivé pour cette appli", + "bandStatusBluetoothDeniedReason": "Le téléphone refuse la radio Bluetooth à OpenStrap, donc rien ne peut être détecté ni connecté. Ce n’est pas la faute du bracelet — se rapprocher n’y changera rien.", + "bandStatusBluetoothDeniedFix": "Ouvrez Réglages → OpenStrap et autorisez le Bluetooth", + "bandStatusBluetoothOffTitle": "Le Bluetooth est éteint", + "bandStatusBluetoothOffReason": "La radio du téléphone est éteinte, donc aucune appli ne peut atteindre le bracelet. Le bracelet continue d’enregistrer pendant ce temps ; rien n’est perdu.", + "bandStatusBluetoothOffFix": "Activez le Bluetooth", + "bandStatusBluetoothUnsupportedTitle": "Ce téléphone n’a pas de radio Bluetooth basse consommation", + "bandStatusBluetoothUnsupportedReason": "Le bracelet n’est joignable que par Bluetooth basse consommation (BLE). Les données importées fonctionnent toujours ; une liaison en direct non.", + "bandStatusReconnectPausedTitle": "La reconnexion a été mise en pause", + "bandStatusReconnectPausedReason": "{n, plural, one{Le bracelet a refusé la clé d’appairage {n} fois d’affilée, donc l’appli a arrêté de réessayer plutôt que d’accaparer la radio et de vider les deux batteries sur une liaison qui ne s’ouvrira pas. Rien ne se reconnecte tant que vous n’agissez pas.} other{Le bracelet a refusé la clé d’appairage {n} fois d’affilée, donc l’appli a arrêté de réessayer plutôt que d’accaparer la radio et de vider les deux batteries sur une liaison qui ne s’ouvrira pas. Rien ne se reconnecte tant que vous n’agissez pas.}}", + "bandStatusRepairNeededTitle": "Le bracelet doit être réappairé", + "bandStatusRepairNeededReason": "La liaison s’établit, mais le bracelet rejette la clé de chiffrement détenue par le téléphone, donc chaque commande est abandonnée et aucune donnée ne circule. Vos enregistrements sont en sécurité sur le bracelet.", + "bandStatusRepairFix": "Oubliez le bracelet dans les réglages Bluetooth du téléphone, puis réappairez-le ici", + "bandStatusSyncStuckTitle": "Un lot d’enregistrements ne finira pas de se transférer", + "bandStatusSyncStuckReason": "Le bracelet continue de renvoyer le même lot parce que l’appli n’arrive pas à lui faire parvenir la confirmation. Tout ce qu’il contient est déjà enregistré ici — rien n’est perdu — mais le bracelet ne peut pas continuer tant que la confirmation n’arrive pas.", + "bandStatusSyncStuckFix": "Reconnectez le bracelet ; si cela se reproduit demain, réappairez-le", + "bandStatusStrapUnresponsiveTitle": "Le bracelet a arrêté de transmettre ses enregistrements", + "bandStatusStrapUnresponsiveReason": "Le bracelet signale des enregistrements plus récents qu’il ne transmet pas. Ces enregistrements restent en sécurité sur le bracelet ; il ne fait simplement pas suivre.", + "bandStatusStrapUnresponsiveFix": "Mettez le bracelet sur son chargeur une minute, puis reconnectez-le", + "bandStatusClockLostTitle": "Les synchronisations se terminent sans aucune donnée", + "bandStatusClockLostReason": "Le bracelet termine chaque synchronisation sans transmettre la moindre mesure, ce qui signifie presque toujours que son horloge interne a perdu la synchro. L’appli la remet à l’heure à chaque connexion.", + "bandStatusClockLostFix": "Laissez le bracelet connecté quelques minutes ; si rien n’arrive d’ici demain, réappairez-le", + "bandStatusConnectedReason": "Le bracelet est lié et transmet ses enregistrements.", + "bandStatusConnectingTitle": "Connexion en cours", + "bandStatusConnectingReason": "Ouverture de la liaison avec le bracelet.", + "bandStatusScanningTitle": "Recherche du bracelet", + "bandStatusScanningReason": "En attente que le bracelet s’annonce.", + "bandStatusDisconnectedReason": "Le bracelet est hors de portée, sur son chargeur, ou connecté à une autre appli. Il continue d’enregistrer dans tous les cas.", + "bandStatusDisconnectedFix": "Rapprochez le bracelet du téléphone et fermez toute autre appli connectée à lui", + "devicesTierBeatToBeatLabel": "Intervalles battement à battement", + "devicesTierBeatToBeatDetail": "Détection électrique du pic R.", + "devicesTierWristOpticalLabel": "Pouls optique au poignet", + "devicesTierWristOpticalDetail": "Pouls continu 24 h/24, sommeil et température. Le rythme des battements est déduit d’une onde de pouls, donc la VFC ici est en réalité de la VFP.", + "devicesTierPhoneLabel": "Pas uniquement", + "devicesTierPhoneDetail": "Le coprocesseur de mouvement du téléphone lui-même. Les pas, rien d’autre.", + "deviceActionNoneLabel": "Ne rien faire", + "deviceActionNoneBlurb": "Le double-tap ne fait rien.", + "deviceActionMediaPlayPauseLabel": "Lecture / pause musique", + "deviceActionMediaPlayPauseBlurb": "Basculer ce qui est en cours de lecture.", + "deviceActionMediaNextLabel": "Piste suivante", + "deviceActionMediaNextBlurb": "Passer à la piste suivante.", + "deviceActionMediaPrevLabel": "Piste précédente", + "deviceActionMediaPrevBlurb": "Revenir à la piste précédente.", + "deviceActionVolumeUpLabel": "Augmenter le volume", + "deviceActionVolumeUpBlurb": "Augmenter le volume multimédia d’un cran.", + "deviceActionVolumeDownLabel": "Baisser le volume", + "deviceActionVolumeDownBlurb": "Baisser le volume multimédia d’un cran.", + "deviceActionRingPhoneLabel": "Faire sonner mon téléphone", + "deviceActionRingPhoneBlurb": "Jouer un son fort pour retrouver votre téléphone.", + "deviceActionTorchLabel": "Lampe torche", + "deviceActionTorchBlurb": "Allumer ou éteindre la lampe torche du téléphone.", + "deviceActionMarkMomentLabel": "Marquer un moment", + "deviceActionMarkMomentBlurb": "Repérer le moment présent dans votre journal.", + "deviceActionWorkoutToggleLabel": "Démarrer / arrêter l’entraînement", + "deviceActionWorkoutToggleBlurb": "Démarrer ou terminer un entraînement depuis votre poignet.", + "deviceActionLogWaterLabel": "Enregistrer de l’eau", + "deviceActionLogWaterBlurb": "Ajoute un verre à l’eau du jour, la même action que le + sur l’écran nutrition.", + "deviceActionBroadcastToTaskerLabel": "Diffuser vers Tasker", + "deviceActionBroadcastToTaskerBlurb": "Envoyer un intent de diffusion pour que Tasker puisse déclencher n’importe quelle automatisation." } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 804860de..28a18ab4 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -293,5 +293,2017 @@ "welcomePartOfFileNotUsedTitle": "उस फ़ाइल का कुछ हिस्सा उपयोग नहीं किया जा सका", "welcomeStrandedDays": "{n, plural, one{1 दिन क्रम से बाहर आया और केवल अगले दिन के संदर्भ के रूप में उपयोग हुआ।} other{{n} दिन क्रम से बाहर आए और केवल अगले दिन के संदर्भ के रूप में उपयोग हुए।}}", "welcomeLateRows": "{n, plural, one{1 पंक्ति उसके दिन के पहले ही स्कोर और बंद होने के बाद आई।} other{{n} पंक्तियां उनके दिन के पहले ही स्कोर और बंद होने के बाद आईं।}}", - "welcomeExportAgainInDateOrder": "तारीख क्रम में फिर से निर्यात करें" + "welcomeExportAgainInDateOrder": "तारीख क्रम में फिर से निर्यात करें", + "scanBarcodeTitle": "बारकोड स्कैन करें", + "scanBarcodeClose": "बंद करें", + "scanBarcodeInstructions": "बारकोड को फ़्रेम के अंदर रखें। कुछ भी रिकॉर्ड नहीं होता — सिर्फ़ अंक पढ़े जाते हैं।", + "scanBarcodeNoAccessTitle": "कैमरा एक्सेस नहीं है", + "scanBarcodeCameraFailedTitle": "कैमरा शुरू नहीं हो सका", + "scanBarcodeNoAccessBody": "स्कैन करने के लिए कैमरे की ज़रूरत होती है, और इस ऐप को इसकी अनुमति नहीं दी गई है।", + "scanBarcodeCameraFailedBody": "यह डिवाइस स्कैनर के लिए अपना कैमरा नहीं खोल सका।", + "scanBarcodeTypeInstead": "इसके बजाय नंबर टाइप करें", + "findingsLogTitle": "अवलोकन", + "findingsLogEmptyTitle": "कुछ भी असामान्य नहीं मिला", + "findingsLogEmptyBody": "बीमारी, रात में असामान्य शरीरक्रिया, त्वचा के तापमान और आपकी विश्राम हृदय गति में बदलाव — इन सभी की निगरानी शांत रही है। यह एक नतीजा है, खाली स्क्रीन नहीं।", + "findingsLogDerivedNote": "हर बार खोलने पर यह आपके अपने दिनों के आधार पर दोबारा निकाला जाता है, घटना के समय सेव नहीं किया जाता — इसलिए अगर किसी दिन का फिर से विश्लेषण होता है, तो यहाँ दिखने वाली जानकारी भी उसके साथ बदल जाती है।", + "startCardDefaultSub": "एक चुनें और शुरू करें", + "monthGridCoverage": "{total} में से {have} दिन", + "monthGridSemanticsLabel": "{title}: {total} में से {have} दिनों में मान मौजूद है। आपकी अपनी सीमा के अनुसार शेड किया गया।", + "monthGridDaysAgo": "{days} दिन पहले", + "monthGridToday": "आज", + "monthGridFootnote": "एक दिन के लिए एक सेल। गहरा रंग यानी आपकी अपनी सीमा में — यानी आपके संग्रहीत हर दिन के 10वें से 90वें पर्सेंटाइल के बीच — ऊपर की ओर; सिर्फ बॉर्डर वाला सेल यानी उस दिन कोई मान नहीं था, कम मान नहीं। ज़्यादा स्ट्रेन बेहतर स्ट्रेन नहीं है, और ज़्यादा नींद बेहतर नींद नहीं है; यह सिर्फ बताता है कि कोई दिन कहाँ बैठा, यह नहीं कि वह दिन कैसा रहा।", + "monthGridNotShadedYetTitle": "{title} अभी शेड नहीं हुआ है", + "monthGridNotShadedYetBody": "{days, plural, one{सिर्फ {days} दिन} other{सिर्फ {days} दिन}} कोई सीमा नहीं बनाते — शेड यह दिखाता है कि कोई दिन आपकी अपनी सीमा में कहाँ बैठता है। यह {min} दिन पर दिखना शुरू होता है।", + "whatChangedTitle": "क्या बदला", + "whatChangedSub": "आपके अपने इतिहास के आधार पर", + "whatChangedNoDataTitle": "इस दिन के लिए अभी तक कोई डेटा नहीं है", + "whatChangedNoDataBody": "यह विश्लेषण किसी दिन की तुलना उससे पहले के दिनों से करता है, और इस दिन के पास तुलना के लिए कोई मान नहीं है। कुछ भी असामान्य नहीं है क्योंकि कुछ भी ज्ञात नहीं है।", + "whatChangedLearningTitle": "अभी आपकी सामान्य स्थिति सीख रहे हैं", + "whatChangedLearningBody": "असामान्य होने का मतलब तभी है जब कोई सीमा हो, और इसके पीछे {days, plural, one{सिर्फ {days} दिन} other{सिर्फ {days} दिन}} का इतिहास है। विश्लेषण {min} दिन से शुरू होता है।", + "whatChangedNothingTitle": "कुछ भी असामान्य नहीं लगा", + "whatChangedNothingBody": "पर्याप्त इतिहास वाले हर मेट्रिक ने आपके अपने दिनों द्वारा तय की गई सीमा के भीतर रहा। यही सामान्य जवाब है, और यह एक पूरा जवाब है।", + "whatChangedMethodologyNote": "आपके अपने पिछले दिनों के मुकाबले, आपकी अपनी इकाइयों में, और साथ में वह समय-सीमा भी दी गई है — ताकि आप इस पर शक कर सकें। यहाँ कुछ भी न तो कारण है और न ही निदान।", + "whatChangedDayLinkTitle": "उस दिन क्या हुआ", + "whatChangedDayLinkSub": "समय के क्रम में नींद, सेशन, भोजन और लॉग", + "whatChangedMonthSection": "इसके पीछे का महीना", + "journalFieldErrorNoName": "इसे एक नाम दें", + "journalFieldErrorInvalidName": "कम से कम एक अक्षर या अंक का उपयोग करें", + "journalFieldErrorNoUnit": "बताएं कि इसे किस इकाई में मापा जाता है (mg, ml, कप…)", + "journalFieldErrorDuplicate": "आप पहले से ही इस नाम से कुछ ट्रैक कर रहे हैं", + "journalFieldTitle": "कुछ और ट्रैक करें", + "journalFieldNameLabel": "आप क्या ट्रैक करना चाहते हैं?", + "journalFieldNameHint": "मैग्नीशियम, स्क्रीन टाइम, सिरदर्द…", + "journalFieldKindQuestion": "यह किस तरह का नंबर है?", + "journalFieldKindRating": "1–5 की रेटिंग", + "journalFieldKindAmount": "एक मात्रा", + "journalFieldKindMinutes": "मिनट", + "journalFieldUnitLabel": "इकाई", + "journalFieldUnitHint": "mg, ml, कप…", + "journalFieldStepSize": "स्टेप साइज़", + "journalFieldMaxPerDay": "एक दिन में आप अधिकतम कितना दर्ज करेंगे", + "journalFieldAskLastTime": "पूछें कि आखिरी बार कब हुआ था", + "journalFieldStartTracking": "ट्रैक करना शुरू करें", + "aiBriefingForDay": "{day} के लिए", + "aiBriefingNoModelTitle": "कोई मॉडल सेट नहीं किया गया है", + "aiBriefingNoModelBody": "ब्रीफिंग आपके चुने हुए मॉडल द्वारा लिखी जाती है। जब तक आप कोई मॉडल नहीं चुनते, तब तक न कुछ जनरेट होगा और न ही कहीं कुछ भेजा गया है।", + "aiBriefingChooseModel": "मॉडल चुनें", + "aiBriefingNothingTitle": "आज के लिए कुछ नहीं लिखा गया", + "aiBriefingNothingBody": "ब्रीफिंग एक समय-सारणी के अनुसार बनती हैं, या यहाँ माँग पर भी बनाई जा सकती हैं।", + "aiBriefingWriting": "लिखा जा रहा है…", + "aiBriefingWriteNow": "अभी एक लिखें", + "aiBriefingWriteAgain": "इसे फिर से लिखें", + "aiBriefingFailedTitle": "यह पूरा नहीं हो पाया", + "aiBriefingFailedGeneric": "विफल: {error}", + "aiBriefingSentSection": "क्या भेजा गया", + "aiBriefingReadSection": "क्या पढ़ा गया", + "aiBriefingNoneBody": "कुछ नहीं। कोई अनुरोध नहीं भेजा गया — ऊपर दिया गया नोट इसी फ़ोन पर लिखा गया था।", + "aiBriefingLocalBody": "ये आंकड़े इसी डिवाइस पर मौजूद {host} को भेजे गए। कुछ भी इससे बाहर नहीं गया।", + "aiBriefingCloudBody": "केवल ये आंकड़े — और कुछ नहीं — {model} के रूप में {host} को भेजे गए। कोई कच्ची रिकॉर्डिंग नहीं, कोई नाम नहीं, कोई पहचानकर्ता नहीं।", + "aiBriefingNoneCardTitle": "कुछ भी उल्लेखनीय नहीं मिला, इसलिए कुछ नहीं पूछा गया", + "aiBriefingNoneCardBody": "यह स्कैन इसी फ़ोन पर चलता है। यह किसी मॉडल को तभी बुलाता है जब उसके पास कोई निष्कर्ष देने के लिए हो, और आज उसके पास कोई नहीं था।", + "aiBriefingEmptyCardTitle": "भेजने के लिए कुछ भी उपलब्ध नहीं था", + "aiBriefingEmptyCardBody": "जब यह लिखा गया, तब किसी भी मेट्रिक का कोई मान नहीं था, इसलिए प्रॉम्प्ट में कोई मान शामिल नहीं था।", + "napsFellAsleepHelp": "आप कब सोए", + "napsWokeUpHelp": "आप कब जागे", + "napsInvalidWindow": "झपकी 5 मिनट से 6 घंटे के बीच होती है। इससे अधिक लंबी अवधि नींद मानी जाती है, और वह रात के हिस्से में आती है जहाँ चरणों को पढ़ा जा सकता है।", + "napsOverlap": "यह इस दिन पहले से मौजूद एक झपकी से टकराता है। उसे पहले हटाएँ, बजाय इसके कि उसी घंटे को दो बार गिना जाए।", + "napsNotReanalysed": "दिन का पुनः विश्लेषण नहीं हुआ — पहले से ही एक अन्य विश्लेषण चल रहा था। आपका बदलाव सहेज लिया गया है और अगली बार लागू होगा।", + "napsTitle": "झपकियाँ", + "napsNoReadingTitle": "इस दिन के लिए कोई झपकी रीडिंग नहीं", + "napsNoReadingBody": "झपकियों की गणना उसी 1 Hz रिकॉर्डिंग से की जाती है जिससे दिन का बाकी हिस्सा तय होता है, और इस दिन के लिए पर्याप्त रिकॉर्डिंग उपलब्ध नहीं है।", + "napsEmptyTitle": "इस दिन कोई झपकी नहीं ली गई", + "napsEmptyBody": "इस दिन कुछ भी इतनी देर तक इतना स्थिर नहीं रहा, साथ ही सोने के दौरान होने वाली हृदय गति की गिरावट भी नहीं दिखी।", + "napsCountsToward": "{mins} की झपकी आज रात की आपकी नींद की ज़रूरत में गिनी जाएगी।", + "napsNotAppliedTitle": "यह लागू नहीं हुआ है", + "napsWorking": "कार्य जारी है…", + "napsLogANap": "एक झपकी दर्ज करें", + "napsRemovedSection": "हटाई गई", + "napsPutBackSemantic": "इस झपकी को वापस जोड़ें", + "napsPutBackLabel": "वापस जोड़ें", + "napsRemovalKept": "हटाने की जानकारी किसी आईडी के बजाय एक समय-सीमा के रूप में रखी जाती है, ताकि डिटेक्टर की सीमाएँ बदलने के बाद भी यह लागू रहे।", + "napsYouLoggedThis": "आपने इसे दर्ज किया", + "napsDetected": "पहचानी गई", + "napsLoggedWithMins": "{mins} · आपने इसे दर्ज किया", + "napsDetectedWithMins": "{mins} नींद · पहचानी गई", + "napsDeleteSemantic": "इस झपकी को हटाएँ", + "napsNotANapSemantic": "यह झपकी नहीं थी", + "napsDeleteLabel": "हटाएँ", + "napsNotANapLabel": "झपकी नहीं", + "readinessDetailTitle": "तैयारी", + "readinessDetailNotScoredTitle": "तैयारी का स्कोर उपलब्ध नहीं है", + "readinessDetailLastNightScored": "स्कोर की गई अंतिम रात {day} थी।", + "readinessDetailWhatWasMissing": "क्या गायब था", + "readinessDetailWhatWentIntoIt": "इसमें क्या शामिल हुआ", + "readinessDetailInputsFooter": "{used}/{total} इनपुट। हर एक की तुलना आपके अपने इतिहास से की जाती है — यह उन्हीं इनपुट का एक समानांतर दृश्य है, ऊपर दिए गए अंक का विभाजन नहीं।", + "readinessDetailNoBreakdownTitle": "अभी कोई विवरण उपलब्ध नहीं है", + "readinessDetailNoBreakdownBody": "हर इनपुट की तुलना आपके अपने इतिहास से करने में लगभग दो सप्ताह की रातें लगती हैं।", + "readinessDetailHistoryTitle": "इतिहास", + "readinessDetailLastNDays": "{n, plural, one{पिछला {n} दिन} other{पिछले {n} दिन}}", + "readinessDetailNoHistoryTitle": "तैयारी का कोई इतिहास नहीं", + "readinessDetailNoHistoryBody": "0 दिनों का स्कोर मिला।", + "readinessDetailWearOvernight": "बैंड को रातभर पहनें", + "readinessDetailUnit": "/100", + "readinessDetailDaysAgo": "{n, plural, one{{n} दिन पहले} other{{n} दिन पहले}}", + "readinessDetailToday": "आज", + "readinessDetailMeasured": "मापा गया", + "readinessDetailNotMeasured": "नहीं मापा गया", + "readinessDetailNightsOfHistory": "{n, plural, one{आपके अपने इतिहास की {n} रात} other{आपके अपने इतिहास की {n} रातें}}", + "readinessDetailNeedSuffix": "{need}। हर इनपुट की तुलना आपकी अपनी रातों से की जाती है, इसलिए पर्याप्त रातें होने से पहले स्कोर शुरू नहीं हो सकता।", + "readinessDetailNoNoteFallback": "ऊपर दी गई सभी जानकारी उपलब्ध थी, फिर भी आपके अपने इतिहास से तुलना नहीं की जा सकी।", + "readinessDetailNotAvailable": "उपलब्ध नहीं", + "readinessDetailContributionNotReported": "योगदान की रिपोर्ट नहीं मिली", + "readinessDetailRelativeUncalibrated": "सापेक्ष, अंशांकित नहीं", + "readinessDetailWithinSpread": "आपकी सामान्य सीमा के भीतर", + "readinessDetailWeightPercent": "{pct}% भार", + "dayStepsTitle": "कदम", + "dayStepsThroughDay": "दिन भर में", + "dayStepsToday": "आज", + "dayStepsOnDay": "{day} को", + "dayStepsNoTimesTitle": "{when} की गिनती के पीछे कोई समय दर्ज नहीं", + "dayStepsNoStepsTitle": "{when} कोई कदम नहीं गिने गए", + "dayStepsStrapCounterBody": "{when} गिने गए {count} कदम बैंड के अपने स्टेप काउंटर से आए हैं, जो पूरे दिन का कुल तो बताता है पर कोई समय नहीं। यहाँ घड़ी पर दिखाने लायक कुछ नहीं है।", + "dayStepsNothingCounted": "{when} कदम गिनने वाला कोई भी उपकरण सक्रिय नहीं था।", + "dayStepsChartTitle": "कब गिने गए", + "dayStepsUnit": "कदम", + "dayStepsYourPhone": "आपका फ़ोन", + "dayStepsYourBand": "आपका बैंड", + "dayStepsCounted": "गिने गए", + "dayStepsHonestyMixed": "कलाई और फ़ोन दोनों से कदम गिने गए, और दोनों की गलती अलग-अलग तरह की है: कलाई असली चलने को कम आँकती है और हाथ की लयबद्ध हरकत को चलना समझ सकती है, जबकि फ़ोन केवल उन्हीं कदमों को गिनता है जब वह आपके साथ था।", + "dayStepsHonestyStrap": "कदम कलाई से गिने गए, जहाँ असली चलना अक्सर कम आँका जाता है और हाथ की लयबद्ध हरकत चलने जैसी लग सकती है।", + "dayStepsHonestyPhone": "कदम आपके फ़ोन से गिने गए, इसलिए यहाँ केवल वही कदम हैं जब फ़ोन आपके साथ था।", + "roughNightSignRhr": "आपकी आराम की हृदय गति सामान्य से अधिक रही", + "roughNightSignHrv": "आपकी HRV सामान्य से कम रही", + "roughNightSignDip": "रात में आपकी हृदय गति सामान्य से कम गिरी", + "roughNightSignTemp": "आपकी त्वचा सामान्य से अधिक गर्म रही", + "roughNightLateTraining": "आपने {at} बजे तक अभ्यास किया, जो अक्सर अकेले ही यह असर डालता है।", + "roughNightIllness": "बीमारी की निगरानी ने भी इस रात को चिह्नित किया — यह आपके अपने आधार से लगातार बढ़ोतरी है, कोई निदान नहीं।", + "roughNightLuteal": "आप ल्यूटियल चरण में हैं, जो अकेले ही आराम की हृदय गति और त्वचा का तापमान बढ़ा देता है।", + "roughNightWarmRoom": "आपकी त्वचा सामान्य से अधिक गर्म रही — गर्म कमरा भी ऐसा कर सकता है।", + "roughNightDismiss": "हटाएं", + "roughNightDefaultHeadline": "सामान्य से कठिन रात", + "roughNightSummary": "{sentence}, आपकी अपनी रातों की तुलना में। यह रात का एक माप है, आप पर कोई निर्णय नहीं।", + "roughNightTellWhatHappened": "बताएं क्या हुआ था", + "roughNightNothingToAnswer": "जवाब देने को कुछ नहीं — यह कार्ड सिर्फ रात की जानकारी देता है।", + "roughNightWhatElse": "और क्या हो रहा था?", + "roughNightAnythingElse": "कुछ और?", + "roughNightSaving": "सहेजा जा रहा है", + "roughNightLogIt": "उस रात के लिए दर्ज करें", + "roughNightAddHowMuch": "मात्रा जोड़ें", + "roughNightDoNotAskAgain": "दोबारा न पूछें", + "roughNightSeveralMoved": "रात के कई माप एक साथ बदले", + "driverBreakdownHigherThanUsual": "आपके सामान्य से अधिक", + "driverBreakdownLowerThanUsual": "आपके सामान्य से कम", + "driverBreakdownRightOnUsual": "{now} · आपके सामान्य के बिल्कुल अनुरूप", + "driverBreakdownAboveUsual": "{now} · आपके सामान्य {usual} से {delta} अधिक", + "driverBreakdownBelowUsual": "{now} · आपके सामान्य {usual} से {delta} कम", + "driverBreakdownWeightPct": "{pct}% भार", + "driverBreakdownNotAvailable": "उपलब्ध नहीं", + "driverBreakdownContributionNotReported": "योगदान की रिपोर्ट नहीं है", + "driverBreakdownRelativeUncalibrated": "सापेक्ष, अंशांकित नहीं", + "driverBreakdownWithinUsualSpread": "आपकी सामान्य विविधता के भीतर", + "driverBreakdownBiggerThanNoise": "मापन शोर से बड़ा", + "driverBreakdownSmallerThanNoise": "आपकी सामान्य विविधता से बाहर, लेकिन इतना छोटा कि यह मापन शोर हो सकता है", + "driverBreakdownWhatHelped": "किससे मदद मिली", + "driverBreakdownWhatHeldYouBack": "किसने आपको पीछे रखा", + "driverBreakdownNeither": "इनमें से कोई नहीं", + "driverBreakdownFooter": "प्रत्येक इनपुट की तुलना आपके अपने इतिहास से की जाती है — यह उन्हीं इनपुट का एक समानांतर दृश्य है, स्कोर के हिस्से नहीं। “मापन शोर” यह बताता है कि बिना कुछ बदले भी एक रीडिंग अपने आप कितना बदल सकती है। यह आपके अपने रिकॉर्ड में पैटर्न हैं, कारण नहीं।", + "driverBreakdownHideHistory": "{label}, इसका इतिहास छिपाएं", + "driverBreakdownShowHistory": "{label}, इसका इतिहास दिखाएं", + "driverBreakdownDaysAgo": "{n, plural, one{{n} दिन पहले} other{{n} दिन पहले}}", + "driverBreakdownToday": "आज", + "driverBreakdownUsualRange": "आपकी सामान्य सीमा {lo}–{hi}{unit}", + "driverBreakdownAbsenceTitle": "दिखाने के लिए कोई विवरण नहीं", + "driverBreakdownAbsenceAlgoVersion": "पिछले अपडेट के साथ तैयारी की गणना करने का तरीका बदल गया है, और इसे फिर से बनाया जा रहा है।", + "driverBreakdownAbsenceStale": "अंतिम रोलअप इतना पुराना है कि उस पर भरोसा नहीं किया जा सकता।", + "driverBreakdownAbsenceNoVersion": "संग्रहीत रोलअप पर कोई संस्करण चिह्न नहीं है।", + "driverBreakdownSyncTheBand": "बैंड सिंक करें", + "driverBreakdownAbsenceNoReason": "कोई भी रिकॉर्ड यह नहीं बताता कि कल रात का विवरण क्यों उपलब्ध नहीं है।", + "coachFiguresCouldNotBeDrawn": "एक आकृति नहीं बनाई जा सकी", + "coachFiguresNoType": "कोच ने बिना प्रकार वाली आकृति भेजी।", + "coachFiguresUnsupportedType": "कोच ने \"{type}\" प्रकार की आकृति मांगी, जिसे यह ऐप नहीं बना सकता।", + "coachFiguresFigure": "आकृति", + "coachFiguresSeriesN": "सीरीज़ {n}", + "coachFiguresLaneN": "लेन {n}", + "coachFiguresNoSleepSegments": "कोई नींद खंड नहीं", + "coachFiguresNoTimeInZone": "ज़ोन में कोई समय नहीं", + "coachFiguresMinTotal": "कुल {n} मिनट", + "coachFiguresGauge": "गेज", + "coachFiguresGaugeNoValue": "कोच ने बिना मान वाला गेज भेजा।", + "coachFiguresSummary": "सारांश", + "coachFiguresEmptySummary": "कोच ने खाली सारांश भेजा।", + "coachFiguresTable": "तालिका", + "coachFiguresTableNoRows": "कोच ने बिना पंक्तियों वाली तालिका भेजी।", + "circadianDetailTitle": "बॉडी क्लॉक", + "circadianDetailNoNightsTitle": "अभी तक दिखाने के लिए कोई रातें नहीं हैं", + "circadianDetailNoNightsBody": "0 रातों का स्कोर बना।", + "circadianDetailNoNightsFix": "रातभर बैंड पहनें", + "circadianDetailSleepTitle": "नींद, रात दर रात", + "circadianDetailAsleep": "सोया हुआ", + "circadianDetailSleepFootnote": "{count, plural, one{{count} रात, हर एक का अलग कॉलम। जितना गहरा रंग, उस घंटे उतनी ही अधिक नींद।} other{{count} रातें, हर एक का अलग कॉलम। जितना गहरा रंग, उस घंटे उतनी ही अधिक नींद।}}", + "circadianDetailYourRhythm": "आपकी लय", + "circadianDetailWhichNights": "कौन-सी रातें", + "circadianDetailHide": "छुपाएं", + "circadianDetailShow": "दिखाएं", + "circadianDetailTodayPredicted": "आज, अनुमानित", + "circadianDetailRhythmStrength": "लय की मज़बूती", + "circadianDetailWhenStill": "जब आप स्थिर होते हैं", + "circadianDetailNoStillTitle": "अभी तक पढ़ने लायक कोई स्थिर पल नहीं मिले", + "circadianDetailNoStillBody": "यह केवल उन सेकंडों से दिल की धड़कन की टाइमिंग पढ़ता है जब आप हिले नहीं थे, और पिछले {days, plural, one{{days} दिन में} other{{days} दिनों में}} इतने कम मिले कि एक पूरा घंटा नहीं बन पाया।", + "circadianDetailStillnessTitle": "स्थिर रहते हुए धड़कन-दर-धड़कन विविधता", + "circadianDetailStillnessFootnote": "हर घंटे का मान उन {lo}–{hi} पांच-मिनट के हिस्सों का मध्य मान है जिनमें आप सचमुच स्थिर थे, पिछले {days, plural, one{{days} दिन} other{{days} दिनों}} में — कभी सिर्फ़ आज का नहीं। 24 में से {drawn} घंटों में कम-से-कम तीन हिस्से मिले; बाकी खाली हैं। यह तनाव का स्कोर नहीं है — बैठना, गर्म कमरा या कॉफ़ी भी इसे उतना ही बदल सकते हैं।", + "circadianDetailForecastTitle": "आज का दिन कैसा रहने की संभावना है", + "circadianDetailForecastFootnote": "कोई पैमाना नहीं — आकार ही पूरा परिणाम है।", + "circadianDetailTroughText": "सबसे सपाट हिस्सा {troughLabel} में आता है, लगभग {start}–{end} के बीच।", + "circadianDetailPredictionDisclaimer": "यह एक अनुमान है, कोई माप नहीं। बैंड पर कुछ भी यह नहीं मापता कि आप कितने सतर्क हैं, और इसे केवल पिछली रात का पता है, कुछ और नहीं — झपकी, कॉफ़ी, या आज होने वाली कोई भी चीज़ इस तक कभी नहीं पहुंचती।", + "circadianDetailAssumedPhaseNote": "आपकी अपनी घड़ी का चरम अभी पता नहीं चला है, इसलिए यहां औसत मान इस्तेमाल किया गया है।", + "circadianDetailNotADrivingCheck": "यह ड्राइविंग-फिटनेस जांच नहीं है और न ही शिफ्ट-सुरक्षा उपकरण है, और यह यह नहीं बताता कि आप अक्षम हैं।", + "circadianDetailChronotype": "क्रोनोटाइप", + "circadianDetailMidSleepFree": "मध्य-नींद, छुट्टी के दिन", + "circadianDetailMidSleepWork": "मध्य-नींद, काम के दिन", + "circadianDetailSocialJetlag": "सामाजिक जेटलैग", + "circadianDetailLater": "देर से", + "circadianDetailEarlier": "जल्दी", + "circadianDetailNightsCompared": "तुलना की गई छुट्टी / काम की रातें", + "circadianDetailRegularityIndex": "नियमितता सूचकांक", + "circadianDetailNightsLeastAlike": "सबसे कम मिलती-जुलती रातें", + "circadianDetailSamePairScale": "वह जोड़ा, वही पैमाना", + "circadianDetailRhythmNotEstablished": "आपकी लय अभी स्थापित नहीं हुई है", + "circadianDetailPairFootnote": "{count} में से सबसे कम मेल खाने वाला जोड़ा। देर तक चलने वाला वीकेंड एक अलग शेड्यूल है, बुरी रात नहीं। जिन जोड़ों में किसी भी दिन का बहुत कम डेटा दर्ज हुआ, उन्हें छोड़ दिया गया है।", + "circadianDetailStability": "दिन-दर-दिन स्थिरता", + "circadianDetailFragmentation": "घंटे-दर-घंटे विखंडन", + "circadianDetailAmplitude": "सापेक्ष आयाम", + "circadianDetailM10Start": "सबसे ऊंची हृदय गति वाले 10 घंटों की शुरुआत", + "circadianDetailL5Start": "सबसे कम हृदय गति वाले 5 घंटों की शुरुआत", + "circadianDetailRhythmPeak": "लय का शिखर", + "circadianDetailPeakSwing": "शिखर-से-औसत उतार-चढ़ाव", + "circadianDetailFitCurve": "24 घंटे के वक्र से मेल", + "circadianDetailStrengthNotMeasured": "लय की मज़बूती अभी मापी नहीं गई है", + "circadianDetailStrengthWhy": "सभी 24 घंटे दर्ज किए गए लगातार दिन चाहिए।", + "circadianDetailStrengthFootnoteKnown": "हृदय गति के {used, plural, one{{used} पूरी तरह दर्ज दिन} other{{used} पूरी तरह दर्ज दिनों}} से लिया गया। ये आपके सबसे ऊंची और सबसे कम हृदय गति वाले घंटे हैं, आपके सबसे व्यस्त घंटे नहीं।", + "circadianDetailStrengthFootnoteUnknown": "हृदय गति के पूरी तरह दर्ज कुछ दिनों की श्रृंखला से लिया गया। ये आपके सबसे ऊंची और सबसे कम हृदय गति वाले घंटे हैं, आपके सबसे व्यस्त घंटे नहीं।", + "beatsTitle": "धड़कनें", + "beatsNoNightTitle": "अभी तक दिखाने के लिए कोई रात नहीं है", + "beatsNoNightBody": "इस फ़ोन ने अभी तक कोई प्रोसेस की गई रात नहीं बनाई है, इसलिए दिखाने के लिए कोई धड़कन अंतराल नहीं है।", + "beatsNoNightFix": "रातभर बैंड पहनें, फिर सिंक करें", + "beatsNightOf": "{date} की रात", + "beatsPoincareSection": "हर धड़कन, पिछली के मुक़ाबले", + "beatsBeatsGoneTitle": "इस रात की धड़कनें अब इस फ़ोन पर नहीं हैं", + "beatsBeatsGoneBody": "रात का स्कोर बनने के कुछ दिनों बाद व्यक्तिगत धड़कन अंतराल हटा दिए जाते हैं। उनसे निकाले गए आंकड़े हमेशा के लिए सुरक्षित रहते हैं", + "beatsBeatsGoneMeasured": " — इस रात SD1 {sd1} ms, SD2 {sd2} ms मापा गया", + "beatsScatterTitle": "हर अंतराल, पिछले के मुक़ाबले प्लॉट किया गया", + "beatsScatterFootnote": "डायगोनल वह जगह है जहां एक धड़कन ठीक पिछली जितनी लंबी रही। इस रेखा के आर-पार फैलाव SD1 है, धड़कन-दर-धड़कन; इसके साथ-साथ फैलाव SD2 है, धीमी बहाव।", + "beatsSd1Label": "SD1", + "beatsSd2Label": "SD2", + "beatsIntervalsLabel": "अंतराल", + "beatsIntervalsSurvived": "{count, plural, one{{count} अंतराल सुधार के बाद बचा} other{{count} अंतराल सुधार के बाद बचे}}", + "beatsDroppedArtifact": " — {count, plural, one{{count} को आर्टिफ़ैक्ट मानकर हटाया गया और वह} other{{count} को आर्टिफ़ैक्ट मानकर हटाया गया और वे}} क्लाउड में नहीं हैं", + "beatsPulseNotEcg": "यह पल्स है, ECG नहीं — असली और आपकी अपनी, लेकिन वह तस्वीर नहीं जो ECG बनाता है।", + "beatsMeasuredOn": "{device} पर मापा गया; अलग-अलग बैंड एक जैसे आंकड़े नहीं देते।", + "beatsVariabilitySection": "रात भर की विविधता", + "beatsUnitNights": "रातें", + "beatsUnitScreenedNotScreened": "जांचा गया / नहीं जांचा गया", + "beatsVariabilityWhy": "इस रात के किसी भी आधे घंटे के हिस्से में RMSSD प्रकाशित करने लायक पर्याप्त साफ़ धड़कनें नहीं थीं।", + "beatsNoBinsStored": "इस रात के लिए कोई हिस्सा सहेजा नहीं गया।", + "beatsRmssdTitle": "आधे घंटे के हिस्सों में RMSSD", + "beatsStart": "शुरुआत", + "beatsBandFootnote": "यह बार दिखाता है कि हमें उस हिस्से पर कितना भरोसा है, न कि वह दायरा जिससे आपका शरीर गुज़रा। अंदर का निशान असली मान है।", + "beatsHolesFootnote": "{count, plural, one{ {count} हिस्से में साफ़ धड़कनें बहुत कम हैं, इसलिए एक भी प्रकाशित नहीं हो सका और वह जोड़ने के बजाय खाली छोड़ दिया गया है।} other{ {count} हिस्सों में साफ़ धड़कनें बहुत कम हैं, इसलिए एक भी प्रकाशित नहीं हो सका और वे जोड़ने के बजाय खाली छोड़ दिए गए हैं।}}", + "beatsFirstThird": "पहला तिहाई", + "beatsLastThird": "आख़िरी तिहाई", + "beatsDcSection": "मंदन क्षमता", + "beatsDcWhy": "अभी तक किसी सहेजी गई रात ने यह मान नहीं दिया है।", + "beatsDcNoData": "अभी तक किसी रात ने यह मान नहीं दिया है।", + "beatsDcChartTitle": "आपकी अपनी रातें, क्रम में", + "beatsDaysAgo": "{count} दिन पहले", + "beatsTodayLabel": "आज", + "beatsAnchorsLastNight": "पिछली रात के एंकर", + "beatsCleanBeats": "साफ़ धड़कनें", + "beatsDcNote": "सिर्फ़ आपकी। इसकी तुलना केवल अपनी अन्य रातों से करें, किसी और चीज़ से नहीं — कलाई के लिए कोई संदर्भ सीमा नहीं है।\n\nयह हर उस पल के आसपास की धड़कनों का औसत निकालता है जब आपका दिल धीमा हुआ। बढ़ती हुई रेखा एक अलग दिल के बजाय साफ़ सिग्नल का नतीजा हो सकती है, इसलिए इसे ऊपर दिए एंकर काउंट और साफ़-धड़कन हिस्से के साथ पढ़ें। अगर आपने इस अवधि में बैंड बदला है, तो दोनों हिस्से तुलनीय नहीं हैं।", + "beatsRhythmSection": "लय जांच", + "beatsRhythmChartTitle": "हर दिन के लिए एक सेल", + "beatsScreenNotFired": "जांच सक्रिय नहीं हुई", + "beatsScreenFired": "जांच सक्रिय हुई", + "beatsNotScreened": "जांच नहीं हुई", + "beatsNoDayScreened": "इस अवधि के किसी भी दिन जांच नहीं हुई", + "beatsScreenNote": "यह एक जांच है, टेस्ट नहीं।\n\nजिस दिन जांच सक्रिय नहीं हुई, उसका मतलब यह नहीं कि आप पूरी तरह सुरक्षित घोषित हुए — यह कुछ भी खारिज नहीं कर सकती, और कभी नहीं कर सकी। जिन दिनों पर सिर्फ़ रूपरेखा है, उन दिनों जांच बिल्कुल नहीं हुई: बहुत कम साफ़ धड़कनें, या बहुत ज़्यादा हलचल।\n\nकलाई की पल्स ECG नहीं है। अगर लक्षणों की वजह से आप यहां आए हैं, तो कोई चिकित्सक इसकी सही जांच कर सकता है।", + "beatsScreenedSummary": "पिछले {win} दिनों में से {screened} की जांच हुई", + "beatsFiredSummary": "; जांच {fired} बार सक्रिय हुई", + "beatsNotScreenedNote": " पिछली रात जांच नहीं हुई: {note}", + "logWorkoutCouldNotLog": "इसे लॉग नहीं किया जा सका — फिर से कोशिश करें।", + "logWorkoutCouldNotDismiss": "इसे खारिज नहीं किया जा सका — फिर से कोशिश करें।", + "logWorkoutAdjustTimes": "समय समायोजित करें", + "logWorkoutDetectedActivityTitle": "पहचानी गई गतिविधि", + "logWorkoutYoursToConfirmSub": "आपको पुष्टि करनी है", + "logWorkoutReadFailedTitle": "आपकी पहचानी गई गतिविधि नहीं पढ़ी जा सकी", + "logWorkoutReadFailedBody": "स्टोर से कोई जवाब नहीं मिला। कुछ भी लॉग या खारिज नहीं किया गया है।", + "logWorkoutTryAgain": "फिर से कोशिश करें", + "logWorkoutReadingSpotted": "बैंड ने जो पहचाना है उसे पढ़ा जा रहा है…", + "logWorkoutNothingToReviewTitle": "समीक्षा के लिए कुछ नहीं", + "logWorkoutNothingToReviewBody": "हो सकता है यह पहले ही लॉग या खारिज की जा चुकी हो।", + "logWorkoutHardMinutesTitle": "यह कठिन मेहनत के मिनट हैं, पूरा सत्र नहीं", + "logWorkoutHardMinutesBody": "डिटेक्शन केवल वह सतत प्रयास बताता है जिसे वह देख सका, इसलिए वार्म-अप और सेट्स के बीच का आराम इसमें शामिल नहीं है। अगर समय-सीमा छोटी है तो लॉग करने से पहले समय समायोजित करें।", + "logWorkoutMinutesOfEffort": "{mins} मिनट का प्रयास", + "logWorkoutAvgHr": "औसत हृदय गति", + "logWorkoutPeakHr": "अधिकतम हृदय गति", + "logWorkoutLooksLike": "यह लगता है", + "logWorkoutLogIt": "लॉग करें", + "logWorkoutNotAWorkout": "यह वर्कआउट नहीं है", + "logWorkoutToday": "आज", + "logWorkoutYesterday": "कल", + "logWorkoutDefaultTitle": "पिछला वर्कआउट लॉग करें", + "logWorkoutWindowRescoredSub": "समय-सीमा, फिर से स्कोर की गई", + "logWorkoutYourOwnTimesSub": "आपके अपने समय", + "logWorkoutWhenGroup": "कब", + "logWorkoutActivityLabel": "गतिविधि", + "logWorkoutDateLabel": "तारीख", + "logWorkoutStartedLabel": "शुरू", + "logWorkoutEndedLabel": "खत्म", + "logWorkoutLengthLabel": "अवधि", + "logWorkoutNextMorningSub": "अगली सुबह", + "logWorkoutWindowInvalidTitle": "यह समय-सीमा सेव नहीं होगी", + "logWorkoutTimesUpdatedTitle": "समय अपडेट किया गया", + "logWorkoutLoggedTitle": "वर्कआउट लॉग किया गया", + "logWorkoutUnscoredSaved": "सेव कर दिया गया। उस समय-सीमा में कोई हृदय गति दर्ज नहीं हुई, इसलिए इसमें न कोई एक्सर्शन स्कोर है न कैलोरी — केवल समय ही दर्ज है।", + "logWorkoutCouldNotSave": "सेव नहीं हो सका — फिर से कोशिश करें।", + "logWorkoutScoredTitle": "बैंड द्वारा दर्ज डेटा से स्कोर किया गया", + "logWorkoutScoredBody": "एक्सर्शन और कैलोरी इन समयों के भीतर की सेकंड-दर-सेकंड हृदय गति से आते हैं, उसी तरीके से जो पूरे दिन के लिए इस्तेमाल होता है। अवधि से कुछ भी अनुमानित नहीं किया जाता।", + "logWorkoutSaving": "सेव हो रहा है…", + "logWorkoutSaveNewTimes": "नए समय सेव करें", + "logWorkoutSearchActivities": "गतिविधियां खोजें", + "logWorkoutNoActivityByName": "इस नाम की कोई गतिविधि नहीं", + "logFoodTitle": "भोजन लॉग करें", + "logFoodClose": "बंद करें", + "logFoodIAte": "मैंने {meal} खाया", + "logFoodAgain": "फिर से", + "logFoodAddNumbers": "आंकड़े जोड़ें", + "logFoodScanBarcode": "बारकोड स्कैन करें", + "logFoodScanSubOn": "बारकोड के बारे में openfoodfacts.org से पूछता है, और जिस पर भरोसा किया जा सकता है वह भर देता है", + "logFoodScanSubOff": "उत्पाद को ऑनलाइन खोजता है। पहले पूछता है", + "logFoodLookingUpTitle": "खोजा जा रहा है", + "logFoodLookingUpBody": "जवाब मिलते ही बॉक्स अपने आप भर जाते हैं।", + "logFoodWhatLabel": "क्या", + "logFoodWhatHint": "चिकन और चावल", + "logFoodPortionLabel": "मात्रा", + "logFoodUnknownHint": "अज्ञात", + "logFoodEnergyLabel": "ऊर्जा", + "logFoodProteinLabel": "प्रोटीन", + "logFoodCarbsLabel": "कार्बोहाइड्रेट", + "logFoodFatLabel": "वसा", + "logFoodFibreLabel": "फाइबर", + "logFoodBlankHint": "खाली छोड़ा गया आंकड़ा खाली ही रहता है। केवल \"क्या\" भरना जरूरी है।", + "logFoodSayWhatFirst": "पहले बताएं कि क्या था।", + "logFoodConsentTitle": "क्या बारकोड ऑनलाइन खोजें?", + "logFoodConsentBody1": "स्कैन करने पर बारकोड openfoodfacts.org को भेजा जाता है, जो एक मुफ़्त, खुला खाद्य डेटाबेस है। वे बारकोड और आपका IP पता देख पाते हैं। आपके बारे में, आपके भोजन या आपके स्वास्थ्य के बारे में कुछ भी इस फ़ोन से बाहर नहीं जाता, और पहले स्कैन किया जा चुका बारकोड आपकी अपनी कॉपी से जवाब देता है, बिना उनसे दोबारा पूछे।", + "logFoodConsentBody2": "इनके आंकड़े आम लोगों द्वारा दर्ज किए जाते हैं और उनमें से काफी गलत होते हैं, इसलिए जो कुछ भी सैनिटी चेक में फेल होता है उसे भरने के बजाय खाली छोड़ दिया जाता है। जो भी अपने आप भर जाता है उसे सेव करने से पहले आप बदल सकते हैं।", + "logFoodConsentBody3": "आप इसे सेटिंग्स › प्राइवेसी में वापस बंद कर सकते हैं। पैकेट पर लिखे आंकड़े टाइप करना हर हाल में काम करता है।", + "logFoodAllowLookups": "खोज की अनुमति दें", + "logFoodNotNow": "अभी नहीं", + "logFoodBreakfast": "नाश्ता", + "logFoodLunch": "दोपहर का भोजन", + "logFoodDinner": "रात का भोजन", + "logFoodSnack": "स्नैक", + "logFoodNoNumbersTitle": "इसके लिए कोई आंकड़े नहीं", + "logFoodNoNumbersBody": "Open Food Facts के पास यह उत्पाद है, लेकिन इसके पोषण संबंधी कोई उपयोगी आंकड़े नहीं हैं — या जो थे वे सैनिटी चेक में पास नहीं हुए।", + "logFoodNotFoundTitle": "Open Food Facts में नहीं है", + "logFoodNotFoundBody": "अभी तक किसी ने यह बारकोड नहीं जोड़ा है।", + "logFoodFlaggedTitle": "यह रिकॉर्ड गलत के रूप में फ़्लैग किया गया है", + "logFoodFlaggedBody": "Open Food Facts इस उत्पाद को त्रुटिपूर्ण के रूप में चिह्नित करता है, इसलिए इसका कोई भी आंकड़ा नहीं भरा गया।", + "logFoodUnreachableTitle": "Open Food Facts से कोई जवाब नहीं", + "logFoodUnreachableBody": "openfoodfacts.org तक नहीं पहुंचा जा सका।", + "logFoodRefusedTitle": "बारकोड खोज बंद है", + "logFoodRefusedBody": "कुछ भी नहीं भेजा गया। आप इसे सेटिंग्स › प्राइवेसी में चालू कर सकते हैं।", + "logFoodPortionNoteBase": "Open Food Facts यह हर 100 ग्राम के हिसाब से बताता है। मात्रा बदलें और आंकड़े उसी के अनुसार बदल जाएंगे।", + "logFoodPortionNoteServing": "Open Food Facts यह हर 100 ग्राम के हिसाब से बताता है। मात्रा बदलें और आंकड़े उसी के अनुसार बदल जाएंगे। पैकेट की अपनी मात्रा {serving} है।", + "logFoodPillOpenFoodFacts": "Open Food Facts", + "logFoodPillYours": "आपका", + "logFoodBareOccasion": "लॉग किया गया · ऊर्जा दर्ज नहीं", + "logFoodOpensInBrowser": "{label}, आपके ब्राउज़र में खुलेगा", + "dayTimelineChargerOn": "चार्जर पर रखा", + "dayTimelineChargerOff": "चार्जर से हटाया", + "dayTimelineDoubleTap": "आपने बैंड पर दो बार टैप किया", + "dayTimelineRestarted": "बैंड फिर से चालू हुआ", + "dayTimelineBatteryPackAttached": "बैटरी पैक जोड़ा गया", + "dayTimelineBatteryPackRemoved": "बैटरी पैक हटाया गया", + "dayTimelineAlarmWentOff": "अलार्म बजा", + "dayTimelineAsleep": "नींद", + "dayTimelineNap": "झपकी", + "dayTimelineWorkout": "वर्कआउट", + "dayTimelineBandOffWrist": "बैंड कलाई पर नहीं था", + "dayTimelineHighestHr": "सबसे अधिक हृदय गति", + "dayTimelineLowestHr": "सबसे कम हृदय गति", + "dayTimelineBpmAt": "{time} पर {bpm} bpm", + "dayTimelineTakenAt": "{time} पर ली गई", + "dayTimelineLastAt": "आख़िरी बार {time} पर", + "dayTimelineTaggedTitle": "टैग किया गया", + "dayTimelineTitle": "आपके दिन का सारांश", + "dayTimelineSub": "आधी रात से आधी रात तक", + "dayTimelineHeartRateTitle": "हृदय गति", + "dayTimelineMidnight": "आधी रात", + "dayTimelineNoon": "दोपहर", + "dayTimelineMoving": "गतिविधि में", + "dayTimelineNotRecorded": "दर्ज नहीं", + "dayTimelineNothingRecordedTitle": "इस दिन कुछ भी दर्ज नहीं हुआ", + "dayTimelineNothingRecordedBody": "न नींद, न सत्र, न कोई लॉग और न समय के साथ कोई बैंड इवेंट। ऐसा खाली दिन आमतौर पर उस दिन को दिखाता है जब बैंड पहना ही नहीं गया था।", + "dayTimelineNoTimeTitle": "इस दिन किसी भी चीज़ के साथ समय दर्ज नहीं है", + "dayTimelineNoTimeBody": "जो दर्ज किया गया वह नीचे है।", + "dayTimelineWhatHappenedSection": "क्या हुआ", + "dayTimelineAlsoLoggedSection": "इस दिन यह भी दर्ज हुआ", + "dayTimelineNoTimeNote": "यह दिन के लिए दर्ज किया गया था लेकिन इसका कोई निश्चित समय नहीं है, इसलिए यह टाइमलाइन पर नहीं दिखाया गया।", + "dayTimelinePatternsNote": "यह आपके अपने रिकॉर्ड में पैटर्न है, कारण नहीं। यहाँ पास-पास दिखने वाली दो चीज़ों का मतलब सिर्फ़ इतना है कि वे समय में करीब हुईं — यह पेज बस इतना ही बताता है।", + "journalComposeNotReady": "अभी तैयार नहीं है — पहले ऐप खोलें।", + "journalComposeSaveFailed": "सेव नहीं हो सका — स्टोरेज जाँचें और फिर से कोशिश करें।", + "journalComposeWhenWasLastOne": "पिछली बार कब हुआ था?", + "journalComposeTitle": "जर्नल", + "journalComposeTodaySection": "आज", + "journalComposeTrackSomethingElse": "कुछ और ट्रैक करें", + "journalComposeAnythingElseLabel": "कुछ और", + "journalComposeAnythingElseHint": "इस दिन के बारे में एक पंक्ति लिखें।", + "journalComposeSavingLabel": "सेव हो रहा है", + "journalComposeHowAreYouFeeling": "आप कैसा महसूस कर रहे हैं?", + "journalComposeNotAnsweredYet": "अभी तक जवाब नहीं दिया", + "journalComposeMoodOfFive": "मूड {value} / 5 · मिटाने के लिए फिर से टैप करें", + "journalComposeMoodOfFiveSelected": "मूड {n} / 5, चयनित। मिटाने के लिए सक्रिय करें।", + "journalComposeMoodOfFiveLabel": "मूड {n} / 5", + "journalComposeNotLogged": "दर्ज नहीं है", + "journalComposeWhenWasLastField": "पिछली बार {field} कब हुआ था", + "journalComposeAddTimeOfLastOne": "पिछली बार का समय जोड़ें", + "journalComposeLastAt": "आख़िरी बार {time} पर", + "journalComposeIncrease": "बढ़ाएँ", + "journalComposeDecrease": "घटाएँ", + "journalComposeWeightLabel": "वज़न", + "journalComposeNotEntered": "दर्ज नहीं किया गया", + "journalComposeEnteredNotMeasured": "{value} · दर्ज किया गया, मापा नहीं गया", + "journalComposeEnterWeight": "वज़न दर्ज करें", + "journalComposeEnter": "दर्ज करें", + "journalComposeChange": "बदलें", + "journalComposeSeeWeightTrend": "वज़न का रुझान देखें", + "journalComposeSeeTheTrend": "रुझान देखें", + "journalComposeWeightToday": "आज का वज़न", + "journalComposeWeightKgLabel": "वज़न (किग्रा)", + "journalComposeWeightScaleNote": "आपने या आपकी तराज़ू ने जो पढ़ा। बैंड इसे नहीं मापता।", + "journalComposeClear": "मिटाएँ", + "journalComposeNotEnoughEntriesTitle": "रुझान के लिए पर्याप्त प्रविष्टियाँ नहीं हैं", + "journalComposeNotEnoughEntriesBody": "यह रेखा आपके द्वारा दर्ज किए गए आँकड़ों का सात-दिन का औसत है, इसलिए इसके लिए कम से कम दो दिन चाहिए। बीच में कुछ भी अपने आप नहीं भरा जाता।", + "journalComposeSevenDayTrend": "सात-दिन का रुझान", + "journalComposeTrendFootnote": "आपके द्वारा दर्ज किया गया। बिना प्रविष्टि वाले दिन खाली रहते हैं।", + "journalComposeWeightTrendExplainer": "आपने या आपकी तराज़ू ने दर्ज किया — बैंड वज़न नहीं मापता। जो दिखाया गया है वह सात दिन का औसत है, क्योंकि तराज़ू केवल पानी और भोजन के कारण एक से दो किलो तक बदल सकती है, और कच्चे रीडिंग इसे शरीर में हुए बदलाव के रूप में दिखाएँगे। {count, plural, one{{count} दिन दर्ज किया गया।} other{{count} दिन दर्ज किए गए।}}", + "nutritionTabToday": "आज", + "nutritionTabWeek": "सप्ताह", + "nutritionTabGoals": "लक्ष्य", + "nutritionLogFood": "भोजन दर्ज करें", + "nutritionTitle": "पोषण", + "nutritionEmptyTodayTitle": "आज कुछ भी दर्ज नहीं किया गया", + "nutritionEmptyTodayBody": "एक टैप में पूरी एंट्री हो जाती है।", + "nutritionLogOccasionFix": "एक भोजन दर्ज करें", + "nutritionOccasionsSection": "भोजन के समय", + "nutritionAddAction": "जोड़ें", + "nutritionFloorTitle": "आज की ऊर्जा एक न्यूनतम है, कुल नहीं", + "nutritionFloorBody": "{total} में से {unknown} भोजन बिना ऊर्जा के आँकड़े के दर्ज किए गए, इसलिए ऊपर की संख्या यह बताती है कि आपने कम से कम कितना खाया, न कि आपने वास्तव में कितना खाया।", + "nutritionAddNumbersFix": "किसी भोजन में आँकड़े जोड़ें", + "nutritionDaysNotCounted": "पिछले {span} दिनों में से {excluded} दिन गिने नहीं जा सके", + "nutritionDayCountsRule": "एक दिन तभी गिना जाता है जब हर भोजन में ऊर्जा का आँकड़ा हो।", + "nutritionDaysLoggedLabel": "जिन दिनों कुछ न कुछ दर्ज हुआ", + "nutritionPartialExcluded": "{partial} दिन दर्ज तो हुए पर अधूरे रहे, इसलिए नीचे हर औसत से बाहर रखे गए", + "nutritionEnergyByDay": "ऊर्जा, दिन दर दिन", + "nutritionSevenDayAvg": "सात-दिन का औसत", + "nutritionNoCompleteDayTitle": "अभी औसत निकालने लायक कोई पूरा दिन नहीं है", + "nutritionNoCompleteDayBody": "आपके पास एक भी नहीं है।", + "nutritionLabelEnergy": "ऊर्जा", + "nutritionLabelProtein": "प्रोटीन", + "nutritionLabelCarbs": "कार्ब्स", + "nutritionLabelFat": "वसा", + "nutritionLabelFibre": "फाइबर", + "nutritionEnergyBalance": "ऊर्जा संतुलन", + "nutritionLabelEaten": "खाया गया", + "nutritionLabelBurned": "जला गया", + "nutritionLabelBalance": "संतुलन", + "nutritionEatenMeanNote": "खाया गया {days} पूरे दिनों का औसत है। जला गया केवल आज का है।", + "nutritionEnergyLoggedTitle": "दर्ज की गई ऊर्जा", + "nutritionPartialFootnote": "{n} अधूरे, नीचे के औसत से बाहर रखे गए।", + "nutritionNothingLoggedYet": "अभी तक कुछ भी दर्ज नहीं हुआ", + "nutritionNoEnergyFiguresYet": "अभी तक कोई ऊर्जा आँकड़ा नहीं", + "nutritionDailyEnergy": "दैनिक ऊर्जा", + "nutritionDailyProtein": "दैनिक प्रोटीन", + "nutritionEnergyWord": "ऊर्जा", + "nutritionProteinWord": "प्रोटीन", + "nutritionYourTargetsSection": "आपके लक्ष्य", + "nutritionHintNone": "कोई नहीं", + "nutritionNoTargetsTitle": "कोई लक्ष्य निर्धारित नहीं", + "nutritionNoTargetsBody": "यहाँ लक्ष्य वही है जो आप खुद टाइप करते हैं।", + "nutritionSetTargetFix": "एक लक्ष्य सेट करें", + "nutritionEditAction": "संपादित करें", + "nutritionBodySpentToday": "आपके शरीर ने आज कितना खर्च किया", + "nutritionEstimatedExpenditure": "अनुमानित खर्च", + "nutritionNotMeasured": "मापा नहीं गया", + "nutritionExpenditureSub": "आज, हृदय गति और आपकी प्रोफ़ाइल के आधार पर", + "nutritionRemoveTitle": "{label} हटाएँ?", + "nutritionRemoveBody": "इससे यह उस दिन और उसे गिनने वाले हर औसत से हट जाएगा। यह पूर्ववत नहीं किया जा सकता।", + "nutritionNothingToMeasure": "अभी {label} की तुलना करने के लिए कुछ भी नहीं है", + "nutritionFloorAverageBody": "हर पूरे दिन में कम से कम एक भोजन में {nutrient} का आँकड़ा दर्ज नहीं था, इसलिए औसत केवल एक निचली सीमा होगी।", + "nutritionCountedNoFigureBody": "पिछले {span} दिनों में से {count} दिन गिने गए, पर किसी में भी {nutrient} का आँकड़ा नहीं था।", + "nutritionDayCountsRuleFull": "एक दिन तभी गिना जाता है जब हर भोजन में आँकड़ा हो और लॉग शाम तक पहुँचे। पिछले {span} दिनों में से किसी ने भी ऐसा नहीं किया।", + "nutritionOnTarget": "लक्ष्य पर", + "nutritionRateAbove": "{amount} {unit}/दिन ऊपर", + "nutritionRateBelow": "{amount} {unit}/दिन नीचे", + "nutritionMeanOfDays": "{n} पूरे दिनों का औसत", + "nutritionLoggedToday": "आज दर्ज हुआ", + "nutritionEatenToday": "आज खाया गया", + "nutritionAtLeast": "कम से कम", + "nutritionOccasionsUnit": "बार", + "nutritionOccasionsCount": "{n} बार", + "nutritionLabelBalanceAtLeast": "संतुलन कम से कम", + "nutritionNotLogged": "दर्ज नहीं हुआ", + "nutritionLoggedNoEnergy": "{n} दर्ज हुए · ऊर्जा दर्ज नहीं हुई", + "nutritionAtLeastPrefix": "कम से कम ", + "nutritionMealBreakfast": "नाश्ता", + "nutritionMealLunch": "दोपहर का भोजन", + "nutritionMealDinner": "रात का भोजन", + "nutritionMealSnacks": "स्नैक्स", + "nutritionNotCounted": "गिना नहीं गया", + "nutritionNotRecorded": "दर्ज नहीं हुआ", + "nutritionEveryDayNoFigure": "हर पूरे दिन में किसी भोजन में {label} का आँकड़ा नहीं था", + "nutritionNoDayRecorded": "किसी भी पूरे दिन ने {label} दर्ज नहीं किया", + "nutritionMeanOfCompleteDaysCaps": "{n} पूरे दिनों का औसत", + "nutritionLeftOutAsFloor": " · {n} को निचली सीमा मानकर बाहर रखा गया", + "nutritionWaterLabel": "पानी", + "nutritionTapToChange": "बदलने के लिए − या + टैप करें", + "nutritionNoneYet": "अभी तक कुछ नहीं", + "nutritionAddWater": "पानी जोड़ें", + "nutritionRemoveWater": "पानी घटाएँ", + "coachApiKeyLabel": "API कुंजी", + "coachApiKeyLocalLabel": "API कुंजी (लोकल के लिए ज़रूरी नहीं)", + "coachAsking": "पूछा जा रहा है…", + "coachAskLabel": "कोच से पूछें", + "coachBaseUrlLabel": "बेस URL", + "coachBriefingMenuSub": "इस डिवाइस से भेजा गया ठीक वही स्नैपशॉट", + "coachBriefingMenuTitle": "ब्रीफिंग, और क्या भेजा गया", + "coachChooseModelFix": "एक मॉडल चुनें", + "coachCloudDataNote": "आपके सवाल और कोच जो डेटा पढ़ता है, वह इस एंडपॉइंट पर भेजा जाता है। यह ठीक क्या है, यह \"क्या भेजा गया\" में देखें।", + "coachDeleteChat": "{title} हटाएँ", + "coachDeleteIt": "इसे हटाएँ", + "coachDestructiveWarning": "इससे इस डिवाइस से डेटा हट जाएगा और इसे पूर्ववत नहीं किया जा सकता।", + "coachEndpointUnreachable": "उस एंडपॉइंट तक नहीं पहुँचा जा सका: {error}", + "coachErrorTitle": "वह पूरा नहीं हो सका", + "coachInputHint": "अपनी सेहत के बारे में पूछें…", + "coachIntroBody": "ऐप जो कुछ भी मापता है उसके बारे में पूछें, और यह भोजन, पानी, वर्कआउट, खुराक और आपका अनुभव दर्ज कर सकता है — हमेशा पहले पूछकर।", + "coachKeychainRefused": "कीचेन ने कुंजी अस्वीकार कर दी: {error}", + "coachKeyStillSavedBody": "इस बार इसे कीचेन से पढ़ा नहीं जा सका, ऐसा तब होता है जब फ़ोन लॉक रहते हुए ऐप जगाया जाता है।", + "coachKeyStillSavedTitle": "आपकी कुंजी अभी भी सहेजी हुई है", + "coachListModels": "मॉडल सूचीबद्ध करें", + "coachLocalDataNote": "आपके सवाल और कोच जो डेटा पढ़ता है, वह आपकी अपनी मशीन पर ही रहता है।", + "coachLocalSub": "इसी नेटवर्क पर। आपकी मशीन से कुछ भी बाहर नहीं जाता।", + "coachMenuSemantic": "चैट और AI सेटिंग्स", + "coachModelHint": "खोजें, या एक आईडी टाइप करें", + "coachModelLabel": "मॉडल", + "coachModelsFound": "{n} मॉडल मिले। किसी एक पर टैप करें।", + "coachNavTitle": "कोच", + "coachNewChat": "नई चैट", + "coachNoChatsYet": "अभी तक कुछ नहीं — यह आपकी पहली बातचीत है।", + "coachNoDataBody": "कोच आपके अपने दिनों के व्युत्पन्न डेटा से जवाब देता है, और इस डिवाइस पर अभी तक ऐसा कोई डेटा नहीं है।", + "coachNoDataTitle": "अभी पढ़ने के लिए कोई डेटा नहीं", + "coachNoModelsListed": "उस एंडपॉइंट ने कोई मॉडल सूचीबद्ध नहीं किया। नीचे एक टाइप करें।", + "coachNotSetUp": "सेट अप नहीं है", + "coachNotSetUpBody": "यह आपके चुने हुए मॉडल पर चलता है — या तो आपकी अपनी मशीन पर, या आपकी अपनी कुंजी के साथ कोई भी OpenAI-संगत प्रदाता। किसी भी स्थिति में कुछ भी OpenStrap से होकर नहीं गुज़रता।", + "coachNotSetUpTitle": "कोच सेट अप नहीं है", + "coachPastChats": "पिछली चैट्स", + "coachPickModelFirst": "पहले एक मॉडल चुनें या टाइप करें।", + "coachSafeWarning": "जब तक आप नीचे टैप नहीं करते, कुछ भी नहीं लिखा जाता।", + "coachSaveIt": "इसे सहेजें", + "coachSendLabel": "भेजें", + "coachSetupNavSub": "अपना खुद का मॉडल लाएँ", + "coachSetupNavTitle": "AI सेटिंग्स", + "coachSomethingWrong": "कुछ गड़बड़ हो गई: {error}", + "coachStarterAteYesterday": "मैंने कल क्या खाया था?", + "coachStarterHrvChart": "पिछले महीने की मेरी HRV का चार्ट बनाएँ", + "coachStarterLogRun": "मैंने आज सुबह 40 मिनट दौड़ लगाई — इसे दर्ज करें", + "coachStarterLogWater": "आज के लिए 500 मिली पानी दर्ज करें", + "coachStarterRecovery": "आज मैं कितना रिकवर हूँ, और क्यों?", + "coachStarterSleep": "इस हफ़्ते मेरी नींद कैसी रही?", + "coachTryAgainFix": "फिर से कोशिश करें", + "coachTryAsking": "पूछकर देखें", + "coachUntitledChat": "बिना शीर्षक वाली चैट", + "coachWhereModelRuns": "मॉडल कहाँ चलता है", + "coachYourDataYourModel": "आपका डेटा, आपका मॉडल", + "investigateNerdStatsLabel": "विस्तृत आँकड़े", + "investigateProvenanceLabel": "स्रोत विवरण", + "investigateDayLabel": "दिन", + "investigateCoverageLabel": "कवरेज", + "investigateSleepWindowLabel": "नींद की अवधि", + "investigateSourceLabel": "स्रोत", + "investigateSourceOnDevice": "बैंड रिकॉर्ड · इसी फ़ोन पर गणना", + "investigateSourceImported": "इंपोर्ट किया गया · {source}", + "investigateAlgoVersionLabel": "एल्गोरिद्म संस्करण", + "investigateWhatHappenedTitle": "उस दिन क्या हुआ", + "investigateWhatHappenedSub": "नींद, सत्र, भोजन और लॉग समय क्रम में", + "investigateWhichSensorCounted": "किस सेंसर ने गिनती की", + "investigateStrapPedometer": "बैंड · 100 Hz पेडोमीटर", + "investigateStrapOnChipCounter": "बैंड · ऑन-चिप काउंटर", + "investigatePhonePedometer": "फ़ोन · पेडोमीटर", + "investigateDayTotal": "दिन का कुल", + "investigateStrapChipReported": "बैंड चिप द्वारा दर्ज", + "investigateTimeDomain": "टाइम डोमेन", + "investigateRmssd": "RMSSD", + "investigateSdnn": "SDNN", + "investigateSdann": "SDANN", + "investigateSdnnIndex": "SDNN इंडेक्स", + "investigatePnn50": "pNN50", + "investigateLnRmssd": "ln RMSSD", + "investigateBaselineRmssd": "आपका बेसलाइन RMSSD", + "investigateStabilityCv": "स्थिरता (CV)", + "investigateFrequencyDomain": "फ़्रीक्वेंसी डोमेन", + "investigateUlfPower": "ULF पावर", + "investigateVlfPower": "VLF पावर", + "investigateLfPower": "LF पावर", + "investigateHfPower": "HF पावर", + "investigateTotalPower": "कुल पावर", + "investigateLfHf": "LF / HF", + "investigateLfNormalised": "LF, सामान्यीकृत", + "investigateHfNormalised": "HF, सामान्यीकृत", + "investigateHfGated": "HF गेटेड", + "investigateYes": "हाँ", + "investigateNo": "नहीं", + "investigateNoFrequencySpectrum": "इस रात के लिए कोई फ़्रीक्वेंसी-डोमेन स्पेक्ट्रम नहीं", + "investigateRecordingTooShort": "बैंड्स को हल करने के लिए रिकॉर्डिंग बहुत छोटी थी।", + "investigateNonLinear": "नॉन-लीनियर", + "investigateSd1Sleep": "SD1, नींद", + "investigateSd2Sleep": "SD2, नींद", + "investigateSd124h": "SD1, 24 घं.", + "investigateSd224h": "SD2, 24 घं.", + "investigateSd1Sd224h": "SD1 / SD2, 24 घं.", + "investigateSuccessiveIntervalsOver70ms": "70 ms से अधिक के लगातार अंतराल", + "investigateIrregularRhythmFlagSleep": "अनियमित लय फ़्लैग, नींद", + "investigateIrregularRhythmFlag24h": "अनियमित लय फ़्लैग, 24 घं.", + "investigateFlagRaised": "उठाया गया", + "investigateFlagClear": "साफ़", + "investigateDecelerationCapacity": "डिसेलेरेशन क्षमता", + "investigateAccelerationCapacity": "एक्सेलेरेशन क्षमता", + "investigateDcAnchors": "DC एंकर", + "investigateSignalQuality": "सिग्नल गुणवत्ता", + "investigateBeatsAnalysed": "विश्लेषित धड़कनें", + "investigateBeatsAnalysed24h": "विश्लेषित धड़कनें, 24 घं.", + "investigateNoShapeForNight": "इस रात के लिए कोई आकार नहीं", + "investigateTooFewBeatsToBin": "इस रात में बिन बनाने के लिए साफ़ धड़कनें बहुत कम थीं।", + "investigateShapeOfTheNight": "रात का आकार", + "investigateBinRmssd": "बिन RMSSD", + "investigateSamplingRange": "सैंपलिंग रेंज", + "investigateShapeFootnote": "कुल {total} बिन में से {drawn} में पढ़ने लायक पर्याप्त धड़कनें थीं; बाकी खाली स्थान हैं, शून्य नहीं। बाहरी जोड़ी अनुमानक की अपनी सैंपलिंग सीमा है, कोई ऐसी सीमा नहीं जिसमें आप रहे हों। यह रात का वर्णन करता है, कारण नहीं बताता — कम पहला तिहाई शराब, देर से भोजन, देर से ट्रेनिंग, गर्म कमरे, बीमारी की शुरुआत या बिना किसी कारण के भी हो सकता है।", + "investigateNightShape": "रात का आकार", + "investigateBinWidth": "बिन चौड़ाई", + "investigateBinsRead": "पढ़े गए बिन", + "investigateFirstThird": "पहला तिहाई", + "investigateLastThird": "आख़िरी तिहाई", + "investigateLastThirdOverFirst": "आख़िरी तिहाई ÷ पहला", + "investigate29DaysAgo": "29 दिन पहले", + "investigateToday": "आज", + "investigateDcFootnoteWithBeats": "केवल आपकी अपनी रातें — पल्स आगमन के लिए कोई संदर्भ सीमा मौजूद नहीं है। सिग्नल की गुणवत्ता हर रात यह रेखा बदल देती है, और पिछली रात {beats} धड़कनें थीं।", + "investigateDcFootnote": "केवल आपकी अपनी रातें — पल्स आगमन के लिए कोई संदर्भ सीमा मौजूद नहीं है। सिग्नल की गुणवत्ता हर रात यह रेखा बदल देती है।", + "investigateIrregularRhythmScreen": "अनियमित-लय स्क्रीन", + "investigateOneSquarePerDay": "प्रति दिन एक वर्ग", + "investigate12WeeksAgo": "12 सप्ताह पहले", + "investigateThisWeek": "इस सप्ताह", + "investigateScreenRan": "स्क्रीन चली", + "investigateRhythmStripFootnote": "{ran} {ran, plural, one{दिन} other{दिनों}} में चली, {raised} में फ़्लैग उठा। रूपरेखा वाला वर्ग वह दिन है जिस दिन यह नहीं चली। साफ़ पट्टी नकारात्मक परिणाम नहीं है: यह पल्स टाइमिंग पर आधारित स्क्रीन है, जो एक्टोपिक धड़कन को छूटी हुई धड़कन या कलाई पर बैंड के हिलने से अलग नहीं बता सकती।", + "investigateNoRestingBreathingRate": "नींद से बाहर आराम की श्वास दर उपलब्ध नहीं", + "investigateNoRestingBreathingRateBody": "यह केवल उन तीन-मिनट के हिस्सों से श्वास पढ़ता है जहाँ बैंड ने आपको नींद की अवधि से बाहर लगभग पूरी तरह स्थिर देखा। ज़्यादातर दिनों में ऐसा कोई हिस्सा नहीं मिलता — बिना डेटा वाला दिन वह दिन है जब आप हिल रहे थे, न कि जब कुछ गलत हुआ।", + "investigateBreathingAtRestAwake": "जागते हुए आराम की श्वास", + "investigateStillStretchesOutsideSleep": "नींद से बाहर स्थिर हिस्से", + "investigateLowest": "सबसे कम", + "investigateNextLowest": "अगला सबसे कम", + "investigateHighestOfThem": "इनमें सबसे अधिक", + "investigateFloorNotRateBody": "यह दिन की दर नहीं, एक निचली सीमा है। केवल वे हिस्से पढ़े जा सकते हैं जहाँ आप लगभग पूरी तरह स्थिर थे, इसलिए ये आपकी नींद के बाहर बैंड द्वारा देखे गए सबसे शांत मिनट हैं — यह आपके दिन के बाकी हिस्से का वर्णन नहीं करता, और हिलते समय की श्वास धड़कन-समय से वापस नहीं पाई जा सकती।", + "investigateCycleScreenDidNotRun": "इस रात साइकल स्क्रीन नहीं चली", + "investigateNotEnoughCleanBeats": "इसे चलाने के लिए पर्याप्त साफ़ धड़कनें नहीं हैं।", + "investigateHeartRateCycles": "हृदय-गति साइकल", + "investigateCyclesCounted": "गिने गए साइकल", + "investigateObservedHoursAnalysed": "विश्लेषित प्रेक्षित घंटे", + "investigateCyclesPerObservedHour": "प्रति प्रेक्षित घंटे साइकल", + "investigateMeanCycleLength": "औसत साइकल अवधि", + "investigateMeanDipDepth": "औसत डिप गहराई", + "investigateCycleLengthQuartiles": "साइकल अवधि, क्वार्टाइल", + "investigateDipDepthQuartiles": "डिप गहराई, क्वार्टाइल", + "investigateNotEnoughNightsAcross": "क्रॉस-नाइट व्यू के लिए पर्याप्त रातें नहीं", + "investigateNeedsSeveralNights": "इसके लिए कुछ प्रेक्षित घंटों वाली कई रातें चाहिए।", + "investigateDroppedIrregular": "अनियमित-लय स्क्रीन द्वारा फ़्लैग किए जाने के कारण {count} रातें बाहर रखी गईं", + "investigateDroppedThin": "बहुत कम प्रेक्षित घंटों के कारण {count} रातें बाहर रखी गईं", + "investigateAcrossNOwnNights": "आपकी अपनी {n} रातों में", + "investigateCvhrAboveUsual": "आपकी हाल की रातों में, इस स्क्रीन द्वारा गिनी गई हृदय-गति साइकलिंग इसके पीछे की {n} रातों के मुक़ाबले अधिक रही है।", + "investigateCvhrInsideUsual": "आपकी हाल की रातों में, इस स्क्रीन द्वारा गिनी गई हृदय-गति साइकलिंग इसके पीछे की {n} रातों की सीमा के भीतर बनी रही है।", + "investigateCvhrExplainer": "यह आपकी पल्स में एक पैटर्न है, आपकी श्वास का माप नहीं, और यह किसी चीज़ की जाँच नहीं है। यही साइकलिंग अनियमित लय, ऊँचाई पर होने, या किसी भी टूटी हुई रात से आती है — और बीटा-ब्लॉकर्स, डायबिटीज़ और नर्व की स्थितियाँ इसे कम कर देती हैं, इसलिए वास्तव में बिगड़ी हुई श्वास अक्सर यहाँ कोई निशान नहीं छोड़ती।", + "investigateCvhrNotNegativeResult": "इसलिए यहाँ कुछ भी नकारात्मक परिणाम नहीं है और यहाँ कुछ भी किसी चीज़ को साफ़ नहीं करता, और इनमें से कोई भी किसी एक रात के बारे में कुछ नहीं कहता — एक अकेली रात की गिनती अपने आप दर्जनों कारणों से बदलती है।", + "investigateCvhrSeeClinicianIfSymptoms": "यदि आप खर्राटे लेते हैं, तरोताज़ा हुए बिना जागते हैं, या किसी ने आपको नींद में साँस रुकते देखा है, तो एक डॉक्टर इसकी सही जाँच कर सकता है।", + "investigateStageMinutesAsCounted": "स्टेज मिनट, जैसे गिने गए", + "investigateLight": "हल्की नींद", + "investigateDeep": "गहरी नींद", + "investigateRem": "REM", + "investigateAwake": "जागृत", + "investigateTotalSleep": "कुल नींद", + "investigateSegmentationConfidence": "विभाजन विश्वास", + "investigateNotPublished": "प्रकाशित नहीं", + "investigateNothingComputedForKey": "इस कुंजी के लिए कुछ भी गणना नहीं हुई", + "investigateNoStoredSeries": "कोई संग्रहीत श्रृंखला नहीं", + "investigateNothingStoredYet": "{metric} के लिए अभी तक कुछ भी संग्रहीत नहीं है।", + "investigateSeries": "श्रृंखला", + "investigateDaysDerived": "गणना किए गए दिन", + "investigateLatest": "नवीनतम", + "investigateMean": "औसत", + "investigateMedian": "मध्यिका", + "investigateSd": "SD", + "investigateMin": "न्यूनतम", + "investigateMax": "अधिकतम", + "investigateUnit": "इकाई", + "investigateUnitless": "बिना इकाई", + "investigateStorage": "संग्रहण", + "investigateOneValuePerDerivedDay": "प्रति गणना-दिन एक मान", + "investigateMethodLabel": "विधि", + "investigateNotDocumented": "दस्तावेज़ीकृत नहीं है।", + "calmBreathingResonanceLabel": "रेज़ोनेंस", + "calmBreathingResonanceDescription": "लगभग {rate} साँस प्रति मिनट की दर से एक समान अंदर-बाहर साँस। कोहेरेंस स्कोर वाला एकमात्र पैटर्न।", + "calmBreathingCloseBreathing": "श्वास स्क्रीन बंद करें", + "calmBreathingFinishNow": "अभी समाप्त करें", + "calmBreathingStop": "रोकें", + "calmBreathingEndSession": "सत्र समाप्त करें", + "calmBreathingBegin": "शुरू करें", + "calmBreathingTakeABreath": "एक साँस लें।", + "calmBreathingRingLeads": "रिंग आगे बढ़ाती है। फ़ोन नीचे रख दें।", + "calmBreathingScoredPill": "स्कोर किया गया", + "calmBreathingHowLong": "कितनी देर", + "calmBreathingMinutesSemantic": "{m} मिनट", + "calmBreathingMinutesAbbrev": "{m} मिनट", + "calmBreathingYourOwnPace": "आपकी अपनी गति", + "calmBreathingWindowRowSemantic": "पहले और बाद में मापें, चार मिनट जुड़ते हैं", + "calmBreathingMeasureBeforeAfter": "पहले और बाद में मापें · 4 मिनट जुड़ते हैं", + "calmBreathingNeedsBandBeatTiming": "बैंड पहना होना ज़रूरी है — तुलना धड़कन-समय से की जाती है।", + "calmBreathingFindYourPace": "वह गति खोजें जिसे आपका दिल फ़ॉलो करता है", + "calmBreathingSweepIntro": "छह मिनट: {rates} साँस प्रति मिनट, प्रत्येक दो मिनट। कुछ भी बदलने से पहले दो सत्रों का सहमत होना ज़रूरी है।", + "calmBreathingSweepAgreed": "दो सत्र {rate} साँस प्रति मिनट पर सहमत हुए, और रेज़ोनेंस उसी गति पर सेट है। जाँचने के लिए इसे फिर से चलाएँ।", + "calmBreathingPaceOfRate": "गति {block} में से {total} · {rate} साँस प्रति मिनट", + "calmBreathingOfClock": "कुल {clock} में से", + "calmBreathingNoScoreForSession": "इस सत्र के लिए कोई कोहेरेंस स्कोर नहीं", + "calmBreathingScoringNeedsBand": "स्कोरिंग के लिए बैंड से धड़कन-समय चाहिए। कनेक्ट नहीं है, इसलिए यह सत्र गति तो दिखाता है पर सेव नहीं होता।", + "calmBreathingBeforeLabel": "पहले", + "calmBreathingAfterLabel": "बाद में", + "calmBreathingSitStill": "एक पल के लिए स्थिर बैठें।", + "calmBreathingStaySitting": "बैठे रहें।", + "calmBreathingNothingPacingScored": "जैसे आप सामान्यतः साँस लेते हैं वैसे ही लें। कुछ भी गति नहीं दिखा रहा और कुछ भी स्कोर नहीं हो रहा।", + "calmBreathingPatternNotScored": "{pattern} स्कोर नहीं होता। रेज़ोनेंस ही वह गति है जिसके लिए स्कोर बनाया गया है।", + "calmBreathingTooFewBeatTimings": "सत्र में स्कोर करने के लिए साफ़ धड़कन-समय बहुत कम हैं।", + "calmBreathingThatIsDone": "हो गया।", + "calmBreathingCardiacCoherence": "कार्डियक कोहेरेंस", + "calmBreathingHowStronglyFollowedPace": "आपकी हृदय-गति ने गति को कितनी मज़बूती से फ़ॉलो किया", + "calmBreathingStoppedThere": "वहीं रोक दिया।", + "calmBreathingHowStronglyEachPace": "आपकी हृदय-गति ने हर गति को कितनी मज़बूती से फ़ॉलो किया", + "calmBreathingBreathsAMinute": "{rate} साँस प्रति मिनट", + "calmBreathingNotReached": "पहुँचा नहीं", + "calmBreathingTooFewCleanBeats": "साफ़ धड़कनें बहुत कम", + "calmBreathingRankingExplainer": "एक ही सत्र से तीन गतियों की रैंकिंग। ब्लॉक लगातार चलते हैं, इसलिए हर गति तब मापी जाती है जब आप पिछली गति से अभी उबर ही रहे होते हैं। यह बताता है कि आपके दिल ने किस गति को सबसे मज़बूती से फ़ॉलो किया, बस इतना ही।", + "calmBreathingVerdictAborted": "आपने बीच में रोक दिया, इसलिए तुलना करने को कुछ नहीं था। कुछ नहीं बदला।", + "calmBreathingVerdictCouldNotScore": "कम से कम एक गति स्कोर नहीं हो सकी, इसलिए रैंक करने को कुछ नहीं है। कुछ नहीं बदला।", + "calmBreathingVerdictTied": "दो गतियों का स्कोर समान रहा, इसलिए यह सत्र इन्हें अलग नहीं कर सकता। कुछ नहीं बदला।", + "calmBreathingVerdictConfirmed": "परखी गई गतियों में से, {w} ने आपकी सबसे मज़बूत प्रतिक्रिया दी — और यह लगातार दूसरा सत्र है। रेज़ोनेंस उसी गति पर सेट है।", + "calmBreathingVerdictFirstWin": "परखी गई गतियों में से, {w} ने आपकी सबसे मज़बूत प्रतिक्रिया दी। अभी कुछ तय नहीं हुआ है: गति तभी बदलती है जब दो सत्र एक ही गति चुनें।", + "breathPatternBoxName": "बॉक्स", + "breathPatternBoxDesc": "हर दिशा में चार गिनती, रोक सहित। जब दिमाग़ बहुत तेज़ चल रहा हो तो स्थिर करने वाला।", + "breathPattern478Name": "4-7-8", + "breathPattern478Desc": "एक लंबी रोक और उससे भी लंबी साँस छोड़ना। आमतौर पर सोने के लिए इस्तेमाल होता है।", + "breathPatternExtendedExhaleName": "लंबी साँस छोड़ना", + "breathPatternExtendedExhaleDesc": "साँस लेने से दोगुने समय तक छोड़ना। कोई रोक नहीं, इसलिए थोड़ी देर तक बनाए रखना आसान है।", + "breathPhaseInhale": "साँस लें", + "breathPhaseHold": "रोकें", + "breathPhaseExhale": "साँस छोड़ें", + "breathPhaseWork": "कार्य", + "breathPhaseRest": "विश्राम", + "metricDetailToday": "आज", + "metricDetailRange7Days": "7 दिन", + "metricDetailRange30Days": "30 दिन", + "metricDetailRange6Months": "6 महीने", + "metricDetailRangeYear": "साल", + "metricDetailLockedNote": "{label} के लिए {needed} दिनों का इतिहास चाहिए। आपके पास {have} है।", + "metricDetailNotShownTitle": "ट्रेंड के रूप में नहीं दिखाया गया", + "metricDetailNothingRecordedToday": "आज कुछ भी दर्ज नहीं हुआ", + "metricDetailNoHistoryYet": "{metric} के लिए अभी तक कोई इतिहास नहीं", + "metricDetailNoValueYet": "आज अभी तक कोई मान नहीं मिला है।", + "metricDetailNoValueYetWiderRanges": "आज अभी तक कोई मान नहीं मिला है। ऊपर दी गई बड़ी समयसीमाओं में वे दिन शामिल हैं जिनमें मान मिला।", + "metricDetailNoValueInWindow": "इस समयावधि में किसी भी दिन कोई मान नहीं मिला।", + "metricDetailWearBandFix": "सीरीज़ शुरू करने के लिए रातभर बैंड पहनें", + "metricDetailBeatsLinkTitle": "धड़कनें", + "metricDetailBeatsLinkSub": "जिन अंतरालों से एक रात बनती है, वे यहाँ खींचे गए हैं", + "metricDetailBreakdownLinkTitle": "ब्यौरा", + "metricDetailBreakdownLinkSub": "आज का हर हिस्सा, और उसे किसने गिना", + "metricDetailNerdStatsTitle": "तकनीकी आँकड़े", + "metricDetailNerdStatsSub": "तस्वीर के पीछे के आँकड़े", + "metricDetailDailyAverage": "दैनिक औसत · {win} में से {count} दिन", + "metricDetailLatestReading": "नवीनतम {value} {unit} · {asOf}", + "metricDetailAlgoBreakFootnote": "{n, plural, one{बिंदीदार रेखा यह दिखाती है कि इन दिनों की गणना किस तरह बदली। इसके दोनों ओर के आँकड़े अलग-अलग वर्ज़न से हैं।} other{बिंदीदार रेखाएँ यह दिखाती हैं कि इन दिनों की गणना किस तरह बदली। हर रेखा के दोनों ओर के आँकड़े अलग-अलग वर्ज़न से हैं।}}", + "metricDetailDaysAgoLabel": "{n, plural, one{{n} दिन पहले} other{{n} दिन पहले}}", + "metricDetailWornChartTitle": "पहना हुआ", + "metricDetailHoursADayUnit": "घंटे/दिन", + "metricDetailWearFootnote": "इन {win} दिनों में से {have} दिन का पहनने का रिकॉर्ड है। बाकी दोनों चार्ट में खाली जगह हैं — ऊपर की रेखा उनके आर-पार नहीं खींची जाती।", + "metricDetailSlotNoRecord": "{day}, कोई रिकॉर्ड नहीं", + "metricDetailSlotWithValue": "{day}, {value} {unit}", + "metricDetailOpenDay": "{day} खोलें", + "metricDetailNoRecordLabel": "कोई रिकॉर्ड नहीं", + "metricDetailLowest": "न्यूनतम", + "metricDetailTypical": "सामान्य", + "metricDetailHighest": "अधिकतम", + "metricDetailFromDaysCount": "आपके अपने {n} दिनों से।", + "metricDetailPercentileTodayNoBand": "आज आपके अपने इतिहास के {ordinal} पर्सेंटाइल पर है।", + "metricDetailPercentileTodayBand": "आज आपके अपने इतिहास के {ordinal} पर्सेंटाइल पर है — {band}।", + "metricDetailPercentileFromNoBand": "{date} की आपकी रीडिंग आपके अपने इतिहास के {ordinal} पर्सेंटाइल पर है।", + "metricDetailPercentileFromBand": "{date} की आपकी रीडिंग आपके अपने इतिहास के {ordinal} पर्सेंटाइल पर है — {band}।", + "metricDetailDaysWithWithout": "{withCount} दिन साथ · {withoutCount} दिन बिना", + "metricDetailPatternsNotCauses": "आपके अपने लॉग में पैटर्न हैं, कारण नहीं।", + "metricDetailChooseDayHelp": "दिन चुनें", + "metricDetailPreviousDay": "पिछला दिन", + "metricDetailNextDay": "अगला दिन", + "metricDetailChooseDayShowing": "दिन चुनें। {day} दिखाया जा रहा है", + "metricDetailNormalRangeSection": "आपकी सामान्य सीमा", + "metricDetailWhatMovesItSection": "इसे क्या प्रभावित करता है", + "cycleRemoveLogTitle": "{date} हटाएँ?", + "cycleRemoveLogBody": "साइकल का दिन, चरण और अगली अनुमानित तारीख — सभी उन दिनों से गिने जाते हैं जो आपने दर्ज किए हैं। केवल आज का दिन दर्ज किया जा सकता है, इसलिए इसे वापस नहीं लाया जा सकेगा।", + "cycleWhatAppliesToYou": "आप पर क्या लागू होता है", + "cyclePreferNotToSay": "बताना नहीं चाहती", + "cyclePreferNotToSayWhy": "ऐप चरण को बंद रखता है।", + "cycleReproCyclingLabel": "मेरा प्राकृतिक साइकल चलता है", + "cycleReproCyclingWhy": "आपकी दर्ज शुरुआतों से एक चरण गिनता है।", + "cycleReproContraceptionLabel": "हार्मोनल गर्भनिरोधक", + "cycleReproContraceptionWhy": "गिनने के लिए कोई ओव्यूलेशन नहीं है, इसलिए कोई चरण नहीं। रक्तस्राव अभी भी दर्ज किया जाता है।", + "cycleReproNoneLabel": "गर्भवती, प्रसव के बाद, या साइकल न चलना", + "cycleReproNoneWhy": "कोई चरण नहीं और कोई अनुमानित अगली तारीख नहीं। आपके बायोमेट्रिक आँकड़े फिर भी दिखते रहेंगे।", + "cycleReproNotSet": "सेट नहीं है", + "cycleTrackingOffTitle": "साइकल ट्रैकिंग बंद है", + "cycleTrackingOffBody": "यह इसी फ़ोन पर रहती है।", + "cycleTurnOnTracking": "साइकल ट्रैकिंग चालू करें", + "cycleNoPeriodTitle": "अभी तक कोई पीरियड दर्ज नहीं हुआ", + "cycleNoPeriodBody": "आपके दर्ज किए दिनों से गिना जाता है।", + "cycleLogPeriodButton": "आज पीरियड की शुरुआत दर्ज करें", + "cycleLogKindStart": "शुरुआत", + "cycleLogKindEnd": "अंत", + "cycleAcrossCyclesTitle": "आपके साइकल के दौरान", + "cycleUnitCompleteCycle": "पूरा साइकल", + "cycleUnitCompleteCycles": "पूरे साइकल", + "cycleOpenAction": "खोलें", + "cycleWhatYouNoticedToday": "आपने आज क्या महसूस किया", + "cycleLoggedDays": "दर्ज किए गए दिन", + "cycleReproOptionalHint": "वैकल्पिक। जब तक आप नहीं बतातीं, ऐप चरण को बंद रखता है।", + "cycleReproPrivateHint": "केवल आप और यह फ़ोन। कभी एक्सपोर्ट नहीं होता।", + "cycleTurnOffTracking": "साइकल ट्रैकिंग बंद करें", + "cycleDayInThisCycle": "इस साइकल का दिन", + "cycleCountedFromLastStart": "आपकी आख़िरी दर्ज शुरुआत से गिना गया", + "cycleOfAboutDays": "लगभग {days} का", + "cyclePhaseMenstrual": "मासिक (मेंस्ट्रुअल)", + "cyclePhaseFollicular": "फॉलिक्युलर", + "cyclePhaseOvulation": "ओव्यूलेशन विंडो", + "cyclePhaseLuteal": "ल्यूटियल", + "cycleNextPeriodBetween": "अगला पीरियड, अनुमानित इस बीच", + "cycleNextPeriodAround": "अगला पीरियड, अनुमानित लगभग", + "cycleFromOneMeasuredGap": "आपके एकमात्र मापे गए अंतराल के आधार पर, जो यह नहीं दिखा सकता कि आपका साइकल कितना बदलता है", + "cyclePastEndOfIt": "इसके अंत के {days} दिन बाद · ", + "cycleInsideItNow": "आप अभी इसके भीतर हैं · ", + "cycleInDaysRange": "{lo}–{hi} दिनों में · ", + "cycleHalfOfMeasuredGaps": "आपके {n} मापे गए अंतरालों में से आधे इतनी ही चौड़ी सीमा के भीतर आए", + "cycleLeadDaysLate": "{days} दिन देर से · ", + "cycleLeadToday": "आज · ", + "cycleLeadInDays": "{days} दिनों में · ", + "cycleWhatYouUsuallyNotice": "आप आमतौर पर क्या महसूस करती हैं", + "cycleSymptomShapeSummary": "चार आँकड़े, साइकल के हर हफ़्ते के लिए एक, आपकी अपनी दर्ज शुरुआतों से गिने गए। {cycles} साइकल में आपने हर हफ़्ते {daysByWeek} दिन कुछ न कुछ दर्ज किया — यही एकमात्र दिन हैं जो यहाँ गिने जाते हैं।", + "cycleRemoveLoggedDay": "{date} हटाएँ", + "cycleSymptomCramps": "ऐंठन", + "cycleSymptomHeadache": "सिरदर्द", + "cycleSymptomBloating": "पेट फूलना", + "cycleSymptomFatigue": "थकान", + "cycleSymptomLowMood": "मूड कम होना", + "cycleSymptomAcne": "मुहांसे", + "cycleSymptomTenderBreasts": "स्तनों में संवेदनशीलता", + "cycleSymptomNausea": "जी मिचलाना", + "cycleThisCycle": "यह साइकल", + "cycleByDayOfYourCycle": "आपके साइकल के दिन के अनुसार", + "cycleHowLongCyclesBeen": "आपके साइकल कितने लंबे रहे हैं", + "cycleRestingHeartRate": "आराम की हृदय गति", + "cycleUnitBpm": "बीपीएम", + "cycleHrvRmssdTitle": "एचआरवी (आरएमएसएसडी)", + "cycleUnitMs": "एमएस", + "cycleNotEnoughDescribeDayTitle": "साइकल के किसी दिन का वर्णन करने के लिए अभी पर्याप्त साइकल नहीं हैं", + "cycleNotEnoughDescribeDayBody": "यहाँ हर बिंदु आपके अपने दो या अधिक साइकल में उसी दिन का मध्य मान है। अभी किसी के पीछे दो साइकल नहीं हैं।", + "cycleOwnPastCyclesDescribed": "आपके अपने बीते साइकल का वर्णन। जिन दिनों तक केवल एक साइकल पहुँचा, उन्हें खींचने के बजाय खाली छोड़ दिया जाता है — एक रात मध्य मान नहीं होती। यह बताता है कि क्या हुआ, यह नहीं कि क्या होगा।", + "cycleDayOneLabel": "दिन 1", + "cycleDayNLabel": "दिन {n}", + "cycleMiddleOfNCycles": "हर दिन पर {n} साइकल का मध्य मान।", + "cycleMiddleOfRangeCycles": "हर दिन पर {lo} से {hi} साइकल के बीच का मध्य मान।", + "cycleNotEnoughCompareTitle": "किसी दिन की खुद से तुलना करने के लिए अभी पर्याप्त साइकल नहीं हैं", + "cycleCompareBodyGeneric": "यह आज को आपके पिछले साइकल के उसी दिन के साथ रखता है। इसके लिए तीन ऐसे साइकल चाहिए जो इतनी दूर तक पहुँचे हों।", + "cycleCompareBodyWithDay": "यह आज को आपके पिछले साइकल के उसी दिन के साथ रखता है। इसके लिए तीन ऐसे साइकल चाहिए जो दिन {day} तक पहुँचे हों।", + "cycleNightOfLabel": "{date} की रात", + "cycleComparisonNotCorrection": "यह एक तुलना है, कोई सुधार नहीं। आपकी रेडीनेस में इससे कुछ भी दोबारा मापा नहीं गया है, और यहाँ कोई भी प्रशिक्षण निर्देश नहीं है।", + "cycleCompareHrvLabel": "एचआरवी", + "cycleCompareLine": "{label} आपके पिछले 3 हफ़्तों की तुलना में {z1}, और आपके पिछले {n} बार के दिन {cycleDay} की तुलना में {z2}।", + "cycleLengthsTitle": "आपके साइकल की लंबाई एक प्रकाशित सीमा के मुक़ाबले", + "cycleLengthsBody": "जब तक आप न माँगें, यह बंद रहता है। यह आपकी अपनी दर्ज शुरुआतों के बीच के दिनों को एक वयस्क साइकल के लिए प्रकाशित सीमा के साथ दिखाता है, और इसके अलावा कुछ नहीं कहता।", + "cycleShowIt": "दिखाएँ", + "cycleNotEnoughLoggedTitle": "अभी पर्याप्त साइकल दर्ज नहीं हुए", + "cycleNotEnoughLoggedBody": "इसके लिए एक लंबा रिकॉर्ड चाहिए: अब तक {total} में से {n} अंतराल, जो हर शुरुआत को लगभग एक साल तक दर्ज करने के बराबर है।", + "cycleGapTitle": "आपकी दर्ज शुरुआतों में एक गैप है", + "cycleGapBody": "उनमें से एक पिछले वाले के {days} दिन से भी अधिक बाद है। जो शुरुआत आपने कभी दर्ज नहीं की और जो साइकल वाक़ई इतना लंबा चला, वे यहाँ से एक जैसे दिखते हैं, इसलिए कुछ भी नहीं खींचा गया है।", + "cycleDaysBetweenStarts": "आपकी दर्ज शुरुआतों के बीच के दिन", + "cycleUnitDays": "दिन", + "cycleLegendYourCycles": "आपके साइकल", + "cycleLegendPublishedRange": "प्रकाशित सीमा", + "cycleTwoLinesFootnote": "दोनों रेखाएँ {low} और {high} दिनों पर हैं।", + "cycleLengthChangesReasons": "साइकल की लंबाई कई कारणों से बदलती है — थायरॉइड, तनाव, वज़न में बदलाव, गर्भनिरोधक, पीसीओएस और अन्य। यह आपका अपना दर्ज डेटा है जो एक प्रकाशित सीमा के साथ रखा गया है। यह डॉक्टर से पूछने की एक वजह है, डॉक्टर की तरफ़ से कोई जवाब नहीं।", + "cycleHideLengths": "साइकल की लंबाई छुपाएँ", + "cycleDescriptiveOnly": "केवल वर्णनात्मक।", + "cycleNotEnoughDerivedNights": "इस साइकल में अभी पर्याप्त व्युत्पन्न रातें नहीं हैं", + "cycleMdcNoteInsideSpread": " यहाँ दिखाया गया हर दिन आपकी अपनी रात-दर-रात भिन्नता के भीतर है: उनमें से किन्हीं दो के बीच सबसे बड़ा अंतर {s} है, और {n} वह सबसे छोटा बदलाव है जिसे यह शोर से अलग बता सकता है। एक आकार है, कोई बदलाव नहीं।", + "cycleMdcNoteVaries": " आपकी रातें अपने आप में {n} तक बदलती हैं, इसलिए इससे नज़दीक के दिन अलग नहीं पहचाने जाते। यहाँ सबसे बड़ा अंतर {s} है।", + "healthTabOverview": "अवलोकन", + "healthTabExplore": "अन्वेषण", + "healthTabTrends": "रुझान", + "healthTabVitals": "जीवन संकेत", + "healthTabLabs": "लैब", + "healthTitle": "स्वास्थ्य", + "healthCouldNotRead": "आपका {what} पढ़ा नहीं जा सका", + "healthReadFailedBody": "सहेजी गई पंक्तियाँ लोड नहीं हो सकीं। कुछ भी हटाया नहीं गया — यह सिर्फ़ एक पढ़ने की गड़बड़ी है।", + "healthTryAgain": "फिर से कोशिश करें", + "healthWhatVitals": "जीवन संकेत", + "healthWhatLabResults": "लैब परिणाम", + "healthMeasuresUnit": "माप", + "healthRowRestingHr": "आराम के समय हृदय गति", + "healthRowHrv": "एचआरवी", + "healthRowSleep": "नींद", + "healthRowStress": "तनाव", + "healthRowRespRate": "श्वसन दर", + "healthSubOvernight": "रात भर", + "healthSubRmssdAsleep": "RMSSD, नींद में", + "healthSubLastNight": "पिछली रात", + "healthSubAsleep": "नींद में", + "healthNoMetric": "कोई {name} नहीं", + "healthWhyReadFromSleep": "यह नींद से मापा जाता है, और कोई रात स्कोर नहीं हुई।", + "healthWhyReadOnlyFromSleep": "यह केवल नींद से मापा जाता है, और कोई रात स्कोर नहीं हुई।", + "healthWhySleepNotLongEnough": "स्कोर करने लायक़ पर्याप्त लंबी नींद दर्ज नहीं हुई।", + "healthWhyReadFromNight": "यह रात से मापा जाता है, और कोई रात स्कोर नहीं हुई।", + "healthWhyNoReadingLastNight": "पिछली रात की कोई रीडिंग नहीं।", + "healthIllnessRedTitle": "लगातार कई रातें आपके सामान्य स्तर से अलग रहीं", + "healthIllnessLastNightTitle": "पिछली रात आपकी सामान्य सीमा से बाहर रही", + "healthIllnessDayTitle": "{day} आपकी सामान्य सीमा से बाहर रहा", + "healthIllnessBodyNoZ": "आपकी रात की आराम-हृदय गति आपकी अपनी बेसलाइन से ऊपर बनी हुई है। यह केवल एक संकेत की निगरानी करता है। यह एक पैटर्न बताता है, कोई कारण नहीं।", + "healthIllnessBodyWithZ": "आपकी रात की आराम-हृदय गति आपकी अपनी बेसलाइन से ऊपर बनी हुई है; उस रात यह बेसलाइन से {z} मानक विचलन {direction} थी। यह केवल एक संकेत की निगरानी करता है। यह एक पैटर्न बताता है, कोई कारण नहीं।", + "healthDirectionAbove": "ऊपर", + "healthDirectionBelow": "नीचे", + "healthIllnessAdvice": "अगर यह कुछ दिनों से ज़्यादा जारी रहे, तो ध्यान देने लायक़ है।", + "healthObservationsTitle": "अवलोकन", + "healthSeeAll": "सभी देखें", + "healthNapsTitle": "झपकी", + "healthNoNapReading": "झपकी की कोई रीडिंग नहीं", + "healthNoNapReadingFor": "{day} के लिए झपकी की कोई रीडिंग नहीं", + "healthNapsBody": "झपकियाँ दिन की बाक़ी उसी सेकंड-दर-सेकंड रिकॉर्डिंग से मिलती हैं, और इस दिन के लिए पर्याप्त रिकॉर्डिंग नहीं है।", + "healthDaytimeSleep": "दिन की नींद", + "healthValueNone": "कोई नहीं", + "healthNoneDetectedOn": "कोई नहीं मिली · {day}", + "healthNapCountLabel": "{n, plural, one{{n} झपकी} other{{n} झपकियाँ}}", + "healthAddOrCorrect": "जोड़ें या सुधारें", + "healthNoTrendYet": "{label} का अभी कोई रुझान नहीं", + "healthZeroDaysStored": "0 दिन सहेजे गए।", + "healthVsDayAverage": "आपके {days}-दिन औसत की तुलना में", + "healthAsOf": " · {date} तक", + "healthNoBaseline": "कोई बेसलाइन नहीं", + "healthFirstReadings": "पहली रीडिंग", + "healthTimeAsleep": "सोने का समय", + "healthVsNeed": "आपकी {need} की ज़रूरत की तुलना में", + "healthBodyClockTitle": "शारीरिक घड़ी", + "healthChronotypeJetlagRegularity": "क्रोनोटाइप, जेट लैग और नियमितता", + "healthChronotypeLabel": "क्रोनोटाइप", + "healthSocialJetlagLabel": "सामाजिक जेट लैग", + "healthRegularityLabel": "नियमितता", + "healthConsistencyTitle": "निरंतरता", + "healthDaysWithRecord": "पिछले 30 दिनों में जितने दिनों का रिकॉर्ड निकाला गया", + "healthToday": "आज", + "healthRowHeartRate": "हृदय गति", + "healthRowSkinTemp": "त्वचा तापमान", + "healthVsOwnNights": "आपकी अपनी रातों की तुलना में", + "healthVsOwnNightsOn": "आपकी अपनी रातों की तुलना में · {day}", + "healthRowWearTime": "पहनने का समय", + "healthTheDay": "पूरे दिन", + "healthCoverageOf": "{day} का {pct}%", + "healthNothingMeasuredDay": "इस दिन कुछ भी मापा नहीं गया", + "healthNoBandRecordings": "इस दिन तक बैंड की कोई रिकॉर्डिंग नहीं पहुँची।", + "healthSyncTheBand": "बैंड सिंक करें", + "healthDeepDivesTitle": "गहन विश्लेषण", + "healthHeartRateVariability": "हृदय गति परिवर्तनशीलता", + "healthTimeFrequencyNonLinear": "समय, आवृत्ति और नॉन-लीनियर", + "healthRmssdOfLastNights": "RMSSD, पिछली {days} रातों में से {have}", + "healthNightsAgo": "{n} रात पहले", + "healthOneNightNotTrend": "एक रात अभी रुझान नहीं बनाती", + "healthMeasuresWithHistory": "इस डिवाइस पर सहेजा गया इतिहास रखने वाली माप", + "healthEachOneOpens": "हर एक खुलने पर उसका चार्ट, आपकी अपनी सीमा, और यह कैसे निकाला जाता है — दिखाता है।", + "healthCatHeartRhythm": "हृदय और लय", + "healthCatBreathing": "श्वसन", + "healthCatMovementLoad": "गतिविधि और भार", + "healthCatBodyWear": "शरीर और पहनावा", + "healthBlurbRestingHr": "रात की सबसे कम स्थिर दर", + "healthBlurbHrv": "नींद के सबसे स्वच्छ समय-खंड पर RMSSD", + "healthBlurbHrvCv": "यह रात-दर-रात कितना बदलता है", + "healthBlurbLfHf": "धड़कन-समय की ऊर्जा विभिन्न आवृत्तियों में कहाँ है", + "healthBlurbDip": "सोते समय आपकी हृदय गति कितनी गिरती है", + "healthBlurbHrr": "किसी कसरत के बाद एक मिनट में यह कितनी तेज़ी से गिरती है", + "healthBlurbSleep": "गति और धड़कन-समय से निकाला गया सोने का समय", + "healthBlurbEfficiency": "बिस्तर में बिताए समय में सोने का हिस्सा", + "healthBlurbDeep": "NREM नींद के भीतर हृदय गति की स्थिरता", + "healthBlurbRem": "धड़कन-परिवर्तनशीलता और गति से निर्धारित", + "healthBlurbNapMin": "मुख्य रात के अलावा मिली नींद", + "healthBlurbRespRate": "प्रति मिनट साँसें, धड़कन-समय से निकाली गई", + "healthBlurbBrv": "यह दर पूरी रात में कितनी बदलती है", + "healthBlurbSteps": "पेडोमीटर से गिने गए, कभी अनुमानित नहीं", + "healthBlurbActiveMin": "गति की मात्रा के मिनट, चलना-फिरना नहीं", + "healthBlurbCalories": "हृदय गति और आपकी प्रोफ़ाइल से निकाली गई सक्रिय ऊर्जा", + "healthBlurbStrain": "पूरे दिन का हृदय-संवहनी भार, 0–21 के पैमाने पर", + "healthBlurbTrimp": "हर ज़ोन में बिताया गया समय, उसकी लागत के अनुसार भारित", + "healthBlurbSkinTemp": "आपकी हाल की अपनी रातों से अंतर", + "healthBlurbWear": "जितने मिनट बैंड की रिकॉर्डिंग मौजूद रही", + "healthNothingMeasuredHere": "यहाँ अभी तक कुछ मापा नहीं गया", + "healthNotMeasuredYet": "अभी मापा नहीं गया", + "healthNoDayProduced": "इस डिवाइस पर अब तक किसी दिन में यह नहीं मिला।", + "healthNoLabResults": "कोई लैब परिणाम नहीं", + "healthNoLabResultsBody": "अभी कुछ भी दर्ज नहीं है। यहाँ जो कुछ भी आप जोड़ते हैं, वह इस डिवाइस पर ही रहता है, और जो हटाते हैं, वह इससे मिट जाता है।", + "healthLastPanel": "पिछला पैनल {date} · हाथ से दर्ज किया गया", + "healthMarkersYouNamed": "आपके द्वारा नामित मार्कर", + "healthAddAResult": "एक परिणाम जोड़ें", + "healthRangesDifferByLab": "सीमाएँ हर लैब में अलग होती हैं। अपनी रिपोर्ट वाली सीमा का इस्तेमाल करें।", + "healthRemoveMarkerFrom": "{date} से {marker} हटाएँ", + "healthNoReferenceInterval": "कोई संदर्भ सीमा नहीं · {date}", + "healthTypicalRange": "सामान्य {low}–{high} · {date}", + "healthRemoveLabelFrom": "{date} से {label} हटाएँ?", + "healthRemoveLabBody": "उस ड्रॉ के लिए आपने दर्ज किए {value} {unit}। यह इस डिवाइस से हट जाएगा और इसे वापस नहीं लाया जा सकता।", + "healthRemoveLabOlderNote": " आपका {date} का ड्रॉ बना रहेगा, और इसकी जगह यहाँ दिखेगा।", + "healthRemovedNoneLeft": "{date} से {label} हटा दिया गया। अब कोई {label} परिणाम शेष नहीं है।", + "healthRemovedShowingOlder": "{date} से {label} हटा दिया गया। अब आपका {older} का ड्रॉ दिखाया जा रहा है।", + "healthRemoveTheMarker": "{label} मार्कर हटाएँ", + "healthNothingLoggedUnderIt": "इसके तहत कुछ भी दर्ज नहीं", + "healthResultsCount": "{n, plural, one{{n} परिणाम · {unit}} other{{n} परिणाम · {unit}}}", + "healthStillHoldsResults": "{count, plural, one{{label} में अभी भी {count} परिणाम मौजूद है। पहले उसे हटाएँ — मार्कर ही उसे नाम देता है।} other{{label} में अभी भी {count} परिणाम मौजूद हैं। पहले उन्हें हटाएँ — मार्कर ही उन्हें नाम देता है।}}", + "healthRemoveMarkerQ": "{label} हटाएँ?", + "healthRemoveMarkerBody": "यह मार्कर सूची से हट जाएगा, इसलिए अब आप इसे दर्ज नहीं कर पाएँगे। इसके साथ कोई मापी गई चीज़ नहीं जाती — इसके तहत आपके कोई परिणाम नहीं हैं।", + "healthMarkerLabel": "मार्कर", + "healthValueUnit": "मान ({unit})", + "healthDateDrawn": "लिए जाने की तारीख़ (YYYY-MM-DD)", + "healthValueMustBeNumber": "मान केवल एक संख्या होना चाहिए, इकाई के बिना। कुछ भी सहेजा नहीं गया।", + "healthDateFormatError": "तारीख़ YYYY-MM-DD प्रारूप में होनी चाहिए। कुछ भी सहेजा नहीं गया।", + "healthCouldNotSaveIt": "सहेजा नहीं जा सका: {error}", + "homeStepSensorStrapPhone": "स्ट्रैप + फ़ोन", + "homeStepSensorStrap": "स्ट्रैप", + "homeStepSensorPhone": "फ़ोन", + "homeOvernightBuilding": "पिछली रात का डेटा अभी भी संसाधित हो रहा है।", + "homeOvernightNothingYet": "पिछली रात का कोई डेटा अब तक ऐप तक नहीं पहुँचा है।", + "homeMonthJanuary": "जनवरी", + "homeMonthFebruary": "फ़रवरी", + "homeMonthMarch": "मार्च", + "homeMonthApril": "अप्रैल", + "homeMonthMay": "मई", + "homeMonthJune": "जून", + "homeMonthJuly": "जुलाई", + "homeMonthAugust": "अगस्त", + "homeMonthSeptember": "सितंबर", + "homeMonthOctober": "अक्टूबर", + "homeMonthNovember": "नवंबर", + "homeMonthDecember": "दिसंबर", + "homeMonthJanuaryShort": "जन", + "homeMonthFebruaryShort": "फ़र", + "homeMonthMarchShort": "मार्च", + "homeMonthAprilShort": "अप्रैल", + "homeMonthMayShort": "मई", + "homeMonthJuneShort": "जून", + "homeMonthJulyShort": "जुल", + "homeMonthAugustShort": "अग", + "homeMonthSeptemberShort": "सित", + "homeMonthOctoberShort": "अक्तू", + "homeMonthNovemberShort": "नव", + "homeMonthDecemberShort": "दिस", + "homeWeekdayMonday": "सोमवार", + "homeWeekdayTuesday": "मंगलवार", + "homeWeekdayWednesday": "बुधवार", + "homeWeekdayThursday": "गुरुवार", + "homeWeekdayFriday": "शुक्रवार", + "homeWeekdaySaturday": "शनिवार", + "homeWeekdaySunday": "रविवार", + "homeReadinessNotScored": "स्कोर नहीं हुआ", + "homeReadinessGoodToGo": "बिल्कुल तैयार", + "homeReadinessSteady": "स्थिर", + "homeReadinessTakeItEasy": "आराम से लें", + "homeReadinessRestToday": "आज आराम करें", + "homeDriverHrv": "एचआरवी", + "homeDriverRhr": "आराम की हृदय गति", + "homeDriverResp": "श्वसन दर", + "homeDriverTemp": "त्वचा का तापमान", + "homeDbRebuiltTitle": "ऐप शुरू करने के लिए आपका डेटाबेस फिर से बनाया गया", + "homeDbRebuiltNothingRecovered": "कुछ भी वापस नहीं पढ़ा जा सका।", + "homeDbRebuiltRecovered": "पुनर्प्राप्त: {list}।", + "homeDbRebuiltEmpty": "खाली: {list}।", + "homeDbRebuiltKept": "मूल फ़ाइल {path} पर सुरक्षित रखी गई है — कुछ भी हटाया नहीं गया।", + "homeWorkoutHoldTitle": "एक वर्कआउट अभी भी चल रहा है", + "homeWorkoutHoldBody": "वर्कआउट चलते समय आज का दिन रुका हुआ है: स्ट्रैप रिकॉर्ड करना जारी रखता है, लेकिन आंकड़े सेशन खत्म होने पर ही गणना होते हैं। नीचे दिए गए बार से वर्कआउट पूरा करें, तभी आज का डेटा भरेगा — केवल सिंक करने से नहीं होगा।", + "homeInsightsRebuildingTitle": "आपकी क्रॉस-डे इनसाइट्स फिर से बनाई जा रही हैं", + "homeInsightsRebuildingAlgoVersion": "इसकी गणना का तरीका पिछले अपडेट के साथ बदल गया है।", + "homeInsightsStaleOverWeek": "आखिरी रोलअप एक हफ़्ते से भी पहले बना था, जो भरोसा करने के लिए बहुत पुराना है।", + "homeInsightsStaleOnDay": "आखिरी रोलअप {day} को बना था, जो भरोसा करने के लिए बहुत पुराना है।", + "homeInsightsNoVersionStamp": "सहेजे गए रोलअप पर कोई वर्शन स्टैंप नहीं है।", + "homeSyncBand": "स्ट्रैप सिंक करें", + "homeWhyLabel": "क्यों?", + "homeCalibrating": "कैलिब्रेट हो रहा है", + "homeCalibratingNights": "{need} में से {have} रातें", + "homeCalibratingDays": "{need} में से {have} दिन", + "homeGapNoReason": "यह क्यों गायब है, इसकी कोई वजह दर्ज नहीं है।", + "homeRingRecovery": "रिकवरी", + "homeRingStrain": "स्ट्रेन", + "homeRingSleep": "नींद", + "homeRingNoStrain": "कोई स्ट्रेन नहीं", + "homeRingNoSleep": "कोई नींद नहीं", + "homeSleepGapFallback": "स्कोर करने लायक लंबी रात दर्ज नहीं हुई।", + "homeStrainOf21": "21 में से", + "homeSleepNoTarget": "अभी कोई लक्ष्य नहीं", + "homeOfSpan": "{duration} में से", + "homeLoadFailedTitle": "आज का डेटा पढ़ा नहीं जा सका", + "homeLoadFailedBody": "सहेजा गया दिन लोड नहीं हो सका। कुछ भी हटाया नहीं गया — यह एक रीड एरर है, डेटा खोना नहीं।", + "homeTryAgain": "फिर से कोशिश करें", + "homeNothingDerivedTitle": "अभी तक कुछ भी नहीं निकाला गया", + "homeNothingDerivedBody": "अभी तक स्ट्रैप की कोई रिकॉर्डिंग प्रोसेस नहीं हुई है।", + "homeAskCoach": "कोच से पूछें", + "homeProfileSettings": "प्रोफ़ाइल और सेटिंग्स", + "homeNothingTodayTitle": "आज के लिए कुछ भी दर्ज नहीं", + "homeNothingTodayBody": "इस ऐप ने जिस आखिरी रात को स्कोर किया वह {day} थी। तब से कुछ भी नहीं पहुँचा है।", + "homeReadinessNotScoredTitle": "आज रिकवरी का स्कोर नहीं है", + "homeReadinessNeedBody": "आपके लिए सामान्य क्या है, यह जानने के लिए {need}।", + "homeReadinessNoReason": "इसे समझाने के लिए कुछ भी दर्ज नहीं है।", + "homeSeeWhatWasMissing": "देखें क्या कमी थी", + "homeAtAGlance": "एक नज़र में", + "homeTodaysPlan": "आज की योजना", + "homeBreakdownTitle": "आपके दिन का विवरण", + "homeBreakdownSubtitle": "घंटे-दर-घंटे", + "homeIllnessRedTitle": "लगातार कई रातें आपके सामान्य स्तर से हटकर हैं", + "homeIllnessAmberSameNight": "पिछली रात आपकी सामान्य सीमा से बाहर रही", + "homeIllnessAmberOtherNight": "{day} आपकी सामान्य सीमा से बाहर रहा", + "homeIllnessBodyNoZ": "आपकी रात की आराम-हृदय गति आपके अपने आधार-स्तर से ऊपर बनी रही है। यह केवल एक संकेत दर्शाता है। यह एक पैटर्न बताता है, कोई कारण नहीं।", + "homeIllnessBodyAbove": "आपकी रात की आराम-हृदय गति आपके अपने आधार-स्तर से ऊपर बनी रही है; उस रात यह आधार-स्तर से {z} मानक विचलन ऊपर रही। यह केवल एक संकेत दर्शाता है। यह एक पैटर्न बताता है, कोई कारण नहीं।", + "homeIllnessBodyBelow": "आपकी रात की आराम-हृदय गति आपके अपने आधार-स्तर से ऊपर बनी रही है; उस रात यह आधार-स्तर से {z} मानक विचलन नीचे रही। यह केवल एक संकेत दर्शाता है। यह एक पैटर्न बताता है, कोई कारण नहीं।", + "homeIllnessAdvice": "अगर यह कुछ दिन और जारी रहे तो ध्यान देने लायक है।", + "homeHeartRate": "हृदय गति", + "homeRestingSub": "आराम", + "homeNoRestingHr": "आराम की हृदय गति नहीं मिली", + "homeNoRestingHrWhy": "आराम की हृदय गति नींद से पढ़ी जाती है, और कोई नींद दर्ज नहीं हुई।", + "homeSteps": "कदम", + "homeStepsNone": "कोई नहीं", + "homeStepsNotRecorded": "दर्ज नहीं हुआ", + "homeStepsPercentGoal": "लक्ष्य का {pct}%", + "homeActiveEnergy": "सक्रिय ऊर्जा", + "homeCaloriesEstimated": "अनुमानित", + "homeCaloriesTotal": "कुल {total}", + "homeNoEnergyEstimate": "कोई ऊर्जा अनुमान नहीं", + "homeStepsLeft": "{left} कदम बाकी", + "homeMovement": "मूवमेंट", + "homeGoalSteps": "लक्ष्य {goal}", + "homeStepGoalMet": "कदम लक्ष्य पूरा हुआ", + "homeStrainTargetMet": "स्ट्रेन लक्ष्य पूरा हुआ", + "homeAimForStrain": "{aim} स्ट्रेन का लक्ष्य रखें", + "homeTraining": "ट्रेनिंग", + "homeSleepNeedRow": "{duration} नींद", + "homeTonight": "आज रात", + "homeNeed": "अभी तय नहीं", + "homeBedTime": "{time} बजे सोएं", + "homeNoPlanTitle": "आज के लिए अभी कोई योजना नहीं", + "homeNoPlanWhyStale": "जिस क्रॉस-डे रोलअप से यह आती है वह फिर से बनाया जा रहा है।", + "homeNoPlanWhyNone": "अभी तक कोई स्थापित नहीं हुआ है।", + "homeGreetingStillUp": "अभी तक जाग रहे हैं", + "homeGreetingMorning": "सुप्रभात", + "homeGreetingAfternoon": "नमस्कार", + "homeGreetingEvening": "शुभ संध्या", + "wellnessTitle": "वेलनेस", + "wellnessTabMind": "मन", + "wellnessTabRecovery": "रिकवरी", + "wellnessTabHabits": "आदतें", + "wellnessTabMedication": "दवाई", + "wellnessTabCycle": "साइकल", + "wellnessStartASitting": "एक सेशन शुरू करें", + "wellnessExercisesNoun": "अभ्यास", + "wellnessPickOneAndGo": "एक चुनें और शुरू करें", + "wellnessLastMinutes": "पिछला: {count} मिनट", + "wellnessWriteTheDayDown": "आज का दिन लिखें", + "wellnessOpen": "खोलें", + "wellnessStressLastNight": "पिछली रात का तनाव", + "wellnessNoStressTitle": "पिछली रात के लिए कोई तनाव रीडिंग नहीं", + "wellnessNoStressBody": "तनाव आपके रात में आराम करते समय हृदय गति के पैटर्न से मापा जाता है, और पिछली रात कोई रीडिंग नहीं मिली।", + "wellnessAutonomicTension": "ऑटोनॉमिक तनाव", + "wellnessStressLevelLow": "कम", + "wellnessStressLevelNormal": "सामान्य", + "wellnessStressLevelElevated": "बढ़ा हुआ", + "wellnessStressLevelHigh": "उच्च", + "wellnessJournalDefaultSubtitle": "आज के बारे में कुछ भी जो आप याद रखना चाहें", + "wellnessJournalSubtitleShort": "{fields} और एक नोट", + "wellnessJournalSubtitleLong": "{fields} और {more} अन्य, साथ ही एक नोट", + "wellnessTurnInBy": "{time} तक सो जाएँ", + "wellnessDebtBody": "आप अपनी खुद की ज़रूरत से {debt} कम हैं, और आज रात की ज़रूरत {need} है।", + "wellnessSeeWhatLastNightCost": "देखें पिछली रात ने आपको क्या कीमत चुकाई", + "wellnessWhatChargedAndDrained": "किससे ऊर्जा मिली और किससे कमी हुई", + "wellnessNoDriversTitle": "अभी तक कोई रेडीनेस ड्राइवर नहीं", + "wellnessNoDriversBody": "आपके लिए सामान्य क्या है, यह जानने के लिए पर्याप्त रातों का डेटा चाहिए।", + "wellnessSleepNeedTonight": "आज रात की नींद की ज़रूरत", + "wellnessNoSleepNeedTitle": "अभी तक नींद की ज़रूरत तय नहीं हुई", + "wellnessNoSleepNeedBody": "आज रात के लिए ज़रूरत न होने का कोई दर्ज कारण नहीं है।", + "wellnessTonightsNeed": "आज रात की ज़रूरत", + "wellnessSleepDebt": "नींद का घाटा", + "wellnessAddedForStrain": "स्ट्रेन के लिए जोड़ा गया", + "wellnessCreditedFromNaps": "झपकियों से क्रेडिट किया गया", + "wellnessTargetBedtime": "लक्षित सोने का समय", + "wellnessTargetWake": "लक्षित जागने का समय", + "wellnessRemoveHabitSemantic": "{label} हटाएँ", + "wellnessDaysYouDidIt": "जितने दिन आपने किया", + "wellnessAddAHabit": "आदत जोड़ें", + "wellnessWhatYouLogTitle": "आप जो दर्ज करते हैं, आपके आँकड़ों के मुक़ाबले", + "wellnessWhatYouLogSubtitle": "खुराक, आदत का अंतर, और हफ़्ते का दिन", + "wellnessRemoveHabitConfirmTitle": "{label} हटाएँ?", + "wellnessRemoveHabitConfirmBody": "अब इसके बारे में नहीं पूछा जाएगा। जो दिन आप पहले ही दर्ज कर चुके हैं, वे बने रहेंगे।", + "wellnessHabitHint": "दोपहर के खाने के बाद टहलना", + "wellnessAlreadyTrack": "आप पहले से ही \"{name}\" ट्रैक कर रहे हैं।", + "wellnessNothingScheduledTitle": "कुछ भी शेड्यूल नहीं है", + "wellnessNothingScheduledBody": "आप क्या और कब लेते हैं, वह जोड़ें।", + "wellnessAddAMedication": "दवाई जोड़ें", + "wellnessNothingDueTodayTitle": "आज कुछ भी ड्यू नहीं है", + "wellnessNothingDueTodayBody": "आपकी दवाई अन्य दिनों या समय के लिए शेड्यूल है।", + "wellnessAdherence": "पालन दर", + "wellnessNothingToScoreTitle": "अभी स्कोर करने के लिए कुछ नहीं", + "wellnessNothingToScoreBody": "अभी तक कोई शेड्यूल की गई खुराक ड्यू नहीं हुई है।", + "wellnessTakenOfScheduled": "पिछले सात दिनों में शेड्यूल की गई खुराकों में से ली गई।", + "wellnessDosesUnit": "खुराकें", + "wellnessUndoSkipped": "छोड़ना पूर्ववत करें", + "wellnessSkippedOnPurpose": "जानबूझकर छोड़ा गया", + "wellnessBackToNotTaken": "फिर से न ली गई।", + "wellnessRecordedAsDecision": "एक जानबूझकर लिए फ़ैसले के रूप में दर्ज, भूल के रूप में नहीं।", + "wellnessWhichDaysDue": "किन दिनों में यह ड्यू है", + "wellnessRemoveMedTitle": "{label} हटाएँ", + "wellnessRemoveMedBody": "अब यह शेड्यूल नहीं होगी। चिह्नित खुराकें बनी रहेंगी।", + "wellnessRemoveMedConfirmTitle": "{label} हटाएँ?", + "wellnessRemoveMedConfirmBody": "अब यह शेड्यूल नहीं होगी और पालन दर में भी नहीं गिनी जाएगी। पहले से चिह्नित खुराकें बनी रहेंगी।", + "wellnessMedHint": "विटामिन डी", + "wellnessNameLabel": "नाम", + "wellnessAdd": "जोड़ें", + "wellnessEveryDay": "हर दिन", + "wellnessWeekdays": "कार्यदिवस", + "wellnessWeekends": "सप्ताहांत", + "wellnessMon": "सोम", + "wellnessTue": "मंगल", + "wellnessWed": "बुध", + "wellnessThu": "गुरु", + "wellnessFri": "शुक्र", + "wellnessSat": "शनि", + "wellnessSun": "रवि", + "wellnessWhenYouTakeIt": "आप इसे कब लेते हैं", + "wellnessChangeTheTime": "समय बदलें", + "wellnessWhichDays": "किन दिनों में", + "wellnessPickAtLeastOneDay": "कम से कम एक दिन चुनें।", + "wellnessDueDays": "{days} को ड्यू।", + "wellnessMoreForMed": "{label} के लिए और विकल्प", + "wellnessMedAtTime": "{label}, {time} बजे", + "wellnessStateTaken": "ली गई", + "wellnessStateSkipped": "छोड़ी गई", + "wellnessStateNotTaken": "नहीं ली गई", + "wellnessStateDueLater": "बाद में ड्यू", + "wellnessMarkDone": "पूर्ण के रूप में चिह्नित करें", + "wellnessWhatYouLogScreenTitle": "आप जो दर्ज करते हैं", + "wellnessNothingSeparatedTitle": "अभी तक कुछ भी अलग नहीं दिखा", + "wellnessNothingSeparatedBody": "आप जो कुछ भी दर्ज करते हैं, उसे आपकी रिकवरी, HRV, आराम की हृदय गति और नींद की क्षमता के मुक़ाबले परखा जाता है। अभी तक कुछ भी इस मापदंड को पार नहीं कर पाया है।", + "wellnessTheDaysYouDidIt": "जितने दिन आपने किया", + "wellnessHowMuchAndWhatFollowed": "कितना, और उसके बाद क्या हुआ", + "wellnessLinkNeverCause": "यह सिर्फ़ आपके अपने दिनों पर एक संबंध है, कभी कारण नहीं। जिन दिनों आप कुछ करते हैं, वे पहले से ही वैसे ही दिन होते हैं।", + "wellnessWhichDayOfWeek": "हफ़्ते का कौन सा दिन", + "wellnessHigher": "अधिक", + "wellnessLower": "कम", + "wellnessHeadlineBinary": "जिन {n} दिनों में आपने {field} दर्ज किया, उन दिनों {outcome} {amount} {direction} रहा", + "wellnessHeadlineNoSlope": "जिन {n} दिनों में आपने {field} दर्ज किया, उतना ही ज़्यादा {outcome} {direction} रहा", + "wellnessHeadlineSlope": "जिन {n} दिनों में आपने {field} दर्ज किया, हर {step} पर {outcome} {amount} {direction} रहा", + "wellnessMatchedSameDay": "उसी दिन के आँकड़ों से मिलाया गया।", + "wellnessMatchedNightFollowed": "अगली रात के आँकड़ों से मिलाया गया।", + "wellnessMatchedNightEnded": "उसी सुबह ख़त्म हुई रात के आँकड़ों से मिलाया गया।", + "wellnessAgainstDaysYouDidNot": "जिन {n} दिनों में आपने ऐसा नहीं किया, उनके मुक़ाबले", + "wellnessRangeTo": "{lo} से {hi}", + "wellnessRankCorrelation": "रैंक सहसंबंध {rho}{ci}। ", + "wellnessCaffeineCaveat": "यह सिर्फ़ दिन की आपकी आख़िरी कैफ़ीन का समय है — दो कप और पाँच कप यहाँ एक जैसे दिखते हैं, इसलिए \"देर से\" का मतलब चुपचाप \"ज़्यादा\" भी हो सकता है। एक लंबा, तनावभरा दिन देर की कॉफ़ी और खराब नींद, दोनों की वजह बनता है।", + "wellnessHourLater": "घंटा देर से", + "wellnessPointUnit": "पॉइंट", + "wellnessNotEnoughWeeksTitle": "अभी पर्याप्त हफ़्ते नहीं हुए", + "wellnessNotEnoughWeeksBody": "सातों दिनों की तुलना के लिए कम से कम आठ हफ़्तों का डेटा चाहिए, जिसमें हर दिन कम से कम पाँच बार आया हो।", + "wellnessNoDayStandsOutTitle": "हफ़्ते का कोई दिन अलग नहीं दिखता", + "wellnessNoDayStandsOutBody": "सातों दिनों की जाँच को ध्यान में रखने के बाद, कोई भी दिन बाक़ी छह से अलग नहीं दिखता।", + "wellnessWeekdayHeadline": "{weekday}: रेडीनेस आपके कुल औसत से {delta} {direction} रहती है", + "wellnessWeekdayDetail": "उनमें से {n} दिनों के आधार पर। हफ़्ते का दिन कोई कारण नहीं है — यह सिर्फ़ इस बात का दायरा है कि आप उस दिन क्या करते हैं। यहाँ कुछ भी सलाह नहीं है।", + "wellnessPluralMonday": "सोमवार को", + "wellnessPluralTuesday": "मंगलवार को", + "wellnessPluralWednesday": "बुधवार को", + "wellnessPluralThursday": "गुरुवार को", + "wellnessPluralFriday": "शुक्रवार को", + "wellnessPluralSaturday": "शनिवार को", + "wellnessPluralSunday": "रविवार को", + "sleepDetailNavTitle": "नींद", + "sleepDetailNoNightTitle": "दिखाने के लिए कोई रात नहीं", + "sleepDetailNoNightBody": "बैंड की रिकॉर्डिंग का कोई हिस्सा स्कोर करने लायक इतना लंबा नहीं है।", + "sleepDetailNoNightFix": "रातभर बैंड पहनें और सुबह सिंक करें", + "sleepDetailStagesSection": "चरण", + "sleepDetailVersusUsualSection": "आपकी सामान्य नींद की तुलना में", + "sleepDetailUnusualLastNight": "पिछली रात कुछ असामान्य", + "sleepDetailUnusualOnDay": "{day} को कुछ असामान्य", + "sleepDetailOvernightSection": "रातभर के संकेत", + "sleepDetailTonightSection": "आज रात", + "sleepDetailTotalSleep": "कुल नींद", + "sleepDetailInBed": "बिस्तर पर", + "sleepDetailWatched": "मॉनिटर किया गया", + "sleepDetailAsleepOfThat": "उसमें से सोया हुआ", + "sleepDetailAsleep": "सोया हुआ", + "sleepDetailWatchedExplain": "हमने बिस्तर पर बिताए आपके {inBed} में से {watched} मॉनिटर किया; बाकी समय एक मापन नहीं है। नींद, और नीचे दिए गए चरणों के हिस्से, केवल मॉनिटर किए गए समय पर आधारित हैं।", + "sleepDetailWindowMine": "यह समय-सीमा आपने खुद तय की है", + "sleepDetailWindowFallback": "यह समय-सीमा हृदय गति से अनुमानित की गई है", + "sleepDetailWindowAuto": "यह समय-सीमा संकेतों से तय की गई है", + "sleepDetailWindowFallbackBody": "विश्लेषण सीमाओं को नहीं ढूंढ पाया, इसलिए ये समय एक अनुमान हैं।", + "sleepDetailWindowSol": "आपकी समय-सीमा की शुरुआत से सोने तक: {band}।", + "sleepDetailConfirmTimes": "ये समय सही हैं", + "sleepDetailChangeTimes": "समय बदलें", + "sleepDetailSetTimesMyself": "समय खुद सेट करें", + "sleepDetailBackToAutomatic": "फिर से ऑटोमैटिक पर जाएं", + "sleepDetailReanalysing": "रात का दोबारा विश्लेषण हो रहा है…", + "sleepDetailCorrectionFailedTitle": "वह सुधार लागू नहीं हो पाया", + "sleepDetailBedTimeHelp": "आप बिस्तर पर कब गए", + "sleepDetailWakeTimeHelp": "आप कब उठे", + "sleepDetailReanalyseFailed": "रात का दोबारा विश्लेषण नहीं हो पाया — या तो कोई और पुनः विश्लेषण पहले से चल रहा था, या यह विफल हो गया। आपके सेट किए गए समय सेव हैं; 'आपका डेटा' में 'सब कुछ दोबारा विश्लेषण करें' उन्हें लागू कर देगा।", + "sleepDetailNoHypnogramTitle": "इस रात के लिए कोई हिप्नोग्राम नहीं", + "sleepDetailNoHypnogramBody": "चरण तय करने के लिए हलचल और हृदय-धड़कन की टाइमिंग चाहिए। इनमें से एक मौजूद नहीं था।", + "sleepDetailThroughTheNight": "पूरी रात के दौरान", + "sleepDetailUnitStage": "चरण", + "sleepDetailTapDragCycles": "{n, plural, one{किसी भी पल को देखने के लिए चार्ट पर टैप या ड्रैग करें। {n} चक्र।} other{किसी भी पल को देखने के लिए चार्ट पर टैप या ड्रैग करें। {n} चक्र।}}", + "sleepDetailTapDragCyclesAvg": "{n, plural, one{किसी भी पल को देखने के लिए चार्ट पर टैप या ड्रैग करें। {n} चक्र, औसतन {avg}।} other{किसी भी पल को देखने के लिए चार्ट पर टैप या ड्रैग करें। {n} चक्र, औसतन {avg}।}}", + "sleepDetailTapDragNone": "रात के किसी भी पल को देखने के लिए चार्ट पर टैप या ड्रैग करें।", + "sleepDetailNoWakeups": "5 मिनट या उससे ज़्यादा की कोई जागृति नहीं; इससे छोटी जागृतियां कलाई के लिए अदृश्य होती हैं।", + "sleepDetailAtLeastWakeups": "{n, plural, one{कम से कम {n} बार 5 मिनट या उससे ज़्यादा की जागृति; इससे छोटी जागृतियां कलाई के लिए अदृश्य होती हैं।} other{कम से कम {n} बार 5 मिनट या उससे ज़्यादा की जागृतियां; इससे छोटी जागृतियां कलाई के लिए अदृश्य होती हैं।}}", + "sleepDetailLongestStretch": "सबसे लंबा अटूट हिस्सा {longest}।", + "sleepDetailHypnogramLabel": "हिप्नोग्राम", + "sleepDetailPercentThroughNight": "रात का {pct}% भाग", + "sleepDetailNotMeasured": "मापा नहीं गया", + "sleepDetailScrubAt": "{at}, {stage}", + "sleepDetailHeartRate": "हृदय गति", + "sleepDetailHrv": "HRV", + "sleepDetailBreathing": "श्वसन", + "sleepDetailTemp": "तापमान", + "sleepDetailNotMeasuredCap": "मापा नहीं गया", + "sleepDetailNoSignalAtMoment": "इस पल कोई संकेत रिकॉर्ड नहीं हुआ।", + "sleepDetailStageAwake": "जागृत", + "sleepDetailStageRem": "REM", + "sleepDetailStageLight": "हल्की नींद", + "sleepDetailStageDeep": "गहरी नींद", + "sleepDetailDeep": "गहरी", + "sleepDetailLight": "हल्की", + "sleepDetailNoStageSplitTitle": "इस रात के लिए कोई चरण विभाजन नहीं", + "sleepDetailNoStageSplitBody": "पूरी समय-सीमा में हृदय-धड़कन की कोई टाइमिंग नहीं मिली।", + "sleepDetailStageRangeExplain": "हर चरण एक सीमा है, गिनती नहीं — हमने रात को जितना बेहतर देखा, वह उतनी ही संकरी होती है। गहरी नींद सबसे चौड़ी है। जागृति एक ही आंकड़े के रूप में रहती है। सटीक गिनती Nerd stats में मौजूद है।", + "sleepDetailTimeAsleep": "सोने का समय", + "sleepDetailShorterThanUsual": "सामान्य से कम", + "sleepDetailLongerThanUsual": "सामान्य से ज़्यादा", + "sleepDetailLessThanUsual": "सामान्य से कम", + "sleepDetailMoreThanUsual": "सामान्य से ज़्यादा", + "sleepDetailAsleepWhileInBed": "बिस्तर पर रहते हुए सोया हुआ", + "sleepDetailLowerThanUsual": "सामान्य से कम", + "sleepDetailHigherThanUsual": "सामान्य से ज़्यादा", + "sleepDetailFellAsleep": "नींद आई", + "sleepDetailEarlierThanUsual": "सामान्य से जल्दी", + "sleepDetailLaterThanUsual": "सामान्य से देर से", + "sleepDetailNotEnoughNightsTitle": "तुलना के लिए पर्याप्त रातें नहीं", + "sleepDetailNightsSoFar": "अब तक {min} में से {have} रातें", + "sleepDetailBarExplain": "यह पट्टी आपकी अपनी रातों के बीच के आधे हिस्से को दर्शाती है।", + "sleepDetailLessThanAny": "{noun} {value} — आपकी पिछली {count} रातों में से किसी से भी कम, जिनमें सबसे कम {lowest} था।", + "sleepDetailMoreThanAny": "{noun} {value} — आपकी पिछली {count} रातों में से किसी से भी ज़्यादा, जिनमें सबसे ज़्यादा {highest} था।", + "sleepDetailYouSlept": "आप सोए", + "sleepDetailShortestNightLately": "हाल में आपकी सबसे छोटी रात", + "sleepDetailLongestNightLately": "हाल में आपकी सबसे लंबी रात", + "sleepDetailSleepingHrHighTitle": "सोते समय हृदय गति अधिक रही", + "sleepDetailSleepingHrHighBody": "आपके अपने बेसलाइन से {bpm} bpm ज़्यादा। यह शराब पीने, देर से खाना खाने, कठिन सत्र या शुरू हो रहे संक्रमण के बाद आम है — यह एक मापन है, निदान नहीं।", + "sleepDetailNothingStoodOut": "कुछ भी असामान्य नहीं दिखा।", + "sleepDetailSleepingHr": "नींद में हृदय गति", + "sleepDetailLowest": "सबसे कम", + "sleepDetailBreathingCaps": "श्वसन", + "sleepDetailSkinTemp": "त्वचा तापमान", + "sleepDetailSleepNeedNotEstablished": "नींद की ज़रूरत अभी तय नहीं हुई है", + "sleepDetailYourNeedIs": "आपकी ज़रूरत {need} है", + "sleepDetailYouAreDown": "आप {debt} पीछे हैं", + "sleepDetailLightsOut": "लाइट बंद करने का समय", + "sleepDetailToAimFor": "लक्ष्य समय", + "sleepDetailNoPersonalRangeYet": "अभी तक कोई व्यक्तिगत सीमा नहीं — {min} में से {count} रातें।", + "sleepDetailNotFarEnoughToCall": "सामान्य से तय करने लायक अंतर नहीं है", + "sleepDetailTypicalForYou": "आपके लिए सामान्य", + "sleepDetailVerdictSummary": "{verdict} · सामान्य सीमा {lo}–{hi}, {n} रातों में", + "sleepDetailNoOvernightTitle": "रातभर की कोई संकेत रेखाएं नहीं", + "sleepDetailNoOvernightBody": "इस दिन कोई रातभर की रिकॉर्डिंग नहीं मिली।", + "sleepDetailSolUnder15": "15 मिनट से कम", + "sleepDetailSolOverHour": "एक घंटे से ज़्यादा", + "sleepDetailSolRange": "{lo}–{hi} मिनट", + "workoutTabForYou": "आपके लिए", + "workoutTabActivities": "गतिविधियाँ", + "workoutTabHistory": "इतिहास", + "workoutScreenTitle": "वर्कआउट", + "workoutStartSessionLabel": "सेशन शुरू करें", + "workoutActivitiesNoun": "गतिविधियाँ", + "workoutThisWeek": "इस सप्ताह", + "workoutTrainingLoad": "ट्रेनिंग लोड", + "workoutTodaysStrainAction": "आज का स्ट्रेन", + "workoutMechanicalLoadTitle": "मैकेनिकल लोड", + "workoutKgLiftedUnit": "उठाया गया किलोग्राम", + "workoutTonnageFootnoteIntro": "वज़न के साथ दर्ज किए गए सेट्स पर रेप्स × लोड। ", + "workoutTonnageFootnotePartial": "बिना वज़न दर्ज किए गए सेट इसमें शामिल नहीं हैं, इसलिए यह कुल के बजाय न्यूनतम सीमा है। ", + "workoutTonnageFootnoteOutro": "आपने जो दर्ज किया उसके लिए सटीक है, पर अलग-अलग एक्सरसाइज़ के बीच तुलना योग्य नहीं—इसी वजह से इसे स्ट्रेन और रिकवरी में शामिल नहीं किया जाता।", + "workoutOverreachHeadline": "आपके पिछले 7 दिनों का लोड आपके सामान्य छह हफ्तों का {ratio}× है, और आपका रेस्टिंग हार्ट रेट {nightsConsidered} में से {nightsElevated} रातों में सामान्य से ऊपर रहा।", + "workoutOverreachBody": "दो माप जो संयोगवश एक ही दिशा में इशारा कर रहे हैं। बीमारी, यात्रा, ऊँचाई, शराब और लगातार खराब नींद वाली रातें—सब एक जैसा पैटर्न बना सकते हैं, और यहाँ इनमें फ़र्क़ करने का कोई तरीका नहीं है।", + "workoutNoLoadTitle": "अभी तक कोई ट्रेनिंग लोड नहीं", + "workoutNoLoadBody": "फिटनेस और थकान क्रमशः 42-दिन और 7-दिन के औसत हैं। इसके लिए लगभग दो हफ्तों के सेशन चाहिए।", + "workoutFitnessLabel": "फिटनेस", + "workoutDailyLoadTitle": "दैनिक लोड", + "workoutTrimpUnit": "TRIMP", + "workoutDailyLoadFootnoteIntro": "बैनिस्टर ट्रेनिंग इम्पल्स — हार्ट-रेट रिज़र्व के अनुसार भारित मिनट। ", + "workoutDailyLoadAllDays": "पिछले सात दिन।", + "workoutDailyLoadPartialDays": "पिछले सात दिनों में से {days} दिनों का आँकड़ा मिला।", + "workoutFatigueLabel": "थकान", + "workoutFormLabel": "फ़ॉर्म", + "workoutNotYet": "अभी नहीं", + "workoutFormFresh": "तरोताज़ा", + "workoutFormSteady": "स्थिर", + "workoutFormBuilding": "बढ़ रहा है", + "workoutFormOverreaching": "ओवररीचिंग", + "workoutSearchActivitiesLabel": "गतिविधियाँ खोजें", + "workoutSearchActivitiesCount": "{count, plural, one{{count} गतिविधि खोजें} other{{count} गतिविधियाँ खोजें}}", + "workoutQuickStartHeader": "क्विक स्टार्ट", + "workoutCalorieNeedWeightTitle": "कैलोरी अनुमान के लिए आपका वज़न चाहिए", + "workoutAddWeightFix": "प्रोफ़ाइल में वज़न जोड़ें", + "workoutCalorieEstimatesTitle": "कैलोरी के आँकड़े अनुमान हैं", + "workoutSuggestionsTitle": "{n, plural, one{{n} प्रयास मिला जो दर्ज नहीं हुआ} other{{n} प्रयास मिले जो दर्ज नहीं हुए}}", + "workoutSuggestionsBody": "बैंड ने लगातार गतिविधि देखी, पर कुछ भी शुरू नहीं किया गया। जब तक आप न कहें, कुछ भी दर्ज नहीं होता।", + "workoutReviewFix": "{n, plural, one{इसे देखें} other{इन्हें देखें}}", + "workoutLogPastTitle": "कुछ किया जो बैंड से छूट गया?", + "workoutLogPastBody": "समय खुद दर्ज करें, और उस अवधि में दर्ज हार्ट रेट के आधार पर स्कोर होगा, बाकी किसी भी सेशन की तरह।", + "workoutLogPastFix": "पुराना वर्कआउट दर्ज करें", + "workoutNoSessionsTitle": "अभी तक कोई सेशन दर्ज नहीं", + "workoutNoSessionsBody": "जैसे ही आप कोई सेशन शुरू करेंगे, वह यहाँ दिखेगा।", + "workoutStartWorkoutFix": "वर्कआउट शुरू करें", + "workoutTrackedLabel": "ट्रैक किए गए", + "workoutWeeklyLoadLabel": "साप्ताहिक लोड", + "workoutNoneLabel": "कोई नहीं", + "workoutImportedThisWeekNote": "इस सप्ताह के {count} सेशन {storeName} से आए। ये यहाँ गिने जाते हैं, पर साप्ताहिक लोड में शामिल नहीं होते—इम्पोर्ट किए गए वर्कआउट में हार्ट-रेट ट्रेस नहीं होता, और उसके बिना लोड का आँकड़ा गढ़ा हुआ होगा।", + "workoutAutoImportOnLabel": "ऑटो-इम्पोर्ट चालू है। बंद करने के लिए टैप करें।", + "workoutAutoImportOffLabel": "ऑटो-इम्पोर्ट बंद है। चालू करने के लिए टैप करें।", + "workoutImportFromStore": "{storeName} से इम्पोर्ट करें", + "workoutFetchNowLabel": "अभी वर्कआउट लाएँ", + "workoutImportDenied": "{storeName} ने वर्कआउट्स की अनुमति नहीं दी। कुछ भी नहीं पढ़ा गया।", + "workoutImportEmpty": "कुछ वापस नहीं आया। साझा की गई अवधि में {storeName} के पास कोई वर्कआउट नहीं है।", + "workoutImportNoRoutes": " {storeName} रूट साझा नहीं करेगा, इसलिए किसी में भी निर्देशांक नहीं होंगे।", + "workoutImportNoneWithRoute": " इनमें से किसी का भी रूट दर्ज नहीं था।", + "workoutImportSomeWithRoute": " {count} रूट के साथ आए।", + "workoutImportBroughtIn": "{count, plural, one{{count} वर्कआउट लाया गया।} other{{count} वर्कआउट लाए गए।}}", + "workoutImportFailed": "विफल: {error}", + "workoutMorningAfterTitle": "अगली सुबह", + "workoutMorningAfterBody": "यह आपका अपना इतिहास है, गतिविधि के बारे में कोई नियम नहीं—इन सुबहों के साथ पिछली शाम भी जुड़ी थी। यहाँ कुछ भी सेशन छोड़ने की वजह नहीं है।", + "workoutAfterActivity": "{name} के बाद", + "workoutUnchangedLabel": "कोई बदलाव नहीं", + "workoutRestingHeartRateLabel": "रेस्टिंग हार्ट रेट", + "workoutHrvLabel": "एचआरवी", + "workoutMorningCount": "{n, plural, one{{n} सुबह} other{{n} सुबहें}}", + "workoutInsideRangeSuffix": " · आपकी सामान्य रात-दर-रात रेंज के भीतर", + "workoutDeleteSessionLabel": "यह सेशन हटाएँ", + "workoutStrainLabel": "स्ट्रेन", + "workoutTimeInZonesTitle": "ज़ोन में समय", + "workoutMinutesUnit": "मिनट", + "workoutFixTimesOnSessionLabel": "इस सेशन के समय ठीक करें", + "workoutFixTimes": "समय ठीक करें", + "workoutTimeStatLabel": "समय", + "workoutDistanceStatLabel": "दूरी", + "workoutCaloriesStatLabel": "कैलोरी", + "workoutNotCostedValue": "गणना नहीं हुई", + "workoutMaxHrStatLabel": "अधिकतम हार्ट रेट", + "workoutNoReadingValue": "कोई रीडिंग नहीं", + "workoutConfirmDeleteTitle": "यह {activity} सेशन हटाएँ?", + "workoutDeleteBodyOwn": "यह OpenStrap से हट जाएगा। {storeName} में मौजूद कॉपी, अगर है, वहीं रहेगी।", + "workoutDeleteBodyImported": "यह OpenStrap से हट जाएगा और दोबारा इम्पोर्ट नहीं होगा। {storeName} में मौजूद मूल रिकॉर्ड बना रहेगा।", + "workoutWhenToday": "आज, {time}", + "workoutWhenYesterday": "कल, {time}", + "workoutWeekdayLetterMon": "सो", + "workoutWeekdayLetterTue": "मं", + "workoutWeekdayLetterWed": "बु", + "workoutWeekdayLetterThu": "गु", + "workoutWeekdayLetterFri": "शु", + "workoutWeekdayLetterSat": "श", + "workoutWeekdayLetterSun": "र", + "workoutWeekdayAbbrMon": "सोम", + "workoutWeekdayAbbrTue": "मंगल", + "workoutWeekdayAbbrWed": "बुध", + "workoutWeekdayAbbrThu": "गुरु", + "workoutWeekdayAbbrFri": "शुक्र", + "workoutWeekdayAbbrSat": "शनि", + "workoutWeekdayAbbrSun": "रवि", + "activitySetupRouteLabel": "रूट", + "activitySetupRouteDetail": "लोकेशन उपलब्ध होने पर रिकॉर्ड किया जाता है, और इस फ़ोन पर ही रखा जाता है", + "activitySetupHeartRateLabel": "हृदय गति", + "activitySetupBandConnected": "बैंड कनेक्टेड", + "activitySetupNoBandConnected": "कोई बैंड कनेक्ट नहीं है", + "activitySetupPrivateLabel": "प्राइवेट सेशन", + "activitySetupPrivateDetail": "सारांश और एक्सपोर्ट से छिपा रहता है", + "activitySetupCaloriesNeedWeight": "कैलोरी के लिए आपके वज़न की ज़रूरत है।", + "activitySetupCalorieEstimate": "{met} MET और आपके वज़न के आधार पर, लगभग {est} kcal प्रति {minutes} मिनट।", + "activitySetupTrackSets": "सेट्स, रेप्स और वज़न — आपके द्वारा दर्ज", + "activitySetupTrackDistanceGps": "दूरी, पेस और हृदय गति", + "activitySetupTrackTime": "समय और हृदय गति", + "activitySetupTrackInterval": "राउंड्स और हृदय गति", + "activitySetupTrackStillness": "समय, श्वास और स्थिरता", + "activitySetupSessionRunningTitle": "एक सेशन पहले से चल रहा है", + "activitySetupSessionRunningBody": "एक समय में केवल एक सेशन चल सकता है।", + "activitySetupOpenRunningSession": "चल रहा सेशन खोलें", + "activitySetupStart": "शुरू करें", + "activityPickerTitle": "गतिविधि चुनें", + "activityPickerSearchLabel": "गतिविधियाँ खोजें", + "activityPickerSearchHint": "{count} गतिविधियों में खोजें", + "activityPickerNoMatchTitle": "कोई गतिविधि मेल नहीं खाती", + "activityPickerNoMatchBody": "कैटलॉग में प्रकाशित ऊर्जा खर्च वाली लगभग सत्तर गतिविधियाँ शामिल हैं। सबसे नज़दीकी गतिविधि चुनें।", + "activityPickerQuickStart": "क्विक स्टार्ट", + "activityPickerRecent": "हाल की", + "activityPickerCalorieEstimatesTitle": "कैलोरी आंकड़े अनुमानित हैं", + "activityPickerMetValue": "{met} MET", + "activityPickerKcalPer30": "{kcal} kcal / 30 मिनट", + "dayStrainToday": "आज", + "dayStrainTitle": "दिन का स्ट्रेन", + "dayStrainNoTraceTitle": "इस दिन के लिए कोई स्ट्रेन ट्रेस नहीं है", + "dayStrainNoMinuteTraceTitle": "इस दिन के लिए मिनट-दर-मिनट ट्रेस नहीं है", + "dayStrainNoReasonBody": "कोई रिकॉर्ड यह नहीं बताता कि इस दिन स्ट्रेन क्यों नहीं बना।", + "dayStrainScoredNoTraceBody": "दिन का स्ट्रेन {strain} है। जिन जागने के मिनटों से यह बना था, वे इस दिन के लिए संग्रहीत नहीं हैं।", + "dayStrainWearBandFix": "पूरे दिन बैंड पहनें", + "dayStrainChartTitle": "दिन भर का स्ट्रेन", + "dayStrainChartFootnote": "यह संचयी है, इसलिए यह केवल बढ़ती है — जो हिस्से खड़ी ढलान जैसे हैं वहीं मेहनत हुई थी। {drawn} दर्ज किए गए जागने के मिनटों से बनाई गई।", + "dayStrainPeakHr": "अधिकतम हृदय गति", + "dayStrainWorn": "पहना गया", + "dayStrainLowCoverageTitle": "बैंड ने इस दिन का {pct}% हिस्सा देखा", + "dayStrainLowCoverageBody": "स्ट्रेन दर्ज किए गए मिनटों का कुल योग है, इसलिए आंशिक रूप से पहना गया दिन पूरे दिन की तुलना में कम दिखता है और दोनों की तुलना नहीं की जा सकती।", + "dayStrainTimeInZonesSection": "ज़ोन में समय", + "dayStrainZonesChartTitle": "ज़ोन में समय", + "dayStrainZoneFootnoteKarvonen": "ज़ोन की सीमाएँ आपकी मापी गई विश्राम हृदय गति और अब तक देखी गई सबसे अधिक हृदय गति ({maxHr} bpm) के बीच के अंतर को कवर करती हैं। दोनों आप पर मापी गई हैं।", + "dayStrainZoneFootnoteObserved": "ज़ोन की सीमाएँ अब तक देखी गई सबसे अधिक हृदय गति ({maxHr} bpm) के प्रतिशत हैं — मापी गई, अनुमानित नहीं।", + "dayStrainHowSet": "ये कैसे तय होते हैं", + "dayStrainInputsSection": "यह किससे बना है", + "dayStrainInputsBase": "आपकी जागने की हृदय गति पर बैनिस्टर TRIMP, 0–21 के पैमाने पर।", + "dayStrainInputsMaxHr": "इसे मान लिए गए अधिकतम {maxHr} bpm के आधार पर गणना की गई — यह आपकी उम्र और स्ट्रैप से अनुमानित है, मापा नहीं गया।", + "dayStrainInputsMeasuredCeilingNote": "ऊपर दिया गया ज़ोन बार इसके बजाय मापी गई ऊपरी सीमा का उपयोग करता है; स्ट्रेन को उस पर स्थानांतरित नहीं किया गया है, क्योंकि इससे अब तक देखे गए सभी स्ट्रेन स्कोर फिर से लिखने पड़ेंगे।", + "dayStrainInputsRhrAnchor": "दूसरा आधार पिछली रात की आपकी विश्राम हृदय गति है, इसलिए जिस रात बैंड चूक गया वह पूरे दिन को प्रभावित करती है।", + "activityZonesTitle": "हृदय गति ज़ोन", + "activityZonesYourZonesSection": "आपके ज़ोन", + "activityZonesIntensitySection": "आपकी तीव्रता कहाँ गई", + "activityZonesNoCeilingTitle": "अभी तक कोई मापी गई ऊपरी सीमा नहीं", + "activityZonesNoCeilingTanakaTail": " जब तक कोई मापा नहीं जाता, नीचे दिए गए ज़ोन आपकी उम्र पर आधारित हैं।", + "activityZonesNoCeilingDefaultBody": "हम केवल वही उच्च रीडिंग गिनते हैं जिसे बैंड ने आपके चलते समय 15 सेकंड तक बनाए रखा हो। एक सेकंड की स्पाइक हृदय गति नहीं है।", + "activityZonesWearBandFix": "अपने सामान्य कठिन सत्रों के दौरान बैंड पहनें", + "activityZonesHighestSeenLabel": "अब तक देखा गया उच्चतम", + "activityZonesBpmUnit": "bpm", + "activityZonesCeilingOnDate": "{date} को", + "activityZonesCeilingDuringSession": "{session} के दौरान", + "activityZonesHighestSeenFootnote": "यह हमारे द्वारा मापा गया उच्चतम मान है, कोई सीमा नहीं — जैसे-जैसे बैंड कठिन प्रयास देखता है, यह धीरे-धीरे बढ़ता जाता है। इसे परखने की कोशिश न करें।", + "activityZonesNoZonesTitle": "अभी तक कोई ज़ोन नहीं", + "catalogueZonesWhy": "ज़ोन की सीमाएँ आपकी उम्र से अनुमानित अधिकतम हृदय गति के प्रतिशत हैं — आप पर मापी नहीं गई हैं।", + "activityZonesNoAgeBody": "ज़ोन की सीमाएँ अधिकतम हृदय गति के प्रतिशत हैं, और आपकी उम्र के बिना प्रतिशत निकालने के लिए कुछ भी नहीं है।", + "activityZonesNoZonesDefaultBody": "कोई रिकॉर्ड यह नहीं बताता कि अभी तक ज़ोन की सीमाएँ क्यों नहीं हैं।", + "activityZonesAddAgeFix": "प्रोफ़ाइल में अपनी उम्र जोड़ें", + "activityZonesAnchorKarvonen": "यह बैंड द्वारा आप पर मापे गए दो आंकड़ों से बनता है: आपकी विश्राम गति ({restingHr}, आपकी पिछली {restingDays} रातों का मध्य मान) और अब तक देखी गई सबसे अधिक गति ({maxHr})। कम विश्राम गति ज़ोन 1 को चौड़ा बनाती है। ये सामान्य बैंड हैं, आपकी अपनी मापी गई सीमाएँ नहीं।", + "activityZonesAnchorObserved": "यह अब तक देखी गई सबसे अधिक हृदय गति ({maxHr}) से बनता है। विश्राम गति की {restingMinDays} रातों के बाद (आपके पास अभी {restingDays} हैं) आपकी विश्राम गति भी इसमें जुड़ जाएगी, जो आपके अनुरूप बेहतर बैठेगी। ये सामान्य बैंड हैं, आपकी अपनी मापी गई सीमाएँ नहीं।", + "activityZonesAnchorTanaka": "यह {maxHr} bpm से बनता है, जो आपकी उम्र से अनुमानित है, आप पर मापा नहीं गया — यह किसी भी दिशा में 20 bpm तक भिन्न हो सकता है। जैसे ही बैंड पर्याप्त कठिन सत्र देखता है, सीमाएँ मापी गई ऊपरी सीमा पर चली जाती हैं।", + "activityZonesAnchorDefault": "ज़ोन की सीमाएँ अधिकतम हृदय गति के प्रतिशत हैं।", + "activityZonesNotShownTitle": "अभी तक नहीं दिखाया गया", + "activityZonesNeedsMonthBody": "इसके लिए लगभग एक महीने के दर्ज सत्र चाहिए, हर एक में मिनट-दर-मिनट हृदय गति के साथ।", + "activityZonesAgeEstimateBody": "बार केवल उम्र के अनुमान की तस्वीर होंगे, आपके प्रशिक्षण की नहीं। ऊपर दी गई ज़ोन सीमाएँ मापे जाने के बाद ये दिखाई देंगे।", + "activityZonesSessionMinutesChartTitle": "सत्र के मिनट, पिछले 28 दिन", + "activityZonesShapePyramidal": "आपके अधिकांश मिनट आसान हैं, बीच में कम, सबसे कम कठिन — एक पिरामिड।", + "activityZonesShapePolarised": "आपके अधिकांश मिनट आसान हैं और बाकी कठिन, बीच में बहुत कम।", + "activityZonesShapeMiddleHeavy": "आपके अधिकांश मिनट आसान या कठिन के बजाय बीच में आते हैं।", + "activityZonesShapeSummary": "{easy} मिनट आसान, {moderate} मध्यम, {hard} कठिन, कुल {sessions} दर्ज सत्रों में। यह एक विवरण है, लक्ष्य नहीं।", + "activityShareTitle": "शेयर करें", + "activityShareOpenFailed": "शेयर शीट नहीं खोली जा सकी।", + "activitySharePhotoHeader": "आपकी फ़ोटो", + "activityShareAddPhoto": "फ़ोटो जोड़ें", + "activityShareChangePhoto": "फ़ोटो बदलें", + "activitySharePhotoHint": "इसी फ़ोन से। कुछ भी अपलोड नहीं होता", + "activityShareRemovePhoto": "फ़ोटो हटाएँ", + "activityShareBasemapHeader": "बेसमैप", + "activityShareDrawMap": "असली मैप दिखाएँ", + "activityShareMapHint": "openstreetmap.org से इस रूट को कवर करने वाली टाइलें माँगता है। बंद होने पर, रूट अपने आप बन जाता है", + "activityShareFetchingMapTitle": "मैप लाया जा रहा है", + "activityShareFetchingMapBody": "हर टाइल आते ही कार्ड बन जाता है।", + "activityShareNoMapTitle": "इस कार्ड के लिए कोई मैप नहीं", + "activityShareNoMapBody": "मैप की टाइलें नहीं मिल सकीं, इसलिए रूट अपने आप बनाया गया है। कार्ड का बाकी हिस्सा वैसा ही है।", + "activityShareStatusPrivateTitle": "यह सेशन निजी है", + "activityShareStatusPrivateBody": "सारांश और एक्सपोर्ट से छिपा हुआ।", + "activityPosterFormatPost": "पोस्ट", + "activityPosterFormatStory": "स्टोरी", + "activitySummaryRpeHeadline": "यह कितना मुश्किल लगा?", + "activitySummaryRpeBody": "यह मेहनत की आपकी अपनी रेटिंग है। यह एक अहसास है, कोई माप नहीं — और यही इसका मक़सद है, क्योंकि यह ऊपर दिए गए आंकड़ों से अलग हो सकता है।", + "activitySummaryRateEffort": "इस मेहनत को 10 में से {n} रेट करें", + "activitySummaryRpeVeryEasy": "1 · बहुत आसान", + "activitySummaryRpeMaximal": "10 · अधिकतम", + "activitySummaryNotNow": "अभी नहीं", + "activitySummaryShareThis": "यह {name} सत्र साझा करें", + "activitySummaryChangeType": "गतिविधि का प्रकार बदलें", + "activitySummaryUnsavedTitle": "यह सत्र अभी सेव नहीं हुआ है", + "activitySummaryUnsavedBody": "इस फ़ोन पर सेव करना विफल रहा।", + "activitySummarySaving": "सेव हो रहा है", + "activitySummaryTryAgain": "फिर से कोशिश करें", + "activitySummaryPrivate": "निजी", + "activitySummaryStepsBasis": "कदम बैंड के अपने मोशन सेंसर से मिले हैं, जो केवल चलने पर ही गिनता है।", + "activitySummaryCaloriesNeedWeight": "कैलोरी के लिए आपका वज़न ज़रूरी है।", + "activitySummaryNoCalorieNoStrain": "इस सत्र के लिए कोई कैलोरी आंकड़ा नहीं है। हृदय गति से ऊर्जा का अनुमान लगाने के लिए आपकी अधिकतम और आराम की हृदय गति चाहिए, और इनमें से एक सेट नहीं है।", + "activitySummaryNoCalorieWithStrain": "इस सत्र के लिए कोई कैलोरी आंकड़ा नहीं है — हृदय गति से ऊर्जा का अनुमान लगाने के लिए आपकी अधिकतम और आराम की हृदय गति चाहिए, और इनमें से एक सेट नहीं है। ऊपर दिखाया गया स्ट्रेन ही वह मेहनत है जो वास्तव में मापी गई, अपने 0–21 पैमाने पर।", + "activitySummaryCalorieNoHr": "{met} MET और आपके वज़न से अनुमानित। इस सत्र में कोई हृदय गति दर्ज नहीं हुई, इसलिए यह आंकड़े में शामिल नहीं है।", + "activitySummaryCalorieWithHr": "{met} MET, आपके वज़न और हृदय गति से अनुमानित।", + "activitySummaryNothingLoggedWithLoad": "किसी भी सेट में वज़न दर्ज नहीं किया गया", + "activitySummarySetUnit": "{n, plural, one{सेट} other{सेट}}", + "activitySummaryVolumeLoadedSets": "वज़न वाले सेट का कुल आयतन", + "activitySummaryTotalVolume": "कुल आयतन", + "activitySummaryElapsedTime": "बीता हुआ समय", + "activitySummaryClimbed": "+{m} मी चढ़ाई", + "activitySummaryLapsCaption": "{n, plural, one{{n} चक्कर} other{{n} चक्कर}}", + "activitySummaryNoRouteTitle": "इस सत्र के लिए कोई मार्ग नहीं है", + "activitySummaryNoRouteBody": "लोकेशन बंद थी, या यह गतिविधि GPS के साथ दर्ज नहीं की गई।", + "activitySummaryRouteTitle": "मार्ग", + "activitySummarySlower": "धीमा", + "activitySummaryFaster": "तेज़", + "activitySummaryStartFinishPinned": "शुरुआत और अंत चिह्नित हैं।", + "activitySummaryRouteFootnote": "{distance} {unit}, शुरुआत और अंत चिह्नित।", + "activitySummaryNoSetsTitle": "कोई सेट दर्ज नहीं किया गया", + "activitySummaryNoSetsBody": "इस सत्र में कुछ भी दर्ज नहीं किया गया, इसलिए न कोई वज़न है न कुल आयतन जोड़ने के लिए।", + "activitySummaryNoRoundsTitle": "कोई राउंड दर्ज नहीं किया गया", + "activitySummaryNoRoundsBody": "0 राउंड दर्ज।", + "activitySummaryIntervalLadderTitle": "इंटरवल लैडर", + "activitySummaryWork": "मेहनत", + "activitySummaryRest": "आराम", + "activitySummaryRoundLabel": "राउंड {n}", + "activitySummaryLongestBlock": "सबसे लंबा हिस्सा {time}।", + "activitySummaryPosesCount": "{n, plural, one{{n} मुद्रा} other{{n} मुद्राएं}}", + "activitySummaryNoLapsTitle": "कोई चक्कर नहीं गिना गया", + "activitySummaryNoLapsBody": "0 चक्कर टैप किए गए।", + "activitySummaryLapsTitle": "चक्कर", + "activitySummarySecondsPerLap": "प्रति चक्कर सेकंड", + "activitySummaryLapLabel": "चक्कर {n}", + "activitySummaryPoolLength": "{m} मी पूल", + "activitySummaryFastest": "सबसे तेज़ {time}", + "activitySummarySlowest": "सबसे धीमा {time}", + "activitySummaryNoElevationTitle": "कोई ऊंचाई प्रोफ़ाइल नहीं", + "activitySummaryNoElevationBody": "कोई मार्ग नहीं, या मार्ग में कोई ऊंचाई दर्ज नहीं थी।", + "activitySummaryElevationTitle": "ऊंचाई", + "activitySummaryStart": "शुरुआत", + "activitySummaryFinish": "अंत", + "activitySummaryGain": "चढ़ाई", + "activitySummaryLoss": "उतराई", + "activitySummaryPeak": "सबसे ऊंचा बिंदु", + "activitySummaryColdPlungeWhy": "ठंड उन रक्त वाहिकाओं को सिकोड़ देती है जिन्हें सेंसर पढ़ता है। यहां कुछ न मिलना अपेक्षित है, कोई खराबी नहीं।", + "activitySummaryHeatWhy": "गर्मी, पसीना और गर्म होने पर ढीली पड़ती स्ट्रैप — ये सब सेंसर को पल्स पहचानने से रोकते हैं। यहां कुछ न मिलना सामान्य है, कोई खराबी नहीं।", + "activitySummaryNoPulseTitle": "इस {activity} सत्र के लिए कोई पल्स रीडिंग नहीं", + "activitySummaryOneMinutePulse": "सिर्फ़ एक मिनट की पल्स, और कुछ नहीं", + "activitySummaryPulseGapNote": "बैंड को {total} मिनट में से {have} मिनट पल्स मिली। ये गैप अपेक्षित हैं, इसलिए जो दिखाया गया है वह वही हिस्सा है जिसे यह देख पाया।", + "activitySummaryTooShortTitle": "चार्ट बनाने के लिए बहुत छोटा", + "activitySummaryTooShortBody": "एक मिनट की हृदय गति एक बिंदु है, कोई रेखा नहीं।", + "activitySummaryNoHrTitle": "इस सत्र के लिए कोई हृदय गति नहीं", + "activitySummaryNoHrBody": "इसके चलने के दौरान बैंड ने कुछ भी रिपोर्ट नहीं किया।", + "activitySummaryCheckBandConnection": "बैंड कनेक्शन जांचें", + "activitySummaryPartialTrace": "आंशिक ट्रेस — बैंड ने इन मिनटों में से केवल {pct}% ही भेजा।", + "activitySummaryHeartRateTitle": "हृदय गति", + "activitySummaryHardMinutesNote": "आपके अधिकतम के 80% से ऊपर {min} मिनट।", + "activitySummaryTimeInZonesTitle": "ज़ोन में बिताया समय", + "activitySummaryTopSet": "सबसे भारी सेट", + "activitySummaryOneRepMax": "अनुमानित 1RM {kg} किग्रा", + "activitySummarySomeSetsNoLoadTitle": "कुछ सेट में वज़न नहीं था", + "activitySummarySomeSetsNoLoadBody": "सेट और रेप्स में गिना गया, लेकिन आयतन में शामिल नहीं।", + "activitySummaryScore": "स्कोर", + "activitySummaryGameSetLabel": "सेट {n}", + "activitySummaryNoSplitsTitle": "इस सत्र के लिए कोई स्प्लिट नहीं", + "activitySummaryNoSplitsBody": "स्प्लिट के लिए दर्ज दूरी ज़रूरी है।", + "activitySummaryKm": "किमी", + "activitySummaryPace": "पेस", + "activitySummaryHr": "हृदय गति", + "activitySummarySetsLoggedZero": "0 सेट दर्ज।", + "activitySummaryRoundHeader": "राउंड", + "activitySummaryWorkHeader": "मेहनत", + "activitySummaryRestHeader": "आराम", + "activitySummaryAvgBpm": "औसत BPM", + "activitySummaryLapHeader": "चक्कर", + "activitySummaryTimeHeader": "समय", + "activitySummarySpeedVsFastest": "गति बनाम सबसे तेज़", + "activitySummaryBodyweightReps": "{n, plural, one{{n} रेप · शरीर का वज़न} other{{n} रेप्स · शरीर का वज़न}}", + "activitySummaryRpeValue": "RPE {v}", + "activitySummaryNothingToPlot": "इस {activity} सत्र के लिए चार्ट बनाने को कुछ नहीं", + "activitySummaryNoSeriesTitle": "चार्ट बनाने के लिए कोई सीरीज़ नहीं", + "activitySummaryNoSeriesBody": "इस सत्र में कोई प्रति-मिनट स्ट्रीम दर्ज नहीं हुई।", + "activitySummaryHeartRateZones": "हृदय गति ज़ोन", + "activitySummaryTabOverview": "अवलोकन", + "activitySummaryTabSplits": "स्प्लिट", + "activitySummaryTabGraphs": "ग्राफ़", + "activityLiveAddALap": "एक चक्कर जोड़ें", + "activityLiveAddExerciseTitle": "व्यायाम जोड़ें", + "activityLiveAllowLocation": "लोकेशन की अनुमति दें", + "activityLiveBestLabel": "सर्वश्रेष्ठ", + "activityLiveBodyweightExcludedNote": "बॉडीवेट — वॉल्यूम में शामिल नहीं", + "activityLiveBodyweightOnly": "केवल बॉडीवेट", + "activityLiveBpmUnit": "bpm", + "activityLiveBwAbbrev": "BW", + "activityLiveChangeStroke": "स्ट्रोक बदलें", + "activityLiveDecrease": "{label} घटाएं", + "activityLiveDeniedForeverBody": "इस ऐप के लिए लोकेशन अस्वीकृत है, इसे केवल सेटिंग्स से बदला जा सकता है।", + "activityLiveDurationHeader": "अवधि", + "activityLiveEffortRpeHeader": "प्रयास (RPE)", + "activityLiveEndSet": "सेट समाप्त करें", + "activityLiveExerciseOf": "व्यायाम {index} में से {total}", + "activityLiveFinishSessionLabel": "सेशन खत्म करें", + "activityLiveHoldTime": "रोकें · {time}", + "activityLiveIncrease": "{label} बढ़ाएं", + "activityLiveIntervalSubtitle": "{workSec} सेकंड काम · {restSec} सेकंड आराम", + "activityLiveKcalEstUnit": "kcal · अनुमानित", + "activityLiveKgVolumeUnit": "kg वॉल्यूम", + "activityLiveLapButtonLabel": "चक्कर", + "activityLiveLapsChartTitle": "चक्कर", + "activityLiveLapsCount": "{count} चक्कर · {stroke}", + "activityLiveLapsFootnote": "सबसे तेज़ {time} · बार की लंबाई उसकी तुलना में गति दिखाती है।", + "activityLiveLapXLabel": "चक्कर {n}", + "activityLiveLogAsBodyweight": "बॉडीवेट के रूप में दर्ज करें", + "activityLiveMatchSetSubtitle": "सेट {n}", + "activityLiveMetrePoolLabel": "{len} मीटर पूल", + "activityLiveMinimiseLabel": "छोटा करें", + "activityLiveNextExercise": "अगला व्यायाम", + "activityLiveNextLabel": "अगला", + "activityLiveNextPose": "अगला आसन", + "activityLiveNextRest": "आराम · {time}", + "activityLiveNextWork": "काम · {time}", + "activityLiveNoHrBody": "बैंड कनेक्ट नहीं है, इसलिए इस सेशन के लिए कोई डेटा नहीं आ रहा।", + "activityLiveNoHrTitle": "हृदय गति नहीं", + "activityLiveNoHrYetBody": "बैंड कनेक्ट है लेकिन अभी तक कोई धड़कन दर्ज नहीं हुई है — इसे कलाई की हड्डी से एक उंगली ऊपर, कसकर पहनना चाहिए।", + "activityLiveNoHrYetTitle": "अभी तक हृदय गति नहीं", + "activityLiveNoneYet": "अभी तक कोई नहीं", + "activityLiveNoRouteFailedBody": "लोकेशन मांगने पर फोन ने त्रुटि लौटाई।", + "activityLiveNoRouteFailedTitle": "कोई रूट नहीं: लोकेशन विफल", + "activityLiveNoRouteNotAllowedTitle": "कोई रूट नहीं: लोकेशन की अनुमति नहीं", + "activityLiveNoRouteOffBody": "इस फोन पर लोकेशन सेवाएं बंद हैं, इसलिए कोई लोकेशन नहीं आ रही।", + "activityLiveNoRouteOffTitle": "कोई रूट नहीं: लोकेशन बंद है", + "activityLiveOneLapFewer": "एक चक्कर कम करें", + "activityLiveOpenSettings": "सेटिंग्स खोलें", + "activityLiveOpponentLabel": "प्रतिद्वंद्वी", + "activityLivePauseLabel": "रोकें", + "activityLivePerLapUnit": "प्रति चक्कर", + "activityLivePointLabel": "{side} का अंक", + "activityLivePoolSubtitle": "{len}M पूल · {stroke}", + "activityLivePoseBridge": "ब्रिज", + "activityLivePoseChair": "चेयर", + "activityLivePoseChildsPose": "चाइल्ड पोज़", + "activityLivePoseForwardFold": "फॉरवर्ड फोल्ड", + "activityLivePoseMountain": "माउंटेन", + "activityLivePoseOf": "आसन {index} में से {total}", + "activityLivePosePigeon": "पिजन", + "activityLivePosePlank": "प्लैंक", + "activityLivePoseSavasana": "शवासन", + "activityLivePoseTriangle": "त्रिकोण", + "activityLivePoseWarriorTwo": "वारियर II", + "activityLivePreviousExercise": "पिछला व्यायाम", + "activityLivePreviousLabel": "पिछला", + "activityLivePrivateSession": "निजी सेशन", + "activityLiveRecordingRoute": "रूट रिकॉर्ड हो रहा है", + "activityLiveRepsBodyweightRow": "{n} रेप्स · बॉडीवेट", + "activityLiveRepsLabel": "रेप्स", + "activityLiveRepsLoggedBodyweight": "{n} रेप्स दर्ज", + "activityLiveRepsOnly": "{n} रेप्स", + "activityLiveRepsUnit": "रेप्स", + "activityLiveRestingHeader": "आराम कर रहे हैं", + "activityLiveRestWord": "आराम", + "activityLiveResumeLabel": "फिर से शुरू करें", + "activityLiveRoundLabel": "राउंड {n}", + "activityLiveRouteFootnoteNoDistance": "शुरुआती बिंदु सेट हो गया; लोकेशन स्थिर होने पर दूरी दिखेगी।", + "activityLiveRouteFootnoteWithDistance": "अब तक दर्ज लोकेशन के अनुसार {distance}।", + "activityLiveRouteSoFarTitle": "अब तक का रूट", + "activityLiveSetNumber": "सेट {n}", + "activityLiveSetsCountSubtitle": "{n} सेट", + "activityLiveSetsListHeader": "सेट्स", + "activityLiveSetsUnit": "सेट्स", + "activityLiveStepsUnit": "कदम", + "activityLiveStrainUnit": "स्ट्रेन", + "activityLiveStrokeBack": "बैकस्ट्रोक", + "activityLiveStrokeBreast": "ब्रेस्टस्ट्रोक", + "activityLiveStrokeFly": "बटरफ्लाई", + "activityLiveStrokeFree": "फ्रीस्टाइल", + "activityLiveThisExerciseLabel": "यह व्यायाम", + "activityLiveTimeInZonesTitle": "ज़ोन में बिताया समय", + "activityLiveTimeUnit": "समय", + "activityLiveTryAgain": "फिर कोशिश करें", + "activityLiveTurnOnLocation": "लोकेशन चालू करें", + "activityLiveVolumeSetsSubtitle": "{kg} kg · {n} सेट", + "activityLiveWeightLabel": "वज़न", + "activityLiveWeightRepsLogged": "{kg} kg × {n} दर्ज", + "activityLiveWorkWord": "काम", + "activityLiveYouLabel": "आप", + "activityLiveZoneLabel": "ज़ोन {z}", + "activityLiveLogSet": "सेट दर्ज करें", + "activityLiveRestOverAnnounce": "आराम खत्म", + "activityLiveSkipRest": "आराम छोड़ें", + "gesturesNavTitle": "डबल-टैप", + "gesturesSectionTitle": "बैंड को दो बार टैप करें", + "gesturesSectionBody": "केवल तभी जब ऐप कनेक्टेड और सक्रिय हो। यदि आपका फ़ोन दूर था तो बैंड द्वारा सहेजा गया टैप पुराने टाइमस्टैंप के साथ बाद में पहुंचता है, और उसे कई घंटे बाद चलाने के बजाय अनदेखा कर दिया जाता है।", + "gesturesItDoesTitle": "यह करता है", + "gesturesNoPhoneActionsTitle": "फ़ोन पर कुछ नहीं?", + "gesturesNoPhoneActionsBody": "अपने फ़ोन को बजाना और फ़्लैशलाइट गायब हैं क्योंकि ऐप यह पूछने के लिए सिस्टम तक नहीं पहुंच सकी कि यह डिवाइस क्या अनुमति देता है। ऐप को फिर से खोलें और वापस आएं; ऊपर दी गई इन-ऐप कार्रवाइयाँ फिर भी काम करती हैं।", + "settingsBarcodeSaveFailed": "यह सहेजा नहीं जा सका — अगली बार ऐप खोलने पर यह वापस आ सकता है।", + "settingsIconRowTitle": "आइकन", + "settingsIconRowConfirmHint": "iPhone आपसे पुष्टि करने के लिए कहेगा", + "settingsIconChoiceLabel": "{label} आइकन।", + "settingsSelectedSuffix": " चयनित।", + "settingsHealthSyncOff": "बंद है। {store} में कुछ भी नहीं लिखा जाता", + "settingsHealthSyncReady": "हर दिन की नींद, विश्राम हृदय गति, HRV, श्वसन दर, ऊर्जा और वर्कआउट अंतिम होने पर {store} में लिखे जाते हैं", + "settingsHealthSyncNeedsPermission": "{store} ने लिखने की अनुमति नहीं दी है। इसे खोलने के लिए टैप करें", + "settingsHealthSyncNotInstalled": "Health Connect इंस्टॉल नहीं है। इसे पाने के लिए टैप करें", + "settingsHealthSyncNeedsUpdate": "Health Connect में लिखने के लिए बहुत पुराना है। इसे अपडेट करने के लिए टैप करें", + "settingsHealthSyncUnsupported": "इस डिवाइस में लिखने के लिए कोई हेल्थ स्टोर नहीं है", + "settingsHealthSyncChecking": "{store} की जाँच हो रही है…", + "settingsWriteToHealthStoreRowTitle": "{store} में लिखें", + "settingsHealthShareOffTitle": "योगदान बंद", + "settingsHealthShareOffNeverUploaded": "कभी कुछ भी अपलोड नहीं हुआ। अब कुछ भी अपलोड नहीं होगा।", + "settingsHealthShareOffDetail": "आगे कुछ भी अपलोड नहीं होगा।\n\nआपके डेटाबेस की एक प्रति {date} को अपलोड की गई थी। सर्वर प्रति डिवाइस केवल सबसे हालिया प्रति रखता है। हमने उसे बताने की कोशिश की कि आपकी सहमति वापस ले ली गई है — वह संदेश एक बार भेजा जाता है और फिर से कोशिश नहीं की जाती, इसलिए यदि यह फ़ोन ऑफ़लाइन था तो वह नहीं पहुंचा होगा, और हम आपको यह भी नहीं दिखा सकते कि वह प्रति हटा दी गई है।", + "settingsOk": "ठीक है", + "settingsHealthShareOnTitle": "क्या आप अपना स्वास्थ्य डेटा योगदान करना चाहते हैं?", + "settingsHealthShareOnBody": "दिन में एक बार, Wi-Fi पर और चार्जिंग के दौरान, आपके पूरे डेटाबेस की एक संकुचित प्रति अपलोड की जाती है — हर व्युत्पन्न दिन और बैंड द्वारा भेजी गई हर कच्ची सेंसर पंक्ति। इसका उपयोग एल्गोरिदम को बेहतर बनाने के लिए किया जाता है।\n\nयह किसी भी सार्थक अर्थ में गुमनाम नहीं है: यह आपका पूरा स्वास्थ्य इतिहास है। आप इसे किसी भी समय बंद कर सकते हैं, और उस क्षण से आगे कुछ भी नहीं भेजा जाएगा।", + "settingsNo": "नहीं", + "settingsContribute": "योगदान करें", + "settingsResetTitle": "सब कुछ मिटा दें?", + "settingsResetBody": "यह स्थायी रूप से और कहीं भी कोई प्रति रखे बिना मिटा देता है:\n\n· हर मापा गया दिन, नींद, वर्कआउट और रूट\n· हर लैब परिणाम, भोजन, दवा की खुराक, आदत, श्वास सत्र और लॉग किया गया सेट\n· आपकी डायरी, चक्र लॉग और चलती बेसलाइन\n· आपकी प्रोफ़ाइल, हर प्राथमिकता और कोई भी सहेजी गई AI कुंजी\n· होम-स्क्रीन विजेट और हर शेड्यूल किया गया रिमाइंडर\n\nबैंड की जोड़ी हटा दी जाती है, और वह पहले से भेजा गया इतिहास दोबारा नहीं भेज सकता। यदि आपको एक प्रति चाहिए तो पहले 'आपका डेटा' से एक्सपोर्ट करें।", + "settingsResetKeepData": "मेरा डेटा रखें", + "settingsResetDeleteEverything": "सब कुछ मिटाएँ", + "settingsNavTitle": "सेटिंग्स", + "settingsGroupTheBand": "बैंड", + "settingsAlarmRowTitle": "अलार्म", + "settingsAlarmRowSub": "आपकी कलाई पर बजता है, बैंड की अपनी घड़ी के अनुसार", + "settingsGroupThisPhone": "यह फ़ोन", + "settingsStepsRowTitle": "कदम", + "settingsStepsRowSub": "इस फ़ोन का अपना कदम काउंटर, उन घंटों के लिए जिन्हें बैंड कवर नहीं करता। कुछ भी डिवाइस से बाहर नहीं जाता", + "settingsGroupNotifications": "सूचनाएं", + "settingsManageNotificationsRowTitle": "सूचनाएं प्रबंधित करें", + "settingsManageNotificationsRowSub": "क्या आपको बाधित कर सकता है, शांत घंटे, और उन सभी के लिए बंद स्विच", + "settingsGroupPreferences": "प्राथमिकताएं", + "settingsUnitsRowTitle": "इकाइयां", + "settingsAppearanceRowTitle": "रूप", + "settingsCycleTrackingRowTitle": "चक्र ट्रैकिंग", + "settingsCycleTrackingRowSub": "वेलनेस में साइकिल टैब जोड़ता है। बंद करने पर यह छिप जाता है और पहले से लॉग किया गया सब कुछ बना रहता है", + "settingsGroupYourData": "आपका डेटा", + "settingsExportBackupImportRowTitle": "एक्सपोर्ट, बैकअप, इम्पोर्ट", + "settingsExportBackupImportRowSub": "स्प्रेडशीट, एक पूर्ण प्रति, और इतिहास लाना", + "settingsGroupAutomation": "स्वचालन", + "settingsDoubleTapRowTitle": "डबल-टैप", + "settingsDoubleTapRowSub": "बैंड पर डबल-टैप करने से क्या होता है", + "settingsTaskerShortcutsRowTitle": "Tasker और शॉर्टकट्स", + "settingsTaskerShortcutsRowSub": "बाहर जाने वाले इवेंट के लिए केवल Android। iOS बैंड को बजा सकता है लेकिन उससे ट्रिगर नहीं हो सकता", + "settingsGroupPrivacy": "गोपनीयता", + "settingsCrashReportsRowTitle": "क्रैश रिपोर्ट", + "settingsCrashReportsRowSub": "जब तक आप न कहें, कुछ भी नहीं भेजा जाता", + "settingsBarcodeLookupRowTitle": "बारकोड ऑनलाइन देखें", + "settingsBarcodeLookupRowSub": "स्कैन किया गया बारकोड openfoodfacts.org पर भेजता है। इसके साथ आपके बारे में कुछ भी नहीं जाता", + "settingsContributeHealthDataRowTitle": "मेरा स्वास्थ्य डेटा योगदान करें", + "settingsContributeHealthDataRowSub": "एल्गोरिदम को बेहतर बनाने के लिए दिन में एक बार, Wi-Fi पर और चार्जिंग के दौरान आपका पूरा डेटाबेस अपलोड करता है", + "settingsCheckForUpdatesRowTitle": "अपडेट के लिए जांचें", + "settingsUpdateBelowMinimum": "यह बिल्ड न्यूनतम समर्थित बिल्ड से नीचे है। GitHub से नया रिलीज़ इंस्टॉल करें", + "settingsUpdateAvailable": "GitHub पर एक नया बिल्ड प्रकाशित हुआ है", + "settingsUpdateCheckSub": "लॉन्च पर रिलीज़ सर्वर से पूछता है। यह आपका IP पता और ऐप खोलने का समय देखता है", + "settingsGroupAbout": "बारे में", + "settingsVersionRowTitle": "संस्करण", + "settingsNoticesLicencesRowTitle": "सूचनाएं और लाइसेंस", + "settingsNoticesLicencesRowSub": "यह ऐप कौन नहीं है, और यह किसका डेटा उपयोग करता है", + "settingsGroupDeveloper": "डेवलपर", + "settingsComponentGalleryRowTitle": "कंपोनेंट गैलरी", + "settingsComponentGalleryRowSub": "किसी भी टेक्स्ट स्केल पर, किसी भी थीम में हर कंपोनेंट", + "settingsDeveloperModeRowTitle": "डेवलपर मोड", + "settingsResetAllDataRowTitle": "सारा डेटा रीसेट करें", + "settingsNotificationsNavTitle": "सूचनाएं", + "settingsNotificationsNavSub": "क्या आपको बाधित कर सकता है", + "settingsNotificationsOffSystemTitle": "सूचनाएं सिस्टम स्तर पर बंद हैं", + "settingsNotificationsOffSystemBody": "जब तक OS इसकी अनुमति नहीं देता, नीचे दिया गया कुछ भी आप तक नहीं पहुंच सकता।", + "settingsTurnThemOn": "उन्हें चालू करें", + "settingsGroupManageNotifications": "सूचनाएं प्रबंधित करें", + "settingsHealthExceptionsRowTitle": "स्वास्थ्य अपवाद", + "settingsHealthExceptionsRowSub": "दिन में अधिकतम एक बार, और केवल तभी जब आपकी अपनी बेसलाइन में कुछ बदला हो", + "settingsBandAlertsRowTitle": "बैंड अलर्ट", + "settingsBandAlertsRowSub": "बैटरी खत्म, चार्जर पर, चुप हो गया", + "settingsAlertMeAtRowTitle": "मुझे अलर्ट करें", + "settingsAlertMeAtRowSub": "जब बैंड इस चार्ज स्तर से नीचे गिरे तो चेतावनी दें", + "settingsRecoveryReadyRowTitle": "रिकवरी तैयार", + "settingsRecoveryReadyRowSub": "जब आपका सुबह का रिकवरी स्कोर आता है तो एक नोट", + "settingsWeeklyLookbackRowTitle": "साप्ताहिक लुकबैक", + "settingsWeeklyLookbackRowSub": "रविवार शाम, लेकिन केवल उस सप्ताह के लिए जिसमें वास्तव में कुछ मिला हो। ज़्यादातर सप्ताह शांत रहते हैं", + "settingsDetectedWorkoutsRowTitle": "पहचाने गए वर्कआउट", + "settingsDetectedWorkoutsRowSub": "उन प्रयासों के बारे में पूछता है जिन्हें बैंड ने पहचाना लेकिन आपने शुरू नहीं किए। बंद करने पर प्रॉम्प्ट और समीक्षा कार्ड छिप जाते हैं; बैंड फिर भी मापता रहता है", + "settingsMovementNudgeRowTitle": "मूवमेंट नज", + "settingsMovementNudgeRowSub": "स्थिर अवधि के बाद आपको याद दिलाता है — बिना किसी हलचल के दो घंटे, या डेस्क मुद्रा में 90 मिनट। फ़ोन नोटिफ़िकेशन के साथ बैंड कनेक्टेड होने पर एक बज़", + "settingsWindDownRowTitle": "विंड-डाउन", + "settingsWindDownRowSub": "आपकी अपनी रातों से सीखे गए सोने के समय से लगभग 45 मिनट पहले एक संकेत, जो आपके शांत घंटों से अलग रखा जाता है। लगभग एक सप्ताह पहनने के बाद दिखाई देता है", + "settingsStepGoalAlertsRowTitle": "स्टेप गोल अलर्ट", + "settingsStepGoalAlertsRowSub": "आपको एक बार बताता है जब आज आपका स्टेप्स लक्ष्य पार हो जाता है", + "settingsMedicationRemindersRowTitle": "दवा रिमाइंडर", + "settingsMedicationRemindersRowSub": "हर निर्धारित खुराक के लिए, आपके द्वारा दर्ज किए गए समय पर एक सूचना — बैंड कनेक्टेड होने पर एक बज़ के साथ। पहले से ली गई या छोड़ी गई खुराक के लिए कुछ नहीं भेजा जाता", + "settingsDailyCheckInRowTitle": "दैनिक चेक-इन", + "settingsDailyCheckInRowSub": "शाम को दिन लिखने के लिए एक प्रॉम्प्ट — मूड, ऊर्जा, तनाव। यदि दिन में पहले से रेटिंग है तो छोड़ दिया जाता है", + "settingsWaterReminderRowTitle": "पानी रिमाइंडर", + "settingsWaterReminderRowSub": "आपके जागने के घंटों के दौरान, पेय लॉग करने की याद दिलाने के लिए स्ट्रैप पर एक बज़ और आपके फ़ोन पर एक सूचना। दोनों ही स्थिति में कुछ भी मापा नहीं जाता", + "settingsRemindMeEveryRowTitle": "मुझे हर बार याद दिलाएं", + "settingsGroupTheStrap": "स्ट्रैप", + "settingsBuzzOnAppNotificationsRowTitle": "ऐप सूचनाओं पर बज़", + "settingsBuzzOnAppNotificationsRowSub": "चुनें कि कौन से फ़ोन ऐप स्ट्रैप को बजाते हैं", + "settingsGroupQuietHours": "शांत घंटे", + "settingsQuietHoursRowTitle": "शांत घंटे", + "settingsQuietHoursRowSub": "इस समय के भीतर कुछ भी नहीं बजता", + "settingsQuietHoursStartsRowTitle": "शुरू होता है", + "settingsQuietHoursEndsRowTitle": "समाप्त होता है", + "settingsHealthExceptionsBreakThroughRowTitle": "स्वास्थ्य अपवाद शांत घंटों को तोड़ते हैं", + "settingsAlarmNotOnListTitle": "अलार्म इस सूची में नहीं है", + "settingsAlarmNotOnListBody": "इसके बजाय अलार्म स्क्रीन पर इसे रद्द करें।", + "settingsImportNoPermission": "{store} ने वे फ़ील्ड नहीं दीं। कुछ भी नहीं पढ़ा गया।", + "settingsImportEmptyWithBirthday": "कुछ भी वापस नहीं आया। {store} के पास आपकी लंबाई, वज़न, जन्मदिन या लिंग नहीं है — इसके बजाय उन्हें यहां टाइप करें।", + "settingsImportEmpty": "कुछ भी वापस नहीं आया। {store} के पास आपकी लंबाई, वज़न या लिंग नहीं है — इसके बजाय उन्हें यहां टाइप करें।", + "settingsImportNoChange": "{fields} पढ़ा गया। आपकी प्रोफ़ाइल पहले से ही वही बात कहती है, इसलिए कुछ नहीं बदला।", + "settingsImportUpdated": "{store} से {fields} अपडेट किया गया।", + "settingsImportFailed": "विफल: {error}", + "settingsAgeFieldLabel": "आयु", + "settingsEditProfileNavTitle": "प्रोफ़ाइल संपादित करें", + "settingsNameFieldLabel": "नाम", + "settingsSexFieldLabel": "लिंग", + "settingsSexMale": "पुरुष", + "settingsSexFemale": "महिला", + "settingsSexPreferNotToSay": "बताना नहीं चाहते", + "settingsAgeYearsFieldLabel": "आयु (वर्ष)", + "settingsFourFieldsTitle": "ये चार चीज़ें आपके नंबर बदलती हैं", + "settingsFourFieldsBody": "ये हृदय गति ज़ोन, कैलोरी अनुमान और प्रशिक्षण भार को फीड करती हैं। एक को साफ़ करें और केवल उसकी ज़रूरत वाले मेट्रिक्स अनुपलब्ध रहेंगे।", + "settingsImportBlockAppleHealth": "लंबाई, वज़न, जन्मदिन और लिंग, सीधे {store} से। लंबाई और वज़न हर बार लिए जाते हैं; आपकी आयु और लिंग केवल एक कमी को भरते हैं, क्योंकि दोनों में बदलाव नहीं होता और यहां पहले से मौजूद मान आपकी अपनी पसंद था।", + "settingsImportBlockOther": "लंबाई और वज़न, सीधे {store} से। इसके पास पढ़ने के लिए न जन्मदिन है न लिंग — कोई भी ऐप नहीं पढ़ सकता — इसलिए वे दोनों ऊपर खुद सेट करें।", + "settingsNotSetHint": "सेट नहीं है", + "settingsAutomationNavTitle": "स्वचालन", + "settingsSyncFinishesSectionTitle": "जब सिंक पूरा हो जाता है", + "settingsSyncFinishesAndroidBody": "ऐप एक इंटेंट प्रसारित करता है जिस पर आपका ऑटोमेशन ऐप एक प्रोफ़ाइल शुरू कर सकता है। नीचे दिए गए एक्शन पर फ़िल्टर करें; यह बताता है कि कितने रिकॉर्ड कब पहुंचे, अधिकतम एक प्रति मिनट।", + "settingsSyncFinishesIosBody": "iOS ऐसा नहीं कर सकता। एक शॉर्टकट्स व्यक्तिगत ऑटोमेशन केवल Apple की अपनी निश्चित इवेंट सूची पर ट्रिगर हो सकता है, और कोई भी ऐप एक नहीं जोड़ सकता — इसलिए यहां कुछ भी आपके लिए शॉर्टकट शुरू नहीं कर सकता। Android में यह है; यह एक प्लेटफ़ॉर्म सीमा है, सेटिंग नहीं।", + "settingsSyncFinishesExtras": "अतिरिक्त: records (int), at (यूनिक्स सेकंड)", + "settingsNeverSendSectionTitle": "यह कभी क्या नहीं भेजेगा", + "settingsNeverSendBody": "न रेडीनेस, न स्ट्रेन, न स्लीप स्कोर — दोनों प्लेटफ़ॉर्म पर। एक नंबर जिसे यह ऐप कारण सहित अनुपस्थित दिखाता, बाहर जाते ही एक साधारण शून्य बन जाता है। सिंक के बारे में तथ्य बाहर जाते हैं; माप नहीं।", + "settingsBuzzFromShortcutSectionTitle": "शॉर्टकट से बैंड को बजाना", + "settingsBuzzFromShortcutAndroidBody": "इस टोकन को “token” स्ट्रिंग एक्स्ट्रा के रूप में wtf.openstrap.openstrap_edge.BUZZ_STRAP भेजें। इसके बिना फ़ोन पर कोई भी ऐप आपके बैंड को बजा सकता है।", + "settingsBuzzFromShortcutIosBody": "यह दिशा iOS पर काम करती है: एक शॉर्टकट जिसे आप खुद चलाते हैं वह ऐप तक पहुंच सकता है। यह जो नहीं कर सकता वह है जब बैंड सिंक हो तो खुद चलना।", + "settingsNoTokenYet": "अभी तक कोई टोकन नहीं — इस स्क्रीन को फिर से खोलें।", + "settingsCopied": "कॉपी हो गया", + "settingsCopyTheToken": "टोकन कॉपी करें", + "bandStatusBluetoothDeniedTitle": "इस ऐप के लिए ब्लूटूथ बंद है", + "bandStatusBluetoothDeniedReason": "फ़ोन OpenStrap को ब्लूटूथ रेडियो नहीं दे रहा, इसलिए कुछ भी स्कैन या कनेक्ट नहीं हो सकता। यह बैंड की गलती नहीं है — पास जाने से मदद नहीं मिलेगी।", + "bandStatusBluetoothDeniedFix": "सेटिंग्स → OpenStrap खोलें और ब्लूटूथ की अनुमति दें", + "bandStatusBluetoothOffTitle": "ब्लूटूथ बंद है", + "bandStatusBluetoothOffReason": "फ़ोन का रेडियो बंद है, इसलिए कोई भी ऐप बैंड तक नहीं पहुँच सकता। इस दौरान बैंड रिकॉर्ड करता रहता है; कुछ भी नहीं खोता।", + "bandStatusBluetoothOffFix": "ब्लूटूथ चालू करें", + "bandStatusBluetoothUnsupportedTitle": "इस फ़ोन में ब्लूटूथ लो एनर्जी रेडियो नहीं है", + "bandStatusBluetoothUnsupportedReason": "बैंड तक केवल ब्लूटूथ लो एनर्जी (BLE) से पहुँचा जा सकता है। आयात किया गया डेटा फिर भी काम करता है; लाइव लिंक नहीं।", + "bandStatusReconnectPausedTitle": "फिर से जोड़ना रोक दिया गया है", + "bandStatusReconnectPausedReason": "{n, plural, other{बैंड ने पेयरिंग की एक ही बार में लगातार {n} बार अस्वीकार कर दी, इसलिए ऐप ने दोबारा कोशिश करना बंद कर दिया — ताकि रेडियो को रोके रखकर और दोनों बैटरी खत्म करके भी एक ऐसा लिंक न बनाया जाए जो खुलेगा ही नहीं। जब तक आप कुछ नहीं करते, कुछ भी दोबारा नहीं जुड़ेगा।}}", + "bandStatusRepairNeededTitle": "बैंड को फिर से पेयर करना होगा", + "bandStatusRepairNeededReason": "लिंक तो बन जाता है, लेकिन बैंड फ़ोन की एन्क्रिप्शन कुंजी अस्वीकार कर देता है, इसलिए हर कमांड छोड़ दिया जाता है और कोई डेटा नहीं जाता। आपकी रिकॉर्डिंग बैंड पर सुरक्षित हैं।", + "bandStatusRepairFix": "फ़ोन की ब्लूटूथ सेटिंग्स में बैंड को भूलें, फिर यहाँ दोबारा पेयर करें", + "bandStatusSyncStuckTitle": "रिकॉर्डिंग का एक बैच पूरी तरह ट्रांसफ़र नहीं हो पा रहा", + "bandStatusSyncStuckReason": "बैंड बार-बार वही बैच भेजता रहता है क्योंकि ऐप उसकी पुष्टि उस तक नहीं पहुँचा पा रहा। इसमें मौजूद हर चीज़ पहले ही यहाँ सेव हो चुकी है — कुछ नहीं खोया — लेकिन पुष्टि मिले बिना बैंड आगे नहीं बढ़ सकता।", + "bandStatusSyncStuckFix": "बैंड को फिर से कनेक्ट करें; अगर यह कल फिर हो, तो दोबारा पेयर करें", + "bandStatusStrapUnresponsiveTitle": "बैंड ने अपनी रिकॉर्डिंग देना बंद कर दिया है", + "bandStatusStrapUnresponsiveReason": "बैंड ऐसी नई रिकॉर्डिंग बता रहा है जो वह भेज नहीं रहा। वे रिकॉर्डिंग अब भी बैंड पर सुरक्षित हैं; बस भेजी नहीं जा रहीं।", + "bandStatusStrapUnresponsiveFix": "बैंड को एक मिनट के लिए चार्जर पर रखें, फिर दोबारा कनेक्ट करें", + "bandStatusClockLostTitle": "सिंक तो पूरे हो रहे हैं पर उनमें कोई डेटा नहीं है", + "bandStatusClockLostReason": "बैंड हर सिंक तो पूरा करता है पर एक भी सेंसर रीडिंग नहीं देता — इसका लगभग हमेशा मतलब है कि उसकी अंदरूनी घड़ी सिंक खो चुकी है। ऐप हर कनेक्शन पर इसे फिर से सेट करता है।", + "bandStatusClockLostFix": "बैंड को कुछ मिनट कनेक्ट रहने दें; अगर कल तक कुछ न आए, तो दोबारा पेयर करें", + "bandStatusConnectedReason": "बैंड जुड़ा हुआ है और अपनी रिकॉर्डिंग दे रहा है।", + "bandStatusConnectingTitle": "कनेक्ट हो रहा है", + "bandStatusConnectingReason": "बैंड से लिंक खोला जा रहा है।", + "bandStatusScanningTitle": "बैंड खोजा जा रहा है", + "bandStatusScanningReason": "बैंड के सिग्नल भेजने का इंतज़ार किया जा रहा है।", + "bandStatusDisconnectedReason": "बैंड सीमा से बाहर है, चार्जर पर है, या किसी दूसरे ऐप से जुड़ा है। दोनों ही स्थितियों में यह रिकॉर्ड करता रहता है।", + "bandStatusDisconnectedFix": "बैंड को फ़ोन के पास लाएँ, और उससे जुड़े किसी भी दूसरे ऐप को बंद करें", + "devicesTierBeatToBeatLabel": "धड़कन-दर-धड़कन अंतराल", + "devicesTierBeatToBeatDetail": "इलेक्ट्रिकल R-पीक डिटेक्शन।", + "devicesTierWristOpticalLabel": "कलाई पर ऑप्टिकल पल्स", + "devicesTierWristOpticalDetail": "लगातार 24/7 पल्स, नींद और तापमान। धड़कन की समयावधि पल्स वेव से अनुमानित होती है, इसलिए यहाँ HRV असल में PRV है।", + "devicesTierPhoneLabel": "केवल कदम", + "devicesTierPhoneDetail": "फ़ोन का अपना मोशन को-प्रोसेसर। सिर्फ़ कदम, और कुछ नहीं।", + "deviceActionNoneLabel": "कुछ न करें", + "deviceActionNoneBlurb": "डबल-टैप से कुछ नहीं होता।", + "deviceActionMediaPlayPauseLabel": "संगीत चलाएँ / रोकें", + "deviceActionMediaPlayPauseBlurb": "जो भी चल रहा है उसे टॉगल करें।", + "deviceActionMediaNextLabel": "अगला ट्रैक", + "deviceActionMediaNextBlurb": "अगले ट्रैक पर जाएँ।", + "deviceActionMediaPrevLabel": "पिछला ट्रैक", + "deviceActionMediaPrevBlurb": "पिछले ट्रैक पर वापस जाएँ।", + "deviceActionVolumeUpLabel": "आवाज़ बढ़ाएँ", + "deviceActionVolumeUpBlurb": "मीडिया वॉल्यूम एक कदम बढ़ाएँ।", + "deviceActionVolumeDownLabel": "आवाज़ घटाएँ", + "deviceActionVolumeDownBlurb": "मीडिया वॉल्यूम एक कदम घटाएँ।", + "deviceActionRingPhoneLabel": "मेरा फ़ोन बजाएँ", + "deviceActionRingPhoneBlurb": "अपना फ़ोन खोजने के लिए एक तेज़ आवाज़ बजाएँ।", + "deviceActionTorchLabel": "फ़्लैशलाइट", + "deviceActionTorchBlurb": "अपने फ़ोन की फ़्लैशलाइट चालू/बंद करें।", + "deviceActionMarkMomentLabel": "एक पल चिह्नित करें", + "deviceActionMarkMomentBlurb": "अपनी जर्नल में मौजूदा पल को टैग करें।", + "deviceActionWorkoutToggleLabel": "वर्कआउट शुरू / बंद करें", + "deviceActionWorkoutToggleBlurb": "अपनी कलाई से वर्कआउट शुरू या समाप्त करें।", + "deviceActionLogWaterLabel": "पानी दर्ज करें", + "deviceActionLogWaterBlurb": "आज के पानी में एक गिलास जोड़ें, न्यूट्रिशन स्क्रीन पर + जैसा ही कदम।", + "deviceActionBroadcastToTaskerLabel": "Tasker को ब्रॉडकास्ट करें", + "deviceActionBroadcastToTaskerBlurb": "एक ब्रॉडकास्ट इंटेंट भेजें ताकि Tasker कोई भी ऑटोमेशन चला सके।" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 013caf80..6a256c8f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -293,5 +293,2017 @@ "welcomePartOfFileNotUsedTitle": "该文件的部分内容无法使用", "welcomeStrandedDays": "{n, plural, other{{n} 天数据顺序错乱,仅作为后一天的上下文使用。}}", "welcomeLateRows": "{n, plural, other{{n} 行数据在其所属日期已评分并关闭后才到达。}}", - "welcomeExportAgainInDateOrder": "按日期顺序重新导出" + "welcomeExportAgainInDateOrder": "按日期顺序重新导出", + "scanBarcodeTitle": "扫描条形码", + "scanBarcodeClose": "关闭", + "scanBarcodeInstructions": "将条形码保持在取景框内。系统不会录制任何内容——只读取数字。", + "scanBarcodeNoAccessTitle": "无法访问摄像头", + "scanBarcodeCameraFailedTitle": "摄像头未能启动", + "scanBarcodeNoAccessBody": "扫描需要使用摄像头,但本应用尚未获得该权限。", + "scanBarcodeCameraFailedBody": "此设备无法为扫描器打开摄像头。", + "scanBarcodeTypeInstead": "改为手动输入数字", + "findingsLogTitle": "观察记录", + "findingsLogEmptyTitle": "没有异常情况", + "findingsLogEmptyBody": "针对疾病、夜间生理异常、皮肤温度以及静息心率变化的监测均未发现异常。这是一个结果,而不是空白页面。", + "findingsLogDerivedNote": "每次打开时都会根据你自己的历史数据重新计算,而不是在事件发生时保存——因此如果某一天的数据被重新分析,这里显示的内容也会随之改变。", + "startCardDefaultSub": "选一个开始吧", + "monthGridCoverage": "{total} 天中有 {have} 天", + "monthGridSemanticsLabel": "{title}:{total} 天中有 {have} 天有数值,根据你自己的范围着色。", + "monthGridDaysAgo": "{days} 天前", + "monthGridToday": "今天", + "monthGridFootnote": "每个格子代表一天。颜色越深,说明这一天在你自己的范围内——即你所有记录天数的第 10 到第 90 百分位——越靠上;带轮廓的格子表示这一天没有数值,而不是数值偏低。更高的运动量不代表更好,更长的睡眠也不代表睡得更好;这只表示某一天所处的位置,而不代表这一天过得如何。", + "monthGridNotShadedYetTitle": "{title} 尚未开始着色", + "monthGridNotShadedYetBody": "{days, plural, other{{days} 天}}还不足以形成一个范围——着色显示的是某一天在你自己范围内的位置,达到 {min} 天后才会出现。", + "whatChangedTitle": "有什么变化", + "whatChangedSub": "基于你自己的历史数据", + "whatChangedNoDataTitle": "这一天还没有数据", + "whatChangedNoDataBody": "该分析会将某一天与之前的天数进行比较,而这一天没有可比较的数值。因为一无所知,所以也谈不上异常。", + "whatChangedLearningTitle": "仍在了解你的日常水平", + "whatChangedLearningBody": "“异常”只有在有范围可比较时才有意义,而这一天之前只有 {days, plural, other{{days} 天}}的历史数据。分析从 {min} 天开始。", + "whatChangedNothingTitle": "没有异常", + "whatChangedNothingBody": "所有有足够历史数据的指标都落在你自己各天所设定的范围之内。这是正常的结果,也是一个完整的结果。", + "whatChangedMethodologyNote": "以你自己此前的天数、你自己的单位来衡量,并附带时间窗口——方便你自行判断是否可信。这里的内容既不是原因,也不是诊断。", + "whatChangedDayLinkTitle": "那天发生了什么", + "whatChangedDayLinkSub": "按时间顺序排列的睡眠、训练、饮食和记录", + "whatChangedMonthSection": "背后的这个月", + "journalFieldErrorNoName": "给它起个名字", + "journalFieldErrorInvalidName": "请至少输入一个字母或数字", + "journalFieldErrorNoUnit": "请说明计量单位(mg、ml、杯…)", + "journalFieldErrorDuplicate": "你已经在用这个名字记录其他内容了", + "journalFieldTitle": "记录其他内容", + "journalFieldNameLabel": "你想记录什么?", + "journalFieldNameHint": "镁、屏幕使用时间、头痛…", + "journalFieldKindQuestion": "这是哪种类型的数值?", + "journalFieldKindRating": "1-5 分评分", + "journalFieldKindAmount": "一个数量", + "journalFieldKindMinutes": "分钟", + "journalFieldUnitLabel": "单位", + "journalFieldUnitHint": "mg、ml、杯…", + "journalFieldStepSize": "步长", + "journalFieldMaxPerDay": "一天内最多会记录多少", + "journalFieldAskLastTime": "询问上一次是什么时候", + "journalFieldStartTracking": "开始记录", + "aiBriefingForDay": "针对 {day}", + "aiBriefingNoModelTitle": "尚未设置模型", + "aiBriefingNoModelBody": "简报由你选择的模型撰写。在你选择模型之前,没有内容可生成,也没有任何数据被发送到任何地方。", + "aiBriefingChooseModel": "选择模型", + "aiBriefingNothingTitle": "今天还没有生成内容", + "aiBriefingNothingBody": "简报会按计划自动生成,也可以在此按需生成。", + "aiBriefingWriting": "正在生成…", + "aiBriefingWriteNow": "立即生成一份", + "aiBriefingWriteAgain": "重新生成", + "aiBriefingFailedTitle": "未能完成", + "aiBriefingFailedGeneric": "失败:{error}", + "aiBriefingSentSection": "已发送的内容", + "aiBriefingReadSection": "已读取的内容", + "aiBriefingNoneBody": "没有。没有发出任何请求——上面的内容是在这部手机上生成的。", + "aiBriefingLocalBody": "这些数据发送到了本机上的 {host},没有任何内容离开这台设备。", + "aiBriefingCloudBody": "只有这些数据(别无其他)以 {model} 的名义发送到了 {host}。没有原始录制数据,没有姓名,没有标识符。", + "aiBriefingNoneCardTitle": "没有发现异常,因此没有发出任何请求", + "aiBriefingNoneCardBody": "扫描在本机上运行。只有在发现异常情况需要提交时才会调用模型,而今天没有发现任何异常。", + "aiBriefingEmptyCardTitle": "没有可发送的数据", + "aiBriefingEmptyCardBody": "生成此内容时没有任何指标存在数值,因此提示词中未包含任何数据。", + "napsFellAsleepHelp": "入睡时间", + "napsWokeUpHelp": "醒来时间", + "napsInvalidWindow": "小睡的时长应在 5 分钟到 6 小时之间。超过这个时长应算作夜间睡眠,属于可以读取睡眠阶段的夜晚记录。", + "napsOverlap": "该时间段与当天已有的一次小睡重叠。请先删除那一次记录,而不是把同一小时计算两次。", + "napsNotReanalysed": "当天未被重新分析——已有另一项分析正在运行。你的修改已保存,将在下次生效。", + "napsTitle": "小睡", + "napsNoReadingTitle": "当天没有小睡数据", + "napsNoReadingBody": "小睡是根据当天其余部分所用的同一份 1 Hz 记录推算得出的,而这一天的记录不足以完成推算。", + "napsEmptyTitle": "这一天没有小睡", + "napsEmptyBody": "这一天没有出现足够长时间的静止状态,也没有伴随睡眠而来的心率下降。", + "napsCountsToward": "{mins} 的小睡时间将计入今晚的睡眠需求。", + "napsNotAppliedTitle": "该操作尚未生效", + "napsWorking": "处理中…", + "napsLogANap": "记录一次小睡", + "napsRemovedSection": "已移除", + "napsPutBackSemantic": "恢复这次小睡", + "napsPutBackLabel": "恢复", + "napsRemovalKept": "删除操作会以时间区间而非编号的形式保存,因此即使检测器的边界发生变化,该删除仍然有效。", + "napsYouLoggedThis": "由你手动记录", + "napsDetected": "系统检测到", + "napsLoggedWithMins": "{mins} · 由你手动记录", + "napsDetectedWithMins": "{mins} 睡眠 · 系统检测到", + "napsDeleteSemantic": "删除这次小睡", + "napsNotANapSemantic": "这不是一次小睡", + "napsDeleteLabel": "删除", + "napsNotANapLabel": "不是小睡", + "readinessDetailTitle": "身体准备度", + "readinessDetailNotScoredTitle": "身体准备度未评分", + "readinessDetailLastNightScored": "最近一次有评分的夜晚是 {day}。", + "readinessDetailWhatWasMissing": "缺少了什么", + "readinessDetailWhatWentIntoIt": "计算依据", + "readinessDetailInputsFooter": "{used}/{total} 项输入。每一项都会与你自己的历史数据进行对比排名——这是对同一组输入的平行视图,而非对上方数值的拆解。", + "readinessDetailNoBreakdownTitle": "暂无详细拆解", + "readinessDetailNoBreakdownBody": "将每项输入与你自己的历史数据进行排名比较,大约需要两周的夜间数据。", + "readinessDetailHistoryTitle": "历史记录", + "readinessDetailLastNDays": "{n, plural, other{最近 {n} 天}}", + "readinessDetailNoHistoryTitle": "没有身体准备度历史记录", + "readinessDetailNoHistoryBody": "已评分天数:0 天。", + "readinessDetailWearOvernight": "整夜佩戴腕带", + "readinessDetailUnit": "/100", + "readinessDetailDaysAgo": "{n, plural, other{{n} 天前}}", + "readinessDetailToday": "今天", + "readinessDetailMeasured": "已测量", + "readinessDetailNotMeasured": "未测量", + "readinessDetailNightsOfHistory": "{n, plural, other{{n} 晚你自己的历史数据}}", + "readinessDetailNeedSuffix": "{need}。每项输入都会与你自己的夜间数据进行对比排名,因此在数据量不足之前无法开始评分。", + "readinessDetailNoNoteFallback": "以上各项数据均已具备,但仍无法与你自己的历史数据进行比较。", + "readinessDetailNotAvailable": "不可用", + "readinessDetailContributionNotReported": "未报告贡献值", + "readinessDetailRelativeUncalibrated": "相对值,未校准", + "readinessDetailWithinSpread": "在你的常规波动范围内", + "readinessDetailWeightPercent": "权重 {pct}%", + "dayStepsTitle": "步数", + "dayStepsThroughDay": "全天分布", + "dayStepsToday": "今天", + "dayStepsOnDay": "{day}", + "dayStepsNoTimesTitle": "{when}的步数没有对应的时间记录", + "dayStepsNoStepsTitle": "{when}没有记录到步数", + "dayStepsStrapCounterBody": "{when}记录的 {count} 步来自设备自带的计步器,它只报告全天累计总数,没有具体时间。这里没有可以标注在时钟上的内容。", + "dayStepsNothingCounted": "{when}没有任何设备记录到步数。", + "dayStepsChartTitle": "计步时间分布", + "dayStepsUnit": "步", + "dayStepsYourPhone": "你的手机", + "dayStepsYourBand": "你的手环", + "dayStepsCounted": "已计步数", + "dayStepsHonestyMixed": "步数由手腕设备和手机共同记录,两者的误差方式不同:手腕设备容易低估真实步行,并可能把有节奏的手部动作误判为步行;而手机只统计你随身携带它时的步数。", + "dayStepsHonestyStrap": "步数由手腕设备记录,真实步行往往被低估,有节奏的手部动作也可能被误判为步行。", + "dayStepsHonestyPhone": "步数由你的手机记录,因此这里只包含你随身携带手机时的步数。", + "roughNightSignRhr": "你的静息心率升高了", + "roughNightSignHrv": "你的心率变异性降低了", + "roughNightSignDip": "你夜间心率的下降幅度比平时小", + "roughNightSignTemp": "你的皮肤温度更高", + "roughNightLateTraining": "你训练到了 {at},这本身就常常会导致这种情况。", + "roughNightIllness": "疾病监测也标记了这一晚——相对于你自己的基线出现了持续上升,这不是诊断。", + "roughNightLuteal": "你正处于黄体期,这本身就会提高静息心率和皮肤温度。", + "roughNightWarmRoom": "你的皮肤比平时更热——闷热的房间也会导致这种情况。", + "roughNightDismiss": "关闭", + "roughNightDefaultHeadline": "比平时更艰难的一晚", + "roughNightSummary": "{sentence},与你自己的夜晚相比。这只是对这一晚的测量,不是对你的评判。", + "roughNightTellWhatHappened": "说说发生了什么", + "roughNightNothingToAnswer": "没有需要回答的——此卡片只报告当晚情况。", + "roughNightWhatElse": "还发生了什么?", + "roughNightAnythingElse": "还有别的吗?", + "roughNightSaving": "保存中", + "roughNightLogIt": "记录这一晚", + "roughNightAddHowMuch": "添加具体数量", + "roughNightDoNotAskAgain": "不再询问", + "roughNightSeveralMoved": "多项夜间指标同时发生了变化", + "driverBreakdownHigherThanUsual": "高于你的通常水平", + "driverBreakdownLowerThanUsual": "低于你的通常水平", + "driverBreakdownRightOnUsual": "{now} · 正好处于你的通常水平", + "driverBreakdownAboveUsual": "{now} · 比你的通常值 {usual} 高 {delta}", + "driverBreakdownBelowUsual": "{now} · 比你的通常值 {usual} 低 {delta}", + "driverBreakdownWeightPct": "{pct}% 权重", + "driverBreakdownNotAvailable": "不可用", + "driverBreakdownContributionNotReported": "未报告贡献值", + "driverBreakdownRelativeUncalibrated": "相对值,未校准", + "driverBreakdownWithinUsualSpread": "在你的通常波动范围内", + "driverBreakdownBiggerThanNoise": "大于测量噪声", + "driverBreakdownSmallerThanNoise": "超出了你的通常波动范围,但小到可能只是测量噪声", + "driverBreakdownWhatHelped": "有帮助的因素", + "driverBreakdownWhatHeldYouBack": "拖累你的因素", + "driverBreakdownNeither": "两者皆非", + "driverBreakdownFooter": "每项数据都会与你自己的历史记录进行比较——是同一组数据的并行视图,而不是分数本身的组成部分。“测量噪声”是指在没有任何实际变化的情况下,一次读数本身可能出现的波动幅度。这些是你自身记录中的规律,而非成因。", + "driverBreakdownHideHistory": "{label},隐藏其历史记录", + "driverBreakdownShowHistory": "{label},显示其历史记录", + "driverBreakdownDaysAgo": "{n, plural, other{{n} 天前}}", + "driverBreakdownToday": "今天", + "driverBreakdownUsualRange": "你的通常范围 {lo}–{hi}{unit}", + "driverBreakdownAbsenceTitle": "没有可显示的明细", + "driverBreakdownAbsenceAlgoVersion": "上次更新改变了恢复力的计算方式,目前正在重新生成。", + "driverBreakdownAbsenceStale": "上一次汇总数据太旧,无法采信。", + "driverBreakdownAbsenceNoVersion": "存储的汇总数据没有版本标记。", + "driverBreakdownSyncTheBand": "同步腕带", + "driverBreakdownAbsenceNoReason": "没有任何记录说明昨晚为什么没有明细数据。", + "coachFiguresCouldNotBeDrawn": "无法绘制图表", + "coachFiguresNoType": "教练发送了一个没有指定类型的图表。", + "coachFiguresUnsupportedType": "教练请求了“{type}”类型的图表,此应用无法绘制。", + "coachFiguresFigure": "图表", + "coachFiguresSeriesN": "系列 {n}", + "coachFiguresLaneN": "通道 {n}", + "coachFiguresNoSleepSegments": "没有睡眠分段数据", + "coachFiguresNoTimeInZone": "没有区间时长数据", + "coachFiguresMinTotal": "共 {n} 分钟", + "coachFiguresGauge": "仪表", + "coachFiguresGaugeNoValue": "教练发送了一个没有数值的仪表。", + "coachFiguresSummary": "摘要", + "coachFiguresEmptySummary": "教练发送了一份空摘要。", + "coachFiguresTable": "表格", + "coachFiguresTableNoRows": "教练发送了一个没有数据行的表格。", + "circadianDetailTitle": "生理时钟", + "circadianDetailNoNightsTitle": "暂无可绘制的夜晚", + "circadianDetailNoNightsBody": "已评分 0 个夜晚。", + "circadianDetailNoNightsFix": "夜间佩戴腕带", + "circadianDetailSleepTitle": "睡眠,逐夜呈现", + "circadianDetailAsleep": "睡眠中", + "circadianDetailSleepFootnote": "{count, plural, other{共 {count} 晚,每晚一列。颜色越深,表示该小时睡眠时间越长。}}", + "circadianDetailYourRhythm": "你的节律", + "circadianDetailWhichNights": "查看具体夜晚", + "circadianDetailHide": "隐藏", + "circadianDetailShow": "显示", + "circadianDetailTodayPredicted": "今日预测", + "circadianDetailRhythmStrength": "节律强度", + "circadianDetailWhenStill": "静止时段", + "circadianDetailNoStillTitle": "暂无可读取的静止时刻", + "circadianDetailNoStillBody": "此项仅在你静止不动的秒数内读取心跳节律,而过去{days, plural, other{ {days} 天}}中这样的时段太少,无法凑成完整一小时的数据。", + "circadianDetailStillnessTitle": "静止状态下的逐搏变异性", + "circadianDetailStillnessFootnote": "每小时取自你真正静止的 {lo}–{hi} 个五分钟片段的中位值,统计范围为过去{days, plural, other{ {days} 天}}——从不只看今天。24 小时中有 {drawn} 小时至少有三个片段;其余留空。这不是压力评分——坐起身、暖和的房间或一杯咖啡同样会使其变化。", + "circadianDetailForecastTitle": "今天大概会如何度过", + "circadianDetailForecastFootnote": "无刻度——形状就是全部结果。", + "circadianDetailTroughText": "最平缓的时段落在{troughLabel},大约在 {start}–{end} 之间。", + "circadianDetailPredictionDisclaimer": "这是一个预测,不是实测。腕带并不测量你的清醒程度,它只知道昨晚的情况,仅此而已——小睡、咖啡或今天发生的任何事都不会被纳入。", + "circadianDetailAssumedPhaseNote": "你自己的生理钟高峰尚未测出,因此这里使用的是平均值。", + "circadianDetailNotADrivingCheck": "这不是驾驶适宜性检测,也不是轮班安全工具,也并不表示你的能力受损。", + "circadianDetailChronotype": "时型", + "circadianDetailMidSleepFree": "睡眠中点,休息日", + "circadianDetailMidSleepWork": "睡眠中点,工作日", + "circadianDetailSocialJetlag": "社交时差", + "circadianDetailLater": "更晚", + "circadianDetailEarlier": "更早", + "circadianDetailNightsCompared": "比较的休息夜 / 工作夜数", + "circadianDetailRegularityIndex": "规律指数", + "circadianDetailNightsLeastAlike": "最不相似的夜晚", + "circadianDetailSamePairScale": "该对比,同一量表", + "circadianDetailRhythmNotEstablished": "你的节律尚未确立", + "circadianDetailPairFootnote": "在 {count} 对中匹配度最低的一对。周末作息延后是另一种日程安排,而非更差的一晚。任一天记录数据过少的配对会被排除。", + "circadianDetailStability": "日间稳定性", + "circadianDetailFragmentation": "小时间碎片化程度", + "circadianDetailAmplitude": "相对振幅", + "circadianDetailM10Start": "心率最高 10 小时起始时间", + "circadianDetailL5Start": "心率最低 5 小时起始时间", + "circadianDetailRhythmPeak": "节律峰值", + "circadianDetailPeakSwing": "峰值与均值差幅", + "circadianDetailFitCurve": "24 小时曲线拟合度", + "circadianDetailStrengthNotMeasured": "节律强度尚未测量", + "circadianDetailStrengthWhy": "需要连续多天记录完整的 24 小时数据。", + "circadianDetailStrengthFootnoteKnown": "基于{used, plural, other{ {used} 个完整记录的}}心率天数计算。这些是你心率最高和最低的时段,而非活动最频繁的时段。", + "circadianDetailStrengthFootnoteUnknown": "基于一段完整记录的心率天数计算。这些是你心率最高和最低的时段,而非活动最频繁的时段。", + "beatsTitle": "心搏", + "beatsNoNightTitle": "暂无可绘制的夜晚", + "beatsNoNightBody": "此手机尚未生成任何已计算的夜晚,因此没有心搏间期可供绘制。", + "beatsNoNightFix": "夜间佩戴腕带,然后同步", + "beatsNightOf": "{date} 之夜", + "beatsPoincareSection": "每次心搏与前一次的对比", + "beatsBeatsGoneTitle": "这一晚的心搏数据已不在此手机上", + "beatsBeatsGoneBody": "单次心搏间期在夜晚评分后会保留几天,然后被删除。由这些数据计算出的结果会永久保留", + "beatsBeatsGoneMeasured": "——这一晚测得 SD1 {sd1} ms,SD2 {sd2} ms", + "beatsScatterTitle": "每个间期与前一个的对比", + "beatsScatterFootnote": "对角线表示某次心搏与前一次时长相同。垂直于该线的离散程度是 SD1,逐搏变化;沿该线的离散程度是 SD2,较缓慢的漂移。", + "beatsSd1Label": "SD1", + "beatsSd2Label": "SD2", + "beatsIntervalsLabel": "间期数", + "beatsIntervalsSurvived": "{count, plural, other{{count} 个间期通过了校正}}", + "beatsDroppedArtifact": "——另有 {count} 个被判定为伪影并未纳入云端", + "beatsPulseNotEcg": "这是脉搏,不是心电图——真实且属于你,但并非心电图所呈现的那种波形。", + "beatsMeasuredOn": "使用 {device} 测得;不同腕带读数并不一致。", + "beatsVariabilitySection": "整夜变异性", + "beatsUnitNights": "夜", + "beatsUnitScreenedNotScreened": "已筛查 / 未筛查", + "beatsVariabilityWhy": "这一晚没有任何半小时区间拥有足够干净的心搏来发布 RMSSD 值。", + "beatsNoBinsStored": "这一晚没有保存任何区间数据。", + "beatsRmssdTitle": "按半小时区间划分的 RMSSD", + "beatsStart": "开始", + "beatsBandFootnote": "该柱状表示我们对该区间的置信程度,而非你身体经历的数值范围。柱内的标记才是数值本身。", + "beatsHolesFootnote": "{count, plural, other{ 另有 {count} 个区间的干净心搏太少,无法发布数值,因此留空,而非与相邻区间连接。}}", + "beatsFirstThird": "前三分之一", + "beatsLastThird": "后三分之一", + "beatsDcSection": "减速能力", + "beatsDcWhy": "尚无已保存的夜晚生成该数值。", + "beatsDcNoData": "尚无夜晚生成该数值。", + "beatsDcChartTitle": "按顺序排列的你的夜晚记录", + "beatsDaysAgo": "{count} 天前", + "beatsTodayLabel": "今天", + "beatsAnchorsLastNight": "昨晚锚点数", + "beatsCleanBeats": "干净心搏比例", + "beatsDcNote": "仅供自我比较。请只与你自己其他夜晚的数据比较——手腕测量没有通用参考范围。\n\n该值对心率每次减速时刻周围的心搏取平均。数值上升可能只是信号更干净,而非心脏状况不同,因此请结合上方的锚点数和干净心搏比例一起看。如果在此期间更换过腕带,前后两段数据不可比较。", + "beatsRhythmSection": "节律筛查", + "beatsRhythmChartTitle": "每天一格", + "beatsScreenNotFired": "筛查未触发", + "beatsScreenFired": "筛查已触发", + "beatsNotScreened": "未筛查", + "beatsNoDayScreened": "此时间段内没有任何一天进行了筛查", + "beatsScreenNote": "这是一项筛查,不是检测。\n\n筛查未触发的一天并不代表你被排除了风险——它从来都无法排除任何情况。带边框的日期表示当天完全未进行筛查:可能是干净心搏太少,或运动过多。\n\n手腕脉搏不是心电图。如果你是因为出现症状才查看此项,请咨询临床医生进行正规检测。", + "beatsScreenedSummary": "过去 {win} 天中有 {screened} 天进行了筛查", + "beatsFiredSummary": ";筛查在 {fired} 天触发", + "beatsNotScreenedNote": " 昨晚未进行筛查:{note}", + "logWorkoutCouldNotLog": "无法记录,请重试。", + "logWorkoutCouldNotDismiss": "无法忽略,请重试。", + "logWorkoutAdjustTimes": "调整时间", + "logWorkoutDetectedActivityTitle": "检测到的活动", + "logWorkoutYoursToConfirmSub": "待你确认", + "logWorkoutReadFailedTitle": "无法读取检测到的活动", + "logWorkoutReadFailedBody": "数据库没有响应。没有任何记录被保存或忽略。", + "logWorkoutTryAgain": "重试", + "logWorkoutReadingSpotted": "正在读取手环检测到的内容…", + "logWorkoutNothingToReviewTitle": "没有需要查看的内容", + "logWorkoutNothingToReviewBody": "这条可能已经被记录或忽略了。", + "logWorkoutHardMinutesTitle": "这是高强度的核心时间,并非整段训练", + "logWorkoutHardMinutesBody": "检测只报告它能识别出的持续用力部分,因此热身和组间休息不包含在内。如果时间范围偏短,记录前请先调整时间。", + "logWorkoutMinutesOfEffort": "{mins} 分钟的用力时间", + "logWorkoutAvgHr": "平均心率", + "logWorkoutPeakHr": "最高心率", + "logWorkoutLooksLike": "看起来像", + "logWorkoutLogIt": "记录", + "logWorkoutNotAWorkout": "不是训练", + "logWorkoutToday": "今天", + "logWorkoutYesterday": "昨天", + "logWorkoutDefaultTitle": "记录一次过去的训练", + "logWorkoutWindowRescoredSub": "时间段已重新计算", + "logWorkoutYourOwnTimesSub": "你自己设定的时间", + "logWorkoutWhenGroup": "时间", + "logWorkoutActivityLabel": "活动", + "logWorkoutDateLabel": "日期", + "logWorkoutStartedLabel": "开始", + "logWorkoutEndedLabel": "结束", + "logWorkoutLengthLabel": "时长", + "logWorkoutNextMorningSub": "次日早上", + "logWorkoutWindowInvalidTitle": "该时间段无法保存", + "logWorkoutTimesUpdatedTitle": "时间已更新", + "logWorkoutLoggedTitle": "训练已记录", + "logWorkoutUnscoredSaved": "已保存。该时间段内没有记录到心率,因此没有用力值和卡路里数据——只保留了时间信息。", + "logWorkoutCouldNotSave": "保存失败,请重试。", + "logWorkoutScoredTitle": "根据手环记录的数据计算", + "logWorkoutScoredBody": "用力值和卡路里来自这段时间内每秒的心率数据,采用与全天相同的计算方法,不会根据时长进行估算。", + "logWorkoutSaving": "正在保存…", + "logWorkoutSaveNewTimes": "保存新的时间", + "logWorkoutSearchActivities": "搜索活动", + "logWorkoutNoActivityByName": "没有该名称的活动", + "logFoodTitle": "记录一次进食", + "logFoodClose": "关闭", + "logFoodIAte": "我吃了{meal}", + "logFoodAgain": "再次记录", + "logFoodAddNumbers": "添加数值", + "logFoodScanBarcode": "扫描条形码", + "logFoodScanSubOn": "向 openfoodfacts.org 查询该条形码,并填入它能确认的数据", + "logFoodScanSubOff": "在线查找该商品,会先询问你", + "logFoodLookingUpTitle": "正在查询", + "logFoodLookingUpBody": "结果一到,各栏就会自动填入。", + "logFoodWhatLabel": "内容", + "logFoodWhatHint": "鸡肉配米饭", + "logFoodPortionLabel": "分量", + "logFoodUnknownHint": "未知", + "logFoodEnergyLabel": "能量", + "logFoodProteinLabel": "蛋白质", + "logFoodCarbsLabel": "碳水化合物", + "logFoodFatLabel": "脂肪", + "logFoodFibreLabel": "膳食纤维", + "logFoodBlankHint": "数值留空即为未知。只有“内容”是必填项。", + "logFoodSayWhatFirst": "请先填写吃了什么。", + "logFoodConsentTitle": "要在线查询条形码吗?", + "logFoodConsentBody1": "扫描会把条形码发送给 openfoodfacts.org,一个免费的开放食品数据库。对方能看到条形码和你的 IP 地址。你的个人信息、饮食或健康数据都不会离开这台手机;已经扫描过的条形码会直接使用本地缓存作答,不会再次请求对方。", + "logFoodConsentBody2": "这些数据由公众录入,其中不少是错误的,因此任何未通过合理性检查的内容都会留空,而不是被填入。凡是自动填入的内容,保存前你都可以修改。", + "logFoodConsentBody3": "你可以随时在设置 › 隐私中重新关闭此功能。无论如何,你都可以手动照包装上的数值输入。", + "logFoodAllowLookups": "允许查询", + "logFoodNotNow": "暂不", + "logFoodBreakfast": "早餐", + "logFoodLunch": "午餐", + "logFoodDinner": "晚餐", + "logFoodSnack": "小吃", + "logFoodNoNumbersTitle": "该商品没有可用数值", + "logFoodNoNumbersBody": "Open Food Facts 有这个商品,但没有可用的营养数据——或者原有数据未通过合理性检查。", + "logFoodNotFoundTitle": "Open Food Facts 中没有该商品", + "logFoodNotFoundBody": "还没有人添加过这个条形码。", + "logFoodFlaggedTitle": "该记录被标记为有误", + "logFoodFlaggedBody": "Open Food Facts 将该商品标记为含有错误,因此没有填入任何数值。", + "logFoodUnreachableTitle": "Open Food Facts 无响应", + "logFoodUnreachableBody": "无法连接到 openfoodfacts.org。", + "logFoodRefusedTitle": "条形码查询已关闭", + "logFoodRefusedBody": "没有发送任何内容。你可以在设置 › 隐私中开启此功能。", + "logFoodPortionNoteBase": "Open Food Facts 按每 100 克列出数据。更改分量后,数值会随之调整。", + "logFoodPortionNoteServing": "Open Food Facts 按每 100 克列出数据。更改分量后,数值会随之调整。包装上标注的分量为 {serving}。", + "logFoodPillOpenFoodFacts": "Open Food Facts", + "logFoodPillYours": "你的", + "logFoodBareOccasion": "已记录 · 未记录能量", + "logFoodOpensInBrowser": "{label},将在浏览器中打开", + "dayTimelineChargerOn": "已放上充电器", + "dayTimelineChargerOff": "已从充电器取下", + "dayTimelineDoubleTap": "你双击了手环", + "dayTimelineRestarted": "手环已重启", + "dayTimelineBatteryPackAttached": "已连接外接电池", + "dayTimelineBatteryPackRemoved": "已拔下外接电池", + "dayTimelineAlarmWentOff": "闹钟已响起", + "dayTimelineAsleep": "睡眠", + "dayTimelineNap": "小睡", + "dayTimelineWorkout": "训练", + "dayTimelineBandOffWrist": "手环离腕", + "dayTimelineHighestHr": "最高心率", + "dayTimelineLowestHr": "最低心率", + "dayTimelineBpmAt": "{time} 达到 {bpm} 次/分", + "dayTimelineTakenAt": "于 {time} 服用", + "dayTimelineLastAt": "最近一次于 {time}", + "dayTimelineTaggedTitle": "已标记", + "dayTimelineTitle": "今日概览", + "dayTimelineSub": "从午夜到午夜", + "dayTimelineHeartRateTitle": "心率", + "dayTimelineMidnight": "午夜", + "dayTimelineNoon": "正午", + "dayTimelineMoving": "活动中", + "dayTimelineNotRecorded": "未记录", + "dayTimelineNothingRecordedTitle": "这一天没有任何记录", + "dayTimelineNothingRecordedBody": "没有睡眠、没有训练、没有日志,也没有带时间戳的手环事件。这样的一天通常意味着当天没有佩戴手环。", + "dayTimelineNoTimeTitle": "这一天没有任何带时间的记录", + "dayTimelineNoTimeBody": "已记录的内容如下。", + "dayTimelineWhatHappenedSection": "发生了什么", + "dayTimelineAlsoLoggedSection": "当天还记录了", + "dayTimelineNoTimeNote": "这些内容记录在当天名下,但没有具体时间,因此没有放在时间轴上。", + "dayTimelinePatternsNote": "这是你自己记录中的规律,不是因果关系。这里相邻出现的两件事只是时间上接近,本页面仅说明这一点。", + "journalComposeNotReady": "尚未就绪 —— 请先打开应用。", + "journalComposeSaveFailed": "保存失败 —— 请检查存储空间后重试。", + "journalComposeWhenWasLastOne": "上一次是什么时候?", + "journalComposeTitle": "日志", + "journalComposeTodaySection": "今天", + "journalComposeTrackSomethingElse": "记录其他内容", + "journalComposeAnythingElseLabel": "其他备注", + "journalComposeAnythingElseHint": "写一句关于这一天的话。", + "journalComposeSavingLabel": "保存中", + "journalComposeHowAreYouFeeling": "你感觉怎么样?", + "journalComposeNotAnsweredYet": "尚未回答", + "journalComposeMoodOfFive": "心情 {value}/5 · 再次点按可清除", + "journalComposeMoodOfFiveSelected": "心情 {n}/5,已选中。点按可清除。", + "journalComposeMoodOfFiveLabel": "心情 {n}/5", + "journalComposeNotLogged": "未记录", + "journalComposeWhenWasLastField": "上一次{field}是什么时候", + "journalComposeAddTimeOfLastOne": "添加上一次的时间", + "journalComposeLastAt": "最近一次于 {time}", + "journalComposeIncrease": "增加", + "journalComposeDecrease": "减少", + "journalComposeWeightLabel": "体重", + "journalComposeNotEntered": "未输入", + "journalComposeEnteredNotMeasured": "{value} · 手动输入,非测量值", + "journalComposeEnterWeight": "输入体重", + "journalComposeEnter": "输入", + "journalComposeChange": "修改", + "journalComposeSeeWeightTrend": "查看体重趋势", + "journalComposeSeeTheTrend": "查看趋势", + "journalComposeWeightToday": "今日体重", + "journalComposeWeightKgLabel": "体重(千克)", + "journalComposeWeightScaleNote": "你或体重秤读到的数值。手环不测量此项。", + "journalComposeClear": "清除", + "journalComposeNotEnoughEntriesTitle": "记录不足,无法生成趋势", + "journalComposeNotEnoughEntriesBody": "这条曲线是你输入数据的七天平均值,因此至少需要两天的数据。中间不会自动填补。", + "journalComposeSevenDayTrend": "七天趋势", + "journalComposeTrendFootnote": "由你输入。没有记录的日子留空。", + "journalComposeWeightTrendExplainer": "由你或体重秤输入 —— 手环不测量体重。图表显示的是七天平均值,因为体重秤仅因水分和饮食就可能有一到两公斤的波动,原始读数会将其显示为身体发生的变化。{count, plural, other{已记录 {count} 天。}}", + "nutritionTabToday": "今天", + "nutritionTabWeek": "本周", + "nutritionTabGoals": "目标", + "nutritionLogFood": "记录饮食", + "nutritionTitle": "营养", + "nutritionEmptyTodayTitle": "今天还没有记录", + "nutritionEmptyTodayBody": "轻点一下即可完成记录。", + "nutritionLogOccasionFix": "记录一次进食", + "nutritionOccasionsSection": "餐次", + "nutritionAddAction": "添加", + "nutritionFloorTitle": "今天的热量是下限,不是总量", + "nutritionFloorBody": "{total} 次进食中有 {unknown} 次没有记录热量,所以上面的数字是你至少吃了多少,而不是实际吃了多少。", + "nutritionAddNumbersFix": "为某次进食补充数值", + "nutritionDaysNotCounted": "最近 {span} 天中有 {excluded} 天无法计入统计", + "nutritionDayCountsRule": "只要每次进食都有热量数值,这一天就会被计入。", + "nutritionDaysLoggedLabel": "有记录的天数", + "nutritionPartialExcluded": "{partial} 天有记录但不完整,因此不计入下面的每项平均值", + "nutritionEnergyByDay": "每日热量", + "nutritionSevenDayAvg": "七日平均", + "nutritionNoCompleteDayTitle": "还没有完整的一天可供平均", + "nutritionNoCompleteDayBody": "你还没有这样的一天。", + "nutritionLabelEnergy": "热量", + "nutritionLabelProtein": "蛋白质", + "nutritionLabelCarbs": "碳水化合物", + "nutritionLabelFat": "脂肪", + "nutritionLabelFibre": "膳食纤维", + "nutritionEnergyBalance": "热量平衡", + "nutritionLabelEaten": "摄入", + "nutritionLabelBurned": "消耗", + "nutritionLabelBalance": "平衡", + "nutritionEatenMeanNote": "摄入量是 {days} 个完整天的平均值。消耗量仅为今天的数值。", + "nutritionEnergyLoggedTitle": "已记录的热量", + "nutritionPartialFootnote": "{n} 天记录不完整,已从下面的平均值中排除。", + "nutritionNothingLoggedYet": "还没有任何记录", + "nutritionNoEnergyFiguresYet": "还没有热量数值", + "nutritionDailyEnergy": "每日热量", + "nutritionDailyProtein": "每日蛋白质", + "nutritionEnergyWord": "热量", + "nutritionProteinWord": "蛋白质", + "nutritionYourTargetsSection": "你的目标", + "nutritionHintNone": "无", + "nutritionNoTargetsTitle": "尚未设置目标", + "nutritionNoTargetsBody": "这里的目标由你自己输入。", + "nutritionSetTargetFix": "设置目标", + "nutritionEditAction": "编辑", + "nutritionBodySpentToday": "你的身体今天消耗了多少", + "nutritionEstimatedExpenditure": "估计消耗", + "nutritionNotMeasured": "未测量", + "nutritionExpenditureSub": "今天,根据心率和你的个人资料估算", + "nutritionRemoveTitle": "删除「{label}」?", + "nutritionRemoveBody": "这会把它从这一天和每个统计过它的平均值中移除,且无法撤销。", + "nutritionNothingToMeasure": "目前还没有可用来衡量{label}的数据", + "nutritionFloorAverageBody": "每个完整的一天里都有进食记录缺少{nutrient}数值,所以平均值只能是一个下限。", + "nutritionCountedNoFigureBody": "最近 {span} 天中有 {count} 天被计入,但没有一天记录了{nutrient}数值。", + "nutritionDayCountsRuleFull": "只要每次进食都有数值且记录持续到晚上,这一天就会被计入。最近 {span} 天都没有做到。", + "nutritionOnTarget": "达到目标", + "nutritionRateAbove": "每天高出 {amount} {unit}", + "nutritionRateBelow": "每天低了 {amount} {unit}", + "nutritionMeanOfDays": "{n} 个完整天的平均值", + "nutritionLoggedToday": "今日已记录", + "nutritionEatenToday": "今日已摄入", + "nutritionAtLeast": "至少", + "nutritionOccasionsUnit": "次", + "nutritionOccasionsCount": "{n} 次", + "nutritionLabelBalanceAtLeast": "平衡(至少)", + "nutritionNotLogged": "未记录", + "nutritionLoggedNoEnergy": "已记录 {n} 次 · 未记录热量", + "nutritionAtLeastPrefix": "至少 ", + "nutritionMealBreakfast": "早餐", + "nutritionMealLunch": "午餐", + "nutritionMealDinner": "晚餐", + "nutritionMealSnacks": "零食", + "nutritionNotCounted": "未计入", + "nutritionNotRecorded": "未记录", + "nutritionEveryDayNoFigure": "每个完整的一天都有进食缺少{label}数值", + "nutritionNoDayRecorded": "没有一个完整的一天记录了{label}", + "nutritionMeanOfCompleteDaysCaps": "{n} 个完整天的平均值", + "nutritionLeftOutAsFloor": "· 其中 {n} 天作为下限被排除", + "nutritionWaterLabel": "饮水", + "nutritionTapToChange": "轻点 − 或 + 以调整", + "nutritionNoneYet": "还没有", + "nutritionAddWater": "增加饮水", + "nutritionRemoveWater": "减少饮水", + "coachApiKeyLabel": "API 密钥", + "coachApiKeyLocalLabel": "API 密钥(本地无需填写)", + "coachAsking": "查询中…", + "coachAskLabel": "向教练提问", + "coachBaseUrlLabel": "基础 URL", + "coachBriefingMenuSub": "从本设备发出的确切快照", + "coachBriefingMenuTitle": "简报,以及发送了什么内容", + "coachChooseModelFix": "选择模型", + "coachCloudDataNote": "你的问题以及教练读取的数据会被发送到这个端点。在「发送了什么」中可以看到具体内容。", + "coachDeleteChat": "删除{title}", + "coachDeleteIt": "删除", + "coachDestructiveWarning": "这将从本设备删除数据,且无法撤销。", + "coachEndpointUnreachable": "无法连接到该端点:{error}", + "coachErrorTitle": "操作未能完成", + "coachInputHint": "问问关于你健康状况的问题…", + "coachIntroBody": "可以询问应用测量的任何内容,它还能记录饮食、饮水、锻炼、剂量和你的感受——但总会先征求你的同意。", + "coachKeychainRefused": "钥匙串拒绝了该密钥:{error}", + "coachKeyStillSavedBody": "这次未能从钥匙串中读取,通常发生在手机锁屏时应用被唤醒的情况下。", + "coachKeyStillSavedTitle": "你的密钥仍已保存", + "coachListModels": "列出模型", + "coachLocalDataNote": "你的问题以及教练读取的数据都保留在你自己的设备上。", + "coachLocalSub": "在本地网络中运行,不会离开你的设备。", + "coachMenuSemantic": "聊天记录与 AI 设置", + "coachModelHint": "搜索或输入 ID", + "coachModelLabel": "模型", + "coachModelsFound": "共 {n} 个模型,点选其中一个。", + "coachNavTitle": "教练", + "coachNewChat": "新对话", + "coachNoChatsYet": "还没有内容——这是你的第一次对话。", + "coachNoDataBody": "教练根据你自己的推导数据作答,而本设备上目前还没有这些数据。", + "coachNoDataTitle": "暂无可读取的数据", + "coachNoModelsListed": "该端点未列出任何模型,请在下方手动输入一个。", + "coachNotSetUp": "尚未设置", + "coachNotSetUpBody": "它运行在你选择的模型上——可以是你自己设备上的模型,也可以是任何兼容 OpenAI 接口、使用你自己密钥的服务商。无论哪种方式,数据都不会经过 OpenStrap。", + "coachNotSetUpTitle": "教练尚未设置", + "coachPastChats": "历史对话", + "coachPickModelFirst": "请先选择或输入一个模型。", + "coachSafeWarning": "在你点击下方按钮之前不会写入任何数据。", + "coachSaveIt": "保存", + "coachSendLabel": "发送", + "coachSetupNavSub": "使用你自己的模型", + "coachSetupNavTitle": "AI 设置", + "coachSomethingWrong": "出了点问题:{error}", + "coachStarterAteYesterday": "我昨天吃了什么?", + "coachStarterHrvChart": "绘制我上个月的 HRV 图表", + "coachStarterLogRun": "我今早跑了 40 分钟——帮我记录一下", + "coachStarterLogWater": "帮我记录今天喝了 500 毫升水", + "coachStarterRecovery": "我今天的恢复情况如何,为什么?", + "coachStarterSleep": "我这周的睡眠怎么样?", + "coachTryAgainFix": "重试", + "coachTryAsking": "试着问问", + "coachUntitledChat": "未命名对话", + "coachWhereModelRuns": "模型运行位置", + "coachYourDataYourModel": "你的数据,你的模型", + "investigateNerdStatsLabel": "详细数据", + "investigateProvenanceLabel": "数据来源", + "investigateDayLabel": "日期", + "investigateCoverageLabel": "覆盖率", + "investigateSleepWindowLabel": "睡眠时段", + "investigateSourceLabel": "来源", + "investigateSourceOnDevice": "腕带记录 · 由本机计算", + "investigateSourceImported": "已导入 · {source}", + "investigateAlgoVersionLabel": "算法版本", + "investigateWhatHappenedTitle": "当天发生了什么", + "investigateWhatHappenedSub": "按时间顺序排列的睡眠、训练、饮食与记录", + "investigateWhichSensorCounted": "哪个传感器计数", + "investigateStrapPedometer": "腕带 · 100 Hz 计步器", + "investigateStrapOnChipCounter": "腕带 · 芯片内置计数器", + "investigatePhonePedometer": "手机 · 计步器", + "investigateDayTotal": "当日总计", + "investigateStrapChipReported": "腕带芯片上报值", + "investigateTimeDomain": "时域指标", + "investigateRmssd": "RMSSD", + "investigateSdnn": "SDNN", + "investigateSdann": "SDANN", + "investigateSdnnIndex": "SDNN 指数", + "investigatePnn50": "pNN50", + "investigateLnRmssd": "ln RMSSD", + "investigateBaselineRmssd": "你的 RMSSD 基线", + "investigateStabilityCv": "稳定性(变异系数)", + "investigateFrequencyDomain": "频域指标", + "investigateUlfPower": "ULF 功率", + "investigateVlfPower": "VLF 功率", + "investigateLfPower": "LF 功率", + "investigateHfPower": "HF 功率", + "investigateTotalPower": "总功率", + "investigateLfHf": "LF / HF", + "investigateLfNormalised": "LF(归一化)", + "investigateHfNormalised": "HF(归一化)", + "investigateHfGated": "HF 是否通过筛选", + "investigateYes": "是", + "investigateNo": "否", + "investigateNoFrequencySpectrum": "当晚没有可用的频域数据", + "investigateRecordingTooShort": "记录时长过短,无法解析各频段。", + "investigateNonLinear": "非线性指标", + "investigateSd1Sleep": "SD1(睡眠)", + "investigateSd2Sleep": "SD2(睡眠)", + "investigateSd124h": "SD1(24 小时)", + "investigateSd224h": "SD2(24 小时)", + "investigateSd1Sd224h": "SD1 / SD2(24 小时)", + "investigateSuccessiveIntervalsOver70ms": "相邻间期差超过 70 毫秒的比例", + "investigateIrregularRhythmFlagSleep": "心律不齐标记(睡眠)", + "investigateIrregularRhythmFlag24h": "心律不齐标记(24 小时)", + "investigateFlagRaised": "已触发", + "investigateFlagClear": "未触发", + "investigateDecelerationCapacity": "减速力", + "investigateAccelerationCapacity": "加速力", + "investigateDcAnchors": "DC 锚点数", + "investigateSignalQuality": "信号质量", + "investigateBeatsAnalysed": "已分析心搏数", + "investigateBeatsAnalysed24h": "已分析心搏数(24 小时)", + "investigateNoShapeForNight": "当晚没有可用的曲线形态", + "investigateTooFewBeatsToBin": "当晚干净心搏数过少,无法分箱。", + "investigateShapeOfTheNight": "夜间曲线形态", + "investigateBinRmssd": "各分箱 RMSSD", + "investigateSamplingRange": "采样波动范围", + "investigateShapeFootnote": "共 {total} 个分箱中有 {drawn} 个拥有足够心搏可读;其余为空缺,而非零值。外侧两条线是估算器自身的采样波动范围,并不代表你的实际状态区间。这只是对当晚的描述,无法解释原因——前三分之一偏低,同样可能是饮酒、晚餐较晚、训练较晚、房间偏暖、疾病初起,或者纯属巧合。", + "investigateNightShape": "夜间形态统计", + "investigateBinWidth": "分箱宽度", + "investigateBinsRead": "已读取分箱数", + "investigateFirstThird": "前三分之一", + "investigateLastThird": "后三分之一", + "investigateLastThirdOverFirst": "后三分之一 ÷ 前三分之一", + "investigate29DaysAgo": "29 天前", + "investigateToday": "今天", + "investigateDcFootnoteWithBeats": "仅显示你自己的历史夜晚——脉搏到达时间没有统一的参考范围。信号质量会导致这条线逐夜波动,昨晚共有 {beats} 次心搏。", + "investigateDcFootnote": "仅显示你自己的历史夜晚——脉搏到达时间没有统一的参考范围。信号质量会导致这条线逐夜波动。", + "investigateIrregularRhythmScreen": "心律不齐筛查", + "investigateOneSquarePerDay": "每天一个方格", + "investigate12WeeksAgo": "12 周前", + "investigateThisWeek": "本周", + "investigateScreenRan": "已完成筛查", + "investigateRhythmStripFootnote": "在 {ran} 天中运行,其中 {raised} 天触发了标记。有边框的方格表示当天未运行。空白条带并不代表阴性结果:这是基于脉搏时序的筛查,无法区分异位心搏、漏检心搏与腕带在手腕上的移动。", + "investigateNoRestingBreathingRate": "睡眠之外没有静息呼吸频率数据", + "investigateNoRestingBreathingRateBody": "该数值仅来自腕带检测到你几乎完全静止的三分钟片段(睡眠时段之外)。大多数日子都没有这样的片段——没有数据的一天,是你在活动的一天,而不是出了问题的一天。", + "investigateBreathingAtRestAwake": "清醒时的静息呼吸", + "investigateStillStretchesOutsideSleep": "睡眠外的静止片段数", + "investigateLowest": "最低值", + "investigateNextLowest": "次低值", + "investigateHighestOfThem": "其中最高值", + "investigateFloorNotRateBody": "这是一个下限,不是全天的呼吸频率。只有你几乎完全静止的片段才能被读取,因此这些是腕带在睡眠之外观察到的最静止的几分钟——它无法描述你一天中的其余时间,运动时的呼吸也无法从心搏时序中还原。", + "investigateCycleScreenDidNotRun": "当晚未运行周期筛查", + "investigateNotEnoughCleanBeats": "干净心搏数不足,无法运行。", + "investigateHeartRateCycles": "心率周期", + "investigateCyclesCounted": "已计周期数", + "investigateObservedHoursAnalysed": "已分析观测时长", + "investigateCyclesPerObservedHour": "每观测小时周期数", + "investigateMeanCycleLength": "平均周期时长", + "investigateMeanDipDepth": "平均下降深度", + "investigateCycleLengthQuartiles": "周期时长四分位数", + "investigateDipDepthQuartiles": "下降深度四分位数", + "investigateNotEnoughNightsAcross": "夜晚数据不足,无法生成跨夜对比", + "investigateNeedsSeveralNights": "需要多个夜晚、每晚有一定观测时长才能生成。", + "investigateDroppedIrregular": "因心律不齐筛查标记而排除 {count} 晚", + "investigateDroppedThin": "因观测时长过短而排除 {count} 晚", + "investigateAcrossNOwnNights": "基于你自己的 {n} 个夜晚", + "investigateCvhrAboveUsual": "在你最近的几个夜晚中,本项筛查所计的心率周期水平高于其背后 {n} 个夜晚的整体水平。", + "investigateCvhrInsideUsual": "在你最近的几个夜晚中,本项筛查所计的心率周期水平一直保持在其背后 {n} 个夜晚的范围之内。", + "investigateCvhrExplainer": "这是脉搏中的一种模式,不是对呼吸的测量,也不是任何疾病的检测。同样的周期性变化可能来自心律不齐、身处高海拔,或任何断续的夜晚——而 β 受体阻滞剂、糖尿病和神经系统疾病会削弱这种信号,因此真正的呼吸紊乱往往在这里完全不留痕迹。", + "investigateCvhrNotNegativeResult": "所以这里的一切都不构成阴性结果,也不能排除任何情况,其中任何一项都不能说明某一个具体夜晚的情况——单单一个夜晚的计数本身就会因十几种原因而波动。", + "investigateCvhrSeeClinicianIfSymptoms": "如果你打鼾、醒来后仍感觉疲惫,或有人发现你睡眠中呼吸暂停,请咨询医生进行正规检测。", + "investigateStageMinutesAsCounted": "各睡眠阶段分钟数(原始计数)", + "investigateLight": "浅睡", + "investigateDeep": "深睡", + "investigateRem": "REM", + "investigateAwake": "清醒", + "investigateTotalSleep": "总睡眠时长", + "investigateSegmentationConfidence": "分期置信度", + "investigateNotPublished": "未公布", + "investigateNothingComputedForKey": "该指标暂无计算结果", + "investigateNoStoredSeries": "没有存储的数据序列", + "investigateNothingStoredYet": "{metric} 暂无存储数据。", + "investigateSeries": "数据序列", + "investigateDaysDerived": "已计算天数", + "investigateLatest": "最新值", + "investigateMean": "平均值", + "investigateMedian": "中位数", + "investigateSd": "标准差", + "investigateMin": "最小值", + "investigateMax": "最大值", + "investigateUnit": "单位", + "investigateUnitless": "无单位", + "investigateStorage": "存储方式", + "investigateOneValuePerDerivedDay": "每个计算日一个值", + "investigateMethodLabel": "计算方法", + "investigateNotDocumented": "暂无说明。", + "calmBreathingResonanceLabel": "共振呼吸", + "calmBreathingResonanceDescription": "以约每分钟 {rate} 次的匀速吸气与呼气。唯一带有连贯性评分的模式。", + "calmBreathingCloseBreathing": "关闭呼吸练习", + "calmBreathingFinishNow": "立即结束", + "calmBreathingStop": "停止", + "calmBreathingEndSession": "结束训练", + "calmBreathingBegin": "开始", + "calmBreathingTakeABreath": "深呼吸一下。", + "calmBreathingRingLeads": "跟随圆环的节奏,放下手机。", + "calmBreathingScoredPill": "可评分", + "calmBreathingHowLong": "时长", + "calmBreathingMinutesSemantic": "{m} 分钟", + "calmBreathingMinutesAbbrev": "{m} 分钟", + "calmBreathingYourOwnPace": "专属节奏", + "calmBreathingWindowRowSemantic": "前后测量,增加四分钟", + "calmBreathingMeasureBeforeAfter": "前后测量 · 增加 4 分钟", + "calmBreathingNeedsBandBeatTiming": "需要佩戴腕带——对比基于心搏时序进行。", + "calmBreathingFindYourPace": "找到你心脏跟随的节奏", + "calmBreathingSweepIntro": "共六分钟:{rates} 次/分钟,每种节奏两分钟。需要连续两次结果一致后才会有变化。", + "calmBreathingSweepAgreed": "已有两次测试一致选出每分钟 {rate} 次,共振呼吸已按此节奏设置。可再次运行以验证。", + "calmBreathingPaceOfRate": "第 {block} 段(共 {total} 段) · 每分钟 {rate} 次呼吸", + "calmBreathingOfClock": "共 {clock}", + "calmBreathingNoScoreForSession": "本次训练无连贯性评分", + "calmBreathingScoringNeedsBand": "评分需要腕带提供心搏时序。当前未连接,因此本次仅引导呼吸,不会保存。", + "calmBreathingBeforeLabel": "训练前", + "calmBreathingAfterLabel": "训练后", + "calmBreathingSitStill": "请静坐片刻。", + "calmBreathingStaySitting": "请继续静坐。", + "calmBreathingNothingPacingScored": "按你平常的方式呼吸即可。这段时间不引导节奏,也不会评分。", + "calmBreathingPatternNotScored": "{pattern} 不参与评分。共振呼吸是唯一按评分所需节奏设置的模式。", + "calmBreathingTooFewBeatTimings": "本次训练中干净的心搏时序数据过少,无法评分。", + "calmBreathingThatIsDone": "完成了。", + "calmBreathingCardiacCoherence": "心脏连贯性", + "calmBreathingHowStronglyFollowedPace": "心率跟随节奏的紧密程度", + "calmBreathingStoppedThere": "已在此中止。", + "calmBreathingHowStronglyEachPace": "心率跟随各节奏的紧密程度", + "calmBreathingBreathsAMinute": "每分钟 {rate} 次呼吸", + "calmBreathingNotReached": "未进行到此段", + "calmBreathingTooFewCleanBeats": "干净心搏数过少", + "calmBreathingRankingExplainer": "这是一次测试中对三种节奏的排名。各段连续进行,因此测量每种节奏时你可能仍未从上一段中平复下来。它只说明你的心率对哪种节奏反应最强,仅此而已。", + "calmBreathingVerdictAborted": "你中途停止了,因此没有可比较的数据,一切保持不变。", + "calmBreathingVerdictCouldNotScore": "至少有一种节奏未能评分,因此无法排名,一切保持不变。", + "calmBreathingVerdictTied": "两种节奏得分相同,本次测试无法区分,一切保持不变。", + "calmBreathingVerdictConfirmed": "在测试的节奏中,{w} 带来了你最强的反应——这已是连续第二次测试得出同样结果。共振呼吸已按此节奏设置。", + "calmBreathingVerdictFirstWin": "在测试的节奏中,{w} 带来了你最强的反应。目前尚未确定:只有连续两次测试选出相同节奏时,设置才会改变。", + "breathPatternBoxName": "箱式呼吸", + "breathPatternBoxDesc": "每个阶段四拍,包括屏息。思绪纷乱时能让你平静下来。", + "breathPattern478Name": "4-7-8 呼吸法", + "breathPattern478Desc": "长时间屏息,呼气时间更长。通常用于帮助入睡。", + "breathPatternExtendedExhaleName": "长呼气", + "breathPatternExtendedExhaleDesc": "呼气时间是吸气的两倍。没有屏息,容易坚持一段时间。", + "breathPhaseInhale": "吸气", + "breathPhaseHold": "屏息", + "breathPhaseExhale": "呼气", + "breathPhaseWork": "运动", + "breathPhaseRest": "休息", + "metricDetailToday": "今天", + "metricDetailRange7Days": "7 天", + "metricDetailRange30Days": "30 天", + "metricDetailRange6Months": "6 个月", + "metricDetailRangeYear": "一年", + "metricDetailLockedNote": "{label}需要 {needed} 天的历史记录。你有 {have} 天。", + "metricDetailNotShownTitle": "不作为趋势显示", + "metricDetailNothingRecordedToday": "今天没有记录", + "metricDetailNoHistoryYet": "{metric}暂无历史记录", + "metricDetailNoValueYet": "今天还没有产生数值。", + "metricDetailNoValueYetWiderRanges": "今天还没有产生数值。上方更宽的时间范围里有已产生数值的日子。", + "metricDetailNoValueInWindow": "此时间范围内没有一天产生数值。", + "metricDetailWearBandFix": "整夜佩戴腕带以开始记录", + "metricDetailBeatsLinkTitle": "心跳", + "metricDetailBeatsLinkSub": "构成一夜的心跳间期,已绘制", + "metricDetailBreakdownLinkTitle": "明细", + "metricDetailBreakdownLinkSub": "今天的每一段,以及是什么统计了它", + "metricDetailNerdStatsTitle": "极客统计", + "metricDetailNerdStatsSub": "图表背后的数字", + "metricDetailDailyAverage": "日均值 · {win} 天中的 {count} 天", + "metricDetailLatestReading": "最新 {value} {unit} · {asOf}", + "metricDetailAlgoBreakFootnote": "{n, plural, other{虚线标记了这些天计算方式发生变化的位置。线两侧的读数来自不同版本。}}", + "metricDetailDaysAgoLabel": "{n, plural, other{{n} 天前}}", + "metricDetailWornChartTitle": "佩戴", + "metricDetailHoursADayUnit": "小时/天", + "metricDetailWearFootnote": "这 {win} 天中有 {have} 天有佩戴记录。其余是两个图表中的缺口——上方的线不会跨越它们绘制。", + "metricDetailSlotNoRecord": "{day},无记录", + "metricDetailSlotWithValue": "{day},{value} {unit}", + "metricDetailOpenDay": "打开{day}", + "metricDetailNoRecordLabel": "无记录", + "metricDetailLowest": "最低", + "metricDetailTypical": "典型", + "metricDetailHighest": "最高", + "metricDetailFromDaysCount": "来自你自己的 {n} 天。", + "metricDetailPercentileTodayNoBand": "今天处于你自身历史记录的第 {ordinal} 百分位。", + "metricDetailPercentileTodayBand": "今天处于你自身历史记录的第 {ordinal} 百分位 — {band}。", + "metricDetailPercentileFromNoBand": "你在 {date} 的读数处于你自身历史记录的第 {ordinal} 百分位。", + "metricDetailPercentileFromBand": "你在 {date} 的读数处于你自身历史记录的第 {ordinal} 百分位 — {band}。", + "metricDetailDaysWithWithout": "{withCount} 天有 · {withoutCount} 天无", + "metricDetailPatternsNotCauses": "这是你自己日志中的模式,不是因果关系。", + "metricDetailChooseDayHelp": "选择日期", + "metricDetailPreviousDay": "前一天", + "metricDetailNextDay": "后一天", + "metricDetailChooseDayShowing": "选择日期。当前显示 {day}", + "metricDetailNormalRangeSection": "你的正常范围", + "metricDetailWhatMovesItSection": "影响它的因素", + "cycleRemoveLogTitle": "删除 {date}?", + "cycleRemoveLogBody": "周期天数、阶段和预测的下次日期都是根据你记录的日期计算的。只能记录今天,所以这条记录一旦删除就无法恢复。", + "cycleWhatAppliesToYou": "适用于你的情况", + "cyclePreferNotToSay": "不愿透露", + "cyclePreferNotToSayWhy": "应用会保持阶段功能关闭。", + "cycleReproCyclingLabel": "我有自然周期", + "cycleReproCyclingWhy": "根据你记录的开始日期计算阶段。", + "cycleReproContraceptionLabel": "激素避孕", + "cycleReproContraceptionWhy": "没有排卵可供计算,因此没有阶段。出血仍会被记录。", + "cycleReproNoneLabel": "怀孕、产后或没有周期", + "cycleReproNoneWhy": "没有阶段,也没有预测的下次日期。你的生物特征数据仍会显示。", + "cycleReproNotSet": "未设置", + "cycleTrackingOffTitle": "周期跟踪已关闭", + "cycleTrackingOffBody": "数据只保存在这台手机上。", + "cycleTurnOnTracking": "开启周期跟踪", + "cycleNoPeriodTitle": "还没有记录经期", + "cycleNoPeriodBody": "根据你记录的日期计算。", + "cycleLogPeriodButton": "记录今天的经期开始", + "cycleLogKindStart": "开始", + "cycleLogKindEnd": "结束", + "cycleAcrossCyclesTitle": "你的周期概览", + "cycleUnitCompleteCycle": "个完整周期", + "cycleUnitCompleteCycles": "个完整周期", + "cycleOpenAction": "打开", + "cycleWhatYouNoticedToday": "你今天记录的感受", + "cycleLoggedDays": "已记录的日期", + "cycleReproOptionalHint": "可选。在你说明之前,应用会保持阶段功能关闭。", + "cycleReproPrivateHint": "仅你本人和这台手机可见,绝不导出。", + "cycleTurnOffTracking": "关闭周期跟踪", + "cycleDayInThisCycle": "本周期的第几天", + "cycleCountedFromLastStart": "从你上次记录的开始日期计算", + "cycleOfAboutDays": "约 {days} 天", + "cyclePhaseMenstrual": "经期", + "cyclePhaseFollicular": "卵泡期", + "cyclePhaseOvulation": "排卵窗口期", + "cyclePhaseLuteal": "黄体期", + "cycleNextPeriodBetween": "预计下次经期在", + "cycleNextPeriodAround": "预计下次经期约在", + "cycleFromOneMeasuredGap": "基于你唯一一次测得的周期间隔,无法体现你自己周期的波动程度", + "cyclePastEndOfIt": "已超出该范围 {days} 天 · ", + "cycleInsideItNow": "目前正处于该范围内 · ", + "cycleInDaysRange": "还有 {lo}–{hi} 天 · ", + "cycleHalfOfMeasuredGaps": "你测得的 {n} 次周期间隔中,有一半落在这样宽的范围内", + "cycleLeadDaysLate": "已延迟 {days} 天 · ", + "cycleLeadToday": "就是今天 · ", + "cycleLeadInDays": "还有 {days} 天 · ", + "cycleWhatYouUsuallyNotice": "你通常会注意到的情况", + "cycleSymptomShapeSummary": "四个数字,分别对应周期中的每一周,均从你自己记录的开始日期算起。在 {cycles} 个周期中,你每周分别有 {daysByWeek} 天记录了内容——这些是唯一被计入的天数。", + "cycleRemoveLoggedDay": "删除 {date}", + "cycleSymptomCramps": "痛经", + "cycleSymptomHeadache": "头痛", + "cycleSymptomBloating": "腹胀", + "cycleSymptomFatigue": "疲劳", + "cycleSymptomLowMood": "情绪低落", + "cycleSymptomAcne": "痤疮", + "cycleSymptomTenderBreasts": "乳房胀痛", + "cycleSymptomNausea": "恶心", + "cycleThisCycle": "本周期", + "cycleByDayOfYourCycle": "按周期天数查看", + "cycleHowLongCyclesBeen": "你的周期长度变化", + "cycleRestingHeartRate": "静息心率", + "cycleUnitBpm": "次/分", + "cycleHrvRmssdTitle": "心率变异性(RMSSD)", + "cycleUnitMs": "毫秒", + "cycleNotEnoughDescribeDayTitle": "周期数据尚不足以描述某个周期天数", + "cycleNotEnoughDescribeDayBody": "这里的每个点都是你自己两个或以上周期中同一天的中位值。目前还没有任何一天积累到两个周期的数据。", + "cycleOwnPastCyclesDescribed": "这是对你过去周期的描述。只有一个周期到达过的天数会留空,而不是画出来——一晚的数据算不上中位值。它描述的是已经发生的情况,而不是将会发生的情况。", + "cycleDayOneLabel": "第 1 天", + "cycleDayNLabel": "第 {n} 天", + "cycleMiddleOfNCycles": "每天为 {n} 个周期的中位值。", + "cycleMiddleOfRangeCycles": "每天为 {lo} 到 {hi} 个周期的中位值。", + "cycleNotEnoughCompareTitle": "周期数据尚不足以将某一天与自身进行比较", + "cycleCompareBodyGeneric": "这会把今天与你以往周期中的同一天进行对比,需要至少三个到达过这一天的周期。", + "cycleCompareBodyWithDay": "这会把今天与你以往周期中的同一天进行对比,需要至少三个到达过第 {day} 天的周期。", + "cycleNightOfLabel": "{date} 夜间", + "cycleComparisonNotCorrection": "这只是一种对比,不是校正。你的身体准备度没有因此被重新调整,这里也不是任何训练指示。", + "cycleCompareHrvLabel": "心率变异性", + "cycleCompareLine": "{label} 较过去 3 周 {z1},较过去 {n} 次第 {cycleDay} 天 {z2}。", + "cycleLengthsTitle": "你的周期长度与公开范围的对比", + "cycleLengthsBody": "默认关闭,除非你主动查看。它会将你自己记录的周期开始日期之间的天数,与成年人周期的公开范围并列显示,除此之外不作任何说明。", + "cycleShowIt": "显示", + "cycleNotEnoughLoggedTitle": "记录的周期数据还不够", + "cycleNotEnoughLoggedBody": "这需要长期的数据积累:目前已有 {total} 个间隔中的 {n} 个,大约相当于坚持记录每次开始日期一年的时间。", + "cycleGapTitle": "你记录的开始日期之间存在缺口", + "cycleGapBody": "其中一次距上一次超过了 {days} 天。一次未记录的开始与一个确实持续这么久的周期,从这里看是无法区分的,因此不会绘制任何内容。", + "cycleDaysBetweenStarts": "你记录的开始日期之间的天数", + "cycleUnitDays": "天", + "cycleLegendYourCycles": "你的周期", + "cycleLegendPublishedRange": "公开范围", + "cycleTwoLinesFootnote": "两条线分别位于 {low} 天和 {high} 天处。", + "cycleLengthChangesReasons": "周期长度会因多种原因而变化——甲状腺、压力、体重变化、避孕、多囊卵巢综合征等。这只是你自己记录的数据与公开范围的对比,是你咨询医生的一个理由,而不是来自医生的答案。", + "cycleHideLengths": "隐藏周期长度", + "cycleDescriptiveOnly": "仅作描述。", + "cycleNotEnoughDerivedNights": "本周期推导出的夜间数据还不够", + "cycleMdcNoteInsideSpread": " 这里显示的每一天都在你自身夜间波动范围之内:其中任意两天之间的最大差异为 {s},而 {n} 是这项指标能从噪声中区分出的最小变化。这只是一种形态,而不是真正的变化。", + "cycleMdcNoteVaries": " 你的夜间数据本身就有 {n} 的波动,因此差异小于此的天数无法被区分。这里的最大差异为 {s}。", + "healthTabOverview": "概览", + "healthTabExplore": "探索", + "healthTabTrends": "趋势", + "healthTabVitals": "生命体征", + "healthTabLabs": "化验", + "healthTitle": "健康", + "healthCouldNotRead": "无法读取你的{what}", + "healthReadFailedBody": "存储的记录加载失败。没有任何数据被删除——这只是一次读取出错。", + "healthTryAgain": "重试", + "healthWhatVitals": "生命体征", + "healthWhatLabResults": "化验结果", + "healthMeasuresUnit": "指标", + "healthRowRestingHr": "静息心率", + "healthRowHrv": "心率变异性", + "healthRowSleep": "睡眠", + "healthRowStress": "压力", + "healthRowRespRate": "呼吸频率", + "healthSubOvernight": "整夜", + "healthSubRmssdAsleep": "RMSSD,睡眠中", + "healthSubLastNight": "昨晚", + "healthSubAsleep": "睡眠中", + "healthNoMetric": "无{name}", + "healthWhyReadFromSleep": "该数据来自睡眠,而这一晚未被评分。", + "healthWhyReadOnlyFromSleep": "该数据仅来自睡眠,而这一晚未被评分。", + "healthWhySleepNotLongEnough": "没有记录到足够长的睡眠时段可供评分。", + "healthWhyReadFromNight": "该数据来自夜间,而这一晚未被评分。", + "healthWhyNoReadingLastNight": "昨晚没有读数。", + "healthIllnessRedTitle": "连续几晚都偏离了你的正常水平", + "healthIllnessLastNightTitle": "昨晚的数值超出了你的正常范围", + "healthIllnessDayTitle": "{day}的数值超出了你的正常范围", + "healthIllnessBodyNoZ": "你的夜间静息心率一直高于自身基线。这里只监测一个信号,指出的是一种模式,而不是原因。", + "healthIllnessBodyWithZ": "你的夜间静息心率一直高于自身基线;那一晚的数值{direction}基线{z}个标准差。这里只监测一个信号,指出的是一种模式,而不是原因。", + "healthDirectionAbove": "高出", + "healthDirectionBelow": "低于", + "healthIllnessAdvice": "如果持续超过几天,值得留意。", + "healthObservationsTitle": "观察", + "healthSeeAll": "查看全部", + "healthNapsTitle": "小睡", + "healthNoNapReading": "没有小睡记录", + "healthNoNapReadingFor": "{day}没有小睡记录", + "healthNapsBody": "小睡数据来自与全天相同的逐秒记录,而这一天的记录量不足。", + "healthDaytimeSleep": "日间睡眠", + "healthValueNone": "无", + "healthNoneDetectedOn": "未检测到 · {day}", + "healthNapCountLabel": "{n, plural, other{{n} 次小睡}}", + "healthAddOrCorrect": "添加或更正", + "healthNoTrendYet": "{label}尚无趋势", + "healthZeroDaysStored": "已存储 0 天。", + "healthVsDayAverage": "对比你的{days}天平均值", + "healthAsOf": " · 截至{date}", + "healthNoBaseline": "无基线", + "healthFirstReadings": "首次读数", + "healthTimeAsleep": "睡眠时长", + "healthVsNeed": "对比你所需的{need}", + "healthBodyClockTitle": "生物钟", + "healthChronotypeJetlagRegularity": "作息类型、时差和规律性", + "healthChronotypeLabel": "作息类型", + "healthSocialJetlagLabel": "社会时差", + "healthRegularityLabel": "规律性", + "healthConsistencyTitle": "一致性", + "healthDaysWithRecord": "过去30天内有推算记录的天数", + "healthToday": "今天", + "healthRowHeartRate": "心率", + "healthRowSkinTemp": "皮肤温度", + "healthVsOwnNights": "对比你自己的历史夜晚", + "healthVsOwnNightsOn": "对比你自己的历史夜晚 · {day}", + "healthRowWearTime": "佩戴时长", + "healthTheDay": "全天", + "healthCoverageOf": "{day}的{pct}%", + "healthNothingMeasuredDay": "这一天没有测得任何数据", + "healthNoBandRecordings": "这一天没有收到腕带的记录数据。", + "healthSyncTheBand": "同步腕带", + "healthDeepDivesTitle": "深入分析", + "healthHeartRateVariability": "心率变异性", + "healthTimeFrequencyNonLinear": "时域、频域与非线性", + "healthRmssdOfLastNights": "RMSSD,最近{days}晚中的{have}晚", + "healthNightsAgo": "{n}晚前", + "healthOneNightNotTrend": "一晚数据还构不成趋势", + "healthMeasuresWithHistory": "此设备上存有历史数据的指标", + "healthEachOneOpens": "点开每一项都能看到它的图表、你的个人范围,以及计算方式。", + "healthCatHeartRhythm": "心脏与心律", + "healthCatBreathing": "呼吸", + "healthCatMovementLoad": "运动与负荷", + "healthCatBodyWear": "身体与佩戴", + "healthBlurbRestingHr": "整夜持续保持的最低心率", + "healthBlurbHrv": "取睡眠中信号最干净时段的 RMSSD", + "healthBlurbHrvCv": "该数值在各夜之间的波动幅度", + "healthBlurbLfHf": "心跳间期功率在各频段的分布", + "healthBlurbDip": "睡眠期间心率下降的幅度", + "healthBlurbHrr": "运动结束后一分钟内心率下降的速度", + "healthBlurbSleep": "根据体动和心跳节律推算的睡眠时长", + "healthBlurbEfficiency": "睡眠时间占卧床时间的比例", + "healthBlurbDeep": "非快速眼动睡眠中心率的平稳程度", + "healthBlurbRem": "根据心跳变异性和体动进行分期", + "healthBlurbNapMin": "在主要夜间睡眠之外检测到的睡眠", + "healthBlurbRespRate": "每分钟呼吸次数,由心跳节律推算得出", + "healthBlurbBrv": "该频率在整夜中的波动程度", + "healthBlurbSteps": "由计步器实际计数,从不依赖模型估算", + "healthBlurbActiveMin": "运动量的分钟数,而非移动距离", + "healthBlurbCalories": "根据心率和你的个人资料计算出的活动能量", + "healthBlurbStrain": "全天的心血管负荷,0至21分制", + "healthBlurbTrimp": "在各心率区间的时间,按其消耗加权", + "healthBlurbSkinTemp": "与你近期夜晚基线的差距", + "healthBlurbWear": "有腕带记录的分钟数", + "healthNothingMeasuredHere": "这里还没有测得任何数据", + "healthNotMeasuredYet": "尚未测量", + "healthNoDayProduced": "此设备上还没有任何一天产生过该数据。", + "healthNoLabResults": "没有化验结果", + "healthNoLabResultsBody": "尚未记录任何内容。你在此处添加的一切都会保留在本设备上,删除的一切也会从本设备中彻底消失。", + "healthLastPanel": "上次面板 {date} · 手动记录", + "healthMarkersYouNamed": "你自定义的指标", + "healthAddAResult": "添加结果", + "healthRangesDifferByLab": "参考范围因化验机构而异,请以你报告上的为准。", + "healthRemoveMarkerFrom": "删除{date}的{marker}", + "healthNoReferenceInterval": "无参考区间 · {date}", + "healthTypicalRange": "常见范围 {low}–{high} · {date}", + "healthRemoveLabelFrom": "要删除{date}的{label}吗?", + "healthRemoveLabBody": "你为这次抽血记录的{value} {unit}。它将从本设备中彻底移除,且无法撤销。", + "healthRemoveLabOlderNote": " 你在{date}的抽血记录会保留,并会在此处显示。", + "healthRemovedNoneLeft": "已删除{date}的{label}。已没有{label}的结果了。", + "healthRemovedShowingOlder": "已删除{date}的{label}。现在显示你在{older}的抽血记录。", + "healthRemoveTheMarker": "删除{label}指标", + "healthNothingLoggedUnderIt": "该项下没有记录", + "healthResultsCount": "{n, plural, other{{n} 项结果 · {unit}}}", + "healthStillHoldsResults": "{count, plural, other{{label}下仍有{count}项结果。请先删除这些结果——是这个指标在为它们打标签。}}", + "healthRemoveMarkerQ": "删除{label}?", + "healthRemoveMarkerBody": "它将从指标列表中移除,之后你将无法再记录它。不会有任何测量数据随之丢失——该指标下目前没有结果。", + "healthMarkerLabel": "指标", + "healthValueUnit": "数值({unit})", + "healthDateDrawn": "抽取日期(YYYY-MM-DD)", + "healthValueMustBeNumber": "数值必须是不带单位的纯数字。未保存任何内容。", + "healthDateFormatError": "日期格式必须为 YYYY-MM-DD。未保存任何内容。", + "healthCouldNotSaveIt": "无法保存:{error}", + "homeStepSensorStrapPhone": "腕带 + 手机", + "homeStepSensorStrap": "腕带", + "homeStepSensorPhone": "手机", + "homeOvernightBuilding": "昨晚的数据仍在处理中。", + "homeOvernightNothingYet": "昨晚的数据还没有同步到应用。", + "homeMonthJanuary": "1月", + "homeMonthFebruary": "2月", + "homeMonthMarch": "3月", + "homeMonthApril": "4月", + "homeMonthMay": "5月", + "homeMonthJune": "6月", + "homeMonthJuly": "7月", + "homeMonthAugust": "8月", + "homeMonthSeptember": "9月", + "homeMonthOctober": "10月", + "homeMonthNovember": "11月", + "homeMonthDecember": "12月", + "homeMonthJanuaryShort": "1月", + "homeMonthFebruaryShort": "2月", + "homeMonthMarchShort": "3月", + "homeMonthAprilShort": "4月", + "homeMonthMayShort": "5月", + "homeMonthJuneShort": "6月", + "homeMonthJulyShort": "7月", + "homeMonthAugustShort": "8月", + "homeMonthSeptemberShort": "9月", + "homeMonthOctoberShort": "10月", + "homeMonthNovemberShort": "11月", + "homeMonthDecemberShort": "12月", + "homeWeekdayMonday": "星期一", + "homeWeekdayTuesday": "星期二", + "homeWeekdayWednesday": "星期三", + "homeWeekdayThursday": "星期四", + "homeWeekdayFriday": "星期五", + "homeWeekdaySaturday": "星期六", + "homeWeekdaySunday": "星期日", + "homeReadinessNotScored": "未评分", + "homeReadinessGoodToGo": "状态极佳", + "homeReadinessSteady": "状态平稳", + "homeReadinessTakeItEasy": "宜放松节奏", + "homeReadinessRestToday": "今天宜休息", + "homeDriverHrv": "心率变异性", + "homeDriverRhr": "静息心率", + "homeDriverResp": "呼吸频率", + "homeDriverTemp": "皮肤温度", + "homeDbRebuiltTitle": "为启动应用,你的数据库已被重建", + "homeDbRebuiltNothingRecovered": "没有任何数据可以恢复。", + "homeDbRebuiltRecovered": "已恢复:{list}。", + "homeDbRebuiltEmpty": "为空:{list}。", + "homeDbRebuiltKept": "原始文件仍保存在 {path},没有任何内容被删除。", + "homeWorkoutHoldTitle": "有一项运动仍在进行中", + "homeWorkoutHoldBody": "运动进行期间,今日数据处于暂停状态:腕带仍在记录,但数值要等到运动结束后才会计算。请在下方的进度条中结束运动,今日数据便会补全——仅同步是不够的。", + "homeInsightsRebuildingTitle": "你的跨日汇总数据正在重建", + "homeInsightsRebuildingAlgoVersion": "计算方式已随最新更新发生变化。", + "homeInsightsStaleOverWeek": "上次汇总是一周多以前生成的,太旧了,不宜依赖。", + "homeInsightsStaleOnDay": "上次汇总是在 {day} 生成的,太旧了,不宜依赖。", + "homeInsightsNoVersionStamp": "已保存的汇总没有版本标记。", + "homeSyncBand": "同步腕带", + "homeWhyLabel": "为什么?", + "homeCalibrating": "校准中", + "homeCalibratingNights": "已有 {have}/{need} 晚", + "homeCalibratingDays": "已有 {have}/{need} 天", + "homeGapNoReason": "没有记录说明缺失的原因。", + "homeRingRecovery": "恢复", + "homeRingStrain": "消耗", + "homeRingSleep": "睡眠", + "homeRingNoStrain": "暂无消耗数据", + "homeRingNoSleep": "暂无睡眠数据", + "homeSleepGapFallback": "没有记录到时长足以评分的夜晚。", + "homeStrainOf21": "满分 21", + "homeSleepNoTarget": "暂无目标", + "homeOfSpan": "共 {duration}", + "homeLoadFailedTitle": "今日数据读取失败", + "homeLoadFailedBody": "已保存的当日数据加载失败。没有任何内容被删除——这是读取出错,不是数据丢失。", + "homeTryAgain": "重试", + "homeNothingDerivedTitle": "尚未计算出任何数据", + "homeNothingDerivedBody": "尚未处理任何腕带记录。", + "homeAskCoach": "咨询教练", + "homeProfileSettings": "个人资料与设置", + "homeNothingTodayTitle": "今天没有记录到数据", + "homeNothingTodayBody": "应用评分的最后一晚是 {day}。此后没有收到任何新数据。", + "homeReadinessNotScoredTitle": "今天的恢复度未评分", + "homeReadinessNeedBody": "{need},才能了解你的正常水平。", + "homeReadinessNoReason": "没有记录说明原因。", + "homeSeeWhatWasMissing": "查看缺失的内容", + "homeAtAGlance": "概览", + "homeTodaysPlan": "今日计划", + "homeBreakdownTitle": "今日详情", + "homeBreakdownSubtitle": "逐小时查看", + "homeIllnessRedTitle": "已连续多晚偏离你的正常水平", + "homeIllnessAmberSameNight": "昨晚超出了你的正常范围", + "homeIllnessAmberOtherNight": "{day} 超出了你的正常范围", + "homeIllnessBodyNoZ": "你的夜间静息心率持续高于自身基线。这只反映了单一信号,说明存在某种趋势,但并不能说明原因。", + "homeIllnessBodyAbove": "你的夜间静息心率持续高于自身基线;那一晚高出基线 {z} 个标准差。这只反映了单一信号,说明存在某种趋势,但并不能说明原因。", + "homeIllnessBodyBelow": "你的夜间静息心率持续高于自身基线;那一晚低于基线 {z} 个标准差。这只反映了单一信号,说明存在某种趋势,但并不能说明原因。", + "homeIllnessAdvice": "如果持续几天,值得留意。", + "homeHeartRate": "心率", + "homeRestingSub": "静息", + "homeNoRestingHr": "暂无静息心率", + "homeNoRestingHrWhy": "静息心率来自睡眠数据,而昨晚没有记录到睡眠。", + "homeSteps": "步数", + "homeStepsNone": "无", + "homeStepsNotRecorded": "未记录", + "homeStepsPercentGoal": "已达目标 {pct}%", + "homeActiveEnergy": "活动能量", + "homeCaloriesEstimated": "估算值", + "homeCaloriesTotal": "共 {total}", + "homeNoEnergyEstimate": "暂无能量估算", + "homeStepsLeft": "还差 {left} 步", + "homeMovement": "运动", + "homeGoalSteps": "目标 {goal}", + "homeStepGoalMet": "已达成步数目标", + "homeStrainTargetMet": "已达成消耗目标", + "homeAimForStrain": "目标消耗值 {aim}", + "homeTraining": "训练", + "homeSleepNeedRow": "需睡眠 {duration}", + "homeTonight": "今晚", + "homeNeed": "待计算", + "homeBedTime": "建议 {time} 就寝", + "homeNoPlanTitle": "今日暂无计划", + "homeNoPlanWhyStale": "计划所依据的跨日汇总数据正在重建。", + "homeNoPlanWhyNone": "尚未建立任何基线。", + "homeGreetingStillUp": "还没睡", + "homeGreetingMorning": "早上好", + "homeGreetingAfternoon": "下午好", + "homeGreetingEvening": "晚上好", + "wellnessTitle": "健康", + "wellnessTabMind": "心境", + "wellnessTabRecovery": "恢复", + "wellnessTabHabits": "习惯", + "wellnessTabMedication": "用药", + "wellnessTabCycle": "周期", + "wellnessStartASitting": "开始一次练习", + "wellnessExercisesNoun": "个练习", + "wellnessPickOneAndGo": "选一个开始吧", + "wellnessLastMinutes": "上次:{count} 分钟", + "wellnessWriteTheDayDown": "记录今天", + "wellnessOpen": "打开", + "wellnessStressLastNight": "昨晚的压力", + "wellnessNoStressTitle": "昨晚没有压力读数", + "wellnessNoStressBody": "压力是根据你夜间休息时的心跳节律计算的,昨晚没有产生读数。", + "wellnessAutonomicTension": "自主神经紧张度", + "wellnessStressLevelLow": "低", + "wellnessStressLevelNormal": "正常", + "wellnessStressLevelElevated": "偏高", + "wellnessStressLevelHigh": "高", + "wellnessJournalDefaultSubtitle": "任何你想记住的关于今天的事", + "wellnessJournalSubtitleShort": "{fields},还有一条笔记", + "wellnessJournalSubtitleLong": "{fields}等,还有 {more} 项,加上一条笔记", + "wellnessTurnInBy": "请在 {time} 前就寝", + "wellnessDebtBody": "你比自己的需求少睡了 {debt},而今晚的需求是 {need}。", + "wellnessSeeWhatLastNightCost": "看看昨晚让你付出了什么代价", + "wellnessWhatChargedAndDrained": "什么给你充电,什么让你耗竭", + "wellnessNoDriversTitle": "暂无恢复力驱动因素", + "wellnessNoDriversBody": "需要足够多的夜晚数据,才能了解你自己的正常水平。", + "wellnessSleepNeedTonight": "今晚的睡眠需求", + "wellnessNoSleepNeedTitle": "暂无睡眠需求数据", + "wellnessNoSleepNeedBody": "没有记录说明今晚为何没有睡眠需求数据。", + "wellnessTonightsNeed": "今晚的需求", + "wellnessSleepDebt": "睡眠债", + "wellnessAddedForStrain": "因负荷而增加", + "wellnessCreditedFromNaps": "因小睡而抵扣", + "wellnessTargetBedtime": "目标就寝时间", + "wellnessTargetWake": "目标起床时间", + "wellnessRemoveHabitSemantic": "移除{label}", + "wellnessDaysYouDidIt": "完成的天数", + "wellnessAddAHabit": "添加习惯", + "wellnessWhatYouLogTitle": "你的记录,对照你的数据", + "wellnessWhatYouLogSubtitle": "剂量、习惯差异,以及星期几", + "wellnessRemoveHabitConfirmTitle": "移除{label}?", + "wellnessRemoveHabitConfirmBody": "将不再询问这项。已记录的天数会保留。", + "wellnessHabitHint": "午饭后散步", + "wellnessAlreadyTrack": "你已经在追踪“{name}”了。", + "wellnessNothingScheduledTitle": "尚未安排任何用药", + "wellnessNothingScheduledBody": "添加你服用的药物及服用时间。", + "wellnessAddAMedication": "添加用药", + "wellnessNothingDueTodayTitle": "今天没有待服用的药物", + "wellnessNothingDueTodayBody": "你的用药安排在其他日期或时间。", + "wellnessAdherence": "依从性", + "wellnessNothingToScoreTitle": "暂无可评估的数据", + "wellnessNothingToScoreBody": "还没有已计划的剂量到期。", + "wellnessTakenOfScheduled": "过去七天已安排剂量中已服用的比例。", + "wellnessDosesUnit": "剂", + "wellnessUndoSkipped": "撤销跳过", + "wellnessSkippedOnPurpose": "有意跳过", + "wellnessBackToNotTaken": "恢复为未服用。", + "wellnessRecordedAsDecision": "记为主动决定,而非遗漏。", + "wellnessWhichDaysDue": "在哪些天需要服用", + "wellnessRemoveMedTitle": "移除{label}", + "wellnessRemoveMedBody": "将不再安排此药。已标记的剂量会保留。", + "wellnessRemoveMedConfirmTitle": "移除{label}?", + "wellnessRemoveMedConfirmBody": "将不再安排此药,也不再计入依从性统计。已标记的剂量会保留。", + "wellnessMedHint": "维生素 D", + "wellnessNameLabel": "名称", + "wellnessAdd": "添加", + "wellnessEveryDay": "每天", + "wellnessWeekdays": "工作日", + "wellnessWeekends": "周末", + "wellnessMon": "周一", + "wellnessTue": "周二", + "wellnessWed": "周三", + "wellnessThu": "周四", + "wellnessFri": "周五", + "wellnessSat": "周六", + "wellnessSun": "周日", + "wellnessWhenYouTakeIt": "服用时间", + "wellnessChangeTheTime": "更改时间", + "wellnessWhichDays": "服用日期", + "wellnessPickAtLeastOneDay": "请至少选择一天。", + "wellnessDueDays": "服用日:{days}。", + "wellnessMoreForMed": "{label}的更多操作", + "wellnessMedAtTime": "{label},{time}", + "wellnessStateTaken": "已服用", + "wellnessStateSkipped": "已跳过", + "wellnessStateNotTaken": "未服用", + "wellnessStateDueLater": "稍后服用", + "wellnessMarkDone": "标记为完成", + "wellnessWhatYouLogScreenTitle": "你的记录", + "wellnessNothingSeparatedTitle": "暂无突出的发现", + "wellnessNothingSeparatedBody": "你记录的一切都会与你的恢复力、心率变异性、静息心率和睡眠效率进行比对。目前还没有达到显著标准的发现。", + "wellnessTheDaysYouDidIt": "你做到的那些天", + "wellnessHowMuchAndWhatFollowed": "做了多少,随后发生了什么", + "wellnessLinkNeverCause": "这只是你自己的日子里的一种关联,绝非因果。你做某件事的那些天,本来就已经是那种类型的日子。", + "wellnessWhichDayOfWeek": "哪个星期几", + "wellnessHigher": "更高", + "wellnessLower": "更低", + "wellnessHeadlineBinary": "在你记录{field}的 {n} 天里,{outcome}{amount}{direction}", + "wellnessHeadlineNoSlope": "在你记录{field}的 {n} 天里,记录得越多,{outcome}就越{direction}", + "wellnessHeadlineSlope": "在你记录{field}的 {n} 天里,{outcome}每{step}就{amount}{direction}", + "wellnessMatchedSameDay": "与当天的数据进行比对。", + "wellnessMatchedNightFollowed": "与随后那一晚进行比对。", + "wellnessMatchedNightEnded": "与当天早上结束的那一晚进行比对。", + "wellnessAgainstDaysYouDidNot": "相对于你没有这样做的 {n} 天", + "wellnessRangeTo": "{lo} 至 {hi}", + "wellnessRankCorrelation": "等级相关系数 {rho}{ci}。", + "wellnessCaffeineCaveat": "这只是你当天最后一次摄入咖啡因的时间:两杯和五杯在这里看起来是一样的,所以“更晚”实际上可能意味着“更多”。漫长而紧张的一天既会导致晚喝咖啡,也会导致睡眠不佳。", + "wellnessHourLater": "小时", + "wellnessPointUnit": "点", + "wellnessNotEnoughWeeksTitle": "数据周数还不够", + "wellnessNotEnoughWeeksBody": "比较七个星期几至少需要八周的数据,且每个星期几都要出现五次以上。", + "wellnessNoDayStandsOutTitle": "没有哪一天特别突出", + "wellnessNoDayStandsOutBody": "在考虑到七天都被检验过之后,没有哪一天明显区别于其他六天。", + "wellnessWeekdayHeadline": "{weekday}:恢复力比你的总体中位数{direction} {delta}", + "wellnessWeekdayDetail": "基于其中 {n} 天的数据。星期几本身不是原因,而是你在那天所做之事的载体。这里的内容都不是建议。", + "wellnessPluralMonday": "周一", + "wellnessPluralTuesday": "周二", + "wellnessPluralWednesday": "周三", + "wellnessPluralThursday": "周四", + "wellnessPluralFriday": "周五", + "wellnessPluralSaturday": "周六", + "wellnessPluralSunday": "周日", + "sleepDetailNavTitle": "睡眠", + "sleepDetailNoNightTitle": "没有可显示的夜间数据", + "sleepDetailNoNightBody": "手环记录的时长不足以进行评分。", + "sleepDetailNoNightFix": "整夜佩戴手环,早上同步数据", + "sleepDetailStagesSection": "睡眠阶段", + "sleepDetailVersusUsualSection": "与你的日常对比", + "sleepDetailUnusualLastNight": "昨晚的异常情况", + "sleepDetailUnusualOnDay": "{day}的异常情况", + "sleepDetailOvernightSection": "夜间信号", + "sleepDetailTonightSection": "今晚", + "sleepDetailTotalSleep": "总睡眠时长", + "sleepDetailInBed": "在床上", + "sleepDetailWatched": "已监测", + "sleepDetailAsleepOfThat": "其中入睡占比", + "sleepDetailAsleep": "入睡占比", + "sleepDetailWatchedExplain": "在你{inBed}的卧床时间中,我们监测到了{watched};其余时间不属于测量范围。入睡比例及下方各阶段占比均以监测到的时间为准计算。", + "sleepDetailWindowMine": "这个时间段是你自己设定的", + "sleepDetailWindowFallback": "这个时间段是根据心率推断的", + "sleepDetailWindowAuto": "这个时间段是根据信号自动判定的", + "sleepDetailWindowFallbackBody": "系统未能找到明确的起止点,因此这些时间是一个估计值。", + "sleepDetailWindowSol": "从时间段开始到入睡:{band}。", + "sleepDetailConfirmTimes": "这些时间是对的", + "sleepDetailChangeTimes": "修改时间", + "sleepDetailSetTimesMyself": "自己设置时间", + "sleepDetailBackToAutomatic": "恢复自动判定", + "sleepDetailReanalysing": "正在重新分析这一夜…", + "sleepDetailCorrectionFailedTitle": "该修正未能应用", + "sleepDetailBedTimeHelp": "上床时间", + "sleepDetailWakeTimeHelp": "起床时间", + "sleepDetailReanalyseFailed": "这一夜未能重新分析——可能另一次重新分析正在进行,也可能分析失败了。你设置的时间已保存;在“你的数据”中点击“重新分析全部”即可应用。", + "sleepDetailNoHypnogramTitle": "这一夜没有睡眠阶段图", + "sleepDetailNoHypnogramBody": "阶段判定需要运动和心跳时序数据,其中有一项缺失。", + "sleepDetailThroughTheNight": "整夜变化", + "sleepDetailUnitStage": "阶段", + "sleepDetailTapDragCycles": "{n, plural, other{点击或拖动图表查看任意时刻。共 {n} 个睡眠周期。}}", + "sleepDetailTapDragCyclesAvg": "{n, plural, other{点击或拖动图表查看任意时刻。共 {n} 个睡眠周期,平均 {avg}。}}", + "sleepDetailTapDragNone": "点击或拖动图表查看整夜的任意时刻。", + "sleepDetailNoWakeups": "没有持续5分钟及以上的醒来;更短的醒来手环无法识别。", + "sleepDetailAtLeastWakeups": "{n, plural, other{至少有 {n} 次持续5分钟及以上的醒来;更短的醒来手环无法识别。}}", + "sleepDetailLongestStretch": "最长连续睡眠时段 {longest}。", + "sleepDetailHypnogramLabel": "睡眠阶段图", + "sleepDetailPercentThroughNight": "整夜的{pct}%处", + "sleepDetailNotMeasured": "未测量", + "sleepDetailScrubAt": "{at},{stage}", + "sleepDetailHeartRate": "心率", + "sleepDetailHrv": "心率变异性", + "sleepDetailBreathing": "呼吸", + "sleepDetailTemp": "体温", + "sleepDetailNotMeasuredCap": "未测量", + "sleepDetailNoSignalAtMoment": "此刻没有记录到信号。", + "sleepDetailStageAwake": "清醒", + "sleepDetailStageRem": "快速眼动", + "sleepDetailStageLight": "浅睡眠", + "sleepDetailStageDeep": "深睡眠", + "sleepDetailDeep": "深睡", + "sleepDetailLight": "浅睡", + "sleepDetailNoStageSplitTitle": "这一夜没有阶段划分数据", + "sleepDetailNoStageSplitBody": "整个时间段内没有心跳时序数据。", + "sleepDetailStageRangeExplain": "每个阶段显示为一个范围而非精确计数——夜间监测得越好,范围就越窄。深睡的范围最宽,清醒仍以单一数值表示。详细统计中有精确计数。", + "sleepDetailTimeAsleep": "入睡时长", + "sleepDetailShorterThanUsual": "比平时短", + "sleepDetailLongerThanUsual": "比平时长", + "sleepDetailLessThanUsual": "比平时少", + "sleepDetailMoreThanUsual": "比平时多", + "sleepDetailAsleepWhileInBed": "卧床期间的入睡比例", + "sleepDetailLowerThanUsual": "比平时低", + "sleepDetailHigherThanUsual": "比平时高", + "sleepDetailFellAsleep": "入睡时间", + "sleepDetailEarlierThanUsual": "比平时早", + "sleepDetailLaterThanUsual": "比平时晚", + "sleepDetailNotEnoughNightsTitle": "夜晚数据不足,无法比较", + "sleepDetailNightsSoFar": "目前已有 {have}/{min} 晚数据", + "sleepDetailBarExplain": "此条形表示你自己夜晚数据的中间一半范围。", + "sleepDetailLessThanAny": "{noun}{value}——低于你最近 {count} 晚中的任何一晚,其中最低的一晚是 {lowest}。", + "sleepDetailMoreThanAny": "{noun}{value}——高于你最近 {count} 晚中的任何一晚,其中最高的一晚是 {highest}。", + "sleepDetailYouSlept": "你睡了", + "sleepDetailShortestNightLately": "你近期最短的一夜", + "sleepDetailLongestNightLately": "你近期最长的一夜", + "sleepDetailSleepingHrHighTitle": "睡眠心率偏高", + "sleepDetailSleepingHrHighBody": "比你自己的基线高 {bpm} 次/分钟。这在饮酒后、晚餐较晚、高强度训练后或感染初期较为常见——这是一项测量结果,而非诊断。", + "sleepDetailNothingStoodOut": "没有异常情况。", + "sleepDetailSleepingHr": "睡眠心率", + "sleepDetailLowest": "最低值", + "sleepDetailBreathingCaps": "呼吸", + "sleepDetailSkinTemp": "皮肤温度", + "sleepDetailSleepNeedNotEstablished": "睡眠需求尚未确定", + "sleepDetailYourNeedIs": "你的睡眠需求为{need}", + "sleepDetailYouAreDown": "你还差{debt}", + "sleepDetailLightsOut": "熄灯时间", + "sleepDetailToAimFor": "目标时长", + "sleepDetailNoPersonalRangeYet": "尚无个人参考范围——目前 {count}/{min} 晚。", + "sleepDetailNotFarEnoughToCall": "与平时的差异还不足以判断", + "sleepDetailTypicalForYou": "属于你的正常水平", + "sleepDetailVerdictSummary": "{verdict}·{n} 晚中的平时范围为 {lo}–{hi}", + "sleepDetailNoOvernightTitle": "没有夜间信号曲线", + "sleepDetailNoOvernightBody": "这一天没有收到任何夜间记录。", + "sleepDetailSolUnder15": "少于15分钟", + "sleepDetailSolOverHour": "超过一小时", + "sleepDetailSolRange": "{lo}–{hi}分钟", + "workoutTabForYou": "为你推荐", + "workoutTabActivities": "活动", + "workoutTabHistory": "历史记录", + "workoutScreenTitle": "锻炼", + "workoutStartSessionLabel": "开始训练", + "workoutActivitiesNoun": "项活动", + "workoutThisWeek": "本周", + "workoutTrainingLoad": "训练负荷", + "workoutTodaysStrainAction": "今日紧张度", + "workoutMechanicalLoadTitle": "力量负荷", + "workoutKgLiftedUnit": "举起公斤数", + "workoutTonnageFootnoteIntro": "次数 × 负荷(记录了重量的组)。", + "workoutTonnageFootnotePartial": "未记录重量的组不计入内,因此这是下限而非总量。", + "workoutTonnageFootnoteOutro": "对你输入的内容是精确的,但在不同动作之间没有可比性——因此不计入紧张度和恢复评分。", + "workoutOverreachHeadline": "你过去 7 天的负荷是你惯常六周的 {ratio} 倍,且静息心率在 {nightsConsidered} 个夜晚中有 {nightsElevated} 个高于平时。", + "workoutOverreachBody": "两项指标恰好指向同一个方向。生病、旅行、海拔、饮酒和连续几晚睡眠不佳都可能造成同样的模式,这里无法区分它们。", + "workoutNoLoadTitle": "暂无训练负荷数据", + "workoutNoLoadBody": "体能和疲劳分别是 42 天和 7 天的平均值,大约需要两周的训练记录。", + "workoutFitnessLabel": "体能", + "workoutDailyLoadTitle": "每日负荷", + "workoutTrimpUnit": "TRIMP", + "workoutDailyLoadFootnoteIntro": "班尼斯特训练冲量——按心率储备加权的分钟数。", + "workoutDailyLoadAllDays": "过去七天。", + "workoutDailyLoadPartialDays": "过去七天中有 {days} 天产生了数据。", + "workoutFatigueLabel": "疲劳", + "workoutFormLabel": "状态", + "workoutNotYet": "暂无", + "workoutFormFresh": "充沛", + "workoutFormSteady": "稳定", + "workoutFormBuilding": "上升中", + "workoutFormOverreaching": "过度训练", + "workoutSearchActivitiesLabel": "搜索活动", + "workoutSearchActivitiesCount": "{count, plural, other{搜索 {count} 项活动}}", + "workoutQuickStartHeader": "快速开始", + "workoutCalorieNeedWeightTitle": "卡路里估算需要你的体重", + "workoutAddWeightFix": "在个人资料中添加体重", + "workoutCalorieEstimatesTitle": "卡路里数值均为估算值", + "workoutSuggestionsTitle": "{n, plural, other{检测到 {n} 次未记录的运动}}", + "workoutSuggestionsBody": "手环检测到持续的运动,但没有开始记录。除非你确认,否则不会记录任何内容。", + "workoutReviewFix": "{n, plural, other{查看}}", + "workoutLogPastTitle": "有手环没监测到的运动吗?", + "workoutLogPastBody": "自己输入时间,系统会根据该时段记录的心率进行评分,与其他训练记录一样。", + "workoutLogPastFix": "记录一次过去的训练", + "workoutNoSessionsTitle": "还没有训练记录", + "workoutNoSessionsBody": "开始一次训练后,记录会显示在这里。", + "workoutStartWorkoutFix": "开始训练", + "workoutTrackedLabel": "已记录", + "workoutWeeklyLoadLabel": "本周负荷", + "workoutNoneLabel": "无", + "workoutImportedThisWeekNote": "本周有 {count} 次训练来自 {storeName}。它们计入这里,但不计入每周负荷——导入的训练没有心率轨迹,若据此编造负荷数值并不真实。", + "workoutAutoImportOnLabel": "自动导入已开启,点击可关闭。", + "workoutAutoImportOffLabel": "自动导入已关闭,点击可开启。", + "workoutImportFromStore": "从 {storeName} 导入", + "workoutFetchNowLabel": "立即获取训练记录", + "workoutImportDenied": "{storeName} 未授权访问训练数据,未读取任何内容。", + "workoutImportEmpty": "没有返回任何数据。{storeName} 在共享的时间范围内没有训练记录。", + "workoutImportNoRoutes": " {storeName} 不会分享路线,因此没有坐标数据。", + "workoutImportNoneWithRoute": " 其中没有记录路线的训练。", + "workoutImportSomeWithRoute": " 有 {count} 次训练带有路线。", + "workoutImportBroughtIn": "{count, plural, other{已导入 {count} 次训练。}}", + "workoutImportFailed": "失败:{error}", + "workoutMorningAfterTitle": "次日清晨", + "workoutMorningAfterBody": "这是你自己的历史记录,而非关于该项运动的规律——这些清晨也伴随着前一晚的情况。这里的任何内容都不是跳过训练的理由。", + "workoutAfterActivity": "{name} 之后", + "workoutUnchangedLabel": "无变化", + "workoutRestingHeartRateLabel": "静息心率", + "workoutHrvLabel": "心率变异性", + "workoutMorningCount": "{n, plural, other{{n} 个清晨}}", + "workoutInsideRangeSuffix": " · 处于你惯常的夜间波动范围内", + "workoutDeleteSessionLabel": "删除此次训练", + "workoutStrainLabel": "紧张度", + "workoutTimeInZonesTitle": "各心率区间时长", + "workoutMinutesUnit": "分钟", + "workoutFixTimesOnSessionLabel": "修正此次训练的时间", + "workoutFixTimes": "修正时间", + "workoutTimeStatLabel": "时长", + "workoutDistanceStatLabel": "距离", + "workoutCaloriesStatLabel": "卡路里", + "workoutNotCostedValue": "未计算", + "workoutMaxHrStatLabel": "最大心率", + "workoutNoReadingValue": "无读数", + "workoutConfirmDeleteTitle": "删除这次{activity}?", + "workoutDeleteBodyOwn": "它将从 OpenStrap 中消失。如果 {storeName} 中有副本,会保留在原处。", + "workoutDeleteBodyImported": "它将从 OpenStrap 中消失,且不会被重新导入。{storeName} 中的原始记录会保留。", + "workoutWhenToday": "今天,{time}", + "workoutWhenYesterday": "昨天,{time}", + "workoutWeekdayLetterMon": "一", + "workoutWeekdayLetterTue": "二", + "workoutWeekdayLetterWed": "三", + "workoutWeekdayLetterThu": "四", + "workoutWeekdayLetterFri": "五", + "workoutWeekdayLetterSat": "六", + "workoutWeekdayLetterSun": "日", + "workoutWeekdayAbbrMon": "周一", + "workoutWeekdayAbbrTue": "周二", + "workoutWeekdayAbbrWed": "周三", + "workoutWeekdayAbbrThu": "周四", + "workoutWeekdayAbbrFri": "周五", + "workoutWeekdayAbbrSat": "周六", + "workoutWeekdayAbbrSun": "周日", + "activitySetupRouteLabel": "路线", + "activitySetupRouteDetail": "有定位信息时会记录,并保存在本机上", + "activitySetupHeartRateLabel": "心率", + "activitySetupBandConnected": "已连接手环", + "activitySetupNoBandConnected": "未连接手环", + "activitySetupPrivateLabel": "隐私训练", + "activitySetupPrivateDetail": "不会出现在摘要或导出内容中", + "activitySetupCaloriesNeedWeight": "计算卡路里需要你的体重。", + "activitySetupCalorieEstimate": "根据 {met} MET 和你的体重,大约每 {minutes} 分钟消耗 {est} 千卡。", + "activitySetupTrackSets": "组数、次数和重量——由你自行记录", + "activitySetupTrackDistanceGps": "距离、配速和心率", + "activitySetupTrackTime": "时长和心率", + "activitySetupTrackInterval": "轮次和心率", + "activitySetupTrackStillness": "时长、呼吸和静止状态", + "activitySetupSessionRunningTitle": "已有一项训练正在进行", + "activitySetupSessionRunningBody": "同一时间只能进行一项训练。", + "activitySetupOpenRunningSession": "打开正在进行的训练", + "activitySetupStart": "开始", + "activityPickerTitle": "选择活动", + "activityPickerSearchLabel": "搜索活动", + "activityPickerSearchHint": "在 {count} 项活动中搜索", + "activityPickerNoMatchTitle": "没有匹配的活动", + "activityPickerNoMatchBody": "活动库收录了约七十项已公布能耗数据的活动,请选择最接近的一项。", + "activityPickerQuickStart": "快速开始", + "activityPickerRecent": "最近使用", + "activityPickerCalorieEstimatesTitle": "卡路里数值为估算值", + "activityPickerMetValue": "{met} MET", + "activityPickerKcalPer30": "{kcal} 千卡 / 30 分钟", + "dayStrainToday": "今天", + "dayStrainTitle": "当日应力", + "dayStrainNoTraceTitle": "这一天没有应力曲线记录", + "dayStrainNoMinuteTraceTitle": "这一天没有逐分钟记录", + "dayStrainNoReasonBody": "没有任何记录说明这一天为何没有产生应力。", + "dayStrainScoredNoTraceBody": "当日应力为 {strain}。用于计算该数值的清醒分钟数据未针对这一天保存。", + "dayStrainWearBandFix": "全天佩戴设备", + "dayStrainChartTitle": "全天应力变化", + "dayStrainChartFootnote": "该曲线为累积值,因此只会上升——陡峭部分即为发力所在。基于记录到的 {drawn} 个清醒分钟计算。", + "dayStrainPeakHr": "最高心率", + "dayStrainWorn": "佩戴时长", + "dayStrainLowCoverageTitle": "设备记录了这一天的 {pct}%", + "dayStrainLowCoverageBody": "应力是所有已记录分钟数的总和,因此只佩戴了部分时间的一天读数会低于完整佩戴的一天,两者不可比较。", + "dayStrainTimeInZonesSection": "各区间时长", + "dayStrainZonesChartTitle": "各区间时长", + "dayStrainZoneFootnoteKarvonen": "区间边界跨越你测得的静息心率与我们观测到的最高心率({maxHr} bpm)之间的差距。两者均基于你本人测量。", + "dayStrainZoneFootnoteObserved": "区间边界是基于我们观测到的最高心率({maxHr} bpm)的百分比——这是实测值,而非估算值。", + "dayStrainHowSet": "如何设定", + "dayStrainInputsSection": "由什么构成", + "dayStrainInputsBase": "基于清醒时心率的 Banister TRIMP,缩放至 0–21。", + "dayStrainInputsMaxHr": "该计算基于假设的最大值 {maxHr} bpm——根据你的年龄和设备估算得出,并非实测。", + "dayStrainInputsMeasuredCeilingNote": "上方的区间条使用的是实测上限;应力值尚未迁移到该上限,因为那将改写你曾看到的所有应力分数。", + "dayStrainInputsRhrAnchor": "另一个锚点是前一晚的静息心率,因此设备漏记的一晚会影响整整一天的数据。", + "activityZonesTitle": "心率区间", + "activityZonesYourZonesSection": "你的区间", + "activityZonesIntensitySection": "你的强度分布", + "activityZonesNoCeilingTitle": "尚无实测上限", + "activityZonesNoCeilingTanakaTail": " 在测得实际值之前,下方的区间基于你的年龄计算。", + "activityZonesNoCeilingDefaultBody": "我们只计入设备在你运动时持续 15 秒的高心率读数。仅持续一秒的峰值不算作心率。", + "activityZonesWearBandFix": "在你平常的高强度训练中佩戴设备", + "activityZonesHighestSeenLabel": "已观测到的最高值", + "activityZonesBpmUnit": "次/分", + "activityZonesCeilingOnDate": "于 {date}", + "activityZonesCeilingDuringSession": "在 {session} 期间", + "activityZonesHighestSeenFootnote": "这是我们测得的最高值,而非上限——随着设备观测到更高强度的运动,它会逐渐上升。请不要刻意去挑战它。", + "activityZonesNoZonesTitle": "尚无区间", + "catalogueZonesWhy": "区间边界是根据你的年龄估算的最大心率的百分比——并非在你身上实测得出。", + "activityZonesNoAgeBody": "区间边界是最大心率的百分比,如果没有你的年龄,就无法计算出这个百分比。", + "activityZonesNoZonesDefaultBody": "没有任何记录说明为什么还没有区间边界。", + "activityZonesAddAgeFix": "在个人资料中添加你的年龄", + "activityZonesAnchorKarvonen": "基于设备在你身上测得的两个数值计算:你的静息心率({restingHr},过去 {restingDays} 晚的中位数)和已观测到的最高心率({maxHr})。较低的静息心率会使区间 1 变宽。这些是通用区间划分,并非你本人的实测阈值。", + "activityZonesAnchorObserved": "基于已观测到的最高心率({maxHr})计算。累积 {restingMinDays} 晚静息心率数据后(你目前有 {restingDays} 晚),你的静息心率将加入计算,更贴合你个人情况。这些是通用区间划分,并非你本人的实测阈值。", + "activityZonesAnchorTanaka": "基于 {maxHr} bpm 计算,该值根据你的年龄估算而非实测——上下浮动可达 20 bpm。一旦设备观测到强度足够的一次训练,边界将切换为实测上限。", + "activityZonesAnchorDefault": "区间边界是最大心率的百分比。", + "activityZonesNotShownTitle": "尚未显示", + "activityZonesNeedsMonthBody": "需要约一个月的训练记录,且每次都有逐分钟心率数据。", + "activityZonesAgeEstimateBody": "这些柱状图只会反映年龄估算值,而非你的训练情况。一旦上方的区间边界被实测,它们才会出现。", + "activityZonesSessionMinutesChartTitle": "训练分钟数,过去 28 天", + "activityZonesShapePyramidal": "大部分时间为轻松强度,中等强度较少,高强度最少——呈金字塔形。", + "activityZonesShapePolarised": "大部分时间为轻松强度,其余为高强度,中等强度很少。", + "activityZonesShapeMiddleHeavy": "大部分时间处于中等强度,而非轻松或高强度。", + "activityZonesShapeSummary": "轻松 {easy} 分钟,中等 {moderate} 分钟,高强度 {hard} 分钟,共 {sessions} 次记录的训练。这是描述,不是目标。", + "activityShareTitle": "分享", + "activityShareOpenFailed": "无法打开分享面板。", + "activitySharePhotoHeader": "你的照片", + "activityShareAddPhoto": "添加照片", + "activityShareChangePhoto": "更换照片", + "activitySharePhotoHint": "来自本机相册,不会上传", + "activityShareRemovePhoto": "移除照片", + "activityShareBasemapHeader": "底图", + "activityShareDrawMap": "显示真实地图", + "activityShareMapHint": "向 openstreetmap.org 请求覆盖这条路线的地图瓦片。关闭时,路线将独立绘制", + "activityShareFetchingMapTitle": "正在获取地图", + "activityShareFetchingMapBody": "每加载完一块瓦片,卡片就会随之绘制。", + "activityShareNoMapTitle": "此卡片没有地图", + "activityShareNoMapBody": "地图瓦片获取失败,因此路线将独立绘制。卡片的其余部分不受影响。", + "activityShareStatusPrivateTitle": "此次活动为私密", + "activityShareStatusPrivateBody": "不会出现在摘要和导出内容中。", + "activityPosterFormatPost": "帖子", + "activityPosterFormatStory": "快拍", + "activitySummaryRpeHeadline": "这次感觉有多累?", + "activitySummaryRpeBody": "这是你自己对运动强度的评价。它是一种感受,而不是测量值——这正是它的意义所在,因为它可能与上面的数据不一致。", + "activitySummaryRateEffort": "将这次强度评为 10 分中的 {n} 分", + "activitySummaryRpeVeryEasy": "1 · 非常轻松", + "activitySummaryRpeMaximal": "10 · 极限", + "activitySummaryNotNow": "暂不评分", + "activitySummaryShareThis": "分享这次{name}", + "activitySummaryChangeType": "更改活动类型", + "activitySummaryUnsavedTitle": "此次记录尚未保存", + "activitySummaryUnsavedBody": "写入本机失败。", + "activitySummarySaving": "正在保存", + "activitySummaryTryAgain": "重试", + "activitySummaryPrivate": "私密", + "activitySummaryStepsBasis": "步数来自腕带自身的运动传感器,仅在步行时计数。", + "activitySummaryCaloriesNeedWeight": "计算卡路里需要你的体重。", + "activitySummaryNoCalorieNoStrain": "此次记录没有卡路里数据。根据心率估算能量消耗需要你的最大心率和静息心率,其中一项尚未设置。", + "activitySummaryNoCalorieWithStrain": "此次记录没有卡路里数据——根据心率估算能量消耗需要你的最大心率和静息心率,其中一项尚未设置。上方的压力值是实际测得的运动强度,采用其自身的 0–21 分制。", + "activitySummaryCalorieNoHr": "根据 {met} MET 和你的体重估算。本次未记录到心率,因此该数值未纳入计算。", + "activitySummaryCalorieWithHr": "根据 {met} MET、你的体重和心率估算。", + "activitySummaryNothingLoggedWithLoad": "未记录任何带负重的项目", + "activitySummarySetUnit": "{n, plural, other{组}}", + "activitySummaryVolumeLoadedSets": "有负重组的总量", + "activitySummaryTotalVolume": "总训练量", + "activitySummaryElapsedTime": "已用时间", + "activitySummaryClimbed": "+{m} 米爬升", + "activitySummaryLapsCaption": "{n, plural, other{{n} 趟}}", + "activitySummaryNoRouteTitle": "此次记录没有路线", + "activitySummaryNoRouteBody": "定位功能未开启,或此次活动未使用 GPS 记录。", + "activitySummaryRouteTitle": "路线", + "activitySummarySlower": "较慢", + "activitySummaryFaster": "较快", + "activitySummaryStartFinishPinned": "起点和终点已标出。", + "activitySummaryRouteFootnote": "{distance} {unit},起点和终点已标出。", + "activitySummaryNoSetsTitle": "未记录任何组", + "activitySummaryNoSetsBody": "此次记录没有录入任何内容,因此没有负重和训练量可统计。", + "activitySummaryNoRoundsTitle": "未记录任何回合", + "activitySummaryNoRoundsBody": "已记录 0 个回合。", + "activitySummaryIntervalLadderTitle": "间歇训练阶梯", + "activitySummaryWork": "训练", + "activitySummaryRest": "休息", + "activitySummaryRoundLabel": "第 {n} 回合", + "activitySummaryLongestBlock": "最长的一段为 {time}。", + "activitySummaryPosesCount": "{n, plural, other{{n} 个体式}}", + "activitySummaryNoLapsTitle": "未记录趟数", + "activitySummaryNoLapsBody": "已记录 0 趟。", + "activitySummaryLapsTitle": "趟数", + "activitySummarySecondsPerLap": "每趟用时(秒)", + "activitySummaryLapLabel": "第 {n} 趟", + "activitySummaryPoolLength": "{m} 米泳池", + "activitySummaryFastest": "最快 {time}", + "activitySummarySlowest": "最慢 {time}", + "activitySummaryNoElevationTitle": "没有海拔曲线", + "activitySummaryNoElevationBody": "没有路线,或路线未记录海拔数据。", + "activitySummaryElevationTitle": "海拔", + "activitySummaryStart": "起点", + "activitySummaryFinish": "终点", + "activitySummaryGain": "爬升", + "activitySummaryLoss": "下降", + "activitySummaryPeak": "最高点", + "activitySummaryColdPlungeWhy": "低温会使传感器读取的血管收缩。此处没有读数是预期结果,并非故障。", + "activitySummaryHeatWhy": "高温、汗水以及升温后变松的腕带都会使传感器无法检测到脉搏。此处没有读数属于正常现象,并非故障。", + "activitySummaryNoPulseTitle": "此次{activity}没有脉搏读数", + "activitySummaryOneMinutePulse": "仅有一分钟的脉搏数据,仅此而已", + "activitySummaryPulseGapNote": "腕带在 {total} 分钟中检测到 {have} 分钟的脉搏。这些空白是预期的,图中显示的是它能够检测到的部分。", + "activitySummaryTooShortTitle": "时长过短,无法绘图", + "activitySummaryTooShortBody": "一分钟的心率只是一个点,构不成一条线。", + "activitySummaryNoHrTitle": "此次记录没有心率数据", + "activitySummaryNoHrBody": "运动期间腕带没有报告任何数据。", + "activitySummaryCheckBandConnection": "检查腕带连接", + "activitySummaryPartialTrace": "部分曲线——腕带仅传回了 {pct}% 时段的数据。", + "activitySummaryHeartRateTitle": "心率", + "activitySummaryHardMinutesNote": "有 {min} 分钟超过最大心率的 80%。", + "activitySummaryTimeInZonesTitle": "心率区间时长", + "activitySummaryTopSet": "最佳一组", + "activitySummaryOneRepMax": "预估单次最大力量 {kg} 千克", + "activitySummarySomeSetsNoLoadTitle": "部分组没有负重", + "activitySummarySomeSetsNoLoadBody": "计入组数和次数,但未计入训练量。", + "activitySummaryScore": "比分", + "activitySummaryGameSetLabel": "第 {n} 局", + "activitySummaryNoSplitsTitle": "此次记录没有分段数据", + "activitySummaryNoSplitsBody": "分段数据需要有记录的距离。", + "activitySummaryKm": "公里", + "activitySummaryPace": "配速", + "activitySummaryHr": "心率", + "activitySummarySetsLoggedZero": "已记录 0 组。", + "activitySummaryRoundHeader": "回合", + "activitySummaryWorkHeader": "训练", + "activitySummaryRestHeader": "休息", + "activitySummaryAvgBpm": "平均心率", + "activitySummaryLapHeader": "趟数", + "activitySummaryTimeHeader": "用时", + "activitySummarySpeedVsFastest": "相对最快速度", + "activitySummaryBodyweightReps": "{n, plural, other{{n} 次 · 自重}}", + "activitySummaryRpeValue": "自觉强度 {v}", + "activitySummaryNothingToPlot": "此次{activity}没有可绘制的数据", + "activitySummaryNoSeriesTitle": "没有可绘制的数据", + "activitySummaryNoSeriesBody": "此次记录没有每分钟的数据流。", + "activitySummaryHeartRateZones": "心率区间", + "activitySummaryTabOverview": "概览", + "activitySummaryTabSplits": "分段", + "activitySummaryTabGraphs": "图表", + "activityLiveAddALap": "增加一趟", + "activityLiveAddExerciseTitle": "添加动作", + "activityLiveAllowLocation": "允许定位", + "activityLiveBestLabel": "最佳", + "activityLiveBodyweightExcludedNote": "自重 — 不计入训练量", + "activityLiveBodyweightOnly": "仅自重", + "activityLiveBpmUnit": "次/分", + "activityLiveBwAbbrev": "自重", + "activityLiveChangeStroke": "切换泳姿", + "activityLiveDecrease": "减少{label}", + "activityLiveDeniedForeverBody": "此应用的定位权限已被拒绝,只能在设置中更改。", + "activityLiveDurationHeader": "时长", + "activityLiveEffortRpeHeader": "费力度(RPE)", + "activityLiveEndSet": "结束一局", + "activityLiveExerciseOf": "动作 {index}/{total}", + "activityLiveFinishSessionLabel": "结束训练", + "activityLiveHoldTime": "保持 · {time}", + "activityLiveIncrease": "增加{label}", + "activityLiveIntervalSubtitle": "{workSec} 秒训练 · {restSec} 秒休息", + "activityLiveKcalEstUnit": "千卡 · 估算", + "activityLiveKgVolumeUnit": "千克训练量", + "activityLiveLapButtonLabel": "一趟", + "activityLiveLapsChartTitle": "趟数", + "activityLiveLapsCount": "{count} 趟 · {stroke}", + "activityLiveLapsFootnote": "最快 {time} · 柱形长度表示相对速度。", + "activityLiveLapXLabel": "第 {n} 趟", + "activityLiveLogAsBodyweight": "记为自重训练", + "activityLiveMatchSetSubtitle": "第 {n} 局", + "activityLiveMetrePoolLabel": "{len} 米泳池", + "activityLiveMinimiseLabel": "最小化", + "activityLiveNextExercise": "下一个动作", + "activityLiveNextLabel": "下一阶段", + "activityLiveNextPose": "下一体式", + "activityLiveNextRest": "休息 · {time}", + "activityLiveNextWork": "训练 · {time}", + "activityLiveNoHrBody": "手环未连接,因此本次训练没有数据传入。", + "activityLiveNoHrTitle": "无心率", + "activityLiveNoHrYetBody": "手环已连接,但尚未测到心跳,需要贴合佩戴,位于腕骨上方约一指宽处。", + "activityLiveNoHrYetTitle": "暂无心率", + "activityLiveNoneYet": "暂无记录", + "activityLiveNoRouteFailedBody": "手机在请求定位时返回了错误。", + "activityLiveNoRouteFailedTitle": "无路线:定位失败", + "activityLiveNoRouteNotAllowedTitle": "无路线:定位未被允许", + "activityLiveNoRouteOffBody": "此手机的定位服务已关闭,因此没有定位数据传入。", + "activityLiveNoRouteOffTitle": "无路线:定位已关闭", + "activityLiveOneLapFewer": "减少一趟", + "activityLiveOpenSettings": "打开设置", + "activityLiveOpponentLabel": "对手", + "activityLivePauseLabel": "暂停", + "activityLivePerLapUnit": "每趟", + "activityLivePointLabel": "{side}得分", + "activityLivePoolSubtitle": "{len} 米泳池 · {stroke}", + "activityLivePoseBridge": "桥式", + "activityLivePoseChair": "椅子式", + "activityLivePoseChildsPose": "婴儿式", + "activityLivePoseForwardFold": "前屈式", + "activityLivePoseMountain": "山式", + "activityLivePoseOf": "体式 {index}/{total}", + "activityLivePosePigeon": "鸽子式", + "activityLivePosePlank": "平板支撑", + "activityLivePoseSavasana": "挺尸式", + "activityLivePoseTriangle": "三角式", + "activityLivePoseWarriorTwo": "战士二式", + "activityLivePreviousExercise": "上一个动作", + "activityLivePreviousLabel": "上一次", + "activityLivePrivateSession": "私密训练", + "activityLiveRecordingRoute": "正在记录路线", + "activityLiveRepsBodyweightRow": "{n} 次 · 自重", + "activityLiveRepsLabel": "次数", + "activityLiveRepsLoggedBodyweight": "已记录 {n} 次", + "activityLiveRepsOnly": "{n} 次", + "activityLiveRepsUnit": "次", + "activityLiveRestingHeader": "休息中", + "activityLiveRestWord": "休息", + "activityLiveResumeLabel": "继续", + "activityLiveRoundLabel": "第 {n} 轮", + "activityLiveRouteFootnoteNoDistance": "起点已标记;定位稳定后将显示距离。", + "activityLiveRouteFootnoteWithDistance": "根据目前记录的定位,已行进 {distance}。", + "activityLiveRouteSoFarTitle": "目前路线", + "activityLiveSetNumber": "第 {n} 组", + "activityLiveSetsCountSubtitle": "{n} 组", + "activityLiveSetsListHeader": "各组记录", + "activityLiveSetsUnit": "组", + "activityLiveStepsUnit": "步数", + "activityLiveStrainUnit": "压力值", + "activityLiveStrokeBack": "仰泳", + "activityLiveStrokeBreast": "蛙泳", + "activityLiveStrokeFly": "蝶泳", + "activityLiveStrokeFree": "自由泳", + "activityLiveThisExerciseLabel": "本动作", + "activityLiveTimeInZonesTitle": "各心率区间时长", + "activityLiveTimeUnit": "用时", + "activityLiveTryAgain": "重试", + "activityLiveTurnOnLocation": "开启定位", + "activityLiveVolumeSetsSubtitle": "{kg} 千克 · {n} 组", + "activityLiveWeightLabel": "重量", + "activityLiveWeightRepsLogged": "已记录 {kg} 千克 × {n}", + "activityLiveWorkWord": "训练", + "activityLiveYouLabel": "你", + "activityLiveZoneLabel": "心率区间 {z}", + "activityLiveLogSet": "记录本组", + "activityLiveRestOverAnnounce": "休息结束", + "activityLiveSkipRest": "跳过休息", + "gesturesNavTitle": "双击", + "gesturesSectionTitle": "轻点手环两次", + "gesturesSectionBody": "仅在应用已连接且处于唤醒状态时有效。如果手机不在身边,手环记录的敲击会带着过时的时间戳延迟到达,届时会被忽略,而不会在几小时后才触发。", + "gesturesItDoesTitle": "它会执行", + "gesturesNoPhoneActionsTitle": "手机上没有可用操作?", + "gesturesNoPhoneActionsBody": "“让手机响铃”和“手电筒”未显示,因为应用无法向系统询问此设备允许哪些操作。重新打开应用后再回到此处;上方的应用内操作无论如何都能使用。", + "settingsBarcodeSaveFailed": "无法保存该设置——下次打开应用时它可能会恢复原状。", + "settingsIconRowTitle": "图标", + "settingsIconRowConfirmHint": "iPhone 会要求你确认", + "settingsIconChoiceLabel": "{label} 图标。", + "settingsSelectedSuffix": " 已选中。", + "settingsHealthSyncOff": "已关闭。不会向 {store} 写入任何内容", + "settingsHealthSyncReady": "每天的睡眠、静息心率、HRV、呼吸频率、能量消耗和锻炼数据在最终确定后会写入 {store}", + "settingsHealthSyncNeedsPermission": "{store} 尚未授予写入权限。点按以打开", + "settingsHealthSyncNotInstalled": "尚未安装 Health Connect。点按以获取", + "settingsHealthSyncNeedsUpdate": "Health Connect 版本过旧,无法写入。点按以更新", + "settingsHealthSyncUnsupported": "此设备没有可写入的健康数据存储", + "settingsHealthSyncChecking": "正在检查 {store}…", + "settingsWriteToHealthStoreRowTitle": "写入 {store}", + "settingsHealthShareOffTitle": "已停止贡献数据", + "settingsHealthShareOffNeverUploaded": "从未上传过任何内容,今后也不会。", + "settingsHealthShareOffDetail": "此后不会再上传任何内容。\n\n你的数据库副本已于 {date} 上传。服务器每台设备只保留最近一次的副本。我们已尝试告知服务器你已撤回同意——该消息只发送一次且不会重试,因此如果此手机当时离线,消息就不会送达,我们也无法向你证实副本已被删除。", + "settingsOk": "好", + "settingsHealthShareOnTitle": "要贡献你的健康数据吗?", + "settingsHealthShareOnBody": "每天一次,在 Wi-Fi 且充电状态下,会上传你整个数据库的压缩副本——包括每一天的衍生数据和手环发送过的每一条原始传感器记录,用于改进算法。\n\n这在任何实际意义上都不是匿名的:这是你完整的健康历史。你可以随时关闭此选项,从那一刻起不再发送任何内容。", + "settingsNo": "不", + "settingsContribute": "贡献", + "settingsResetTitle": "删除全部数据?", + "settingsResetBody": "此操作将永久删除,且不会在其他任何地方留有副本:\n\n· 每一天的测量数据、睡眠、锻炼和路线\n· 每一项化验结果、饮食、用药剂量、习惯、呼吸训练和记录的组数\n· 你的日记、周期记录和滚动基线\n· 你的个人资料、所有偏好设置以及保存的任何 AI 密钥\n· 主屏幕小组件和所有已安排的提醒\n\n手环会解除配对,并且无法重新发送已经交出的历史数据。如果需要保留副本,请先在“你的数据”中导出。", + "settingsResetKeepData": "保留我的数据", + "settingsResetDeleteEverything": "删除全部", + "settingsNavTitle": "设置", + "settingsGroupTheBand": "手环", + "settingsAlarmRowTitle": "闹钟", + "settingsAlarmRowSub": "在你的手腕上振动,依据手环自身的时钟", + "settingsGroupThisPhone": "此手机", + "settingsStepsRowTitle": "步数", + "settingsStepsRowSub": "此手机自带的步数计数器,用于弥补手环未覆盖的时段。数据不会离开设备", + "settingsGroupNotifications": "通知", + "settingsManageNotificationsRowTitle": "管理通知", + "settingsManageNotificationsRowSub": "哪些内容可能会打扰你、免打扰时段,以及所有通知的关闭开关", + "settingsGroupPreferences": "偏好设置", + "settingsUnitsRowTitle": "单位", + "settingsAppearanceRowTitle": "外观", + "settingsCycleTrackingRowTitle": "周期追踪", + "settingsCycleTrackingRowSub": "在“健康”中添加“周期”标签页。关闭后会隐藏该标签页,但已记录的内容仍会保留", + "settingsGroupYourData": "你的数据", + "settingsExportBackupImportRowTitle": "导出、备份、导入", + "settingsExportBackupImportRowSub": "电子表格、完整副本,以及导入历史数据", + "settingsGroupAutomation": "自动化", + "settingsDoubleTapRowTitle": "双击", + "settingsDoubleTapRowSub": "在手环上双击会执行的操作", + "settingsTaskerShortcutsRowTitle": "Tasker 和快捷指令", + "settingsTaskerShortcutsRowSub": "仅 Android 支持事件外发。iOS 可以让手环振动,但无法被手环触发", + "settingsGroupPrivacy": "隐私", + "settingsCrashReportsRowTitle": "崩溃报告", + "settingsCrashReportsRowSub": "在你同意之前不会发送任何内容", + "settingsBarcodeLookupRowTitle": "在线查询条形码", + "settingsBarcodeLookupRowSub": "将扫描到的条形码发送到 openfoodfacts.org。不会附带任何与你相关的信息", + "settingsContributeHealthDataRowTitle": "贡献我的健康数据", + "settingsContributeHealthDataRowSub": "每天一次,在 Wi-Fi 且充电状态下上传你的完整数据库,用于改进算法", + "settingsCheckForUpdatesRowTitle": "检查更新", + "settingsUpdateBelowMinimum": "此版本低于受支持的最低版本。请从 GitHub 安装更新版本", + "settingsUpdateAvailable": "GitHub 上已发布更新版本", + "settingsUpdateCheckSub": "启动时会向发布服务器发起请求,服务器会看到你的 IP 地址以及打开应用的时间", + "settingsGroupAbout": "关于", + "settingsVersionRowTitle": "版本", + "settingsNoticesLicencesRowTitle": "声明与许可", + "settingsNoticesLicencesRowSub": "本应用不是谁,以及它使用了谁的数据", + "settingsGroupDeveloper": "开发者", + "settingsComponentGalleryRowTitle": "组件库", + "settingsComponentGalleryRowSub": "所有组件,在任意文字大小、任意主题下的展示", + "settingsDeveloperModeRowTitle": "开发者模式", + "settingsResetAllDataRowTitle": "重置所有数据", + "settingsNotificationsNavTitle": "通知", + "settingsNotificationsNavSub": "哪些内容可能打扰你", + "settingsNotificationsOffSystemTitle": "系统级别的通知已关闭", + "settingsNotificationsOffSystemBody": "在系统允许之前,下方的任何内容都无法通知到你。", + "settingsTurnThemOn": "开启通知", + "settingsGroupManageNotifications": "管理通知", + "settingsHealthExceptionsRowTitle": "健康异常提醒", + "settingsHealthExceptionsRowSub": "每天最多一次,且仅在你自身基线出现变化时才会提醒", + "settingsBandAlertsRowTitle": "手环提醒", + "settingsBandAlertsRowSub": "电量耗尽、正在充电、失去响应", + "settingsAlertMeAtRowTitle": "在此电量提醒我", + "settingsAlertMeAtRowSub": "当手环电量低于此水平时发出提醒", + "settingsRecoveryReadyRowTitle": "恢复值已就绪", + "settingsRecoveryReadyRowSub": "当早晨的恢复分数生成时会收到一条提醒", + "settingsWeeklyLookbackRowTitle": "每周回顾", + "settingsWeeklyLookbackRowSub": "周日晚上发送,但仅限于确实发现了问题的那一周,大多数周都不会有提醒", + "settingsDetectedWorkoutsRowTitle": "检测到的锻炼", + "settingsDetectedWorkoutsRowSub": "询问手环检测到但你并未主动开始的锻炼。关闭后会隐藏提示和回顾卡片;手环仍会照常继续测量", + "settingsMovementNudgeRowTitle": "活动提醒", + "settingsMovementNudgeRowSub": "在长时间静止后提醒你——完全无活动两小时,或以久坐姿势持续 90 分钟。手机会推送通知,同时手环在连接状态下会振动", + "settingsWindDownRowTitle": "睡前提醒", + "settingsWindDownRowSub": "在根据你自己的夜间数据学习到的就寝时间前约 45 分钟提醒,且不会出现在你的免打扰时段内。佩戴大约一周后才会出现", + "settingsStepGoalAlertsRowTitle": "步数目标提醒", + "settingsStepGoalAlertsRowSub": "当今日步数达到目标时会提醒你一次", + "settingsMedicationRemindersRowTitle": "用药提醒", + "settingsMedicationRemindersRowSub": "为每一次已安排的剂量在你输入的时间发送一条通知——如果手环已连接,还会伴有振动。已标记为已服用或已跳过的剂量不会再发送提醒", + "settingsDailyCheckInRowTitle": "每日打卡", + "settingsDailyCheckInRowSub": "晚间提示你记录当天状态——心情、精力、压力。若当天已有评分则会跳过", + "settingsWaterReminderRowTitle": "喝水提醒", + "settingsWaterReminderRowSub": "在你清醒的时段,手环会振动、手机也会推送通知,提醒你记录一次饮水。无论如何都不会实际测量饮水量", + "settingsRemindMeEveryRowTitle": "每隔多久提醒我", + "settingsGroupTheStrap": "手环", + "settingsBuzzOnAppNotificationsRowTitle": "应用通知震动", + "settingsBuzzOnAppNotificationsRowSub": "选择哪些手机应用的通知会触发手环振动", + "settingsGroupQuietHours": "免打扰时段", + "settingsQuietHoursRowTitle": "免打扰时段", + "settingsQuietHoursRowSub": "在此时段内不会有任何振动", + "settingsQuietHoursStartsRowTitle": "开始时间", + "settingsQuietHoursEndsRowTitle": "结束时间", + "settingsHealthExceptionsBreakThroughRowTitle": "健康异常提醒可穿透免打扰时段", + "settingsAlarmNotOnListTitle": "闹钟不在此列表中", + "settingsAlarmNotOnListBody": "请改到“闹钟”页面取消它。", + "settingsImportNoPermission": "{store} 未授权这些字段,未读取任何数据。", + "settingsImportEmptyWithBirthday": "没有获取到任何数据。{store} 中没有你的身高、体重、生日或性别信息——请在此处手动输入。", + "settingsImportEmpty": "没有获取到任何数据。{store} 中没有你的身高、体重或性别信息——请在此处手动输入。", + "settingsImportNoChange": "已读取 {fields}。你的资料已经是相同的数值,因此没有任何变化。", + "settingsImportUpdated": "已从 {store} 更新 {fields}。", + "settingsImportFailed": "失败:{error}", + "settingsAgeFieldLabel": "年龄", + "settingsEditProfileNavTitle": "编辑资料", + "settingsNameFieldLabel": "姓名", + "settingsSexFieldLabel": "性别", + "settingsSexMale": "男", + "settingsSexFemale": "女", + "settingsSexPreferNotToSay": "不愿透露", + "settingsAgeYearsFieldLabel": "年龄(岁)", + "settingsFourFieldsTitle": "这四项会影响你的数值", + "settingsFourFieldsBody": "它们会用于心率区间、卡路里估算和训练负荷。清空其中一项,只有依赖它的指标会变为不可用。", + "settingsImportBlockAppleHealth": "身高、体重、生日和性别,直接来自 {store}。身高和体重每次都会读取;你的年龄和性别只会在缺失时才会填入,因为两者都不会随时间漂移,而已有的值是你自己设置的。", + "settingsImportBlockOther": "身高和体重,直接来自 {store}。它没有生日和性别信息可读——任何应用都读不到——所以请自行在上方设置这两项。", + "settingsNotSetHint": "未设置", + "settingsAutomationNavTitle": "自动化", + "settingsSyncFinishesSectionTitle": "同步完成时", + "settingsSyncFinishesAndroidBody": "应用会广播一个 intent,你的自动化应用可以据此启动配置。请按下方的 action 进行过滤;它携带了到达的记录数量和时间,最多每分钟一次。", + "settingsSyncFinishesIosBody": "iOS 无法做到这一点。个人快捷指令自动化只能由苹果自己固定的事件列表触发,任何应用都无法添加新的事件——因此这里的任何设置都无法为你启动快捷指令。Android 具备该功能;这是平台限制,不是可调设置。", + "settingsSyncFinishesExtras": "附加数据:records(整数),at(Unix 秒数)", + "settingsNeverSendSectionTitle": "它绝不会发送的内容", + "settingsNeverSendBody": "无论哪个平台,都不会发送 readiness、strain 或睡眠分数。本应用原本会标注为“缺失并附带原因”的数字,一旦离开设备就会变成一个毫无说明的零。发送出去的只是关于同步本身的事实,而不是测量数据。", + "settingsBuzzFromShortcutSectionTitle": "通过快捷指令让手环振动", + "settingsBuzzFromShortcutAndroidBody": "发送 wtf.openstrap.openstrap_edge.BUZZ_STRAP,并将此令牌作为字符串附加参数“token”传入。如果没有它,手机上的任何应用都能让你的手环振动。", + "settingsBuzzFromShortcutIosBody": "这个方向在 iOS 上是可行的:你自己运行的快捷指令可以到达应用。但它无法在手环同步时自动运行自身。", + "settingsNoTokenYet": "尚无令牌——请重新打开此页面。", + "settingsCopied": "已复制", + "settingsCopyTheToken": "复制令牌", + "bandStatusBluetoothDeniedTitle": "此应用的蓝牙已被关闭", + "bandStatusBluetoothDeniedReason": "手机没有把蓝牙权限交给 OpenStrap,因此无法扫描或连接任何设备。这与手环无关——靠近它也无济于事。", + "bandStatusBluetoothDeniedFix": "打开“设置” → OpenStrap,并允许使用蓝牙", + "bandStatusBluetoothOffTitle": "蓝牙已关闭", + "bandStatusBluetoothOffReason": "手机的蓝牙已关闭,任何应用都无法连接到手环。手环在此期间仍会继续记录,不会丢失任何数据。", + "bandStatusBluetoothOffFix": "打开蓝牙", + "bandStatusBluetoothUnsupportedTitle": "此手机没有低功耗蓝牙硬件", + "bandStatusBluetoothUnsupportedReason": "只能通过低功耗蓝牙(BLE)连接手环。导入的数据仍然可用;实时连接则不行。", + "bandStatusReconnectPausedTitle": "重新连接已暂停", + "bandStatusReconnectPausedReason": "{n, plural, other{手环已连续 {n} 次拒绝配对密钥,因此应用停止了重试,以免一直占用蓝牙并耗尽双方电量却始终连不上。除非你采取行动,否则不会自动重连。}}", + "bandStatusRepairNeededTitle": "手环需要重新配对", + "bandStatusRepairNeededReason": "连接可以建立,但手环拒绝了手机持有的加密密钥,因此每条指令都被丢弃,没有数据能够传输。你的记录在手环上仍然安全。", + "bandStatusRepairFix": "在手机的蓝牙设置中忘记该手环,然后在此重新配对", + "bandStatusSyncStuckTitle": "有一批记录始终无法传输完成", + "bandStatusSyncStuckReason": "手环不断重发同一批数据,因为应用未能把确认信息发送给它。其中的内容已经全部保存在本机——不会丢失——但手环要收到确认后才能继续。", + "bandStatusSyncStuckFix": "重新连接手环;如果明天仍然如此,请重新配对", + "bandStatusStrapUnresponsiveTitle": "手环已停止交出它的记录", + "bandStatusStrapUnresponsiveReason": "手环报告有更新的记录,但没有发送过来。这些记录仍安全地保存在手环上,只是没有被传输。", + "bandStatusStrapUnresponsiveFix": "把手环放在充电器上一分钟,然后重新连接", + "bandStatusClockLostTitle": "同步完成但没有任何数据", + "bandStatusClockLostReason": "手环每次同步都完成了,却没有传回任何一条传感器数据,这几乎总是意味着它内部的时钟已失去同步。应用会在每次连接时重新校准它。", + "bandStatusClockLostFix": "让手环保持连接几分钟;如果到明天还是没有数据,请重新配对", + "bandStatusConnectedReason": "手环已连接,正在交出它的记录。", + "bandStatusConnectingTitle": "正在连接", + "bandStatusConnectingReason": "正在与手环建立连接。", + "bandStatusScanningTitle": "正在寻找手环", + "bandStatusScanningReason": "正在等待手环发出广播信号。", + "bandStatusDisconnectedReason": "手环不在范围内、正在充电,或已连接到另一个应用。无论哪种情况它都会继续记录。", + "bandStatusDisconnectedFix": "把手环拿到手机附近,并关闭其他已连接它的应用", + "devicesTierBeatToBeatLabel": "逐搏心跳间期", + "devicesTierBeatToBeatDetail": "基于电信号的 R 波峰检测。", + "devicesTierWristOpticalLabel": "手腕光学脉搏", + "devicesTierWristOpticalDetail": "全天候持续监测脉搏、睡眠和体温。心跳节律是从脉搏波推算出来的,因此这里的心率变异性(HRV)实际上是脉搏变异性(PRV)。", + "devicesTierPhoneLabel": "仅步数", + "devicesTierPhoneDetail": "手机自身的运动协处理器。只有步数,没有其他数据。", + "deviceActionNoneLabel": "不执行任何操作", + "deviceActionNoneBlurb": "双击不会有任何反应。", + "deviceActionMediaPlayPauseLabel": "播放 / 暂停音乐", + "deviceActionMediaPlayPauseBlurb": "切换当前播放状态。", + "deviceActionMediaNextLabel": "下一曲", + "deviceActionMediaNextBlurb": "跳到下一首曲目。", + "deviceActionMediaPrevLabel": "上一曲", + "deviceActionMediaPrevBlurb": "返回上一首曲目。", + "deviceActionVolumeUpLabel": "调高音量", + "deviceActionVolumeUpBlurb": "将媒体音量调高一档。", + "deviceActionVolumeDownLabel": "调低音量", + "deviceActionVolumeDownBlurb": "将媒体音量调低一档。", + "deviceActionRingPhoneLabel": "让手机响铃", + "deviceActionRingPhoneBlurb": "播放响亮的声音以便找到你的手机。", + "deviceActionTorchLabel": "手电筒", + "deviceActionTorchBlurb": "开关手机的手电筒。", + "deviceActionMarkMomentLabel": "标记此刻", + "deviceActionMarkMomentBlurb": "在日记中标记当前时刻。", + "deviceActionWorkoutToggleLabel": "开始 / 结束训练", + "deviceActionWorkoutToggleBlurb": "从手腕上开始或结束一次训练。", + "deviceActionLogWaterLabel": "记录饮水", + "deviceActionLogWaterBlurb": "为今天的饮水量增加一杯,与营养页面上的“+”步骤相同。", + "deviceActionBroadcastToTaskerLabel": "广播给 Tasker", + "deviceActionBroadcastToTaskerBlurb": "发送一条广播 intent,让 Tasker 可以触发任意自动化流程。" } diff --git a/lib/ui2/activity/catalogue.dart b/lib/ui2/activity/catalogue.dart index 8baa5b40..81bff577 100644 --- a/lib/ui2/activity/catalogue.dart +++ b/lib/ui2/activity/catalogue.dart @@ -16,6 +16,7 @@ import 'package:flutter/widgets.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../theme.dart'; /// How a session is tracked — which decides which live screen it opens, and @@ -242,6 +243,10 @@ const kCalorieWhy = 'MET value × your weight, refined by heart rate.'; const kZonesWhy = 'Zone edges are percentages of a maximum heart rate ' 'estimated from your age — not one measured on you.'; +/// English, non-localized fallback/test seam — see [zonesWhyFootnote] and +/// [zonesWhy] for the localized callers actually used by the UI. +String zonesWhyFootnote([AppLocalizations? l]) => l?.catalogueZonesWhy ?? kZonesWhy; + /// THE sentence a zone chart carries, for the anchors THAT chart was banded on. /// /// [source] is the set's own stamp (`karvonen` · `observed` · `tanaka`) and @@ -253,20 +258,24 @@ const kZonesWhy = 'Zone edges are percentages of a maximum heart rate ' /// minutes above a boundary its own footnote misattributes is unanswerable — /// there is no number on screen to check it against. /// -/// [kZonesWhy] is the fallback rather than a fourth branch: an unknown stamp -/// and a measured stamp with no ceiling to name are both "we cannot say this -/// was measured on you", which is what the estimate sentence already says. -String zonesWhy(String? source, num? maxHr) => maxHr == null - ? kZonesWhy +/// [kZonesWhy] (via [zonesWhyFootnote]) is the fallback rather than a fourth +/// branch: an unknown stamp and a measured stamp with no ceiling to name are +/// both "we cannot say this was measured on you", which is what the estimate +/// sentence already says. [l] is optional so the non-UI test seam +/// (`hr_ceiling_zones_test.dart`) can call this with the plain English copy. +String zonesWhy(String? source, num? maxHr, [AppLocalizations? l]) => maxHr == null + ? zonesWhyFootnote(l) : switch (source) { 'karvonen' => - 'Zone edges span the gap between your measured resting heart rate ' - 'and the highest we have seen (${maxHr.round()} bpm). Both ' - 'measured on you.', + l?.dayStrainZoneFootnoteKarvonen(maxHr.round()) ?? + 'Zone edges span the gap between your measured resting heart rate ' + 'and the highest we have seen (${maxHr.round()} bpm). Both ' + 'measured on you.', 'observed' => - 'Zone edges are percentages of the highest heart rate we have seen ' - '(${maxHr.round()} bpm) — measured, not estimated.', - _ => kZonesWhy, + l?.dayStrainZoneFootnoteObserved(maxHr.round()) ?? + 'Zone edges are percentages of the highest heart rate we have seen ' + '(${maxHr.round()} bpm) — measured, not estimated.', + _ => zonesWhyFootnote(l), }; /// The row that means most people never open the catalogue. diff --git a/lib/ui2/activity/day_strain.dart b/lib/ui2/activity/day_strain.dart index 613f8a4f..5e155194 100644 --- a/lib/ui2/activity/day_strain.dart +++ b/lib/ui2/activity/day_strain.dart @@ -23,8 +23,9 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/day_label.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart' show whyFromNote; -import '../screens/home_screen.dart' show repoOf; +import '../screens/home_screen.dart' show repoOf, monthName; import '../screens/metric_detail.dart' show detailScaffold; import '../ui2.dart'; import 'catalogue.dart' show zonesWhy; @@ -153,10 +154,6 @@ class DayStrainData { } } -const _months = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', -]; class DayStrainDetail extends StatefulWidget { /// Preloaded, for goldens. Null means read the repo on open. @@ -199,6 +196,7 @@ class _DayStrainDetailState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d ?? const DayStrainData(); final day = d.day; // The date the drawn day IS. `getDayStrain` serves the last settled bundle @@ -207,20 +205,21 @@ class _DayStrainDetailState extends State { final sub = day == null ? '' : dayLabelOf(day) == todayLabel() - ? 'TODAY' - : '${_months[day.month - 1]} ${day.day}'.toUpperCase(); + ? (l?.dayStrainToday ?? 'TODAY') + : '${monthName(day.month, l)} ${day.day}'.toUpperCase(); return detailScaffold( c, - 'Day strain', + l?.dayStrainTitle ?? 'Day strain', [ if (_loading && _d == null) ...[ const SizedBox(height: S.x8), const Center(child: CircularProgressIndicator()), ] else ...[ - ..._trace(p, d), - ..._zones(p, d), - Section('What this is made of', _inputs(p, d)), + ..._trace(p, l, d), + ..._zones(p, l, d), + Section(l?.dayStrainInputsSection ?? 'What this is made of', + _inputs(p, l, d)), ], ], sub: sub, @@ -228,7 +227,7 @@ class _DayStrainDetailState extends State { } // ── the curve, and only then the number ──────────────────────────────────── - List _trace(P p, DayStrainData d) { + List _trace(P p, AppLocalizations? l, DayStrainData d) { if (!d.hasCurve) { // THE BUNDLE'S REASON, or none. This card used to state one — "it needs a // resting heart rate from a scored night and a day the band was on your @@ -251,13 +250,19 @@ class _DayStrainDetailState extends State { return [ StatusCard( s == null - ? 'No strain trace for this day' - : 'No minute-by-minute trace for this day', + ? (l?.dayStrainNoTraceTitle ?? 'No strain trace for this day') + : (l?.dayStrainNoMinuteTraceTitle ?? + 'No minute-by-minute trace for this day'), s == null - ? why ?? 'Nothing recorded says why this day produced no strain.' - : 'The day strain is ${s.toStringAsFixed(1)}. The waking minutes ' - 'it was built from are not stored for this day.', - fix: (s == null && !saw) ? 'Wear the band through the day' : '', + ? why ?? + (l?.dayStrainNoReasonBody ?? + 'Nothing recorded says why this day produced no strain.') + : (l?.dayStrainScoredNoTraceBody(s.toStringAsFixed(1)) ?? + 'The day strain is ${s.toStringAsFixed(1)}. The waking minutes ' + 'it was built from are not stored for this day.'), + fix: (s == null && !saw) + ? (l?.dayStrainWearBandFix ?? 'Wear the band through the day') + : '', icon: LucideIcons.trendingUp, ), ]; @@ -268,15 +273,16 @@ class _DayStrainDetailState extends State { Surface( child: Column(children: [ ChartFrame( - title: 'STRAIN THROUGH THE DAY', + title: l?.dayStrainChartTitle ?? 'STRAIN THROUGH THE DAY', unit: '0–21', height: 170, yAxis: axis, xLabels: const ['00:00', '12:00', '24:00'], series: d.curve, - footnote: 'Accumulated, so it only ever climbs — the STEEP parts ' - 'are where the effort was. Built from $drawn recorded waking ' - 'minutes.', + footnote: l?.dayStrainChartFootnote(drawn) ?? + 'Accumulated, so it only ever climbs — the STEEP parts ' + 'are where the effort was. Built from $drawn recorded waking ' + 'minutes.', child: CustomPaint( size: Size.infinite, painter: LineChart(d.curve, p.on(C.purple), @@ -287,10 +293,12 @@ class _DayStrainDetailState extends State { const SizedBox(height: S.x4), InlineMetrics([ if (d.strain != null) - ('Day strain', d.strain!.toStringAsFixed(1), C.purple), - if (d.peakHr != null) ('Peak HR', '${d.peakHr} bpm', C.red), + (l?.dayStrainTitle ?? 'Day strain', d.strain!.toStringAsFixed(1), + C.purple), + if (d.peakHr != null) + (l?.dayStrainPeakHr ?? 'Peak HR', '${d.peakHr} bpm', C.red), if (d.wornMin != null) - ('Worn', '${d.wornMin} min', C.teal), + (l?.dayStrainWorn ?? 'Worn', '${d.wornMin} min', C.teal), ]), ], ]), @@ -299,10 +307,12 @@ class _DayStrainDetailState extends State { Padding( padding: const EdgeInsets.only(top: S.x4), child: StatusCard( - 'The band saw ${d.coveragePct}% of this day', - 'Strain is a total over the minutes that were recorded, so a ' - 'partly-worn day reads lower than a full one and the two are ' - 'not comparable.', + l?.dayStrainLowCoverageTitle(d.coveragePct!) ?? + 'The band saw ${d.coveragePct}% of this day', + l?.dayStrainLowCoverageBody ?? + 'Strain is a total over the minutes that were recorded, so a ' + 'partly-worn day reads lower than a full one and the two are ' + 'not comparable.', icon: LucideIcons.watch, ), ), @@ -310,17 +320,17 @@ class _DayStrainDetailState extends State { } // ── where the effort sat ─────────────────────────────────────────────────── - List _zones(P p, DayStrainData d) { + List _zones(P p, AppLocalizations? l, DayStrainData d) { final z = d.zoneMin; if (z == null) return const []; final total = z.fold(0, (a, b) => a + b); if (total <= 0) return const []; return [ Section( - 'Time in zones', + l?.dayStrainTimeInZonesSection ?? 'Time in zones', Surface( child: ChartFrame( - title: 'TIME IN ZONES', + title: l?.dayStrainZonesChartTitle ?? 'TIME IN ZONES', unit: 'minutes', height: 10, legend: [ @@ -333,7 +343,7 @@ class _DayStrainDetailState extends State { // "estimated from your age" would then be false. The 28-day // distribution is NOT here — it lives one tap away and is gated on // the same anchors (TS-05). - footnote: zonesWhy(d.zoneSource, d.zoneMaxHr), + footnote: zonesWhy(d.zoneSource, d.zoneMaxHr, l), child: CustomPaint( size: Size.infinite, painter: ZoneBar([for (final v in z) v / total], p), @@ -343,7 +353,7 @@ class _DayStrainDetailState extends State { // Progressive disclosure: this day screen gains a LINK, not a row. The // ceiling, the edges in bpm and the 28-day distribution are all one tap // behind it. - action: 'How these are set', + action: l?.dayStrainHowSet ?? 'How these are set', onAction: () => Navigator.of(context) .push(MaterialPageRoute(builder: (_) => const ZonesDetail())), ), @@ -351,28 +361,32 @@ class _DayStrainDetailState extends State { } // ── the inputs, named ────────────────────────────────────────────────────── - Widget _inputs(P p, DayStrainData d) { + Widget _inputs(P p, AppLocalizations? l, DayStrainData d) { final max = d.maxHrUsed; return Surface( elevation: 0, color: p.card2, child: Text( [ - 'Banister TRIMP over your waking heart rate, scaled to 0–21.', + l?.dayStrainInputsBase ?? + 'Banister TRIMP over your waking heart rate, scaled to 0–21.', if (max != null) - 'It was integrated against an assumed maximum of ' - '${max.round()} bpm — estimated from your age and your strap, ' - 'not measured.', + l?.dayStrainInputsMaxHr(max.round()) ?? + 'It was integrated against an assumed maximum of ' + '${max.round()} bpm — estimated from your age and your strap, ' + 'not measured.', // Said out loud because the zone bar above can now be banded on a // MEASURED ceiling while this number is still the age estimate, and // two different ceilings on one screen with nothing saying so is // exactly the defect TS-03a removed. if (max != null && d.zoneSource != null && d.zoneSource != 'tanaka') - 'The zone bar above uses the measured ceiling instead; strain has ' - 'not been moved onto it, because that would rewrite every ' - 'strain score you have ever seen.', - 'The other anchor is your resting heart rate from the night before, ' - 'so a night the band missed moves the whole day.', + l?.dayStrainInputsMeasuredCeilingNote ?? + 'The zone bar above uses the measured ceiling instead; strain has ' + 'not been moved onto it, because that would rewrite every ' + 'strain score you have ever seen.', + l?.dayStrainInputsRhrAnchor ?? + 'The other anchor is your resting heart rate from the night before, ' + 'so a night the band missed moves the whole day.', ].join(' '), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), diff --git a/lib/ui2/activity/live.dart b/lib/ui2/activity/live.dart index f6361952..4504fdc9 100644 --- a/lib/ui2/activity/live.dart +++ b/lib/ui2/activity/live.dart @@ -30,6 +30,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; // screens describe a session, the caller owns it. Prefs is a synchronous // key/value façade, not the app. import '../../gps/gps_source.dart' show GpsPermissionStatus; +import '../../l10n/app_localizations.dart'; import '../../state/prefs.dart'; import '../../state/units_controller.dart'; import '../screens/home_screen.dart' show unitsOf; @@ -514,6 +515,7 @@ class LiveShellState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final a = widget.a; return Scaffold( backgroundColor: p.bg, @@ -527,7 +529,7 @@ class LiveShellState extends State { // back, or by an iOS edge-swipe — puts it away rather than // destroying it. The bar above the tab bar brings it back. Pressable( - semanticLabel: 'Minimise', + semanticLabel: l?.activityLiveMinimiseLabel ?? 'Minimise', onTap: () => Navigator.maybePop(c), child: Icon(LucideIcons.chevronDown, size: 24, color: p.ink3), @@ -588,7 +590,9 @@ class LiveShellState extends State { const SizedBox(width: S.x4), Expanded( child: Pressable( - semanticLabel: paused ? 'Resume' : 'Pause', + semanticLabel: paused + ? (l?.activityLiveResumeLabel ?? 'Resume') + : (l?.activityLivePauseLabel ?? 'Pause'), onTap: () => setState(() { paused = !paused; LiveDraft.current?.setPaused(paused); @@ -607,7 +611,8 @@ class LiveShellState extends State { ), const SizedBox(width: S.x4), _round(p, LucideIcons.square, p.card2, p.on(C.red), - 'Finish session', finish), + l?.activityLiveFinishSessionLabel ?? 'Finish session', + finish), ]), ), ]), @@ -739,20 +744,23 @@ class LiveHeart extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); if (feed.hr == null) { // One card used to cover both, and it told a user whose band had // dropped mid-session to adjust the fit of a band that was not there. return feed.bandConnected - ? const StatusCard( - 'No heart rate yet', - 'The band is connected but has not reported a beat, so it needs ' - 'to be snug, a finger-width above the wrist bone.', + ? StatusCard( + l?.activityLiveNoHrYetTitle ?? 'No heart rate yet', + l?.activityLiveNoHrYetBody ?? + 'The band is connected but has not reported a beat, so it ' + 'needs to be snug, a finger-width above the wrist bone.', icon: LucideIcons.heartPulse, ) - : const StatusCard( - 'No heart rate', - 'The band is not connected, so nothing is arriving for this ' - 'session.', + : StatusCard( + l?.activityLiveNoHrTitle ?? 'No heart rate', + l?.activityLiveNoHrBody ?? + 'The band is not connected, so nothing is arriving for ' + 'this session.', icon: LucideIcons.heartPulse, ); } @@ -773,18 +781,20 @@ class LiveHeart extends StatelessWidget { const SizedBox(width: S.x2), Text('${feed.hr}', style: F.n24.copyWith(color: p.ink)), const SizedBox(width: S.x1), - Text('bpm', style: F.cap.copyWith(color: p.ink3)), + Text(l?.activityLiveBpmUnit ?? 'bpm', + style: F.cap.copyWith(color: p.ink3)), ]), if (z != null) // The zone's OWN colour, the one the bar underneath paints it in. A // fixed green said "zone 5" and "zone 1" in the same breath. - Pill('Zone $z', ZoneBar.pigment[(z - 1).clamp(0, 4)]), + Pill(l?.activityLiveZoneLabel(z) ?? 'Zone $z', + ZoneBar.pigment[(z - 1).clamp(0, 4)]), ], ), if (feed.zoneMinutes.length == 5) ...[ const SizedBox(height: S.x4), ChartFrame( - title: 'TIME IN ZONES', + title: l?.activityLiveTimeInZonesTitle ?? 'TIME IN ZONES', unit: 'minutes', height: 10, legend: [ @@ -809,43 +819,53 @@ class LiveHeart extends StatelessWidget { /// Why this session is being recorded without a route, and the one thing that /// would fix it. The session keeps running either way — a missing map is not a /// reason to stop a run — but it is named rather than left blank. -Widget? _routeIssueCard(GpsPermissionStatus? issue, VoidCallback? onFix) => - switch (issue) { - null => null, - // Title and action, no body. The body was cut as slop; the AFFORDANCE is - // not slop — denied is the one branch a user can actually resolve from - // here, and dropping the whole card left them with a missing map and no - // way to fix it. - GpsPermissionStatus.denied => StatusCard( - 'No route: location not allowed', - '', - fix: 'Allow location', - onFix: onFix, - icon: LucideIcons.mapPin, - ), - GpsPermissionStatus.serviceOff => StatusCard( - 'No route: location is off', - 'Location services are off on this phone, so no fixes are arriving.', - fix: 'Turn on location', - onFix: onFix, - icon: LucideIcons.mapPin, - ), - GpsPermissionStatus.deniedForever => StatusCard( - 'No route: location not allowed', - 'Location is denied for this app, which only Settings can change.', - fix: 'Open Settings', - onFix: onFix, - icon: LucideIcons.mapPin, - ), - // `granted` cannot reach here: it is never stored as an issue. - _ => StatusCard( - 'No route: location failed', - 'The phone returned an error when asked for a fix.', - fix: 'Try again', - onFix: onFix, - icon: LucideIcons.mapPin, - ), - }; +Widget? _routeIssueCard( + BuildContext c, GpsPermissionStatus? issue, VoidCallback? onFix) { + final l = AppLocalizations.of(c); + return switch (issue) { + null => null, + // Title and action, no body. The body was cut as slop; the AFFORDANCE is + // not slop — denied is the one branch a user can actually resolve from + // here, and dropping the whole card left them with a missing map and no + // way to fix it. + GpsPermissionStatus.denied => StatusCard( + l?.activityLiveNoRouteNotAllowedTitle ?? + 'No route: location not allowed', + '', + fix: l?.activityLiveAllowLocation ?? 'Allow location', + onFix: onFix, + icon: LucideIcons.mapPin, + ), + GpsPermissionStatus.serviceOff => StatusCard( + l?.activityLiveNoRouteOffTitle ?? 'No route: location is off', + l?.activityLiveNoRouteOffBody ?? + 'Location services are off on this phone, so no fixes are ' + 'arriving.', + fix: l?.activityLiveTurnOnLocation ?? 'Turn on location', + onFix: onFix, + icon: LucideIcons.mapPin, + ), + GpsPermissionStatus.deniedForever => StatusCard( + l?.activityLiveNoRouteNotAllowedTitle ?? + 'No route: location not allowed', + l?.activityLiveDeniedForeverBody ?? + 'Location is denied for this app, which only Settings can ' + 'change.', + fix: l?.activityLiveOpenSettings ?? 'Open Settings', + onFix: onFix, + icon: LucideIcons.mapPin, + ), + // `granted` cannot reach here: it is never stored as an issue. + _ => StatusCard( + l?.activityLiveNoRouteFailedTitle ?? 'No route: location failed', + l?.activityLiveNoRouteFailedBody ?? + 'The phone returned an error when asked for a fix.', + fix: l?.activityLiveTryAgain ?? 'Try again', + onFix: onFix, + icon: LucideIcons.mapPin, + ), + }; +} /// MET-derived calories for the elapsed time, or the feed's own figure when /// the band produced one. Null when body weight is unknown — the whole point @@ -876,7 +896,7 @@ List _distanceStats(BuildContext ctx, P p, LiveFeed f, Activity a, statRow(p, [ if (value != null) (value.toStringAsFixed(2), unit), if (pacePerUnit != null) (pacePerUnit, '/$unit'), - ..._commonStats(a, f, weightKg, elapsed), + ..._commonStats(ctx, a, f, weightKg, elapsed), ].take(3).toList()), ]; } @@ -891,14 +911,49 @@ String _distanceText(BuildContext c, double km) { String _distanceUnit(BuildContext c) => unitsOf(c)?.distanceUnit ?? 'km'; +/// Display text for a swim stroke — [key] stays the canonical English value +/// stored on the session (see [LiveSwim]'s `strokes` and [_baseResult]'s +/// `stroke:`); only what the user reads is translated. +String _strokeLabel(BuildContext c, String key) { + final l = AppLocalizations.of(c); + return switch (key) { + 'Free' => l?.activityLiveStrokeFree ?? 'Free', + 'Back' => l?.activityLiveStrokeBack ?? 'Back', + 'Breast' => l?.activityLiveStrokeBreast ?? 'Breast', + 'Fly' => l?.activityLiveStrokeFly ?? 'Fly', + _ => key, + }; +} + +/// Display text for a yoga pose — [key] stays the canonical English value +/// stored in [LiveFlow]'s `poses` and passed to [_baseResult]'s `poses:`. +String _poseLabel(BuildContext c, String key) { + final l = AppLocalizations.of(c); + return switch (key) { + 'Mountain' => l?.activityLivePoseMountain ?? 'Mountain', + 'Forward fold' => l?.activityLivePoseForwardFold ?? 'Forward fold', + 'Plank' => l?.activityLivePosePlank ?? 'Plank', + 'Warrior II' => l?.activityLivePoseWarriorTwo ?? 'Warrior II', + 'Triangle' => l?.activityLivePoseTriangle ?? 'Triangle', + 'Chair' => l?.activityLivePoseChair ?? 'Chair', + 'Pigeon' => l?.activityLivePosePigeon ?? 'Pigeon', + 'Bridge' => l?.activityLivePoseBridge ?? 'Bridge', + "Child's pose" => l?.activityLivePoseChildsPose ?? "Child's pose", + 'Savasana' => l?.activityLivePoseSavasana ?? 'Savasana', + _ => key, + }; +} + /// The three-up row every live screen ends with. List<(String, String)> _commonStats( - Activity a, LiveFeed feed, double? weightKg, int elapsed) { + BuildContext c, Activity a, LiveFeed feed, double? weightKg, int elapsed) { + final l = AppLocalizations.of(c); final kcal = _kcal(a, feed, weightKg, elapsed); return [ - if (kcal != null) ('$kcal', 'kcal · est'), - if (feed.strain != null) (feed.strain!.toStringAsFixed(1), 'strain'), - if (feed.steps != null) ('${feed.steps}', 'steps'), + if (kcal != null) ('$kcal', l?.activityLiveKcalEstUnit ?? 'kcal · est'), + if (feed.strain != null) + (feed.strain!.toStringAsFixed(1), l?.activityLiveStrainUnit ?? 'strain'), + if (feed.steps != null) ('${feed.steps}', l?.activityLiveStepsUnit ?? 'steps'), ]; } @@ -984,10 +1039,12 @@ class LiveMeasured extends StatelessWidget { private), body: (ctx, elapsed) { final p = P.of(ctx); + final l = AppLocalizations.of(ctx); final f = feed?.call() ?? LiveFeed.none; return Column(children: [ const SizedBox(height: S.x6), - Text('DURATION', style: F.over.copyWith(color: p.ink3)), + Text(l?.activityLiveDurationHeader ?? 'DURATION', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), bigNum(p, clock(elapsed), ''), // Only once fixes are actually arriving. The catalogue's `gps` flag @@ -995,8 +1052,9 @@ class LiveMeasured extends StatelessWidget { // it claimed "GPS ACTIVE" with location denied. if (f.gpsActive) ...[ const SizedBox(height: S.x3), - const Pill('Recording route', C.green, icon: LucideIcons.mapPin), - ] else if (_routeIssueCard(f.routeIssue, f.onFixRoute) + Pill(l?.activityLiveRecordingRoute ?? 'Recording route', C.green, + icon: LucideIcons.mapPin), + ] else if (_routeIssueCard(ctx, f.routeIssue, f.onFixRoute) case final card?) ...[ const SizedBox(height: S.x4), card, @@ -1010,13 +1068,16 @@ class LiveMeasured extends StatelessWidget { if (f.route.length > 1) ...[ const SizedBox(height: S.x5), ChartFrame( - title: 'ROUTE SO FAR', + title: l?.activityLiveRouteSoFarTitle ?? 'ROUTE SO FAR', unit: _distanceUnit(ctx), height: 150, footnote: f.distanceKm == null - ? 'Start pinned; distance appears once the fixes settle.' - : '${_distanceText(ctx, f.distanceKm!)} from the fixes ' - 'recorded so far.', + ? (l?.activityLiveRouteFootnoteNoDistance ?? + 'Start pinned; distance appears once the fixes settle.') + : (l?.activityLiveRouteFootnoteWithDistance( + _distanceText(ctx, f.distanceKm!)) ?? + '${_distanceText(ctx, f.distanceKm!)} from the fixes ' + 'recorded so far.'), child: ClipRRect( borderRadius: R.rLg, child: Container( @@ -1038,7 +1099,7 @@ class LiveMeasured extends StatelessWidget { Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.lock, size: 12, color: p.ink3), const SizedBox(width: S.x1), - Text('Private session', + Text(l?.activityLivePrivateSession ?? 'Private session', style: F.over.copyWith(color: p.ink3)), ]), ], @@ -1200,7 +1261,9 @@ class _LiveStrengthState extends State { HapticFeedback.mediumImpact(); // A buzz is not a message. The rest-over moment was reachable only by // feeling the watch, or by watching a number nobody was told to watch. - say(context, 'Rest over'); + say(context, + AppLocalizations.of(context)?.activityLiveRestOverAnnounce ?? + 'Rest over'); } }); } @@ -1223,11 +1286,12 @@ class _LiveStrengthState extends State { shape: const RoundedRectangleBorder(borderRadius: R.rXl), builder: (c) { final p = P.of(c); + final l = AppLocalizations.of(c); return SafeArea( child: ListView(shrinkWrap: true, children: [ Padding( padding: const EdgeInsets.all(S.x4), - child: Text('Add exercise', + child: Text(l?.activityLiveAddExerciseTitle ?? 'Add exercise', style: F.head.copyWith(color: p.ink)), ), for (final e in exerciseLibrary) @@ -1260,11 +1324,14 @@ class _LiveStrengthState extends State { @override Widget build(BuildContext c) { final volume = log.volumeKg; + final l = AppLocalizations.of(c); return LiveShell( widget.a, subtitle: volume == null - ? '${log.setCount} SETS' - : '${grouped(volume)} KG · ${log.setCount} SETS', + ? (l?.activityLiveSetsCountSubtitle(log.setCount) ?? + '${log.setCount} SETS') + : (l?.activityLiveVolumeSetsSubtitle(grouped(volume), log.setCount) ?? + '${grouped(volume)} KG · ${log.setCount} SETS'), private: widget.private, weightKg: widget.weightKg, onFinish: widget.onFinish, @@ -1284,7 +1351,9 @@ class _LiveStrengthState extends State { ), const SizedBox(width: S.x3), Expanded( - child: BigButton('Skip rest', + child: BigButton( + AppLocalizations.of(ctx)?.activityLiveSkipRest ?? + 'Skip rest', icon: LucideIcons.skipForward, color: C.teal, onTap: () { @@ -1294,7 +1363,7 @@ class _LiveStrengthState extends State { }), ), ]) - : BigButton('Log set', + : BigButton(AppLocalizations.of(ctx)?.activityLiveLogSet ?? 'Log set', icon: LucideIcons.plus, color: C.purple, onTap: logSet), // Nothing here is measured: the sets, reps and load are typed, and the // one live thing on the screen is the heart-rate block, which asks for @@ -1306,32 +1375,39 @@ class _LiveStrengthState extends State { Widget _body(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final volume = log.volumeKg; final hist = widget.history[key]; return Column(children: [ // running totals — the numbers that actually matter here Row(children: [ Expanded( - child: _total(p, volume == null ? 'BW' : grouped(volume), - volume == null ? 'bodyweight only' : 'kg volume')), + child: _total( + p, + volume == null ? (l?.activityLiveBwAbbrev ?? 'BW') : grouped(volume), + volume == null + ? (l?.activityLiveBodyweightOnly ?? 'bodyweight only') + : (l?.activityLiveKgVolumeUnit ?? 'kg volume'))), Container(width: 1, height: 26, color: p.line), - Expanded(child: _total(p, '${log.setCount}', 'sets')), + Expanded(child: _total(p, '${log.setCount}', l?.activityLiveSetsUnit ?? 'sets')), Container(width: 1, height: 26, color: p.line), - Expanded(child: _total(p, '${log.repCount}', 'reps')), + Expanded(child: _total(p, '${log.repCount}', l?.activityLiveRepsUnit ?? 'reps')), ]), const SizedBox(height: S.x5), // exercise navigation Row(children: [ Pressable( - semanticLabel: 'Previous exercise', + semanticLabel: l?.activityLivePreviousExercise ?? 'Previous exercise', onTap: () => goExercise(index - 1), child: Icon(LucideIcons.chevronLeft, size: 20, color: index == 0 ? p.line : p.ink3), ), Expanded( child: Column(children: [ - Text('EXERCISE ${index + 1} OF ${plan.length}', + Text( + l?.activityLiveExerciseOf(index + 1, plan.length) ?? + 'EXERCISE ${index + 1} OF ${plan.length}', style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x1), Text(def?.label ?? key, @@ -1340,7 +1416,7 @@ class _LiveStrengthState extends State { ]), ), Pressable( - semanticLabel: 'Next exercise', + semanticLabel: l?.activityLiveNextExercise ?? 'Next exercise', onTap: () => index == plan.length - 1 ? addExercise() : goExercise(index + 1), @@ -1374,7 +1450,8 @@ class _LiveStrengthState extends State { const SizedBox(height: S.x2), // Tabular: this counts up mid-lift, and proportional digits made the // label shuffle sideways on every set. - Text('Set ${setsHere.length + 1}', + Text(l?.activityLiveSetNumber(setsHere.length + 1) ?? + 'Set ${setsHere.length + 1}', style: F.cap.copyWith( color: p.ink3, fontFeatures: const [FontFeature.tabularFigures()])), @@ -1383,17 +1460,17 @@ class _LiveStrengthState extends State { if (restLeft.value > 0) ValueListenableBuilder( valueListenable: restLeft, - builder: (_, _, _) => _rest_(p), + builder: (_, _, _) => _rest_(p, c), ) else - ..._entry(p), + ..._entry(p, c), const SizedBox(height: S.x6), if (setsHere.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, - child: - Text('THIS EXERCISE', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activityLiveThisExerciseLabel ?? 'THIS EXERCISE', + style: F.over.copyWith(color: p.ink3))), const SizedBox(height: S.x3), Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), @@ -1415,7 +1492,9 @@ class _LiveStrengthState extends State { Expanded( child: Text( setsHere[i].loadKg == null - ? '${setsHere[i].reps} reps · bodyweight' + ? (l?.activityLiveRepsBodyweightRow( + setsHere[i].reps) ?? + '${setsHere[i].reps} reps · bodyweight') : '${_fmt(setsHere[i].loadKg!)} kg × ' '${setsHere[i].reps}', style: F.body.copyWith(color: p.ink)), @@ -1441,9 +1520,13 @@ class _LiveStrengthState extends State { // references if (hist?.previous != null || hist?.best != null) ...[ Row(children: [ - Expanded(child: _ref(p, 'Previous', hist?.previous)), + Expanded( + child: _ref(c, p, l?.activityLivePreviousLabel ?? 'Previous', + hist?.previous)), const SizedBox(width: S.x3), - Expanded(child: _ref(p, 'Best', hist?.best, gold: true)), + Expanded( + child: _ref(c, p, l?.activityLiveBestLabel ?? 'Best', + hist?.best, gold: true)), ]), const SizedBox(height: S.x5), ], @@ -1451,11 +1534,14 @@ class _LiveStrengthState extends State { ]); } - List _entry(P p) => [ + List _entry(P p, BuildContext c) { + final l = AppLocalizations.of(c); + return [ _stepper( + c, p, - 'WEIGHT', - bodyweight ? 'BW' : _fmt(kg), + l?.activityLiveWeightLabel ?? 'WEIGHT', + bodyweight ? (l?.activityLiveBwAbbrev ?? 'BW') : _fmt(kg), bodyweight ? '' : 'kg', () => setState(() => kg = (kg - (def?.step ?? 2.5)).clamp(0, 500).toDouble()), @@ -1465,19 +1551,20 @@ class _LiveStrengthState extends State { onTap: () => setState(() => bodyweight = !bodyweight), child: Text( bodyweight - ? 'Bodyweight — left out of volume' - : 'Log as bodyweight', + ? (l?.activityLiveBodyweightExcludedNote ?? + 'Bodyweight — left out of volume') + : (l?.activityLiveLogAsBodyweight ?? 'Log as bodyweight'), style: F.cap.copyWith(color: p.on(C.purple))), ), const SizedBox(height: S.x5), - _stepper(p, 'REPS', '$reps', '', + _stepper(c, p, l?.activityLiveRepsLabel ?? 'REPS', '$reps', '', () => setState(() => reps = (reps - 1).clamp(1, 100)), () => setState(() => reps = reps + 1)), const SizedBox(height: S.x5), Align( alignment: Alignment.centerLeft, - child: - Text('EFFORT (RPE)', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activityLiveEffortRpeHeader ?? 'EFFORT (RPE)', + style: F.over.copyWith(color: p.ink3))), const SizedBox(height: S.x3), Row( children: List.generate(5, (i) { @@ -1504,9 +1591,13 @@ class _LiveStrengthState extends State { }), ), ]; + } - Widget _rest_(P p) => Column(children: [ - Text('RESTING', style: F.over.copyWith(color: p.on(C.teal))), + Widget _rest_(P p, BuildContext c) { + final l = AppLocalizations.of(c); + return Column(children: [ + Text(l?.activityLiveRestingHeader ?? 'RESTING', + style: F.over.copyWith(color: p.on(C.teal))), const SizedBox(height: S.x3), SizedBox( width: 170, @@ -1521,26 +1612,35 @@ class _LiveStrengthState extends State { if (logged.isNotEmpty) Text( logged.last.loadKg == null - ? '${logged.last.reps} reps logged' - : '${_fmt(logged.last.loadKg!)} kg × ' - '${logged.last.reps} logged', + ? (l?.activityLiveRepsLoggedBodyweight( + logged.last.reps) ?? + '${logged.last.reps} reps logged') + : (l?.activityLiveWeightRepsLogged( + _fmt(logged.last.loadKg!), logged.last.reps) ?? + '${_fmt(logged.last.loadKg!)} kg × ' + '${logged.last.reps} logged'), style: F.cap.copyWith(color: p.ink3)), ]), ]), ), ]); + } - Widget _stepper(P p, String label, String value, String unit, - VoidCallback minus, VoidCallback plus) => - Column(children: [ + Widget _stepper(BuildContext c, P p, String label, String value, + String unit, VoidCallback minus, VoidCallback plus) { + final l = AppLocalizations.of(c); + return Column(children: [ Text(label, style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Row(children: [ - counterButton(p, LucideIcons.minus, p.ink, '$label down', minus), + counterButton(p, LucideIcons.minus, p.ink, + l?.activityLiveDecrease(label) ?? '$label down', minus), Expanded(child: bigNum(p, value, unit)), - counterButton(p, LucideIcons.plus, p.ink, '$label up', plus), + counterButton(p, LucideIcons.plus, p.ink, + l?.activityLiveIncrease(label) ?? '$label up', plus), ]), ]); + } Widget _total(P p, String v, String l) => Column(children: [ Text(v, style: F.n17.copyWith(color: p.ink)), @@ -1549,7 +1649,10 @@ class _LiveStrengthState extends State { textAlign: TextAlign.center), ]); - Widget _ref(P p, String label, LoggedSet? s, {bool gold = false}) => Surface( + Widget _ref(BuildContext c, P p, String label, LoggedSet? s, + {bool gold = false}) { + final l = AppLocalizations.of(c); + return Surface( pad: const EdgeInsets.symmetric(horizontal: S.x3, vertical: S.x3), child: Column(children: [ Row(mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -1564,14 +1667,15 @@ class _LiveStrengthState extends State { const SizedBox(height: S.x2), Text( s == null - ? 'None yet' + ? (l?.activityLiveNoneYet ?? 'None yet') : s.loadKg == null - ? '${s.reps} reps' + ? (l?.activityLiveRepsOnly(s.reps) ?? '${s.reps} reps') : '${_fmt(s.loadKg!)} kg × ${s.reps}', style: F.cap .copyWith(color: p.ink, fontWeight: FontWeight.w600)), ]), ); + } String _fmt(double d) => d == d.roundToDouble() ? d.round().toString() : d.toStringAsFixed(1); @@ -1644,9 +1748,12 @@ class _LiveSwimState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); return LiveShell( widget.a, - subtitle: '${poolLen}M POOL · ${strokes[stroke].toUpperCase()}', + subtitle: l?.activityLivePoolSubtitle( + poolLen, _strokeLabel(c, strokes[stroke]).toUpperCase()) ?? + '${poolLen}M POOL · ${strokes[stroke].toUpperCase()}', private: widget.private, weightKg: widget.weightKg, onFinish: widget.onFinish, @@ -1658,29 +1765,34 @@ class _LiveSwimState extends State { stroke: strokes[stroke]), body: (ctx, elapsed) { final p = P.of(ctx); + final l = AppLocalizations.of(ctx); final f = widget.feed?.call() ?? LiveFeed.none; return Column(children: [ const SizedBox(height: S.x5), bigNum(p, '${laps * poolLen}', 'm'), const SizedBox(height: S.x2), - Text('$laps ${laps == 1 ? 'lap' : 'laps'} · ${strokes[stroke]}', + Text( + l?.activityLiveLapsCount(laps, _strokeLabel(ctx, strokes[stroke])) ?? + '$laps ${laps == 1 ? 'lap' : 'laps'} · ${strokes[stroke]}', style: F.body.copyWith(color: p.ink3)), const SizedBox(height: S.x6), statRow(p, [ - (clock(elapsed), 'time'), - if (laps > 0) (clock(elapsed ~/ laps), 'per lap'), - ..._commonStats(widget.a, f, widget.weightKg, elapsed), + (clock(elapsed), l?.activityLiveTimeUnit ?? 'time'), + if (laps > 0) + (clock(elapsed ~/ laps), l?.activityLivePerLapUnit ?? 'per lap'), + ..._commonStats(ctx, widget.a, f, widget.weightKg, elapsed), ].take(3).toList()), const SizedBox(height: S.x8), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - counterButton(p, LucideIcons.minus, p.ink, 'One lap fewer', () { + counterButton(p, LucideIcons.minus, p.ink, + l?.activityLiveOneLapFewer ?? 'One lap fewer', () { if (lapAt.isEmpty) return; setState(() => lapAt.removeLast()); _persist(); }), const SizedBox(width: S.x6), Pressable( - semanticLabel: 'Add a lap', + semanticLabel: l?.activityLiveAddALap ?? 'Add a lap', onTap: () { HapticFeedback.mediumImpact(); setState(() => lapAt.add(elapsed)); @@ -1695,13 +1807,14 @@ class _LiveSwimState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.plus, size: 30, color: p.inkOnFill), - Text('LAP', + Text(l?.activityLiveLapButtonLabel ?? 'LAP', style: F.over.copyWith(color: p.inkOnFill)), ]), ), ), const SizedBox(width: S.x6), - counterButton(p, LucideIcons.repeat2, p.ink, 'Change stroke', () { + counterButton(p, LucideIcons.repeat2, p.ink, + l?.activityLiveChangeStroke ?? 'Change stroke', () { setState(() => stroke = (stroke + 1) % strokes.length); _persist(); }), @@ -1712,7 +1825,8 @@ class _LiveSwimState extends State { children: [ for (final len in pools) ...[ Pressable( - semanticLabel: '$len metre pool', + semanticLabel: + l?.activityLiveMetrePoolLabel(len) ?? '$len metre pool', onTap: () { setState(() => poolLen = len); _persist(); @@ -1740,12 +1854,16 @@ class _LiveSwimState extends State { if (secs.isEmpty) return const SizedBox.shrink(); final fastest = secs.reduce((x, y) => x < y ? x : y); return ChartFrame( - title: 'LAPS', + title: l?.activityLiveLapsChartTitle ?? 'LAPS', unit: 'seconds per lap', height: 20.0 * secs.length, - xLabels: ['Lap 1', 'Lap ${secs.length}'], - footnote: 'Fastest ${clock(fastest)} · bar length is speed ' - 'against it.', + xLabels: [ + l?.activityLiveLapXLabel(1) ?? 'Lap 1', + l?.activityLiveLapXLabel(secs.length) ?? 'Lap ${secs.length}', + ], + footnote: l?.activityLiveLapsFootnote(clock(fastest)) ?? + 'Fastest ${clock(fastest)} · bar length is speed ' + 'against it.', child: CustomPaint( size: Size.infinite, painter: LapBars( @@ -1866,9 +1984,11 @@ class _LiveFlowState extends State @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); return LiveShell( widget.a, - subtitle: 'POSE ${pose + 1} OF ${poses.length}', + subtitle: l?.activityLivePoseOf(pose + 1, poses.length) ?? + 'POSE ${pose + 1} OF ${poses.length}', private: widget.private, weightKg: widget.weightKg, onFinish: widget.onFinish, @@ -1878,6 +1998,7 @@ class _LiveFlowState extends State poses: poses.sublist(0, reached + 1)), body: (ctx, elapsed) { final p = P.of(ctx); + final l = AppLocalizations.of(ctx); final f = widget.feed?.call() ?? LiveFeed.none; return Column(children: [ const SizedBox(height: S.x4), @@ -1904,9 +2025,11 @@ class _LiveFlowState extends State Icon(LucideIcons.personStanding, size: 46, color: p.on(C.teal)), const SizedBox(height: S.x2), - Text(poses[pose], style: F.t2.copyWith(color: p.ink)), + Text(_poseLabel(ctx, poses[pose]), + style: F.t2.copyWith(color: p.ink)), const SizedBox(height: S.x1), - Text('Hold · ${clock(hold)}', + Text(l?.activityLiveHoldTime(clock(hold)) ?? + 'Hold · ${clock(hold)}', style: F.cap.copyWith(color: p.on(C.teal))), ]), ]), @@ -1914,7 +2037,7 @@ class _LiveFlowState extends State const SizedBox(height: S.x4), Row(children: [ Expanded( - child: BigButton('Previous', + child: BigButton(l?.activityLivePreviousLabel ?? 'Previous', icon: LucideIcons.chevronLeft, color: C.teal, soft: true, @@ -1922,7 +2045,7 @@ class _LiveFlowState extends State ), const SizedBox(width: S.x3), Expanded( - child: BigButton('Next pose', + child: BigButton(l?.activityLiveNextPose ?? 'Next pose', icon: LucideIcons.chevronRight, color: C.teal, onTap: () => _go(pose + 1)), @@ -2009,9 +2132,11 @@ class _LiveMatchState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); return LiveShell( widget.a, - subtitle: 'SET ${sets.length + 1}', + subtitle: l?.activityLiveMatchSetSubtitle(sets.length + 1) ?? + 'SET ${sets.length + 1}', private: widget.private, weightKg: widget.weightKg, onFinish: widget.onFinish, @@ -2021,6 +2146,7 @@ class _LiveMatchState extends State { gameScore: [...sets, if (me > 0 || them > 0) (me, them)]), body: (ctx, elapsed) { final p = P.of(ctx); + final l = AppLocalizations.of(ctx); final f = widget.feed?.call() ?? LiveFeed.none; return Column(children: [ const SizedBox(height: S.x4), @@ -2028,17 +2154,19 @@ class _LiveMatchState extends State { const SizedBox(height: S.x6), Row(children: [ Expanded( - child: _side(p, 'YOU', me, p.on(widget.a.color), + child: _side(ctx, p, l?.activityLiveYouLabel ?? 'YOU', me, + p.on(widget.a.color), () => _score(() => me++), () => _score(() => me = (me - 1).clamp(0, 99)))), Container(width: 1, height: 120, color: p.line), Expanded( - child: _side(p, 'OPPONENT', them, p.ink2, + child: _side(ctx, p, l?.activityLiveOpponentLabel ?? 'OPPONENT', + them, p.ink2, () => _score(() => them++), () => _score(() => them = (them - 1).clamp(0, 99)))), ]), const SizedBox(height: S.x5), - BigButton('End set', + BigButton(l?.activityLiveEndSet ?? 'End set', color: widget.a.color, soft: true, onTap: () => _score(() { @@ -2050,7 +2178,8 @@ class _LiveMatchState extends State { if (sets.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, - child: Text('SETS', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activityLiveSetsListHeader ?? 'SETS', + style: F.over.copyWith(color: p.ink3))), const SizedBox(height: S.x3), Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), @@ -2060,7 +2189,8 @@ class _LiveMatchState extends State { padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row(children: [ Expanded( - child: Text('Set ${i + 1}', + child: Text( + l?.activityLiveSetNumber(i + 1) ?? 'Set ${i + 1}', style: F.body.copyWith(color: p.ink3))), Text('${sets[i].$1} — ${sets[i].$2}', style: F.n17.copyWith( @@ -2081,13 +2211,14 @@ class _LiveMatchState extends State { ); } - Widget _side(P p, String label, int v, Color col, VoidCallback up, - VoidCallback down) => - Column(children: [ + Widget _side(BuildContext c, P p, String label, int v, Color col, + VoidCallback up, VoidCallback down) { + final l = AppLocalizations.of(c); + return Column(children: [ Text(label, style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Pressable( - semanticLabel: '$label point', + semanticLabel: l?.activityLivePointLabel(label) ?? '$label point', onTap: () { HapticFeedback.mediumImpact(); up(); @@ -2096,12 +2227,15 @@ class _LiveMatchState extends State { ), const SizedBox(height: S.x3), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - counterButton(p, LucideIcons.minus, p.ink2, '$label down', down, + counterButton(p, LucideIcons.minus, p.ink2, + l?.activityLiveDecrease(label) ?? '$label down', down, size: 40), const SizedBox(width: S.x3), - counterButton(p, LucideIcons.plus, col, '$label up', up, size: 40), + counterButton(p, LucideIcons.plus, col, + l?.activityLiveIncrease(label) ?? '$label up', up, size: 40), ]), ]); + } } // ══════════════ INTERVAL — the rounds run themselves ══════════════ @@ -2169,7 +2303,12 @@ class _LiveIntervalState extends State { return; } HapticFeedback.mediumImpact(); - say(context, work ? 'Rest' : 'Work'); + final l = AppLocalizations.of(context); + say( + context, + work + ? (l?.activityLiveRestWord ?? 'Rest') + : (l?.activityLiveWorkWord ?? 'Work')); if (work) { work = false; left = restSec; @@ -2201,9 +2340,11 @@ class _LiveIntervalState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); return LiveShell( widget.a, - subtitle: '$workSec S WORK · $restSec S REST', + subtitle: l?.activityLiveIntervalSubtitle(workSec, restSec) ?? + '$workSec S WORK · $restSec S REST', private: widget.private, weightKg: widget.weightKg, onFinish: widget.onFinish, @@ -2213,6 +2354,7 @@ class _LiveIntervalState extends State { rounds: done), body: (ctx, elapsed) { final p = P.of(ctx); + final l = AppLocalizations.of(ctx); final f = widget.feed?.call() ?? LiveFeed.none; final col = work ? C.red : C.teal; // At least the eight the row is drawn for, and more once the session @@ -2220,11 +2362,16 @@ class _LiveIntervalState extends State { final pips = round > rounds ? round : rounds; return Column(children: [ const SizedBox(height: S.x5), - Text('ROUND $round', style: F.over.copyWith(color: p.ink3)), + Text(l?.activityLiveRoundLabel(round) ?? 'ROUND $round', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x4), Text(clock(left), style: F.n48.copyWith(color: p.on(col))), const SizedBox(height: S.x2), - Text(work ? 'WORK' : 'REST', + Text( + (work + ? (l?.activityLiveWorkWord ?? 'Work') + : (l?.activityLiveRestWord ?? 'Rest')) + .toUpperCase(), style: F.t2.copyWith(color: p.on(col), letterSpacing: 3)), const SizedBox(height: S.x5), ClipRRect( @@ -2238,10 +2385,15 @@ class _LiveIntervalState extends State { const SizedBox(height: S.x6), Surface( child: Row(children: [ - Text('NEXT', style: F.over.copyWith(color: p.ink3)), + Text(l?.activityLiveNextLabel ?? 'NEXT', + style: F.over.copyWith(color: p.ink3)), const Spacer(), - Text(work ? 'Rest · ${clock(restSec)}' - : 'Work · ${clock(workSec)}', + Text( + work + ? (l?.activityLiveNextRest(clock(restSec)) ?? + 'Rest · ${clock(restSec)}') + : (l?.activityLiveNextWork(clock(workSec)) ?? + 'Work · ${clock(workSec)}'), style: F.body .copyWith(color: p.ink, fontWeight: FontWeight.w600)), ]), @@ -2262,7 +2414,7 @@ class _LiveIntervalState extends State { )), ), const SizedBox(height: S.x6), - statRow(p, _commonStats(widget.a, f, widget.weightKg, elapsed)), + statRow(p, _commonStats(ctx, widget.a, f, widget.weightKg, elapsed)), const SizedBox(height: S.x6), LiveHeart(f), ]); diff --git a/lib/ui2/activity/picker.dart b/lib/ui2/activity/picker.dart index 6074fcfb..19b4e380 100644 --- a/lib/ui2/activity/picker.dart +++ b/lib/ui2/activity/picker.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../grammar.dart'; import '../theme.dart'; import 'catalogue.dart'; @@ -63,6 +64,7 @@ class _ActivityPickerState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final searching = q.trim().isNotEmpty; final needle = q.trim().toLowerCase(); final results = searching @@ -76,9 +78,9 @@ class _ActivityPickerState extends State { backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Choose activity'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.activityPickerTitle ?? 'Choose activity'), ), Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), @@ -95,7 +97,7 @@ class _ActivityPickerState extends State { // search, and a hint-only field announces nothing once a // character is in it. child: Semantics( - label: 'Search activities', + label: l?.activityPickerSearchLabel ?? 'Search activities', textField: true, child: TextField( autofocus: true, @@ -103,7 +105,9 @@ class _ActivityPickerState extends State { style: F.body.copyWith(color: p.ink), cursorColor: p.on(C.purple), decoration: InputDecoration.collapsed( - hintText: 'Search ${allActivities.length} activities', + hintText: l?.activityPickerSearchHint( + allActivities.length) ?? + 'Search ${allActivities.length} activities', hintStyle: F.body.copyWith(color: p.ink3)), ), ), @@ -121,10 +125,13 @@ class _ActivityPickerState extends State { // No `fix`: the 'Custom activity' row it pointed at is // gone (it carried an invented MET), and a fix string // paints a call to action that cannot be tapped. - const StatusCard( - 'No activity matches that', - 'The catalogue covers about seventy activities with a ' - 'published energy cost. Pick the closest one.', + StatusCard( + l?.activityPickerNoMatchTitle ?? + 'No activity matches that', + l?.activityPickerNoMatchBody ?? + 'The catalogue covers about seventy activities ' + 'with a published energy cost. Pick the ' + 'closest one.', icon: LucideIcons.search, ) else @@ -143,7 +150,10 @@ class _ActivityPickerState extends State { else ...[ // "RECENT" over a fixed six-item constant was a lie the // first time anyone read it. The heading follows the data. - Text(widget.recent.isEmpty ? 'QUICK START' : 'RECENT', + Text( + widget.recent.isEmpty + ? (l?.activityPickerQuickStart ?? 'QUICK START') + : (l?.activityPickerRecent ?? 'RECENT'), style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Builder(builder: (_) { @@ -220,8 +230,9 @@ class _ActivityPickerState extends State { ], if (widget.weightKg != null) ...[ const SizedBox(height: S.x5), - const StatusCard( - 'Calorie figures are estimates', + StatusCard( + l?.activityPickerCalorieEstimatesTitle ?? + 'Calorie figures are estimates', kCalorieWhy, icon: LucideIcons.flame, ), @@ -247,6 +258,7 @@ class ActivityRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final kcal = a.kcal(weightKg, 30); return Pressable( onTap: onTap, @@ -281,8 +293,10 @@ class ActivityRow extends StatelessWidget { const SizedBox(width: S.x2), Text( kcal == null - ? '${a.met.toStringAsFixed(1)} MET' - : '$kcal kcal / 30 min', + ? (l?.activityPickerMetValue(a.met.toStringAsFixed(1)) ?? + '${a.met.toStringAsFixed(1)} MET') + : (l?.activityPickerKcalPer30(kcal) ?? + '$kcal kcal / 30 min'), style: F.over.copyWith(color: p.ink3)), ], const SizedBox(width: S.x2), diff --git a/lib/ui2/activity/poster.dart b/lib/ui2/activity/poster.dart index c08bee3a..24ca2e35 100644 --- a/lib/ui2/activity/poster.dart +++ b/lib/ui2/activity/poster.dart @@ -35,6 +35,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/units_controller.dart'; import '../screens/home_screen.dart' show unitsOf; import '../theme.dart'; @@ -68,6 +69,16 @@ enum PosterFormat { Size get size => Size(kPosterW, height); } +/// The format's user-facing name, for the format switcher — `label` itself +/// stays a plain English enum field so it can serve as the fallback here. +String posterFormatLabel(BuildContext c, PosterFormat f) { + final l = AppLocalizations.of(c); + return switch (f) { + PosterFormat.post => l?.activityPosterFormatPost ?? f.label, + PosterFormat.story => l?.activityPosterFormatStory ?? f.label, + }; +} + /// Every card is authored at this width and exported at 3×, so a post lands /// at 900×900 and a story at 900×1600. const kPosterW = 300.0; diff --git a/lib/ui2/activity/setup.dart b/lib/ui2/activity/setup.dart index 5616e0ac..c2ed75c2 100644 --- a/lib/ui2/activity/setup.dart +++ b/lib/ui2/activity/setup.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../grammar.dart'; import '../theme.dart'; import 'catalogue.dart'; @@ -93,6 +94,7 @@ class _ActivitySetupState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final a = widget.a; final est = a.kcal(widget.weightKg, _estimateMin); @@ -120,7 +122,7 @@ class _ActivitySetupState extends State { const SizedBox(height: S.x4), Text(a.name, style: F.t2.copyWith(color: p.ink)), const SizedBox(height: S.x1), - Text(_trackLabel(a.track), + Text(_trackLabel(a.track, l), textAlign: TextAlign.center, style: F.cap.copyWith(color: p.ink3)), ]), @@ -133,28 +135,36 @@ class _ActivitySetupState extends State { // No tick. Nothing here has asked for location yet, and // an unconditional green check claimed a fix that a // permission dialog had not even been shown for. - _row(p, LucideIcons.mapPin, 'Route', - 'Recorded if location is available, and kept on ' - 'this phone', + _row( + p, + LucideIcons.mapPin, + l?.activitySetupRouteLabel ?? 'Route', + l?.activitySetupRouteDetail ?? + 'Recorded if location is available, and kept ' + 'on this phone', null), Divider(color: p.line, height: 1), ], _row( p, LucideIcons.heartPulse, - 'Heart rate', + l?.activitySetupHeartRateLabel ?? 'Heart rate', widget.host.bandConnected - ? 'Band connected' - : 'No band connected', + ? (l?.activitySetupBandConnected ?? + 'Band connected') + : (l?.activitySetupNoBandConnected ?? + 'No band connected'), widget.host.bandConnected), Divider(color: p.line, height: 1), _toggle( p, LucideIcons.lock, - 'Private session', - 'Hidden from summaries and exports', + l?.activitySetupPrivateLabel ?? 'Private session', + l?.activitySetupPrivateDetail ?? + 'Hidden from summaries and exports', private, - () => setState(() => private = !private)), + () => setState(() => private = !private), + l), ]), ), const SizedBox(height: S.x4), @@ -165,10 +175,13 @@ class _ActivitySetupState extends State { Expanded( child: Text( est == null - ? 'Calories need your weight.' - : 'About $est kcal per $_estimateMin min, from ' - '${a.met.toStringAsFixed(1)} MET and your ' - 'weight.', + ? (l?.activitySetupCaloriesNeedWeight ?? + 'Calories need your weight.') + : (l?.activitySetupCalorieEstimate(est, + _estimateMin, a.met.toStringAsFixed(1)) ?? + 'About $est kcal per $_estimateMin min, ' + 'from ${a.met.toStringAsFixed(1)} MET ' + 'and your weight.'), style: F.cap.copyWith(color: p.ink3, height: 1.5)), ), ]), @@ -176,17 +189,20 @@ class _ActivitySetupState extends State { if (_refused) ...[ const SizedBox(height: S.x4), StatusCard( - 'A session is already running', - 'Only one can be live at a time.', + l?.activitySetupSessionRunningTitle ?? + 'A session is already running', + l?.activitySetupSessionRunningBody ?? + 'Only one can be live at a time.', fix: LiveDraft.current == null ? '' - : 'Open the running session', + : (l?.activitySetupOpenRunningSession ?? + 'Open the running session'), onFix: LiveDraft.current == null ? null : _resume, icon: LucideIcons.circleAlert, ), ], const SizedBox(height: S.x8), - BigButton('Start', + BigButton(l?.activitySetupStart ?? 'Start', icon: LucideIcons.play, color: a.color, onTap: _starting ? null : _start), @@ -201,12 +217,17 @@ class _ActivitySetupState extends State { /// What this session will actually produce. Distance and pace are promised /// only where a route can be recorded — a treadmill was being sold "distance /// and pace" that nothing in this app can measure indoors. - String _trackLabel(Track t) => switch (t) { - Track.sets => 'Sets, reps and load — logged by you', - Track.distance when widget.a.gps => 'Distance, pace and heart rate', - Track.distance || Track.duration => 'Time and heart rate', - Track.interval => 'Rounds and heart rate', - Track.stillness => 'Time, breathing and heart rate', + String _trackLabel(Track t, AppLocalizations? l) => switch (t) { + Track.sets => + l?.activitySetupTrackSets ?? 'Sets, reps and load — logged by you', + Track.distance when widget.a.gps => + l?.activitySetupTrackDistanceGps ?? 'Distance, pace and heart rate', + Track.distance || Track.duration => + l?.activitySetupTrackTime ?? 'Time and heart rate', + Track.interval => + l?.activitySetupTrackInterval ?? 'Rounds and heart rate', + Track.stillness => + l?.activitySetupTrackStillness ?? 'Time, breathing and heart rate', }; /// [on] null means "not known yet" — no glyph at all, rather than a tick @@ -231,9 +252,10 @@ class _ActivitySetupState extends State { ); Widget _toggle(P p, IconData i, String n, String s, bool on, - VoidCallback onTap) => + VoidCallback onTap, AppLocalizations? l) => Pressable( - semanticLabel: '$n, ${on ? 'on' : 'off'}', + semanticLabel: + '$n, ${on ? (l?.stateOn ?? 'On') : (l?.stateOff ?? 'Off')}', onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), diff --git a/lib/ui2/activity/share.dart b/lib/ui2/activity/share.dart index 0b195b66..4c4edb74 100644 --- a/lib/ui2/activity/share.dart +++ b/lib/ui2/activity/share.dart @@ -25,6 +25,7 @@ import 'package:flutter/rendering.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:share_plus/share_plus.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/units_controller.dart'; import '../grammar.dart'; import '../profile/profile.dart' show SetRow; @@ -118,8 +119,10 @@ class _ShareSheetState extends State { // A share that quietly does nothing is worse than one that says it // failed: this swallowed every iPad share for the life of the screen. if (mounted) { - messenger.showSnackBar( - const SnackBar(content: Text('Could not open the share sheet.'))); + final l = AppLocalizations.of(context); + messenger.showSnackBar(SnackBar( + content: Text( + l?.activityShareOpenFailed ?? 'Could not open the share sheet.'))); } debugPrint('share failed: $e'); } finally { @@ -219,6 +222,7 @@ class _ShareSheetState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Scaffold( backgroundColor: p.bg, body: SafeArea( @@ -226,7 +230,7 @@ class _ShareSheetState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), - child: const NavBar('Share'), + child: NavBar(l?.activityShareTitle ?? 'Share'), ), Expanded( child: ListView( @@ -262,7 +266,7 @@ class _ShareSheetState extends State { // own ratio, so this is the difference between posting the // card and posting a crop of it. SubTabs( - [for (final f in PosterFormat.values) f.label], + [for (final f in PosterFormat.values) posterFormatLabel(c, f)], PosterFormat.values.indexOf(_format), (i) { setState(() => _format = PosterFormat.values[i]); @@ -271,20 +275,28 @@ class _ShareSheetState extends State { color: r.activity.color, ), const SizedBox(height: S.x6), - Text('YOUR PHOTO', style: F.over.copyWith(color: p.ink3)), + Text(l?.activitySharePhotoHeader ?? 'YOUR PHOTO', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: Column(children: [ - SetRow(LucideIcons.imagePlus, r.activity.color, - _photo == null ? 'Add a photo' : 'Change photo', + SetRow( + LucideIcons.imagePlus, + r.activity.color, + _photo == null + ? (l?.activityShareAddPhoto ?? 'Add a photo') + : (l?.activityShareChangePhoto ?? + 'Change photo'), sub: _photo == null - ? 'From this phone. Nothing is uploaded' + ? (l?.activitySharePhotoHint ?? + 'From this phone. Nothing is uploaded') : _photo!.path.split('/').last, onTap: _pickPhoto), if (_photo != null) ...[ Divider(color: p.line, height: 1), - SetRow(LucideIcons.trash2, C.red, 'Remove the photo', + SetRow(LucideIcons.trash2, C.red, + l?.activityShareRemovePhoto ?? 'Remove the photo', chevron: false, onTap: () => setState(() => _photo = null)), ], @@ -296,17 +308,21 @@ class _ShareSheetState extends State { // them tells openstreetmap.org roughly where you were. if (_hasRoute) ...[ const SizedBox(height: S.x6), - Text('BASEMAP', style: F.over.copyWith(color: p.ink3)), + Text(l?.activityShareBasemapHeader ?? 'BASEMAP', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: SetRow( LucideIcons.map, r.activity.color, - 'Draw the real map', - sub: 'Asks openstreetmap.org for the tiles covering ' - 'this route. Off, the route draws on its own', - value: _mapConsent ? 'On' : 'Off', + l?.activityShareDrawMap ?? 'Draw the real map', + sub: l?.activityShareMapHint ?? + 'Asks openstreetmap.org for the tiles covering ' + 'this route. Off, the route draws on its own', + value: _mapConsent + ? (l?.stateOn ?? 'On') + : (l?.stateOff ?? 'Off'), chevron: false, onTap: _toggleMap, ), @@ -318,9 +334,10 @@ class _ShareSheetState extends State { // first await. if (_hasRoute && _mapBusy) ...[ const SizedBox(height: S.x3), - const StatusCard( - 'Fetching the map', - 'The card draws as soon as every tile is here.', + StatusCard( + l?.activityShareFetchingMapTitle ?? 'Fetching the map', + l?.activityShareFetchingMapBody ?? + 'The card draws as soon as every tile is here.', icon: LucideIcons.map, ), ] @@ -333,11 +350,12 @@ class _ShareSheetState extends State { _mapTried_ && _mosaic == null) ...[ const SizedBox(height: S.x3), - const StatusCard( - 'No map for this card', - 'The map tiles could not be fetched, so the route is ' - 'drawn on its own. Everything else on the card is ' - 'unchanged.', + StatusCard( + l?.activityShareNoMapTitle ?? 'No map for this card', + l?.activityShareNoMapBody ?? + 'The map tiles could not be fetched, so the route is ' + 'drawn on its own. Everything else on the card is ' + 'unchanged.', icon: LucideIcons.mapPinOff, ), ], @@ -347,16 +365,18 @@ class _ShareSheetState extends State { // four destination tiles that used to sit here were four // integrations this app does not have. BigButton( - 'Share', + l?.activityShareTitle ?? 'Share', icon: LucideIcons.share2, color: r.activity.color, onTap: _share, ), if (r.private) ...[ const SizedBox(height: S.x4), - const StatusCard( - 'This session is private', - 'Hidden from summaries and exports.', + StatusCard( + l?.activityShareStatusPrivateTitle ?? + 'This session is private', + l?.activityShareStatusPrivateBody ?? + 'Hidden from summaries and exports.', icon: LucideIcons.lock, ), ], diff --git a/lib/ui2/activity/summary.dart b/lib/ui2/activity/summary.dart index 85f49ea3..5686c996 100644 --- a/lib/ui2/activity/summary.dart +++ b/lib/ui2/activity/summary.dart @@ -22,6 +22,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/db.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/prefs.dart'; import '../../state/units_controller.dart'; import '../charts.dart'; @@ -778,57 +779,65 @@ class _ActivitySummaryState extends State { /// is replaced by the rating in [SessionStats] — there is no confirm step, /// because a picker with a default and a Save button is how a 7 ends up in /// the database on behalf of somebody who never touched it. - Widget _rpePrompt(P p) => Surface( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('HOW HARD DID THAT FEEL?', - style: F.over.copyWith(color: p.ink3)), - const SizedBox(height: S.x2), - Text( - 'Your own rating of the effort. It is a feeling, not a ' - 'measurement — which is the point, because it can disagree with ' - 'the numbers above.', - style: F.cap.copyWith(color: p.ink2, height: 1.4)), - const SizedBox(height: S.x4), - for (var row = 0; row < 2; row++) ...[ - if (row > 0) const SizedBox(height: S.x2), - Row(children: [ - for (var i = 0; i < 5; i++) ...[ - if (i > 0) const SizedBox(width: S.x2), - Expanded( - child: Pressable( - semanticLabel: 'Rate this effort ${row * 5 + i + 1} of 10', - onTap: () => _saveRpe(row * 5 + i + 1), - child: Container( - padding: const EdgeInsets.symmetric(vertical: S.x3), - alignment: Alignment.center, - decoration: BoxDecoration( - color: p.wash(a.color), borderRadius: R.rMd), - child: Text('${row * 5 + i + 1}', - style: F.n17.copyWith(color: p.on(a.color))), - ), + Widget _rpePrompt(P p) { + final l = AppLocalizations.of(context); + return Surface( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(l?.activitySummaryRpeHeadline ?? 'HOW HARD DID THAT FEEL?', + style: F.over.copyWith(color: p.ink3)), + const SizedBox(height: S.x2), + Text( + l?.activitySummaryRpeBody ?? + 'Your own rating of the effort. It is a feeling, not a ' + 'measurement — which is the point, because it can ' + 'disagree with the numbers above.', + style: F.cap.copyWith(color: p.ink2, height: 1.4)), + const SizedBox(height: S.x4), + for (var row = 0; row < 2; row++) ...[ + if (row > 0) const SizedBox(height: S.x2), + Row(children: [ + for (var i = 0; i < 5; i++) ...[ + if (i > 0) const SizedBox(width: S.x2), + Expanded( + child: Pressable( + semanticLabel: l?.activitySummaryRateEffort( + row * 5 + i + 1) ?? + 'Rate this effort ${row * 5 + i + 1} of 10', + onTap: () => _saveRpe(row * 5 + i + 1), + child: Container( + padding: const EdgeInsets.symmetric(vertical: S.x3), + alignment: Alignment.center, + decoration: BoxDecoration( + color: p.wash(a.color), borderRadius: R.rMd), + child: Text('${row * 5 + i + 1}', + style: F.n17.copyWith(color: p.on(a.color))), ), ), - ], - ]), - ], - const SizedBox(height: S.x3), - Row(children: [ - Text('1 · very easy', style: F.over.copyWith(color: p.ink3)), - const Spacer(), - Text('10 · maximal', style: F.over.copyWith(color: p.ink3)), + ), + ], ]), - const SizedBox(height: S.x2), - Align( - alignment: Alignment.centerLeft, - child: Pressable( - onTap: _skipRpe, - child: Text('Not now', - style: F.body.copyWith( - color: p.ink2, fontWeight: FontWeight.w600)), - ), - ), + ], + const SizedBox(height: S.x3), + Row(children: [ + Text(l?.activitySummaryRpeVeryEasy ?? '1 · very easy', + style: F.over.copyWith(color: p.ink3)), + const Spacer(), + Text(l?.activitySummaryRpeMaximal ?? '10 · maximal', + style: F.over.copyWith(color: p.ink3)), ]), - ); + const SizedBox(height: S.x2), + Align( + alignment: Alignment.centerLeft, + child: Pressable( + onTap: _skipRpe, + child: Text(l?.activitySummaryNotNow ?? 'Not now', + style: F.body.copyWith( + color: p.ink2, fontWeight: FontWeight.w600)), + ), + ), + ]), + ); + } /// Correct a session's activity type — the band's own guess, or a hand-typed /// one that was wrong. `LocalDb.setSessionType` is the narrow UPDATE this @@ -895,6 +904,7 @@ class _ActivitySummaryState extends State { // for a row that only ever draws one icon would shove the title left on // every unsaved-session summary for no reason. final canChangeType = r.sessionId != null; + final l = AppLocalizations.of(c); return Scaffold( backgroundColor: p.bg, body: SafeArea( @@ -912,14 +922,17 @@ class _ActivitySummaryState extends State { trailing: Row(mainAxisSize: MainAxisSize.min, children: [ if (canChangeType) ...[ Pressable( - semanticLabel: 'Change activity type', + semanticLabel: + l?.activitySummaryChangeType ?? 'Change activity type', onTap: () => _changeType(c), child: Icon(LucideIcons.pencil, size: 18, color: p.ink2), ), const SizedBox(width: S.x3), ], Pressable( - semanticLabel: 'Share this ${a.name.toLowerCase()}', + semanticLabel: l?.activitySummaryShareThis( + a.name.toLowerCase()) ?? + 'Share this ${a.name.toLowerCase()}', onTap: () => Navigator.of(c).push(MaterialPageRoute( builder: (_) => ShareSheet(r))), child: Icon(LucideIcons.share2, size: 19, color: p.ink2), @@ -929,7 +942,19 @@ class _ActivitySummaryState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), - child: SubTabs(_tabs, tab, (i) => setState(() => tab = i), + // Display labels are localized; `_tabs` itself stays the fixed + // English keys the switch below matches by NAME. + child: SubTabs( + [ + for (final t in _tabs) + switch (t) { + 'Overview' => l?.activitySummaryTabOverview ?? 'Overview', + 'Splits' => l?.activitySummaryTabSplits ?? 'Splits', + _ => l?.activitySummaryTabGraphs ?? 'Graphs', + }, + ], + tab, + (i) => setState(() => tab = i), color: a.color), ), Expanded( @@ -952,12 +977,15 @@ class _ActivitySummaryState extends State { // ─────────────────── OVERVIEW ─────────────────── List _overview(BuildContext c, P p) { final hero = _hero(); + final l = AppLocalizations.of(c); return [ if (unsaved) ...[ StatusCard( - 'This session is not saved yet', - 'Writing it to this phone failed.', - fix: _saving ? 'Saving' : 'Try again', + l?.activitySummaryUnsavedTitle ?? 'This session is not saved yet', + l?.activitySummaryUnsavedBody ?? 'Writing it to this phone failed.', + fix: _saving + ? (l?.activitySummarySaving ?? 'Saving') + : (l?.activitySummaryTryAgain ?? 'Try again'), onFix: _saving ? null : _retrySave, icon: LucideIcons.triangleAlert, ), @@ -989,7 +1017,8 @@ class _ActivitySummaryState extends State { ), if (r.private) ...[ const SizedBox(width: S.x3), - const Pill('Private', C.n500, icon: LucideIcons.lock), + Pill(l?.activitySummaryPrivate ?? 'Private', C.n500, + icon: LucideIcons.lock), ], ]), const SizedBox(height: S.x1), @@ -1018,22 +1047,26 @@ class _ActivitySummaryState extends State { /// always here; the step one joins it whenever a count is on the card, /// because a step row that does not name its sensor is weaker than the day /// screens beside it, which have named theirs all along. - Widget _basisNote(P p) => Surface( - elevation: 0, - color: p.card2, - child: Row(children: [ - Expanded( - child: Text( - [ - _calorieBasis(), - if (r.stepsCounted != null) - "Steps came from the strap's own motion sensor, which only " - 'counts them on foot.', - ].join(' '), - style: F.cap.copyWith(color: p.ink3, height: 1.5)), - ), - ]), - ); + Widget _basisNote(P p) { + final l = AppLocalizations.of(context); + return Surface( + elevation: 0, + color: p.card2, + child: Row(children: [ + Expanded( + child: Text( + [ + _calorieBasis(), + if (r.stepsCounted != null) + l?.activitySummaryStepsBasis ?? + "Steps came from the strap's own motion sensor, which " + 'only counts them on foot.', + ].join(' '), + style: F.cap.copyWith(color: p.ink3, height: 1.5)), + ), + ]), + ); + } /// What the calorie figure was actually made of — or why there isn't one. /// @@ -1051,28 +1084,37 @@ class _ActivitySummaryState extends State { /// When there is no number, say which anchor is missing and point at the /// effort measure that does not need one. String _calorieBasis() { - if (widget.weightKg == null) return 'Calories need your weight.'; + final l = AppLocalizations.of(context); + if (widget.weightKg == null) { + return l?.activitySummaryCaloriesNeedWeight ?? + 'Calories need your weight.'; + } final met = a.met.toStringAsFixed(1); if (r.calories == null) { return r.strain == null - ? 'No calorie figure for this session. An energy estimate from heart ' - 'rate needs your maximum and resting heart rates, and one of ' - 'them is not set.' - : 'No calorie figure for this session — an energy estimate from ' - 'heart rate needs your maximum and resting heart rates, and one ' - 'of them is not set. Strain above is the effort that was ' - 'measured, on its own 0–21 scale.'; + ? l?.activitySummaryNoCalorieNoStrain ?? + 'No calorie figure for this session. An energy estimate from ' + 'heart rate needs your maximum and resting heart rates, and ' + 'one of them is not set.' + : l?.activitySummaryNoCalorieWithStrain ?? + 'No calorie figure for this session — an energy estimate from ' + 'heart rate needs your maximum and resting heart rates, and ' + 'one of them is not set. Strain above is the effort that ' + 'was measured, on its own 0–21 scale.'; } return r.avgHr == null - ? 'Estimated from $met MET and your weight. No heart rate reached ' - 'this session, so none of it is in the figure.' - : 'Estimated from $met MET, your weight and heart rate.'; + ? l?.activitySummaryCalorieNoHr(met) ?? + 'Estimated from $met MET and your weight. No heart rate reached ' + 'this session, so none of it is in the figure.' + : l?.activitySummaryCalorieWithHr(met) ?? + 'Estimated from $met MET, your weight and heart rate.'; } /// (value, unit, caption). The hero is the archetype's own headline — and /// falls back to elapsed time, which is the one number every session has. (String, String, String) _hero() { - final fallback = (hms(r.duration), '', 'Elapsed time'); + final l = AppLocalizations.of(context); + final fallback = (hms(r.duration), '', l?.activitySummaryElapsedTime ?? 'Elapsed time'); return switch (arch) { Arch.route || Arch.journey => _distance == null ? fallback @@ -1080,25 +1122,34 @@ class _ActivitySummaryState extends State { _distance!.$1.toStringAsFixed(2), _distance!.$2, arch == Arch.journey && r.gainM != null - ? '+${r.gainM!.round()} m climbed' + ? (l?.activitySummaryClimbed(r.gainM!.round()) ?? + '+${r.gainM!.round()} m climbed') : a.name ), Arch.strength => r.strength.volumeKg == null ? ( '${r.strength.setCount}', - r.strength.setCount == 1 ? 'set' : 'sets', - 'Nothing was logged with a load' + l?.activitySummarySetUnit(r.strength.setCount) ?? + (r.strength.setCount == 1 ? 'set' : 'sets'), + l?.activitySummaryNothingLoggedWithLoad ?? + 'Nothing was logged with a load' ) : ( grouped(r.strength.volumeKg!), 'kg', r.strength.hasUnloadedSets - ? 'Volume of the loaded sets' - : 'Total volume' + ? (l?.activitySummaryVolumeLoadedSets ?? + 'Volume of the loaded sets') + : (l?.activitySummaryTotalVolume ?? 'Total volume') ), Arch.laps => r.swimMetres == null ? fallback - : (grouped(r.swimMetres!), 'm', '${r.lapCount} laps'), + : ( + grouped(r.swimMetres!), + 'm', + l?.activitySummaryLapsCaption(r.lapCount ?? 0) ?? + '${r.lapCount} laps' + ), Arch.flow || Arch.match || Arch.interval || @@ -1109,6 +1160,7 @@ class _ActivitySummaryState extends State { // ─────────── THE DEFINING VISUAL OBJECT ─────────── List _definingObject(BuildContext c, P p) { + final l = AppLocalizations.of(c); switch (arch) { case Arch.route: if (r.route.length < 2) { @@ -1116,9 +1168,11 @@ class _ActivitySummaryState extends State { // No `fix`: there is no "how route recording works" screen, and // StatusCard paints any fix string as a blue call to action — // a button that cannot be tapped is worse than no button. - const StatusCard( - 'No route for this session', - 'Location was off, or this activity was not recorded with GPS.', + StatusCard( + l?.activitySummaryNoRouteTitle ?? 'No route for this session', + l?.activitySummaryNoRouteBody ?? + 'Location was off, or this activity was not recorded with ' + 'GPS.', icon: LucideIcons.map, ), ]; @@ -1126,7 +1180,7 @@ class _ActivitySummaryState extends State { return [ Surface( child: ChartFrame( - title: 'ROUTE', + title: l?.activitySummaryRouteTitle ?? 'ROUTE', unit: _distanceUnit, height: 200, legend: r.routePace == null @@ -1135,11 +1189,17 @@ class _ActivitySummaryState extends State { // These were opposite: the same run read green at its // slowest here and green at its fastest on the poster, so a // card and the screen it came from disagreed about the run. - : [('Slower', p.on(C.red)), ('Faster', p.on(C.green))], + : [ + (l?.activitySummarySlower ?? 'Slower', p.on(C.red)), + (l?.activitySummaryFaster ?? 'Faster', p.on(C.green)), + ], footnote: _distance == null - ? 'Start and finish are pinned.' - : '${_distance!.$1.toStringAsFixed(2)} ${_distance!.$2}, ' - 'start and finish pinned.', + ? (l?.activitySummaryStartFinishPinned ?? + 'Start and finish are pinned.') + : (l?.activitySummaryRouteFootnote( + _distance!.$1.toStringAsFixed(2), _distance!.$2) ?? + '${_distance!.$1.toStringAsFixed(2)} ${_distance!.$2}, ' + 'start and finish pinned.'), child: ClipRRect( borderRadius: R.rLg, child: Container( @@ -1172,11 +1232,12 @@ class _ActivitySummaryState extends State { // an empty log. case Arch.strength: return r.strength.isEmpty - ? const [ + ? [ StatusCard( - 'No sets logged', - 'Nothing was entered for this session, so there is no load ' - 'and no volume to total.', + l?.activitySummaryNoSetsTitle ?? 'No sets logged', + l?.activitySummaryNoSetsBody ?? + 'Nothing was entered for this session, so there is no ' + 'load and no volume to total.', icon: LucideIcons.dumbbell, ), ] @@ -1185,9 +1246,9 @@ class _ActivitySummaryState extends State { case Arch.interval: if (r.rounds.isEmpty) { return [ - const StatusCard( - 'No rounds recorded', - '0 rounds logged.', + StatusCard( + l?.activitySummaryNoRoundsTitle ?? 'No rounds recorded', + l?.activitySummaryNoRoundsBody ?? '0 rounds logged.', icon: LucideIcons.timer, ), ]; @@ -1199,12 +1260,20 @@ class _ActivitySummaryState extends State { return [ Surface( child: ChartFrame( - title: 'INTERVAL LADDER', + title: l?.activitySummaryIntervalLadderTitle ?? 'INTERVAL LADDER', unit: 'seconds', height: 110, - legend: [('Work', p.on(C.red)), ('Rest', p.on(C.teal))], - xLabels: ['Round 1', 'Round ${r.rounds.length}'], - footnote: 'Longest block ${clock(peak.round())}.', + legend: [ + (l?.activitySummaryWork ?? 'Work', p.on(C.red)), + (l?.activitySummaryRest ?? 'Rest', p.on(C.teal)), + ], + xLabels: [ + l?.activitySummaryRoundLabel(1) ?? 'Round 1', + l?.activitySummaryRoundLabel(r.rounds.length) ?? + 'Round ${r.rounds.length}', + ], + footnote: l?.activitySummaryLongestBlock(clock(peak.round())) ?? + 'Longest block ${clock(peak.round())}.', child: CustomPaint( size: Size.infinite, painter: IntervalLadder([ @@ -1240,7 +1309,8 @@ class _ActivitySummaryState extends State { Text( r.poses.isEmpty ? hms(r.duration) - : '${r.poses.length} poses', + : (l?.activitySummaryPosesCount(r.poses.length) ?? + '${r.poses.length} poses'), style: F.head.copyWith(color: p.ink)), Text( r.breathsPerMin == null @@ -1254,9 +1324,9 @@ class _ActivitySummaryState extends State { case Arch.laps: if (r.lapSecs.isEmpty) { return [ - const StatusCard( - 'No laps counted', - '0 laps tapped.', + StatusCard( + l?.activitySummaryNoLapsTitle ?? 'No laps counted', + l?.activitySummaryNoLapsBody ?? '0 laps tapped.', icon: LucideIcons.waves, ), ]; @@ -1266,14 +1336,22 @@ class _ActivitySummaryState extends State { return [ Surface( child: ChartFrame( - title: 'LAPS', - unit: 'seconds per lap', + title: l?.activitySummaryLapsTitle ?? 'LAPS', + unit: l?.activitySummarySecondsPerLap ?? 'seconds per lap', height: 150, - xLabels: ['Lap 1', 'Lap ${r.lapSecs.length}'], + xLabels: [ + l?.activitySummaryLapLabel(1) ?? 'Lap 1', + l?.activitySummaryLapLabel(r.lapSecs.length) ?? + 'Lap ${r.lapSecs.length}', + ], footnote: [ - if (r.poolLengthM != null) '${r.poolLengthM} m pool', - 'fastest ${clock(fastest)}', - 'slowest ${clock(slowest)}', + if (r.poolLengthM != null) + l?.activitySummaryPoolLength(r.poolLengthM!) ?? + '${r.poolLengthM} m pool', + l?.activitySummaryFastest(clock(fastest)) ?? + 'fastest ${clock(fastest)}', + l?.activitySummarySlowest(clock(slowest)) ?? + 'slowest ${clock(slowest)}', ].join(' · '), child: CustomPaint( size: Size.infinite, @@ -1285,9 +1363,10 @@ class _ActivitySummaryState extends State { case Arch.journey: if (r.elevationM.length < 2) { return [ - const StatusCard( - 'No elevation profile', - 'No route, or the route carried no altitude.', + StatusCard( + l?.activitySummaryNoElevationTitle ?? 'No elevation profile', + l?.activitySummaryNoElevationBody ?? + 'No route, or the route carried no altitude.', icon: LucideIcons.mountain, ), ]; @@ -1298,11 +1377,14 @@ class _ActivitySummaryState extends State { Surface( child: Column(children: [ ChartFrame( - title: 'ELEVATION', + title: l?.activitySummaryElevationTitle ?? 'ELEVATION', unit: 'm', height: 130, yAxis: axis, - xLabels: const ['Start', 'Finish'], + xLabels: [ + l?.activitySummaryStart ?? 'Start', + l?.activitySummaryFinish ?? 'Finish', + ], series: r.elevationM, child: CustomPaint( size: Size.infinite, @@ -1311,9 +1393,14 @@ class _ActivitySummaryState extends State { ), const SizedBox(height: S.x4), InlineMetrics([ - if (r.gainM != null) ('Gain', '+${r.gainM!.round()} m', C.green), - if (r.lossM != null) ('Loss', '−${r.lossM!.round()} m', C.orange), - ('Peak', '${grouped(peak)} m', C.n500), + if (r.gainM != null) + (l?.activitySummaryGain ?? 'Gain', + '+${r.gainM!.round()} m', C.green), + if (r.lossM != null) + (l?.activitySummaryLoss ?? 'Loss', + '−${r.lossM!.round()} m', C.orange), + (l?.activitySummaryPeak ?? 'Peak', '${grouped(peak)} m', + C.n500), ]), ]), ), @@ -1355,19 +1442,24 @@ class _ActivitySummaryState extends State { /// [_noHrCard] cannot end up telling three different stories about the same /// silent sensor. Offering "Check band connection" for a plunge is a fix for /// a problem the user does not have. - String? get _thermalWhy => !thermal - ? null - : a.name == 'Cold plunge' - ? 'Cold closes the blood vessels the sensor reads through. Finding ' - 'nothing here is expected, not a fault.' - : 'Heat, sweat and a strap that loosens as you warm up all stop the ' - 'sensor seeing a pulse. Finding nothing here is ordinary, not a ' - 'fault.'; + String? get _thermalWhy { + if (!thermal) return null; + final l = AppLocalizations.of(context); + return a.name == 'Cold plunge' + ? l?.activitySummaryColdPlungeWhy ?? + 'Cold closes the blood vessels the sensor reads through. Finding ' + 'nothing here is expected, not a fault.' + : l?.activitySummaryHeatWhy ?? + 'Heat, sweat and a strap that loosens as you warm up all stop ' + 'the sensor seeing a pulse. Finding nothing here is ' + 'ordinary, not a fault.'; + } IconData get _thermalIcon => a.name == 'Cold plunge' ? LucideIcons.snowflake : LucideIcons.thermometer; List _thermalObject(P p) { + final l = AppLocalizations.of(context); final (have, total) = r.hrMinutes; // Under two readings there is no line to draw — one point is not a trace. // The two headlines are not the same statement: nothing arrived, or one @@ -1376,8 +1468,10 @@ class _ActivitySummaryState extends State { return [ StatusCard( have == 0 - ? 'No pulse reading for this ${a.name.toLowerCase()}' - : 'One minute of pulse, and no more', + ? (l?.activitySummaryNoPulseTitle(a.name.toLowerCase()) ?? + 'No pulse reading for this ${a.name.toLowerCase()}') + : (l?.activitySummaryOneMinutePulse ?? + 'One minute of pulse, and no more'), _thermalWhy!, icon: _thermalIcon, ), @@ -1388,8 +1482,10 @@ class _ActivitySummaryState extends State { child: _hrFrame( p, extra: have < total - ? 'The band found a pulse in $have of $total minutes. The gaps ' - 'are expected, so what is drawn is the part it could see.' + ? (l?.activitySummaryPulseGapNote(have, total) ?? + 'The band found a pulse in $have of $total minutes. The ' + 'gaps are expected, so what is drawn is the part it ' + 'could see.') : null, ), ), @@ -1403,30 +1499,38 @@ class _ActivitySummaryState extends State { /// used to get 'The band reported nothing while this was running' and a /// Check-band button, which is a false reason and a fix for a problem the /// user does not have. - Widget _noHrCard(BuildContext c) => r.hr.any((v) => v != null) - ? const StatusCard( - 'Too short to chart', - 'One minute of heart rate is a point, not a line.', - icon: LucideIcons.heartPulse, - ) - // MT-08 — a third reason, and it is not a fault. On a heat or cold - // session the sensor is working and the blood is not where it can see - // it, so there is no connection to check and no button that helps. - : thermal - ? StatusCard( - 'No pulse reading for this ${a.name.toLowerCase()}', - _thermalWhy!, - icon: _thermalIcon, - ) - : StatusCard( - 'No heart rate for this session', - 'The band reported nothing while this was running.', - fix: 'Check band connection', - // The band, its battery and its link all live behind the - // profile's sources list. The CTA used to be paint. - onFix: () => openProfile(c), - icon: LucideIcons.heartPulse, - ); + Widget _noHrCard(BuildContext c) { + final l = AppLocalizations.of(c); + return r.hr.any((v) => v != null) + ? StatusCard( + l?.activitySummaryTooShortTitle ?? 'Too short to chart', + l?.activitySummaryTooShortBody ?? + 'One minute of heart rate is a point, not a line.', + icon: LucideIcons.heartPulse, + ) + // MT-08 — a third reason, and it is not a fault. On a heat or cold + // session the sensor is working and the blood is not where it can + // see it, so there is no connection to check and no button that + // helps. + : thermal + ? StatusCard( + l?.activitySummaryNoPulseTitle(a.name.toLowerCase()) ?? + 'No pulse reading for this ${a.name.toLowerCase()}', + _thermalWhy!, + icon: _thermalIcon, + ) + : StatusCard( + l?.activitySummaryNoHrTitle ?? 'No heart rate for this session', + l?.activitySummaryNoHrBody ?? + 'The band reported nothing while this was running.', + fix: l?.activitySummaryCheckBandConnection ?? + 'Check band connection', + // The band, its battery and its link all live behind the + // profile's sources list. The CTA used to be paint. + onFix: () => openProfile(c), + icon: LucideIcons.heartPulse, + ); + } /// The heart-rate trace, framed — the one chart both `basic` and `match` /// are built on, so they cannot end up with two different axes for one @@ -1442,23 +1546,28 @@ class _ActivitySummaryState extends State { String? get _traceNote { final pct = r.traceCoveragePct; if (pct == null || pct >= 90) return null; - return 'Partial trace — the band handed over $pct% of these minutes.'; + final l = AppLocalizations.of(context); + return l?.activitySummaryPartialTrace(pct) ?? + 'Partial trace — the band handed over $pct% of these minutes.'; } Widget _hrFrame(P p, {double height = 130, String? extra}) { + final l = AppLocalizations.of(context); final axis = AxisSpec.of(r.hr.whereType()); final hard = r.hardMinutes; final note = [ - if (hard != null) '${hard.round()} min above 80% of your maximum.', + if (hard != null) + l?.activitySummaryHardMinutesNote(hard.round()) ?? + '${hard.round()} min above 80% of your maximum.', ?_traceNote, ?extra, ]; return ChartFrame( - title: 'HEART RATE', + title: l?.activitySummaryHeartRateTitle ?? 'HEART RATE', unit: 'bpm', height: height, yAxis: axis, - xLabels: ['Start', hms(r.duration)], + xLabels: [l?.activitySummaryStart ?? 'Start', hms(r.duration)], footnote: note.isEmpty ? null : note.join(' '), series: r.hr, child: CustomPaint( @@ -1471,20 +1580,22 @@ class _ActivitySummaryState extends State { /// The zone split, with the minutes in the key rather than in a second row /// underneath it that has to be kept in step by hand. Widget _zoneFrame(P p) => ChartFrame( - title: 'TIME IN ZONES', + title: AppLocalizations.of(context)?.activitySummaryTimeInZonesTitle ?? + 'TIME IN ZONES', unit: 'minutes', height: 10, legend: [ for (var i = 0; i < 5; i++) ('Z${i + 1} · ${r.zoneMinutes[i].round()}m', ZoneBar.cols(p)[i]), ], - footnote: zonesWhy(r.zoneSource, r.zoneMaxHr), + footnote: zonesWhy(r.zoneSource, r.zoneMaxHr, AppLocalizations.of(context)), child: CustomPaint( size: Size.infinite, painter: ZoneBar(_zoneFractions(), p)), ); // ─────────── ARCHETYPE BODY ─────────── List _body(BuildContext c, P p) { + final l = AppLocalizations.of(c); switch (arch) { case Arch.strength: final top = r.strength.topSet; @@ -1492,7 +1603,7 @@ class _ActivitySummaryState extends State { return [ if (top != null) Section( - 'Top set', + l?.activitySummaryTopSet ?? 'Top set', Surface( child: Row(children: [ Container( @@ -1515,7 +1626,10 @@ class _ActivitySummaryState extends State { color: p.ink, fontWeight: FontWeight.w600)), Row(children: [ Flexible( - child: Text('1RM estimate ${rm!.round()} kg', + child: Text( + l?.activitySummaryOneRepMax( + rm!.round()) ?? + '1RM estimate ${rm!.round()} kg', maxLines: 1, overflow: TextOverflow.ellipsis, style: F.over.copyWith(color: p.ink3))), @@ -1533,11 +1647,13 @@ class _ActivitySummaryState extends State { ), ), if (r.strength.hasUnloadedSets) - const Padding( - padding: EdgeInsets.only(top: S.x4), + Padding( + padding: const EdgeInsets.only(top: S.x4), child: StatusCard( - 'Some sets had no load', - 'Counted in sets and reps, but left out of volume.', + l?.activitySummarySomeSetsNoLoadTitle ?? + 'Some sets had no load', + l?.activitySummarySomeSetsNoLoadBody ?? + 'Counted in sets and reps, but left out of volume.', icon: LucideIcons.info, ), ), @@ -1553,7 +1669,7 @@ class _ActivitySummaryState extends State { return [ if (r.gameScore.isNotEmpty) Section( - 'Score', + l?.activitySummaryScore ?? 'Score', Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: Column(children: [ @@ -1562,7 +1678,9 @@ class _ActivitySummaryState extends State { padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row(children: [ Expanded( - child: Text('Set ${i + 1}', + child: Text( + l?.activitySummaryGameSetLabel(i + 1) ?? + 'Set ${i + 1}', style: F.body.copyWith(color: p.ink3))), Text('${r.gameScore[i].$1} — ${r.gameScore[i].$2}', style: F.n17.copyWith( @@ -1590,7 +1708,12 @@ class _ActivitySummaryState extends State { List _zoneSection(P p) => r.zoneMinutes.length != 5 ? const [] - : [Section('Heart-rate zones', Surface(child: _zoneFrame(p)))]; + : [ + Section( + AppLocalizations.of(context)?.activitySummaryHeartRateZones ?? + 'Heart-rate zones', + Surface(child: _zoneFrame(p))), + ]; List _zoneFractions() { final total = r.zoneMinutes.fold(0, (x, y) => x + y); @@ -1605,13 +1728,15 @@ class _ActivitySummaryState extends State { // "Splits" is whatever this archetype breaks into: kilometres for a run, // sets for a lift, rounds for HIIT, laps for a swim. List _splits(BuildContext c, P p) { + final l = AppLocalizations.of(c); switch (arch) { case Arch.route || Arch.journey: if (r.splits.isEmpty) { return [ - const StatusCard( - 'No splits for this session', - 'Splits need a recorded distance.', + StatusCard( + l?.activitySummaryNoSplitsTitle ?? 'No splits for this session', + l?.activitySummaryNoSplitsBody ?? + 'Splits need a recorded distance.', icon: LucideIcons.list, ), ]; @@ -1641,14 +1766,16 @@ class _ActivitySummaryState extends State { Row(children: [ SizedBox( width: 26, - child: Text('KM', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryKm ?? 'KM', + style: F.over.copyWith(color: p.ink3))), SizedBox( width: 46, - child: Text('PACE', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryPace ?? 'PACE', + style: F.over.copyWith(color: p.ink3))), const Expanded(child: SizedBox()), SizedBox( width: 34, - child: Text('HR', + child: Text(l?.activitySummaryHr ?? 'HR', textAlign: TextAlign.right, style: F.over.copyWith(color: p.ink3))), ]), @@ -1692,9 +1819,9 @@ class _ActivitySummaryState extends State { case Arch.strength: if (r.strength.isEmpty) { return [ - const StatusCard( - 'No sets logged', - '0 sets logged.', + StatusCard( + l?.activitySummaryNoSetsTitle ?? 'No sets logged', + l?.activitySummarySetsLoggedZero ?? '0 sets logged.', icon: LucideIcons.dumbbell, ), ]; @@ -1722,8 +1849,8 @@ class _ActivitySummaryState extends State { case Arch.interval: if (r.rounds.isEmpty) { return [ - const StatusCard('No rounds recorded', - '0 rounds logged.', + StatusCard(l?.activitySummaryNoRoundsTitle ?? 'No rounds recorded', + l?.activitySummaryNoRoundsBody ?? '0 rounds logged.', icon: LucideIcons.timer), ]; } @@ -1737,15 +1864,18 @@ class _ActivitySummaryState extends State { Row(children: [ SizedBox( width: 34, - child: Text('R', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryRoundHeader ?? 'R', + style: F.over.copyWith(color: p.ink3))), Expanded( - child: Text('WORK', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryWorkHeader ?? 'WORK', + style: F.over.copyWith(color: p.ink3))), Expanded( - child: Text('REST', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryRestHeader ?? 'REST', + style: F.over.copyWith(color: p.ink3))), if (anyHr) SizedBox( width: 52, - child: Text('AVG BPM', + child: Text(l?.activitySummaryAvgBpm ?? 'AVG BPM', textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -1791,7 +1921,8 @@ class _ActivitySummaryState extends State { case Arch.laps: if (r.lapSecs.isEmpty) { return [ - const StatusCard('No laps counted', '0 laps tapped.', + StatusCard(l?.activitySummaryNoLapsTitle ?? 'No laps counted', + l?.activitySummaryNoLapsBody ?? '0 laps tapped.', icon: LucideIcons.waves), ]; } @@ -1803,12 +1934,15 @@ class _ActivitySummaryState extends State { Row(children: [ SizedBox( width: 34, - child: Text('LAP', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryLapHeader ?? 'LAP', + style: F.over.copyWith(color: p.ink3))), SizedBox( width: 52, - child: Text('TIME', style: F.over.copyWith(color: p.ink3))), + child: Text(l?.activitySummaryTimeHeader ?? 'TIME', + style: F.over.copyWith(color: p.ink3))), Expanded( - child: Text('SPEED vs FASTEST', + child: Text( + l?.activitySummarySpeedVsFastest ?? 'SPEED vs FASTEST', textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -1844,38 +1978,44 @@ class _ActivitySummaryState extends State { } } - Widget _setRow(P p, int n, LoggedSet s) => Padding( - padding: const EdgeInsets.symmetric(vertical: S.x3), - child: Row(children: [ - Container( - width: 24, - height: 24, - alignment: Alignment.center, - decoration: - BoxDecoration(color: p.wash(C.purple), borderRadius: R.rSm), - child: Text('$n', style: F.over.copyWith(color: p.on(C.purple))), - ), + Widget _setRow(P p, int n, LoggedSet s) { + final l = AppLocalizations.of(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: S.x3), + child: Row(children: [ + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: + BoxDecoration(color: p.wash(C.purple), borderRadius: R.rSm), + child: Text('$n', style: F.over.copyWith(color: p.on(C.purple))), + ), + const SizedBox(width: S.x3), + Expanded( + child: Text( + s.loadKg == null + ? (l?.activitySummaryBodyweightReps(s.reps) ?? + '${s.reps} reps · bodyweight') + : '${_kg(s.loadKg!)} × ${s.reps}', + style: F.body.copyWith(color: p.ink)), + ), + if (s.rpe != null) + Text(l?.activitySummaryRpeValue(s.rpe!) ?? 'RPE ${s.rpe}', + style: F.cap.copyWith(color: p.ink3)), + if (s.volume != null) ...[ const SizedBox(width: S.x3), - Expanded( - child: Text( - s.loadKg == null - ? '${s.reps} reps · bodyweight' - : '${_kg(s.loadKg!)} × ${s.reps}', - style: F.body.copyWith(color: p.ink)), - ), - if (s.rpe != null) - Text('RPE ${s.rpe}', style: F.cap.copyWith(color: p.ink3)), - if (s.volume != null) ...[ - const SizedBox(width: S.x3), - Text('${grouped(s.volume!)} kg', - style: F.cap - .copyWith(color: p.ink2, fontWeight: FontWeight.w600)), - ], - ]), - ); + Text('${grouped(s.volume!)} kg', + style: F.cap + .copyWith(color: p.ink2, fontWeight: FontWeight.w600)), + ], + ]), + ); + } // ─────────────────── GRAPHS ─────────────────── List _graphs(BuildContext c, P p) { + final l = AppLocalizations.of(c); final series = <(String, String, Color, List)>[ if (r.hr.length > 1) ('Heart rate', 'bpm', C.red, r.hr), if (r.elevationM.length > 1) @@ -1887,24 +2027,28 @@ class _ActivitySummaryState extends State { // from the sources list. return [ if (r.hr.any((v) => v != null)) - const StatusCard( - 'Too short to chart', - 'One minute of heart rate is a point, not a line.', + StatusCard( + l?.activitySummaryTooShortTitle ?? 'Too short to chart', + l?.activitySummaryTooShortBody ?? + 'One minute of heart rate is a point, not a line.', icon: LucideIcons.chartLine, ) // MT-08 — same guard as `_noHrCard`: a plunge with no trace has a // reason, and "check band connection" is not it. else if (thermal) StatusCard( - 'Nothing to plot for this ${a.name.toLowerCase()}', + l?.activitySummaryNothingToPlot(a.name.toLowerCase()) ?? + 'Nothing to plot for this ${a.name.toLowerCase()}', _thermalWhy!, icon: _thermalIcon, ) else StatusCard( - 'No series to plot', - 'This session recorded no per-minute streams.', - fix: 'Check band connection', + l?.activitySummaryNoSeriesTitle ?? 'No series to plot', + l?.activitySummaryNoSeriesBody ?? + 'This session recorded no per-minute streams.', + fix: l?.activitySummaryCheckBandConnection ?? + 'Check band connection', onFix: () => openProfile(c), icon: LucideIcons.chartLine, ), diff --git a/lib/ui2/activity/zones.dart b/lib/ui2/activity/zones.dart index 39771d7b..a47a805a 100644 --- a/lib/ui2/activity/zones.dart +++ b/lib/ui2/activity/zones.dart @@ -25,31 +25,17 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart' show whyFromNote; -import '../screens/home_screen.dart' show repoOf; +import '../screens/home_screen.dart' show repoOf, monthName; import '../screens/metric_detail.dart' show detailScaffold; import '../ui2.dart'; -const _months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', -]; - /// 'YYYY-MM-DD' → '3 Aug'. Returns the raw label if it does not parse — a date /// we cannot format is still better than dropping the attribution. -String _prettyDay(String iso) { +String _prettyDay(String iso, [AppLocalizations? l]) { final d = DateTime.tryParse(iso); - return d == null ? iso : '${d.day} ${_months[d.month - 1]}'; + return d == null ? iso : '${d.day} ${monthName(d.month, l)}'; } /// One zone row as the repository serves it. @@ -207,21 +193,23 @@ class _ZonesDetailState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d ?? const ZonesData(); - return detailScaffold(c, 'Heart-rate zones', [ + return detailScaffold(c, l?.activityZonesTitle ?? 'Heart-rate zones', [ if (_loading && _d == null) ...[ const SizedBox(height: S.x8), const Center(child: CircularProgressIndicator()), ] else ...[ - _ceiling(p, d), - Section('Your zones', _zones(p, d)), - ..._distribution(p, d), + _ceiling(p, l, d), + Section(l?.activityZonesYourZonesSection ?? 'Your zones', + _zones(p, l, d)), + ..._distribution(p, l, d), ], ]); } // ── the ceiling, said the only way it can honestly be said ───────────────── - Widget _ceiling(P p, ZonesData d) { + Widget _ceiling(P p, AppLocalizations? l, ZonesData d) { final bpm = d.ceilingBpm; if (bpm == null) { // THE CEILING METRIC'S OWN REASON, when it gave one. The hold sentence @@ -233,29 +221,41 @@ class _ZonesDetailState extends State { // Only when there ARE age-estimated edges below. Said // unconditionally it described a section that, on every database in // the measured run, was itself empty. - ? ' Until one is measured, the zones below come off your age.' + ? (l?.activityZonesNoCeilingTanakaTail ?? + ' Until one is measured, the zones below come off your age.') : ''; return StatusCard( - 'No measured ceiling yet', + l?.activityZonesNoCeilingTitle ?? 'No measured ceiling yet', why != null ? '$why$tail' - : 'We only count a high reading the band held for 15 seconds while ' - 'you were moving. A one-second spike is not a heart ' - 'rate.$tail', + : (l?.activityZonesNoCeilingDefaultBody ?? + 'We only count a high reading the band held for 15 seconds while ' + 'you were moving. A one-second spike is not a heart ' + 'rate.') + + tail, // The hard-session instruction belongs to the hold gate alone. - fix: why == null ? 'Wear the band for your normal hard sessions' : '', + fix: why == null + ? (l?.activityZonesWearBandFix ?? + 'Wear the band for your normal hard sessions') + : '', icon: LucideIcons.heartPulse, ); } final where = [ - if (d.ceilingDate != null) 'on ${_prettyDay(d.ceilingDate!)}', - if (d.ceilingSession != null) 'during ${d.ceilingSession!.toLowerCase()}', + if (d.ceilingDate != null) + l?.activityZonesCeilingOnDate(_prettyDay(d.ceilingDate!, l)) ?? + 'on ${_prettyDay(d.ceilingDate!, l)}', + if (d.ceilingSession != null) + l?.activityZonesCeilingDuringSession( + d.ceilingSession!.toLowerCase()) ?? + 'during ${d.ceilingSession!.toLowerCase()}', ].join(', '); return Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('HIGHEST WE HAVE SEEN', style: F.over.copyWith(color: p.ink3)), + Text(l?.activityZonesHighestSeenLabel ?? 'HIGHEST WE HAVE SEEN', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x2), Row( crossAxisAlignment: CrossAxisAlignment.baseline, @@ -263,7 +263,8 @@ class _ZonesDetailState extends State { children: [ Text('$bpm', style: F.n34.copyWith(color: p.on(C.red))), const SizedBox(width: S.x2), - Text('bpm', style: F.body.copyWith(color: p.ink3)), + Text(l?.activityZonesBpmUnit ?? 'bpm', + style: F.body.copyWith(color: p.ink3)), ], ), if (where.isNotEmpty) ...[ @@ -275,8 +276,9 @@ class _ZonesDetailState extends State { ], const SizedBox(height: S.x3), Text( - 'The highest we have measured, not a limit — it creeps up as the ' - 'band sees harder efforts. Do not go and test it.', + l?.activityZonesHighestSeenFootnote ?? + 'The highest we have measured, not a limit — it creeps up as the ' + 'band sees harder efforts. Do not go and test it.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], @@ -285,7 +287,7 @@ class _ZonesDetailState extends State { } // ── the edges, and the two numbers they were built from ──────────────────── - Widget _zones(P p, ZonesData d) { + Widget _zones(P p, AppLocalizations? l, ZonesData d) { if (d.zones.isEmpty) { // THE REPOSITORY'S REASON, or none. This card used to name one — no age // — and offer "Add your age in Profile" as the fix, on a screen whose own @@ -295,13 +297,15 @@ class _ZonesDetailState extends State { final why = whyFromNote(d.note, unit: 'days'); final noAge = (d.age ?? 0) <= 0; return StatusCard( - 'No zones yet', + l?.activityZonesNoZonesTitle ?? 'No zones yet', why ?? (noAge - ? 'Zone edges are percentages of a maximum heart rate, and ' - 'without your age there is nothing to take a percentage of.' - : 'Nothing recorded says why there are no zone edges yet.'), - fix: noAge ? 'Add your age in Profile' : '', + ? (l?.activityZonesNoAgeBody ?? + 'Zone edges are percentages of a maximum heart rate, and ' + 'without your age there is nothing to take a percentage of.') + : (l?.activityZonesNoZonesDefaultBody ?? + 'Nothing recorded says why there are no zone edges yet.')), + fix: noAge ? (l?.activityZonesAddAgeFix ?? 'Add your age in Profile') : '', icon: LucideIcons.activity, ); } @@ -337,13 +341,14 @@ class _ZonesDetailState extends State { style: F.n17.copyWith(color: p.ink2), ), const SizedBox(width: S.x2), - Text('bpm', style: F.cap.copyWith(color: p.ink3)), + Text(l?.activityZonesBpmUnit ?? 'bpm', + style: F.cap.copyWith(color: p.ink3)), ], ), ], const SizedBox(height: S.x4), Text( - _anchorCopy(d), + _anchorCopy(l, d), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], @@ -353,52 +358,60 @@ class _ZonesDetailState extends State { /// WHERE THESE EDGES CAME FROM — the whole point of the screen. Three /// different claims, three different sentences, never a shared hedge. - String _anchorCopy(ZonesData d) { + String _anchorCopy(AppLocalizations? l, ZonesData d) { final max = d.maxHr; switch (d.source) { case 'karvonen': - return 'Built from two numbers the band measured on you: your resting ' - 'rate (${d.restingHr}, the middle of your last ${d.restingDays} ' - 'nights) and the highest we have seen ($max). A low resting rate ' - 'makes zone 1 wide. These are the usual bands, not your own ' - 'measured thresholds.'; + return l?.activityZonesAnchorKarvonen( + d.restingHr ?? 0, d.restingDays, max ?? 0) ?? + 'Built from two numbers the band measured on you: your resting ' + 'rate (${d.restingHr}, the middle of your last ${d.restingDays} ' + 'nights) and the highest we have seen ($max). A low resting rate ' + 'makes zone 1 wide. These are the usual bands, not your own ' + 'measured thresholds.'; case 'observed': - return 'Built from the highest heart rate we have seen ($max). After ' - '${d.restingMinDays} nights of resting rate (you have ' - '${d.restingDays}) your resting rate joins it, which fits you ' - 'better. These are the usual bands, not your own measured ' - 'thresholds.'; + return l?.activityZonesAnchorObserved( + max ?? 0, d.restingMinDays, d.restingDays) ?? + 'Built from the highest heart rate we have seen ($max). After ' + '${d.restingMinDays} nights of resting rate (you have ' + '${d.restingDays}) your resting rate joins it, which fits you ' + 'better. These are the usual bands, not your own measured ' + 'thresholds.'; case 'tanaka': - return 'Built from $max bpm, estimated from your age rather than ' - 'measured on you — it can be 20 bpm out either way. The edges move ' - 'to a measured ceiling once the band sees a hard enough session.'; + return l?.activityZonesAnchorTanaka(max ?? 0) ?? + 'Built from $max bpm, estimated from your age rather than ' + 'measured on you — it can be 20 bpm out either way. The edges move ' + 'to a measured ceiling once the band sees a hard enough session.'; default: - return 'Zone edges are percentages of a maximum heart rate.'; + return l?.activityZonesAnchorDefault ?? + 'Zone edges are percentages of a maximum heart rate.'; } } // ── TS-05 — drawn only when both anchors were measured ───────────────────── - List _distribution(P p, ZonesData d) { + List _distribution(P p, AppLocalizations? l, ZonesData d) { final mins = d.distMinutes; if (mins == null) { // NOT a chart with a caveat. The absence IS the honest state, so it says // what would have to be true for the chart to mean anything. return [ Section( - 'Where your intensity went', + l?.activityZonesIntensitySection ?? 'Where your intensity went', StatusCard( - 'Not shown yet', + l?.activityZonesNotShownTitle ?? 'Not shown yet', // THE REPOSITORY'S REASON. This has three distinct causes — no // edges at all, edges off the age estimate, or a reserve anchor // still short — and the screen was choosing between two of them // off `source` alone. whyFromNote(d.distNote, unit: 'days') ?? (d.measured - ? 'Needs about a month of recorded sessions, each with a ' - 'minute-by-minute heart rate.' - : 'The bars would be a picture of the age estimate, not of ' - 'your training. They appear once the zone edges above ' - 'are measured.'), + ? (l?.activityZonesNeedsMonthBody ?? + 'Needs about a month of recorded sessions, each with a ' + 'minute-by-minute heart rate.') + : (l?.activityZonesAgeEstimateBody ?? + 'The bars would be a picture of the age estimate, not of ' + 'your training. They appear once the zone edges above ' + 'are measured.')), icon: LucideIcons.chartColumn, ), ), @@ -408,17 +421,18 @@ class _ZonesDetailState extends State { if (total <= 0) return const []; return [ Section( - 'Where your intensity went', + l?.activityZonesIntensitySection ?? 'Where your intensity went', Surface( child: ChartFrame( - title: 'SESSION MINUTES, LAST 28 DAYS', + title: l?.activityZonesSessionMinutesChartTitle ?? + 'SESSION MINUTES, LAST 28 DAYS', unit: 'minutes', height: 10, legend: [ for (var i = 0; i < 5; i++) ('Z${i + 1} · ${mins[i]}m', ZoneBar.cols(p)[i]), ], - footnote: _shapeCopy(d), + footnote: _shapeCopy(l, d), child: CustomPaint( size: Size.infinite, painter: ZoneBar([for (final v in mins) v / total], p), @@ -435,21 +449,24 @@ class _ZonesDetailState extends State { /// against lab-defined thresholds, and these are %HRR bands off a ceiling a /// wrist sensor happened to catch. Naming the shape is a mirror; calling a /// share of it correct would be a prescription we cannot support. - String _shapeCopy(ZonesData d) { + String _shapeCopy(AppLocalizations? l, ZonesData d) { final shape = switch (d.distShape) { - 'pyramidal' => - 'Most of your minutes are easy, fewer in the middle, ' - 'fewest hard — a pyramid.', - 'polarised' => - 'Most of your minutes are easy and the rest are hard, ' - 'with little in between.', - 'middle-heavy' => - 'Most of your minutes sit in the middle rather than ' - 'easy or hard.', + 'pyramidal' => l?.activityZonesShapePyramidal ?? + 'Most of your minutes are easy, fewer in the middle, ' + 'fewest hard — a pyramid.', + 'polarised' => l?.activityZonesShapePolarised ?? + 'Most of your minutes are easy and the rest are hard, ' + 'with little in between.', + 'middle-heavy' => l?.activityZonesShapeMiddleHeavy ?? + 'Most of your minutes sit in the middle rather than ' + 'easy or hard.', _ => '', }; - return '$shape ${d.distEasy} min easy, ${d.distModerate} moderate, ' - '${d.distHard} hard, over ${d.distSessions} recorded sessions. A ' - 'description, not a target.'; + final summary = l?.activityZonesShapeSummary( + d.distEasy, d.distModerate, d.distHard, d.distSessions) ?? + '${d.distEasy} min easy, ${d.distModerate} moderate, ' + '${d.distHard} hard, over ${d.distSessions} recorded sessions. A ' + 'description, not a target.'; + return '$shape $summary'; } } diff --git a/lib/ui2/onboarding/pairing.dart b/lib/ui2/onboarding/pairing.dart index 83d8a988..57c1420b 100644 --- a/lib/ui2/onboarding/pairing.dart +++ b/lib/ui2/onboarding/pairing.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; +import '../../ble/band_status_l10n.dart' show localizedBandStatus; import '../../ble/ble_state.dart'; import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; @@ -184,7 +185,8 @@ class PairingView extends StatelessWidget { // The copy for this one lives in the BLE layer, so this screen and the // Devices screen cannot drift into two different accounts of one state. final blocked = phase == PairPhase.bluetoothBlocked - ? bandStatusFor(connection: 'disconnected', blocker: blocker) + ? localizedBandStatus( + c, bandStatusFor(connection: 'disconnected', blocker: blocker)) : null; return Scaffold( backgroundColor: p.bg, @@ -241,8 +243,9 @@ class PairingView extends StatelessWidget { static String _title(BuildContext c, PairPhase phase, [BleBlocker? blocker]) { final l = AppLocalizations.of(c); return switch (phase) { - PairPhase.bluetoothBlocked => - bandStatusFor(connection: 'disconnected', blocker: blocker).title, + PairPhase.bluetoothBlocked => localizedBandStatus( + c, bandStatusFor(connection: 'disconnected', blocker: blocker)) + .title, PairPhase.idle => l?.pairingIdleTitle ?? 'Wake the band and hold it close', PairPhase.scanning => l?.pairingScanningTitle ?? 'Looking for your band', PairPhase.notFound => l?.pairingNotFoundTitle ?? 'No band in range', @@ -257,8 +260,9 @@ class PairingView extends StatelessWidget { static String _body(BuildContext c, PairPhase phase, [BleBlocker? blocker]) { final l = AppLocalizations.of(c); return switch (phase) { - PairPhase.bluetoothBlocked => - bandStatusFor(connection: 'disconnected', blocker: blocker).reason, + PairPhase.bluetoothBlocked => localizedBandStatus( + c, bandStatusFor(connection: 'disconnected', blocker: blocker)) + .reason, PairPhase.idle => l?.pairingIdleBody ?? 'Take the band off the charger, put it on your wrist and keep the ' 'phone within arm’s reach.', diff --git a/lib/ui2/onboarding/profile_setup.dart b/lib/ui2/onboarding/profile_setup.dart index 0806ba59..f2e095ca 100644 --- a/lib/ui2/onboarding/profile_setup.dart +++ b/lib/ui2/onboarding/profile_setup.dart @@ -17,6 +17,7 @@ import 'package:provider/provider.dart'; import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; import '../../state/units_controller.dart'; +import '../screens/home_screen.dart' show monthShortName, weekdayShortName; import '../ui2.dart'; class ProfileSetupScreen extends StatefulWidget { @@ -276,13 +277,7 @@ class _Field extends StatelessWidget { // is carried by `StatusCard.forMetric`, which already says how many more // nights the metric needs. -const _days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; -const _months = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', -]; - /// "Thu 4 Sep" — a date a person can hold, not an ISO string. Local by /// construction; the app's day labels are local everywhere. -String formatDay(DateTime d) => - '${_days[d.weekday - 1]} ${d.day} ${_months[d.month - 1]}'; +String formatDay(DateTime d, [AppLocalizations? l]) => + '${weekdayShortName(d.weekday, l)} ${d.day} ${monthShortName(d.month, l)}'; diff --git a/lib/ui2/profile/devices.dart b/lib/ui2/profile/devices.dart index de20f353..66d9b2a2 100644 --- a/lib/ui2/profile/devices.dart +++ b/lib/ui2/profile/devices.dart @@ -34,6 +34,7 @@ import '../../ble/adapters/_registry.dart' show BandEntry, kBandRegistry, kBleHrs, kOura; import '../../ble/hrs_link.dart' show HrsLink, HrsReading; import '../../ble/oura_link.dart' show OuraLink, pairOuraRing; +import '../../ble/band_status_l10n.dart' show localizedBandStatus; import '../../ble/ble_state.dart' show BandStatus; import '../../data/db.dart' show LocalDb; import '../../l10n/app_localizations.dart'; @@ -82,6 +83,34 @@ enum SourceTier { final Color accent; } +/// Localized label/detail for a [SourceTier]. The enum's own `.label`/ +/// `.detail` stay English-only (a const enum constructor can't take a +/// BuildContext) — this is the wrapper every render call site should use +/// instead, same split as `sourceState`/`_localizedSourceState` below. +String sourceTierLabel(BuildContext c, SourceTier t) { + final l = AppLocalizations.of(c); + switch (t) { + case SourceTier.beatToBeat: + return l?.devicesTierBeatToBeatLabel ?? t.label; + case SourceTier.wristOptical: + return l?.devicesTierWristOpticalLabel ?? t.label; + case SourceTier.phone: + return l?.devicesTierPhoneLabel ?? t.label; + } +} + +String sourceTierDetail(BuildContext c, SourceTier t) { + final l = AppLocalizations.of(c); + switch (t) { + case SourceTier.beatToBeat: + return l?.devicesTierBeatToBeatDetail ?? t.detail; + case SourceTier.wristOptical: + return l?.devicesTierWristOpticalDetail ?? t.detail; + case SourceTier.phone: + return l?.devicesTierPhoneDetail ?? t.detail; + } +} + /// This band's name, from the registry entry that decodes it — never asserted. /// /// The two call sites here and in `day_steps.dart` both used to be @@ -510,7 +539,9 @@ class MyDevicesView extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); - final fault = status?.isFault == true ? status : null; + final localizedStatus = + status == null ? null : localizedBandStatus(c, status!); + final fault = localizedStatus?.isFault == true ? localizedStatus : null; // A BAND, not "a source". The phone is a source and it is not a substitute // for one: gating this on `sources.isEmpty` meant a phone counting steps // hid the only route back to pairing, and forgetting a band left the user @@ -766,13 +797,15 @@ class TierRow extends StatelessWidget { child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(c)?.devicesTierRankLabel(tier.rank, tier.label) ?? + AppLocalizations.of(c)?.devicesTierRankLabel( + tier.rank, sourceTierLabel(c, tier)) ?? 'Tier ${tier.rank} · ${tier.label}', style: F.body.copyWith( color: filled ? p.ink : p.ink2, fontWeight: FontWeight.w600)), const SizedBox(height: S.x1), - Text(tier.detail, style: F.cap.copyWith(color: p.ink3, height: 1.5)), + Text(sourceTierDetail(c, tier), + style: F.cap.copyWith(color: p.ink3, height: 1.5)), if (!filled) ...[ const SizedBox(height: S.x1), // A dashed circle and a paler wash are not a statement. Without @@ -1076,9 +1109,12 @@ class DeviceDetailView extends StatelessWidget { Widget build(BuildContext c) { final p = P.of(c); final l = AppLocalizations.of(c); + final localizedStatus = + status == null ? null : localizedBandStatus(c, status!); final battery = s.batteryPct; final last = s.lastData; - final fault = status?.isFault == true ? status : null; + final fault = + localizedStatus?.isFault == true ? localizedStatus : null; final calibration = calibrationDisclosure(s); return Scaffold( backgroundColor: p.bg, @@ -1109,7 +1145,8 @@ class DeviceDetailView extends StatelessWidget { // here would say the same thing twice in two type sizes. if (fault == null) Center( - child: Text(status?.title ?? _localizedSourceState(c, s), + child: Text( + localizedStatus?.title ?? _localizedSourceState(c, s), style: F.cap.copyWith( color: s.connected ? p.on(C.green) : p.ink3))), const SizedBox(height: S.x6), @@ -1156,7 +1193,7 @@ class DeviceDetailView extends StatelessWidget { Divider(color: p.line, height: 1), SetRow(LucideIcons.refreshCw, C.purple, l?.devicesLastData ?? 'Last data', - value: last == null ? '' : formatDayTime(last), + value: last == null ? '' : formatDayTime(last, l), sub: last == null ? (l?.devicesNothingBankedYet ?? 'Nothing banked yet') : '', @@ -1238,7 +1275,7 @@ class DeviceDetailView extends StatelessWidget { ], SetRow(LucideIcons.refreshCw, C.purple, l?.devicesLastData ?? 'Last data', - value: last == null ? '' : formatDayTime(last), + value: last == null ? '' : formatDayTime(last, l), sub: last == null ? (l?.devicesNothingBankedYet ?? 'Nothing banked yet') : '', @@ -1328,8 +1365,8 @@ String? _chargeHistory(Map? h) { /// /// It used to render `4/9, 07:12`, which a US reader reads as 9 April. The /// month name is the whole point; `formatDay` already writes one. -String formatDayTime(DateTime d) { +String formatDayTime(DateTime d, [AppLocalizations? l]) { final t = '${d.hour.toString().padLeft(2, '0')}:' '${d.minute.toString().padLeft(2, '0')}'; - return '${formatDay(d)}, $t'; + return '${formatDay(d, l)}, $t'; } diff --git a/lib/ui2/profile/gallery.dart b/lib/ui2/profile/gallery.dart index 0ae5cae3..a87d982d 100644 --- a/lib/ui2/profile/gallery.dart +++ b/lib/ui2/profile/gallery.dart @@ -1104,6 +1104,7 @@ Map extraCases() => { const _roughFull = RoughNight( day: '2026-08-15', signs: 4, + illnessFlagged: true, descriptor: 'a rougher night than usual for you — your body worked harder ' 'overnight', moved: [ diff --git a/lib/ui2/profile/gestures.dart b/lib/ui2/profile/gestures.dart index 690dc41d..4aa4f8b8 100644 --- a/lib/ui2/profile/gestures.dart +++ b/lib/ui2/profile/gestures.dart @@ -19,6 +19,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; import '../../gestures/device_action.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; import '../ui2.dart'; import 'profile.dart'; @@ -61,6 +62,7 @@ class BandGesturesView extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); // Enum order, filtered to this phone: nothing first (it is the default and // the way back out), then the in-app actions, then whatever the OS offered. final offered = [ @@ -74,27 +76,28 @@ class BandGesturesView extends StatelessWidget { backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Double-tap'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.gesturesNavTitle ?? 'Double-tap'), ), Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(S.x4, 0, S.x4, S.x10), children: [ Section( - 'Tap the band twice', + l?.gesturesSectionTitle ?? 'Tap the band twice', Surface( child: Text( - 'Only while the app is connected and awake. A tap the ' - 'band stored while your phone was away arrives later with ' - 'an old timestamp, and is ignored rather than fired hours ' - 'after you meant it.', + l?.gesturesSectionBody ?? + 'Only while the app is connected and awake. A tap the ' + 'band stored while your phone was away arrives later with ' + 'an old timestamp, and is ignored rather than fired hours ' + 'after you meant it.', style: F.body.copyWith(color: p.ink2, height: 1.4), ), ), ), - settingsGroup(c, 'It does', [ + settingsGroup(c, l?.gesturesItDoesTitle ?? 'It does', [ for (final a in offered) _ActionRow( action: a, @@ -105,13 +108,14 @@ class BandGesturesView extends StatelessWidget { if (noPhoneActions) ...[ const SizedBox(height: S.x5), Section( - 'Nothing on the phone?', + l?.gesturesNoPhoneActionsTitle ?? 'Nothing on the phone?', Surface( child: Text( - 'Ringing your phone and the flashlight are missing ' - 'because the app could not reach the system to ask what ' - 'this device allows. Reopen the app and come back; the ' - 'in-app actions above work either way.', + l?.gesturesNoPhoneActionsBody ?? + 'Ringing your phone and the flashlight are missing ' + 'because the app could not reach the system to ask what ' + 'this device allows. Reopen the app and come back; the ' + 'in-app actions above work either way.', style: F.body.copyWith(color: p.ink2, height: 1.4), ), ), @@ -137,10 +141,11 @@ class _ActionRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Pressable( onTap: onTap, - semanticLabel: - '${action.label}. ${action.blurb}${selected ? ' Selected.' : ''}', + semanticLabel: '${action.localizedLabel(c)}. ${action.localizedBlurb(c)}' + '${selected ? (l?.settingsSelectedSuffix ?? ' Selected.') : ''}', child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row(children: [ @@ -150,11 +155,12 @@ class _ActionRow extends StatelessWidget { Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(action.label, + Text(action.localizedLabel(c), style: F.body.copyWith( color: selected ? p.on(C.indigo) : p.ink, fontWeight: selected ? FontWeight.w600 : null)), - Text(action.blurb, style: F.over.copyWith(color: p.ink3)), + Text(action.localizedBlurb(c), + style: F.over.copyWith(color: p.ink3)), ]), ), const SizedBox(width: S.x2), diff --git a/lib/ui2/profile/pair_sensor.dart b/lib/ui2/profile/pair_sensor.dart index 70bd04cf..ab866337 100644 --- a/lib/ui2/profile/pair_sensor.dart +++ b/lib/ui2/profile/pair_sensor.dart @@ -40,6 +40,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; import '../../ble/adapters/_registry.dart'; +import '../../ble/band_status_l10n.dart' show localizedBandStatus; import '../../ble/ble_state.dart' show BleUnavailableException, bandStatusFor, classifyBleBlocker; import '../../ble/hrs_link.dart'; @@ -144,7 +145,9 @@ class _PairSensorScreenState extends State { e is BleUnavailableException ? e.blocker : classifyBleBlocker(error: e); if (!mounted) return; setState(() => _problem = blocker != null - ? bandStatusFor(connection: 'disconnected', blocker: blocker).reason + ? localizedBandStatus(context, + bandStatusFor(connection: 'disconnected', blocker: blocker)) + .reason : (AppLocalizations.of(context)?.pairSensorScanDidNotRun(e.toString()) ?? 'The scan did not run: $e')); } finally { diff --git a/lib/ui2/profile/settings.dart b/lib/ui2/profile/settings.dart index a330084b..2a716213 100644 --- a/lib/ui2/profile/settings.dart +++ b/lib/ui2/profile/settings.dart @@ -23,6 +23,7 @@ import '../../data/off_lookup.dart'; import '../../health/health_export.dart' show HealthLinkState; import '../../health/health_import_state.dart'; import '../../health/health_profile_import.dart'; +import '../../l10n/app_localizations.dart'; import '../../platform/tasker_bridge.dart'; import '../../notify/notification_prefs.dart'; import '../../notify/notification_service.dart'; @@ -125,9 +126,11 @@ class _MoreSettingsState extends State { if (!mounted) return; setState(() => _barcode = want); if (!saved) { - messenger.showSnackBar(const SnackBar( - content: Text('That could not be saved — it may be back next time you ' - 'open the app.'), + final l = AppLocalizations.of(context); + messenger.showSnackBar(SnackBar( + content: Text(l?.settingsBarcodeSaveFailed ?? + 'That could not be saved — it may be back next time you ' + 'open the app.'), )); } } @@ -213,6 +216,7 @@ class _IconRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row(children: [ @@ -227,10 +231,11 @@ class _IconRow extends StatelessWidget { const SizedBox(width: S.x3), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Icon', style: F.body.copyWith(color: p.ink)), + Text(l?.settingsIconRowTitle ?? 'Icon', + style: F.body.copyWith(color: p.ink)), // The cost, stated where the choice is made. iOS shows its own // alert on every change and there is no way to turn that off. - Text('iPhone will ask you to confirm', + Text(l?.settingsIconRowConfirmHint ?? 'iPhone will ask you to confirm', style: F.over.copyWith(color: p.ink3)), ]), ), @@ -259,6 +264,7 @@ class _IconChoice extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); // Decoded at the size it is drawn at: the source is the 1024 px launcher // master, and decoding that in full to paint a 36 pt thumbnail is 4 MB of // bitmap per option. @@ -266,7 +272,8 @@ class _IconChoice extends StatelessWidget { return Pressable( onTap: onTap, semanticLabel: - '${choice.label} icon.${selected ? ' Selected.' : ''}', + '${l?.settingsIconChoiceLabel(choice.label) ?? '${choice.label} icon.'}' + '${selected ? (l?.settingsSelectedSuffix ?? ' Selected.') : ''}', child: Container( padding: const EdgeInsets.all(S.x1 / 2), decoration: BoxDecoration( @@ -317,19 +324,28 @@ Future _toggleHealthSync(AppState app) async { } /// One line saying what the export is actually doing right now. -String healthSyncSub(bool on, HealthLinkState state, String store) { - if (!on) return 'Off. Nothing is written to $store'; +String healthSyncSub( + BuildContext c, bool on, HealthLinkState state, String store) { + final l = AppLocalizations.of(c); + if (!on) { + return l?.settingsHealthSyncOff(store) ?? 'Off. Nothing is written to $store'; + } return switch (state) { - HealthLinkState.ready => 'Writes each day’s sleep, resting heart rate, ' - 'HRV, respiratory rate, energy and workouts to $store once it is final', + HealthLinkState.ready => l?.settingsHealthSyncReady(store) ?? + 'Writes each day’s sleep, resting heart rate, ' + 'HRV, respiratory rate, energy and workouts to $store once it is final', HealthLinkState.needsPermission => - '$store has not granted write access. Tap to open it', - HealthLinkState.notInstalled => 'Health Connect is not installed. Tap to ' - 'get it', - HealthLinkState.needsUpdate => - 'Health Connect is too old to write to. Tap to update it', - HealthLinkState.unsupported => 'This device has no health store to write to', - HealthLinkState.unknown => 'Checking $store…', + l?.settingsHealthSyncNeedsPermission(store) ?? + '$store has not granted write access. Tap to open it', + HealthLinkState.notInstalled => l?.settingsHealthSyncNotInstalled ?? + 'Health Connect is not installed. Tap to ' + 'get it', + HealthLinkState.needsUpdate => l?.settingsHealthSyncNeedsUpdate ?? + 'Health Connect is too old to write to. Tap to update it', + HealthLinkState.unsupported => l?.settingsHealthSyncUnsupported ?? + 'This device has no health store to write to', + HealthLinkState.unknown => + l?.settingsHealthSyncChecking(store) ?? 'Checking $store…', }; } @@ -346,52 +362,60 @@ Future _toggleHealthShare(BuildContext c, AppState app) async { await app.setHealthShareConsent(false); final last = await HealthUploader.instance.lastUploadAt(); if (!c.mounted) return; + final l = AppLocalizations.of(c); await showDialog( context: c, builder: (d) => AlertDialog( - title: const Text('Contribution off'), + title: Text(l?.settingsHealthShareOffTitle ?? 'Contribution off'), content: Text( last == null - ? 'Nothing was ever uploaded. Nothing will be.' + ? (l?.settingsHealthShareOffNeverUploaded ?? + 'Nothing was ever uploaded. Nothing will be.') // What we KNOW, not what we hope: the revocation is posted // once, unawaited, with no retry queue, so offline it never // arrives and nothing here can tell. - : 'Nothing further will be uploaded.\n\n' - 'One copy of your database was uploaded on ' - '${last.toLocal().toString().split('.').first}. The server ' - 'keeps only the most recent copy per device. We tried to ' - 'tell it your consent is withdrawn — that message is sent ' - 'once and is not retried, so if this phone is offline it ' - 'will not have arrived, and we cannot show you that the copy ' - 'is gone either.', + : (l?.settingsHealthShareOffDetail( + last.toLocal().toString().split('.').first) ?? + 'Nothing further will be uploaded.\n\n' + 'One copy of your database was uploaded on ' + '${last.toLocal().toString().split('.').first}. The server ' + 'keeps only the most recent copy per device. We tried to ' + 'tell it your consent is withdrawn — that message is sent ' + 'once and is not retried, so if this phone is offline it ' + 'will not have arrived, and we cannot show you that the copy ' + 'is gone either.'), ), actions: [ TextButton( - onPressed: () => Navigator.of(d).pop(), child: const Text('OK')), + onPressed: () => Navigator.of(d).pop(), + child: Text(l?.settingsOk ?? 'OK')), ], ), ); return; } + final l = AppLocalizations.of(c); final ok = await showDialog( context: c, builder: (d) => AlertDialog( - title: const Text('Contribute your health data?'), - content: const Text( - 'Once a day, on Wi-Fi and while charging, a compressed copy of your ' - 'ENTIRE database is uploaded — every derived day and every raw sensor ' - 'row the band has sent. It is used to improve the algorithms.\n\n' - 'It is not anonymous in any meaningful sense: it is your whole health ' - 'history. You can switch this off at any time, and nothing further ' - 'is sent from that moment.', + title: Text( + l?.settingsHealthShareOnTitle ?? 'Contribute your health data?'), + content: Text( + l?.settingsHealthShareOnBody ?? + 'Once a day, on Wi-Fi and while charging, a compressed copy of your ' + 'ENTIRE database is uploaded — every derived day and every raw sensor ' + 'row the band has sent. It is used to improve the algorithms.\n\n' + 'It is not anonymous in any meaningful sense: it is your whole health ' + 'history. You can switch this off at any time, and nothing further ' + 'is sent from that moment.', ), actions: [ TextButton( onPressed: () => Navigator.of(d).pop(false), - child: const Text('No')), + child: Text(l?.settingsNo ?? 'No')), TextButton( onPressed: () => Navigator.of(d).pop(true), - child: const Text('Contribute')), + child: Text(l?.settingsContribute ?? 'Contribute')), ], ), ); @@ -399,34 +423,36 @@ Future _toggleHealthShare(BuildContext c, AppState app) async { } Future _confirmReset(BuildContext c, AppState app) async { + final l = AppLocalizations.of(c); final ok = await showDialog( context: c, builder: (d) => AlertDialog( - title: const Text('Delete everything?'), + title: Text(l?.settingsResetTitle ?? 'Delete everything?'), // Enumerated, because the previous wording ("every measured day, session // and profile field") was false in about twenty places: it deleted the // derived days and left the labs, the meals, the doses, the breathing // sessions, the logged sets, the baselines, the consent flags, the // install id, the stored API key and the home-screen widget standing. // It now removes all of that, so it can say so. - content: const Text( - 'This deletes, permanently and with no copy anywhere else:\n\n' - '· every measured day, sleep, workout and route\n' - '· every lab result, meal, medication dose, habit, breathing session ' - 'and logged set\n' - '· your journal, cycle log and rolling baselines\n' - '· your profile, every preference and any stored AI key\n' - '· the home-screen widget and every scheduled reminder\n\n' - 'The band is unpaired, and it cannot re-send history it has already ' - 'handed over. Export from Your data first if you want a copy.', + content: Text( + l?.settingsResetBody ?? + 'This deletes, permanently and with no copy anywhere else:\n\n' + '· every measured day, sleep, workout and route\n' + '· every lab result, meal, medication dose, habit, breathing session ' + 'and logged set\n' + '· your journal, cycle log and rolling baselines\n' + '· your profile, every preference and any stored AI key\n' + '· the home-screen widget and every scheduled reminder\n\n' + 'The band is unpaired, and it cannot re-send history it has already ' + 'handed over. Export from Your data first if you want a copy.', ), actions: [ TextButton( onPressed: () => Navigator.of(d).pop(false), - child: const Text('Keep my data')), + child: Text(l?.settingsResetKeepData ?? 'Keep my data')), TextButton( onPressed: () => Navigator.of(d).pop(true), - child: const Text('Delete everything')), + child: Text(l?.settingsResetDeleteEverything ?? 'Delete everything')), ], ), ); @@ -546,13 +572,16 @@ class MoreSettingsView extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); + final on = l?.stateOn ?? 'On'; + final off = l?.stateOff ?? 'Off'; return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Settings'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.settingsNavTitle ?? 'Settings'), ), Expanded( child: ListView( @@ -561,9 +590,11 @@ class MoreSettingsView extends StatelessWidget { // No "Edit profile" here. It lives in one place — Quick access // on the Profile screen — because two doors to one form is how // a user ends up unsure which one is the real setting. - settingsGroup(c, 'The band', [ - SetRow(LucideIcons.alarmClock, C.orange, 'Alarm', - sub: 'Buzzes on your wrist, on the band’s own clock', + settingsGroup(c, l?.settingsGroupTheBand ?? 'The band', [ + SetRow(LucideIcons.alarmClock, C.orange, + l?.settingsAlarmRowTitle ?? 'Alarm', + sub: l?.settingsAlarmRowSub ?? + 'Buzzes on your wrist, on the band’s own clock', onTap: onAlarm), ]), // NOT in Preferences. Units and Appearance change how numbers @@ -572,127 +603,168 @@ class MoreSettingsView extends StatelessWidget { // band, because the two together are the step ladder — the // band covers the workout, the phone covers the rest — and // "This phone" is what the sources screen already calls it. - settingsGroup(c, 'This phone', [ - SetRow(LucideIcons.footprints, C.teal, 'Steps', - sub: 'This phone’s own step counter, for the hours the ' - 'band doesn’t cover. Nothing leaves the device', - value: phoneSteps ? 'On' : 'Off', + settingsGroup(c, l?.settingsGroupThisPhone ?? 'This phone', [ + SetRow(LucideIcons.footprints, C.teal, + l?.settingsStepsRowTitle ?? 'Steps', + sub: l?.settingsStepsRowSub ?? + 'This phone’s own step counter, for the hours the ' + 'band doesn’t cover. Nothing leaves the device', + value: phoneSteps ? on : off, onTap: onTogglePhoneSteps), ]), - settingsGroup(c, 'Notifications', [ - SetRow(LucideIcons.bell, C.blue, 'Manage notifications', - sub: 'What may interrupt you, quiet hours, and off ' - 'switches for all of them', + settingsGroup( + c, l?.settingsGroupNotifications ?? 'Notifications', [ + SetRow(LucideIcons.bell, C.blue, + l?.settingsManageNotificationsRowTitle ?? + 'Manage notifications', + sub: l?.settingsManageNotificationsRowSub ?? + 'What may interrupt you, quiet hours, and off ' + 'switches for all of them', onTap: onNotifications), ]), - settingsGroup(c, 'Preferences', [ - SetRow(LucideIcons.ruler, C.blue, 'Units', + settingsGroup(c, l?.settingsGroupPreferences ?? 'Preferences', [ + SetRow(LucideIcons.ruler, C.blue, + l?.settingsUnitsRowTitle ?? 'Units', value: units, onTap: onCycleUnits), - SetRow(LucideIcons.sun, C.yellow, 'Appearance', + SetRow(LucideIcons.sun, C.yellow, + l?.settingsAppearanceRowTitle ?? 'Appearance', value: appearance, onTap: onCycleAppearance), if (appIcon != null) _IconRow(chosen: appIcon!, onPick: onPickIcon), // Opt-in, and it says what it does rather than what it is // about — "Cycle tracking" alone leaves you guessing whether // switching it off throws the entries away. - SetRow(LucideIcons.droplet, C.pink, 'Cycle tracking', - sub: 'Adds the Cycle tab to Wellness. Off hides it and ' - 'keeps everything already logged', - value: cycleTracking ? 'On' : 'Off', + SetRow(LucideIcons.droplet, C.pink, + l?.settingsCycleTrackingRowTitle ?? 'Cycle tracking', + sub: l?.settingsCycleTrackingRowSub ?? + 'Adds the Cycle tab to Wellness. Off hides it and ' + 'keeps everything already logged', + value: cycleTracking ? on : off, onTap: onToggleCycleTracking), ]), - settingsGroup(c, 'Your data', [ - SetRow(LucideIcons.download, C.green, 'Export, backup, import', - sub: 'Spreadsheets, a full copy, and bringing history in', + settingsGroup(c, l?.settingsGroupYourData ?? 'Your data', [ + SetRow(LucideIcons.download, C.green, + l?.settingsExportBackupImportRowTitle ?? + 'Export, backup, import', + sub: l?.settingsExportBackupImportRowSub ?? + 'Spreadsheets, a full copy, and bringing history in', onTap: onData), // The row P1 was missing. Everything behind it — the // permission request, the retry/backoff, the four gates — // was already written and simply had no way to be switched // on, so the write entitlement and usage strings described a // path that could not run. - SetRow(LucideIcons.heartPulse, C.red, 'Write to $healthStore', - sub: healthSyncSub(healthSync, healthState, healthStore), - value: healthSync ? 'On' : 'Off', + SetRow(LucideIcons.heartPulse, C.red, + l?.settingsWriteToHealthStoreRowTitle(healthStore) ?? + 'Write to $healthStore', + sub: healthSyncSub(c, healthSync, healthState, healthStore), + value: healthSync ? on : off, onTap: onToggleHealthSync), ]), - settingsGroup(c, 'Automation', [ + settingsGroup(c, l?.settingsGroupAutomation ?? 'Automation', [ // The picker died with the old ui tree and the engine kept // running against a mapping nothing could set — the whole // feature was live code pinned at "do nothing". Builder( builder: (c) => SetRow( - LucideIcons.hand, C.orange, 'Double-tap', - sub: 'What a double-tap on the band does', + LucideIcons.hand, C.orange, + AppLocalizations.of(c)?.settingsDoubleTapRowTitle ?? + 'Double-tap', + sub: AppLocalizations.of(c) + ?.settingsDoubleTapRowSub ?? + 'What a double-tap on the band does', onTap: () => goto(c, const BandGestures()))), - SetRow(LucideIcons.workflow, C.indigo, 'Tasker and Shortcuts', + SetRow(LucideIcons.workflow, C.indigo, + l?.settingsTaskerShortcutsRowTitle ?? + 'Tasker and Shortcuts', // The row states the asymmetry rather than leaving it to // the screen: someone on an iPhone should learn what they // are not getting before they tap into it. - sub: 'Android only for events out. iOS can buzz the band ' - 'but cannot be triggered by it', + sub: l?.settingsTaskerShortcutsRowSub ?? + 'Android only for events out. iOS can buzz the band ' + 'but cannot be triggered by it', onTap: onAutomation), ]), - settingsGroup(c, 'Privacy', [ - SetRow(LucideIcons.bug, C.orange, 'Crash reports', - sub: 'Nothing is sent until you say so', - value: telemetry ? 'On' : 'Off', + settingsGroup(c, l?.settingsGroupPrivacy ?? 'Privacy', [ + SetRow(LucideIcons.bug, C.orange, + l?.settingsCrashReportsRowTitle ?? 'Crash reports', + sub: l?.settingsCrashReportsRowSub ?? + 'Nothing is sent until you say so', + value: telemetry ? on : off, onTap: onToggleTelemetry), // The food log's one outbound call. Named by what it sends, // not by the feature it powers — a scan is the only thing // that triggers it and the barcode is the whole payload. SetRow(LucideIcons.scanBarcode, C.domFood, - 'Look barcodes up online', - sub: 'Sends a scanned barcode to openfoodfacts.org. ' - 'Nothing about you goes with it', - value: barcodeLookup ? 'On' : 'Off', + l?.settingsBarcodeLookupRowTitle ?? + 'Look barcodes up online', + sub: l?.settingsBarcodeLookupRowSub ?? + 'Sends a scanned barcode to openfoodfacts.org. ' + 'Nothing about you goes with it', + value: barcodeLookup ? on : off, onTap: onToggleBarcodeLookup), if (showHealthShare) SetRow(LucideIcons.cloudUpload, C.red, - 'Contribute my health data', - sub: 'Uploads your whole database once a day, on ' - 'Wi-Fi and charging, to improve the algorithms', - value: healthShare ? 'On' : 'Off', + l?.settingsContributeHealthDataRowTitle ?? + 'Contribute my health data', + sub: l?.settingsContributeHealthDataRowSub ?? + 'Uploads your whole database once a day, on ' + 'Wi-Fi and charging, to improve the algorithms', + value: healthShare ? on : off, onTap: onToggleHealthShare), if (showUpdateChecks) - SetRow(LucideIcons.refreshCw, C.blue, 'Check for updates', + SetRow(LucideIcons.refreshCw, C.blue, + l?.settingsCheckForUpdatesRowTitle ?? + 'Check for updates', sub: updateMandatory - ? 'This build is below the minimum supported ' - 'build. Install the newer release from GitHub' + ? (l?.settingsUpdateBelowMinimum ?? + 'This build is below the minimum supported ' + 'build. Install the newer release from GitHub') : updateAvailable - ? 'A newer build is published on GitHub' - : 'Asks the release server on launch. It sees ' - 'your IP address and when you open the app', - value: updateChecks ? 'On' : 'Off', + ? (l?.settingsUpdateAvailable ?? + 'A newer build is published on GitHub') + : (l?.settingsUpdateCheckSub ?? + 'Asks the release server on launch. It sees ' + 'your IP address and when you open the app'), + value: updateChecks ? on : off, onTap: onToggleUpdateChecks), ]), - settingsGroup(c, 'About', [ + settingsGroup(c, l?.settingsGroupAbout ?? 'About', [ if (version.isNotEmpty) - SetRow(LucideIcons.info, C.n500, 'Version', + SetRow(LucideIcons.info, C.n500, + l?.settingsVersionRowTitle ?? 'Version', value: version, chevron: false, onTap: onVersionTap), // Where the licences of what this app uses are written out // in full. Open Food Facts' ODbL asks for the notice to be // reachable, not only for the credit beside the numbers. - SetRow(LucideIcons.scale, C.n500, 'Notices and licences', - sub: 'Who this app is not, and whose data it uses', + SetRow(LucideIcons.scale, C.n500, + l?.settingsNoticesLicencesRowTitle ?? + 'Notices and licences', + sub: l?.settingsNoticesLicencesRowSub ?? + 'Who this app is not, and whose data it uses', onTap: () => launchUrl( Uri.parse( 'https://openstrap.github.io/edge/notice.html'), mode: LaunchMode.externalApplication)), ]), if (devMode) - settingsGroup(c, 'Developer', [ + settingsGroup(c, l?.settingsGroupDeveloper ?? 'Developer', [ SetRow(LucideIcons.layoutGrid, C.purple, - 'Component gallery', - sub: 'Every component, at any text scale, in either ' - 'theme', + l?.settingsComponentGalleryRowTitle ?? + 'Component gallery', + sub: l?.settingsComponentGalleryRowSub ?? + 'Every component, at any text scale, in either ' + 'theme', onTap: onGallery), - SetRow(LucideIcons.code, C.n500, 'Developer mode', - value: 'On', chevron: false, onTap: onToggleDev), + SetRow(LucideIcons.code, C.n500, + l?.settingsDeveloperModeRowTitle ?? 'Developer mode', + value: on, chevron: false, onTap: onToggleDev), ]), const SizedBox(height: S.x6), Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), - child: SetRow(LucideIcons.trash2, C.red, 'Reset all data', + child: SetRow(LucideIcons.trash2, C.red, + l?.settingsResetAllDataRowTitle ?? 'Reset all data', danger: true, chevron: false, onTap: onReset), ), ], @@ -825,14 +897,19 @@ class NotificationSettingsView extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); + final on = l?.stateOn ?? 'On'; + final off = l?.stateOff ?? 'Off'; void set(NotificationPrefs next) => onChanged?.call(next); return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Notifications', sub: 'WHAT MAY INTERRUPT YOU'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.settingsNotificationsNavTitle ?? 'Notifications', + sub: l?.settingsNotificationsNavSub ?? + 'WHAT MAY INTERRUPT YOU'), ), Expanded( child: ListView( @@ -840,24 +917,34 @@ class NotificationSettingsView extends StatelessWidget { children: [ if (!granted) StatusCard( - 'Notifications are off at the system level', - 'Nothing below can reach you until the OS lets it.', - fix: 'Turn them on', + l?.settingsNotificationsOffSystemTitle ?? + 'Notifications are off at the system level', + l?.settingsNotificationsOffSystemBody ?? + 'Nothing below can reach you until the OS lets it.', + fix: l?.settingsTurnThemOn ?? 'Turn them on', icon: LucideIcons.bellOff, onFix: onRequestPermission, ), if (loaded) ...[ - settingsGroup(c, 'Manage notifications', [ - SetRow(LucideIcons.heartPulse, C.red, 'Health exceptions', - sub: 'One a day at most, and only when something in ' - 'your own baseline moved', - value: prefs.healthEnabled ? 'On' : 'Off', + settingsGroup( + c, + l?.settingsGroupManageNotifications ?? + 'Manage notifications', [ + SetRow(LucideIcons.heartPulse, C.red, + l?.settingsHealthExceptionsRowTitle ?? + 'Health exceptions', + sub: l?.settingsHealthExceptionsRowSub ?? + 'One a day at most, and only when something in ' + 'your own baseline moved', + value: prefs.healthEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( healthEnabled: !prefs.healthEnabled))), - SetRow(LucideIcons.watch, C.orange, 'Band alerts', - sub: 'Flat battery, on the charger, gone quiet', - value: prefs.deviceEnabled ? 'On' : 'Off', + SetRow(LucideIcons.watch, C.orange, + l?.settingsBandAlertsRowTitle ?? 'Band alerts', + sub: l?.settingsBandAlertsRowSub ?? + 'Flat battery, on the charger, gone quiet', + value: prefs.deviceEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( deviceEnabled: !prefs.deviceEnabled))), @@ -866,9 +953,10 @@ class NotificationSettingsView extends StatelessWidget { // rule as the water row below. if (prefs.deviceEnabled) SetRow(LucideIcons.batteryLow, C.orange, - 'Alert me at', - sub: 'Warn when the band drops under this charge ' - 'level', + l?.settingsAlertMeAtRowTitle ?? 'Alert me at', + sub: l?.settingsAlertMeAtRowSub ?? + 'Warn when the band drops under this charge ' + 'level', value: '${prefs.batteryAlertPct}%', chevron: false, onTap: () => set(prefs.copyWith( @@ -878,10 +966,12 @@ class NotificationSettingsView extends StatelessWidget { // event behind it again: the morning "recovery is ready" // note. It was cut in the three-class cull and sat dead — // emitted, classified null, dropped. - SetRow(LucideIcons.activity, C.green, 'Recovery ready', - sub: 'One note when your morning recovery score ' - 'lands', - value: prefs.recoveryEnabled ? 'On' : 'Off', + SetRow(LucideIcons.activity, C.green, + l?.settingsRecoveryReadyRowTitle ?? 'Recovery ready', + sub: l?.settingsRecoveryReadyRowSub ?? + 'One note when your morning recovery score ' + 'lands', + value: prefs.recoveryEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( recoveryEnabled: !prefs.recoveryEnabled))), @@ -890,10 +980,12 @@ class NotificationSettingsView extends StatelessWidget { // plainly-stated resting-HR drift), and most weeks still // say nothing — which is the point. SetRow(LucideIcons.calendarDays, C.purple, - 'Weekly lookback', - sub: 'Sunday evening, but only for a week that ' - 'actually found something. Most weeks are quiet', - value: prefs.remindersEnabled ? 'On' : 'Off', + l?.settingsWeeklyLookbackRowTitle ?? + 'Weekly lookback', + sub: l?.settingsWeeklyLookbackRowSub ?? + 'Sunday evening, but only for a week that ' + 'actually found something. Most weeks are quiet', + value: prefs.remindersEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( remindersEnabled: !prefs.remindersEnabled))), @@ -902,11 +994,14 @@ class NotificationSettingsView extends StatelessWidget { // prompt was emitted, and nothing anywhere could stop // either. The sub-line says exactly what it stops, // because it does NOT stop the detection itself. - SetRow(LucideIcons.radar, C.green, 'Detected workouts', - sub: 'Ask about efforts the band spotted that you did ' - 'not start. Off hides the prompt and the review ' - 'cards; the band goes on measuring either way', - value: prefs.autoDetectEnabled ? 'On' : 'Off', + SetRow(LucideIcons.radar, C.green, + l?.settingsDetectedWorkoutsRowTitle ?? + 'Detected workouts', + sub: l?.settingsDetectedWorkoutsRowSub ?? + 'Ask about efforts the band spotted that you did ' + 'not start. Off hides the prompt and the review ' + 'cards; the band goes on measuring either way', + value: prefs.autoDetectEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( autoDetectEnabled: !prefs.autoDetectEnabled))), @@ -917,32 +1012,39 @@ class NotificationSettingsView extends StatelessWidget { // sedentary surfaces: the OS-scheduled two-hour-still // one-shot, and the foreground desk-posture check (which // also buzzes the band when it fires). - SetRow(LucideIcons.footprints, C.orange, 'Movement nudge', - sub: 'Nudges you after a still stretch — two hours ' - 'with no movement at all, or 90 minutes in a ' - 'desk posture. Phone notification plus a buzz ' - 'on the band while it is connected', - value: prefs.movementEnabled ? 'On' : 'Off', + SetRow(LucideIcons.footprints, C.orange, + l?.settingsMovementNudgeRowTitle ?? 'Movement nudge', + sub: l?.settingsMovementNudgeRowSub ?? + 'Nudges you after a still stretch — two hours ' + 'with no movement at all, or 90 minutes in a ' + 'desk posture. Phone notification plus a buzz ' + 'on the band while it is connected', + value: prefs.movementEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( movementEnabled: !prefs.movementEnabled))), // The wind-down nudge. Silent until the Sleep Coach has // actually LEARNED a bedtime — the nudge's whole content // is that time, so there is no honest fallback. - SetRow(LucideIcons.moonStar, C.indigo, 'Wind-down', - sub: 'A heads-up about 45 minutes before the bedtime ' - 'learned from your own nights, kept clear of your ' - 'quiet hours. Appears after about a week of wear', - value: prefs.windDownEnabled ? 'On' : 'Off', + SetRow(LucideIcons.moonStar, C.indigo, + l?.settingsWindDownRowTitle ?? 'Wind-down', + sub: l?.settingsWindDownRowSub ?? + 'A heads-up about 45 minutes before the bedtime ' + 'learned from your own nights, kept clear of your ' + 'quiet hours. Appears after about a week of wear', + value: prefs.windDownEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( windDownEnabled: !prefs.windDownEnabled))), // The step-goal achievement's off switch. On by default: // once a day at most, and only on a real crossing. - SetRow(LucideIcons.trophy, C.orange, 'Step goal alerts', - sub: 'Tells you once when today crosses your steps ' - 'goal', - value: prefs.stepGoalEnabled ? 'On' : 'Off', + SetRow(LucideIcons.trophy, C.orange, + l?.settingsStepGoalAlertsRowTitle ?? + 'Step goal alerts', + sub: l?.settingsStepGoalAlertsRowSub ?? + 'Tells you once when today crosses your steps ' + 'goal', + value: prefs.stepGoalEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( stepGoalEnabled: !prefs.stepGoalEnabled))), @@ -951,12 +1053,15 @@ class NotificationSettingsView extends StatelessWidget { // dose still due is armed, and the notification names no // drug — it lands on a lock screen in front of whoever is // in the room. - SetRow(LucideIcons.pill, C.blue, 'Medication reminders', - sub: 'One notification per scheduled dose, at the ' - 'times you entered — with a buzz on the band if ' - 'it is connected. Nothing is sent for a dose ' - 'already marked taken or skipped', - value: prefs.medsEnabled ? 'On' : 'Off', + SetRow(LucideIcons.pill, C.blue, + l?.settingsMedicationRemindersRowTitle ?? + 'Medication reminders', + sub: l?.settingsMedicationRemindersRowSub ?? + 'One notification per scheduled dose, at the ' + 'times you entered — with a buzz on the band if ' + 'it is connected. Nothing is sent for a dose ' + 'already marked taken or skipped', + value: prefs.medsEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith(medsEnabled: !prefs.medsEnabled))), @@ -964,28 +1069,34 @@ class NotificationSettingsView extends StatelessWidget { // mood, energy, stress and the rest are all the same // screen, so five rows would be five interruptions for one // minute of typing. - SetRow(LucideIcons.notebookPen, C.purple, 'Daily check-in', - sub: 'One prompt in the evening to write the day — ' - 'mood, energy, stress. Skipped once the day ' - 'already has a rating in it', - value: prefs.checkInEnabled ? 'On' : 'Off', + SetRow(LucideIcons.notebookPen, C.purple, + l?.settingsDailyCheckInRowTitle ?? 'Daily check-in', + sub: l?.settingsDailyCheckInRowSub ?? + 'One prompt in the evening to write the day — ' + 'mood, energy, stress. Skipped once the day ' + 'already has a rating in it', + value: prefs.checkInEnabled ? on : off, chevron: false, onTap: () => set(prefs.copyWith( checkInEnabled: !prefs.checkInEnabled))), // A prompt to log, not a reading. The app measures no // hydration and this row may never imply it does. - SetRow(LucideIcons.glassWater, C.teal, 'Water reminder', - sub: 'A buzz on the strap and a notification on your ' - 'phone through your waking hours, to remind you to ' - 'log a drink. Nothing is measured either way', - value: prefs.waterEnabled ? 'On' : 'Off', + SetRow(LucideIcons.glassWater, C.teal, + l?.settingsWaterReminderRowTitle ?? 'Water reminder', + sub: l?.settingsWaterReminderRowSub ?? + 'A buzz on the strap and a notification on your ' + 'phone through your waking hours, to remind you to ' + 'log a drink. Nothing is measured either way', + value: prefs.waterEnabled ? on : off, chevron: false, onTap: () => set( prefs.copyWith(waterEnabled: !prefs.waterEnabled))), // only while it's on — the group is dense enough, and an // interval for a reminder nobody armed is furniture. if (prefs.waterEnabled) - SetRow(LucideIcons.timer, C.teal, 'Remind me every', + SetRow(LucideIcons.timer, C.teal, + l?.settingsRemindMeEveryRowTitle ?? + 'Remind me every', value: _everyLabel(prefs.waterIntervalMin), chevron: false, onTap: () => set(prefs.copyWith( @@ -993,31 +1104,37 @@ class NotificationSettingsView extends StatelessWidget { _nextEvery(prefs.waterIntervalMin)))), ]), if (relaySupported) - settingsGroup(c, 'The strap', [ + settingsGroup(c, l?.settingsGroupTheStrap ?? 'The strap', [ // The other direction: not what this app sends you, but // what your phone's apps make the band do. The permission // for it has been in the manifest all along with nothing // in the app that could reach it. SetRow(LucideIcons.bellRing, C.purple, - 'Buzz on app notifications', - sub: 'Pick which phone apps make the strap buzz', + l?.settingsBuzzOnAppNotificationsRowTitle ?? + 'Buzz on app notifications', + sub: l?.settingsBuzzOnAppNotificationsRowSub ?? + 'Pick which phone apps make the strap buzz', onTap: () => goto(c, const BandNotifications())), ]), - settingsGroup(c, 'Quiet hours', [ - SetRow(LucideIcons.moon, C.indigo, 'Quiet hours', - sub: 'Nothing buzzes inside this window', - value: prefs.quietEnabled ? 'On' : 'Off', + settingsGroup(c, l?.settingsGroupQuietHours ?? 'Quiet hours', [ + SetRow(LucideIcons.moon, C.indigo, + l?.settingsQuietHoursRowTitle ?? 'Quiet hours', + sub: l?.settingsQuietHoursRowSub ?? + 'Nothing buzzes inside this window', + value: prefs.quietEnabled ? on : off, chevron: false, onTap: () => set( prefs.copyWith(quietEnabled: !prefs.quietEnabled))), - SetRow(LucideIcons.sunset, C.blue, 'Starts', + SetRow(LucideIcons.sunset, C.blue, + l?.settingsQuietHoursStartsRowTitle ?? 'Starts', value: _hhmm(prefs.quietStartMin), chevron: false, onTap: () async { final v = await _pickMinute(c, prefs.quietStartMin); if (v != null) set(prefs.copyWith(quietStartMin: v)); }), - SetRow(LucideIcons.sunrise, C.yellow, 'Ends', + SetRow(LucideIcons.sunrise, C.yellow, + l?.settingsQuietHoursEndsRowTitle ?? 'Ends', value: _hhmm(prefs.quietEndMin), chevron: false, onTap: () async { @@ -1025,8 +1142,9 @@ class NotificationSettingsView extends StatelessWidget { if (v != null) set(prefs.copyWith(quietEndMin: v)); }), SetRow(LucideIcons.triangleAlert, C.red, - 'Health exceptions break through', - value: prefs.criticalOverridesQuiet ? 'On' : 'Off', + l?.settingsHealthExceptionsBreakThroughRowTitle ?? + 'Health exceptions break through', + value: prefs.criticalOverridesQuiet ? on : off, chevron: false, onTap: () => set(prefs.copyWith( criticalOverridesQuiet: @@ -1034,9 +1152,11 @@ class NotificationSettingsView extends StatelessWidget { ]), ], const SizedBox(height: S.x3), - const StatusCard( - 'The alarm is not on this list', - 'Cancel it on the Alarm screen instead.', + StatusCard( + l?.settingsAlarmNotOnListTitle ?? + 'The alarm is not on this list', + l?.settingsAlarmNotOnListBody ?? + 'Cancel it on the Alarm screen instead.', icon: LucideIcons.alarmClock, ), ], @@ -1118,13 +1238,15 @@ class EditProfile extends StatelessWidget { // come back so the form shows what arrived rather than claiming it. onImport: () async { final importer = HealthProfileImporter(); + final l = AppLocalizations.of(c); // Asked HERE, on the tap, and for these four types only. Nothing at // launch and nothing in onboarding: a permission sheet for data the // user has not asked us to read is how the whole set gets denied at // once. if (!await importer.requestPermission()) { return ( - '$storeName did not grant those fields. Nothing was read.', + l?.settingsImportNoPermission(storeName) ?? + '$storeName did not grant those fields. Nothing was read.', true, null, ); @@ -1132,9 +1254,13 @@ class EditProfile extends StatelessWidget { final snap = await importer.read(); if (snap.isEmpty) { return ( - 'Nothing came back. $storeName holds no height, weight' - '${isAppleHealth ? ', birthday' : ''} or sex for you — type ' - 'them in here instead.', + isAppleHealth + ? (l?.settingsImportEmptyWithBirthday(storeName) ?? + 'Nothing came back. $storeName holds no height, weight, ' + 'birthday or sex for you — type them in here instead.') + : (l?.settingsImportEmpty(storeName) ?? + 'Nothing came back. $storeName holds no height, weight' + ' or sex for you — type them in here instead.'), false, null, ); @@ -1148,14 +1274,20 @@ class EditProfile extends StatelessWidget { // notifies every listener and re-scores the day. if (changes.isEmpty) { return ( - 'Read ${snap.found.join(', ')}. Your profile already says the ' - 'same thing, so nothing changed.', + l?.settingsImportNoChange(snap.found.join(', ')) ?? + 'Read ${snap.found.join(', ')}. Your profile already says the ' + 'same thing, so nothing changed.', false, merged, ); } await app.updateProfile(merged); - return ('Updated ${changes.join(', ')} from $storeName.', false, merged); + return ( + l?.settingsImportUpdated(changes.join(', '), storeName) ?? + 'Updated ${changes.join(', ')} from $storeName.', + false, + merged + ); }, onSave: (fields) async { // A field the user CLEARED must be removed, not merged over — the @@ -1262,8 +1394,9 @@ class _EditProfileViewState extends State { }); } catch (e) { if (mounted) { + final l = AppLocalizations.of(context); setState(() { - _importNote = 'Failed: $e'; + _importNote = l?.settingsImportFailed('$e') ?? 'Failed: $e'; _importFailed = true; }); } @@ -1288,11 +1421,12 @@ class _EditProfileViewState extends State { /// parsed to null and wiped the stored weight while the screen popped as if /// it had saved. Blank still clears; a typo now stops the save and says so. void _save() { + final l = AppLocalizations.of(context); final age = Typed.of(_age.text); final height = Typed.of(_height.text); final weight = Typed.of(_weight.text); final bad = [ - if (age.bad) 'Age', + if (age.bad) (l?.settingsAgeFieldLabel ?? 'Age'), if (height.bad) _u.heightLabel, if (weight.bad) _u.weightLabel, ]; @@ -1313,17 +1447,18 @@ class _EditProfileViewState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Edit profile', + child: NavBar(l?.settingsEditProfileNavTitle ?? 'Edit profile', trailing: Pressable( - semanticLabel: 'Save', + semanticLabel: l?.actionSave ?? 'Save', onTap: _save, - child: Text('Save', + child: Text(l?.actionSave ?? 'Save', style: F.body.copyWith( color: p.on(C.green), fontWeight: FontWeight.w600)), )), @@ -1332,15 +1467,17 @@ class _EditProfileViewState extends State { child: ListView( padding: const EdgeInsets.fromLTRB(S.x4, 0, S.x4, S.x10), children: [ - _text(c, _name, 'NAME', TextInputType.name), + _text(c, _name, l?.settingsNameFieldLabel ?? 'NAME', + TextInputType.name), const SizedBox(height: S.x4), - Text('SEX', style: F.over.copyWith(color: p.ink3)), + Text(l?.settingsSexFieldLabel ?? 'SEX', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x2), Wrap(spacing: S.x2, runSpacing: S.x2, children: [ - for (final (key, label) in const [ - ('m', 'Male'), - ('f', 'Female'), - ('other', 'Prefer not to say'), + for (final (key, label) in [ + ('m', l?.settingsSexMale ?? 'Male'), + ('f', l?.settingsSexFemale ?? 'Female'), + ('other', l?.settingsSexPreferNotToSay ?? 'Prefer not to say'), ]) Pressable( onTap: () => setState(() => _sex = key), @@ -1362,20 +1499,22 @@ class _EditProfileViewState extends State { ), ]), const SizedBox(height: S.x4), - _text(c, _age, 'AGE (YEARS)', TextInputType.number), + _text(c, _age, l?.settingsAgeYearsFieldLabel ?? 'AGE (YEARS)', + TextInputType.number), const SizedBox(height: S.x4), _text(c, _height, _u.heightLabel.toUpperCase(), TextInputType.number), const SizedBox(height: S.x4), _text(c, _weight, _u.weightLabel.toUpperCase(), TextInputType.number), - ..._importBlock(p), + ..._importBlock(p, c), const SizedBox(height: S.x6), - const StatusCard( - 'These four change your numbers', - 'They feed heart-rate zones, calorie estimates and training ' - 'load. Clear one and only the metrics that need it stay ' - 'unavailable.', + StatusCard( + l?.settingsFourFieldsTitle ?? 'These four change your numbers', + l?.settingsFourFieldsBody ?? + 'They feed heart-rate zones, calorie estimates and training ' + 'load. Clear one and only the metrics that need it stay ' + 'unavailable.', icon: LucideIcons.info, ), ], @@ -1389,21 +1528,24 @@ class _EditProfileViewState extends State { /// The health-store read, on the form it fills. Empty when the caller passed /// no [EditProfileView.onImport] — the gallery and the golden sweep must not /// carry a control that raises a real permission sheet. - List _importBlock(P p) { + List _importBlock(P p, BuildContext c) { if (widget.onImport == null) return const []; + final l = AppLocalizations.of(c); return [ const SizedBox(height: S.x6), Surface( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isAppleHealth - ? 'Height, weight, birthday and sex, straight out of ' - '$storeName. Height and weight are taken every time; your ' - 'age and sex only fill a gap, because neither drifts and a ' - 'value already here was your choice.' - : 'Height and weight, straight out of $storeName. It has no ' - 'birthday and no sex to read — no app can — so set those ' - 'two above yourself.', + ? (l?.settingsImportBlockAppleHealth(storeName) ?? + 'Height, weight, birthday and sex, straight out of ' + '$storeName. Height and weight are taken every time; your ' + 'age and sex only fill a gap, because neither drifts and a ' + 'value already here was your choice.') + : (l?.settingsImportBlockOther(storeName) ?? + 'Height and weight, straight out of $storeName. It has no ' + 'birthday and no sex to read — no app can — so set those ' + 'two above yourself.'), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), const SizedBox(height: S.x4), @@ -1430,6 +1572,7 @@ class _EditProfileViewState extends State { Widget _text(BuildContext c, TextEditingController ctl, String label, TextInputType kind) { final p = P.of(c); + final l = AppLocalizations.of(c); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: F.over.copyWith(color: p.ink3)), TextField( @@ -1437,7 +1580,7 @@ class _EditProfileViewState extends State { keyboardType: kind, style: F.head.copyWith(color: p.ink), decoration: InputDecoration( - hintText: 'Not set', + hintText: l?.settingsNotSetHint ?? 'Not set', hintStyle: F.head.copyWith(color: p.ink3), isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: S.x3), @@ -1498,38 +1641,42 @@ class _AutomationSettingsState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final android = defaultTargetPlatform == TargetPlatform.android; final token = _token; return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Automation'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.settingsAutomationNavTitle ?? 'Automation'), ), Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(S.x4, 0, S.x4, S.x10), children: [ Section( - 'When a sync finishes', + l?.settingsSyncFinishesSectionTitle ?? + 'When a sync finishes', Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( android - ? 'The app broadcasts an intent your automation ' - 'app can start a profile on. Filter on the ' - 'action below; it carries how many records ' - 'landed and when, at most one a minute.' - : 'iOS cannot do this. A Shortcuts personal ' - 'automation can only trigger on Apple’s ' - 'own fixed list of events, and no app can ' - 'add one — so nothing here can start a ' - 'shortcut for you. Android gets it; this ' - 'is a platform limit, not a setting.', + ? (l?.settingsSyncFinishesAndroidBody ?? + 'The app broadcasts an intent your automation ' + 'app can start a profile on. Filter on the ' + 'action below; it carries how many records ' + 'landed and when, at most one a minute.') + : (l?.settingsSyncFinishesIosBody ?? + 'iOS cannot do this. A Shortcuts personal ' + 'automation can only trigger on Apple’s ' + 'own fixed list of events, and no app can ' + 'add one — so nothing here can start a ' + 'shortcut for you. Android gets it; this ' + 'is a platform limit, not a setting.'), style: F.body.copyWith(color: p.ink2, height: 1.4), ), if (android) ...[ @@ -1539,7 +1686,9 @@ class _AutomationSettingsState extends State { style: F.cap.copyWith(color: p.ink), ), const SizedBox(height: S.x1), - Text('Extras: records (int), at (unix seconds)', + Text( + l?.settingsSyncFinishesExtras ?? + 'Extras: records (int), at (unix seconds)', style: F.over.copyWith(color: p.ink3)), ], ]), @@ -1547,48 +1696,58 @@ class _AutomationSettingsState extends State { ), const SizedBox(height: S.x5), Section( - 'What it will never send', - const Surface( + l?.settingsNeverSendSectionTitle ?? 'What it will never send', + Surface( child: Text( - 'No readiness, no strain, no sleep score — on either ' - 'platform. A number this app would have shown as absent, ' - 'with a reason attached, becomes a bare zero the moment ' - 'it leaves. Facts about the sync go out; measurements do ' - 'not.', + l?.settingsNeverSendBody ?? + 'No readiness, no strain, no sleep score — on either ' + 'platform. A number this app would have shown as absent, ' + 'with a reason attached, becomes a bare zero the moment ' + 'it leaves. Facts about the sync go out; measurements do ' + 'not.', style: F.body, ), ), ), const SizedBox(height: S.x5), Section( - 'Buzzing the band from a shortcut', + l?.settingsBuzzFromShortcutSectionTitle ?? + 'Buzzing the band from a shortcut', Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( android - ? 'Send ' - 'wtf.openstrap.openstrap_edge.BUZZ_STRAP ' - 'with this token as the “token” string ' - 'extra. Without it any app on the phone ' - 'could buzz your band.' - : 'This direction works on iOS: a shortcut you ' - 'run yourself can reach the app. What it ' - 'cannot do is run itself when the band ' - 'syncs.', + ? (l?.settingsBuzzFromShortcutAndroidBody ?? + 'Send ' + 'wtf.openstrap.openstrap_edge.BUZZ_STRAP ' + 'with this token as the “token” string ' + 'extra. Without it any app on the phone ' + 'could buzz your band.') + : (l?.settingsBuzzFromShortcutIosBody ?? + 'This direction works on iOS: a shortcut you ' + 'run yourself can reach the app. What it ' + 'cannot do is run itself when the band ' + 'syncs.'), style: F.body.copyWith(color: p.ink2, height: 1.4), ), if (android) ...[ const SizedBox(height: S.x4), if (token == null) - Text('No token yet — reopen this screen.', + Text( + l?.settingsNoTokenYet ?? + 'No token yet — reopen this screen.', style: F.cap.copyWith(color: p.ink3)) else ...[ SelectableText(token, style: F.cap.copyWith(color: p.ink)), const SizedBox(height: S.x3), - BigButton(_copied ? 'Copied' : 'Copy the token', + BigButton( + _copied + ? (l?.settingsCopied ?? 'Copied') + : (l?.settingsCopyTheToken ?? + 'Copy the token'), icon: _copied ? LucideIcons.check : LucideIcons.copy, diff --git a/lib/ui2/revision.dart b/lib/ui2/revision.dart index 51284281..6f602ede 100644 --- a/lib/ui2/revision.dart +++ b/lib/ui2/revision.dart @@ -23,6 +23,7 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../state/app_state.dart'; +import '../state/locale_controller.dart'; /// Re-read on [AppState.insightsRevision]. /// @@ -91,6 +92,11 @@ mixin RevisionReload on State { /// preview): there is nothing behind it to re-read. bool get revisionReloads => true; + /// Sentinel so a system-default locale (`code == null`) is not mistaken for + /// "never seen yet" on the first pass. + static const Object _unset = Object(); + Object? _seenLocale = _unset; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -103,10 +109,37 @@ mixin RevisionReload on State { } catch (_) { return; } - if (identical(_rev, app.insightsRevision)) return; - _rev?.removeListener(_onRevision); - _rev = app.insightsRevision..addListener(_onRevision); - _seen = app.insightsRevision.value; + if (!identical(_rev, app.insightsRevision)) { + _rev?.removeListener(_onRevision); + _rev = app.insightsRevision..addListener(_onRevision); + _seen = app.insightsRevision.value; + } + + // Cached data built with strings baked in from `AppLocalizations` — every + // `reload()` here re-derives them, not just the DB rows — so a language + // switch mid-session is stale in exactly the same way a stale revision + // is: nothing tells the screen to look again. `watch` (not `read`) is + // what makes that happen: it subscribes this State to `LocaleController` + // so `didChangeDependencies` re-runs the moment `setCode` notifies, the + // same mechanism `app.dart` uses to rebuild `MaterialApp` with the new + // locale, kept off `AppState` itself because that one ticks at ~1 Hz. + // `code` alone misses a SYSTEM locale change while it is null (no + // in-app override): `AppLocalizations.of(context)` follows the OS, but + // `code` doesn't move, so the old compare never saw it. The resolved + // locale is the thing that actually decides which strings got baked in. + final Object localeKey; + try { + final code = context.watch().code; + localeKey = code ?? Localizations.localeOf(context); + } catch (_) { + return; + } + if (identical(_seenLocale, _unset)) { + _seenLocale = localeKey; + } else if (_seenLocale != localeKey) { + _seenLocale = localeKey; + reload(); + } } // ponytail: a parked tab re-reads too — the IndexedStack keeps all five diff --git a/lib/ui2/screens/ai_briefing.dart b/lib/ui2/screens/ai_briefing.dart index 831a4459..f989e3eb 100644 --- a/lib/ui2/screens/ai_briefing.dart +++ b/lib/ui2/screens/ai_briefing.dart @@ -19,6 +19,7 @@ import '../../ai/briefing.dart'; import '../../ai/briefing_engine.dart'; import '../../coach/coach_config.dart'; import '../../coach/coach_engine.dart' show CoachException; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'coach.dart' show CoachSetup, kCoachAccent; import 'home_screen.dart' show go, pad, repoOf; @@ -57,8 +58,11 @@ class _AiBriefingScreenState extends State { if (mounted) setState(() => _b = b); } catch (e) { if (mounted) { + final l = AppLocalizations.of(context); setState( - () => _error = e is CoachException ? e.message : 'It failed: $e', + () => _error = e is CoachException + ? e.message + : (l?.aiBriefingFailedGeneric('$e') ?? 'It failed: $e'), ); } } finally { @@ -69,6 +73,7 @@ class _AiBriefingScreenState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final cfg = c.watch(); final b = _b; return Scaffold( @@ -80,7 +85,9 @@ class _AiBriefingScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: S.x4), child: NavBar( widget.period.title, - sub: b == null ? '' : 'FOR ${b.day}', + sub: b == null + ? '' + : (l?.aiBriefingForDay(b.day) ?? 'FOR ${b.day}'), ), ), Expanded( @@ -90,20 +97,24 @@ class _AiBriefingScreenState extends State { const SizedBox(height: S.x2), if (!cfg.configured) StatusCard( - 'No model is set up', - 'A briefing is written by a model you choose. Until you ' - 'pick one there is nothing to generate and nothing ' - 'has been sent anywhere.', - fix: 'Choose a model', + l?.aiBriefingNoModelTitle ?? 'No model is set up', + l?.aiBriefingNoModelBody ?? + 'A briefing is written by a model you choose. Until you ' + 'pick one there is nothing to generate and nothing ' + 'has been sent anywhere.', + fix: l?.aiBriefingChooseModel ?? 'Choose a model', icon: LucideIcons.sparkles, onFix: () => go(c, const CoachSetup()), ) else if (b == null) StatusCard( - 'Nothing written for today', - 'Briefings are generated on a schedule, or on demand ' - 'here.', - fix: _busy ? 'Writing…' : 'Write one now', + l?.aiBriefingNothingTitle ?? 'Nothing written for today', + l?.aiBriefingNothingBody ?? + 'Briefings are generated on a schedule, or on demand ' + 'here.', + fix: _busy + ? (l?.aiBriefingWriting ?? 'Writing…') + : (l?.aiBriefingWriteNow ?? 'Write one now'), icon: LucideIcons.sun, onFix: _busy ? null : _generate, ) @@ -128,7 +139,9 @@ class _AiBriefingScreenState extends State { ), const SizedBox(height: S.x3), BigButton( - _busy ? 'Writing…' : 'Write it again', + _busy + ? (l?.aiBriefingWriting ?? 'Writing…') + : (l?.aiBriefingWriteAgain ?? 'Write it again'), icon: LucideIcons.refreshCw, color: kCoachAccent, soft: true, @@ -138,7 +151,7 @@ class _AiBriefingScreenState extends State { if (_error != null) ...[ const SizedBox(height: S.x3), StatusCard( - 'That did not go through', + l?.aiBriefingFailedTitle ?? 'That did not go through', _error!, icon: LucideIcons.triangleAlert, ), @@ -201,6 +214,7 @@ class SentPayload extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final host = Uri.tryParse(config.apiBase)?.host ?? config.apiBase; final local = isLocal(config.apiBase); final keys = inputs.keys.toList()..sort(); @@ -209,7 +223,9 @@ class SentPayload extends StatelessWidget { // on describing a request that never happened. final none = !asked; return Section( - none || !local ? 'What was sent' : 'What was read', + none || !local + ? (l?.aiBriefingSentSection ?? 'What was sent') + : (l?.aiBriefingReadSection ?? 'What was read'), Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -229,14 +245,17 @@ class SentPayload extends StatelessWidget { Expanded( child: Text( none - ? 'Nothing. There was no request — the note above was ' - 'written on this phone.' + ? (l?.aiBriefingNoneBody ?? + 'Nothing. There was no request — the note above was ' + 'written on this phone.') : local - ? 'These numbers went to $host, on this machine. ' - 'Nothing left it.' - : 'These numbers, and nothing else, were sent to ' - '$host as ${config.model}. No raw ' - 'recordings, no name, no identifier.', + ? (l?.aiBriefingLocalBody(host) ?? + 'These numbers went to $host, on this machine. ' + 'Nothing left it.') + : (l?.aiBriefingCloudBody(host, config.model) ?? + 'These numbers, and nothing else, were sent to ' + '$host as ${config.model}. No raw ' + 'recordings, no name, no identifier.'), style: F.cap.copyWith(color: p.ink, height: 1.5), ), ), @@ -245,17 +264,19 @@ class SentPayload extends StatelessWidget { ), const SizedBox(height: S.x3), if (none) - const StatusCard( - 'Nothing stood out, so nothing was asked', - 'The sweep runs on this phone. It only calls a model when it has ' - 'a finding to hand it, and today it had none.', + StatusCard( + l?.aiBriefingNoneCardTitle ?? 'Nothing stood out, so nothing was asked', + l?.aiBriefingNoneCardBody ?? + 'The sweep runs on this phone. It only calls a model when it has ' + 'a finding to hand it, and today it had none.', icon: LucideIcons.circleSlash, ) else if (keys.isEmpty) - const StatusCard( - 'Nothing was available to send', - 'No metric had a value when this was written, so the prompt ' - 'carried none.', + StatusCard( + l?.aiBriefingEmptyCardTitle ?? 'Nothing was available to send', + l?.aiBriefingEmptyCardBody ?? + 'No metric had a value when this was written, so the prompt ' + 'carried none.', icon: LucideIcons.circleSlash, ) else diff --git a/lib/ui2/screens/beats.dart b/lib/ui2/screens/beats.dart index 6379e8a7..ab9637af 100644 --- a/lib/ui2/screens/beats.dart +++ b/lib/ui2/screens/beats.dart @@ -54,6 +54,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/journal_fields.dart' show formatMinuteOfDay; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../ui2.dart'; import 'home_screen.dart'; @@ -243,10 +244,11 @@ class _BeatsState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final d = _d ?? const BeatsData(); return detailScaffold( c, - 'Beats', + l?.beatsTitle ?? 'Beats', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), if (_loading) @@ -255,11 +257,12 @@ class _BeatsState extends State { child: Center(child: CircularProgressIndicator()), ) else if (d.day == null) - const StatusCard( - 'No night to draw yet', - 'Nothing on this phone has produced a derived night, so there are ' - 'no beat intervals to plot.', - fix: 'Wear the band overnight, then sync', + StatusCard( + l?.beatsNoNightTitle ?? 'No night to draw yet', + l?.beatsNoNightBody ?? + 'Nothing on this phone has produced a derived night, so ' + 'there are no beat intervals to plot.', + fix: l?.beatsNoNightFix ?? 'Wear the band overnight, then sync', icon: LucideIcons.heartPulse, ) else ...[ @@ -278,7 +281,10 @@ class _BeatsState extends State { // WHICH NIGHT, in the app's one day format. Every panel below describes // this one night; saying so once at the top is what lets them sit // together without each having to date itself. - sub: d.day == null ? '' : 'Night of ${prettyDay(d.day)}', + sub: d.day == null + ? '' + : (l?.beatsNightOf(prettyDay(d.day, l)) ?? + 'Night of ${prettyDay(d.day, l)}'), ); } @@ -289,28 +295,32 @@ class _BeatsState extends State { // and printed as bare integers. The scatter IS the two numbers. Widget _poincare(BuildContext c, BeatsData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final sd1 = (d.poincare['sd1'] as num?)?.toDouble(); final sd2 = (d.poincare['sd2'] as num?)?.toDouble(); if (d.nn.length < 2) { return Section( - 'Every beat against the one before it', + l?.beatsPoincareSection ?? 'Every beat against the one before it', StatusCard( - 'The beats for this night are no longer on this phone', + l?.beatsBeatsGoneTitle ?? + 'The beats for this night are no longer on this phone', // The metric's own reason first, but there rarely is one: this is // not an abstention, it is retention. The raw records are deleted a // few days after the night is scored, and everything computed FROM // them survives — which is why SD1 and SD2 can still be printed on // Nerd stats for a night whose cloud can no longer be drawn. - 'Individual beat intervals are kept for a few days after the night ' - 'is scored, then deleted. The numbers taken from them are kept ' - 'for good' + '${l?.beatsBeatsGoneBody ?? 'Individual beat intervals are kept ' + 'for a few days after the night is scored, then deleted. The ' + 'numbers taken from them are kept for good'}' // NEVER `sd2 ?? 0`: a missing long-term axis printed as "SD2 // 0 ms" is a fabricated measurement, and 0 is the one value that // reads as a finding. - '${sd1 == null || sd2 == null ? '' : ' — this night measured ' - 'SD1 ${metricValue('ms', sd1)} ms, ' - 'SD2 ${metricValue('ms', sd2)} ms'}.', + '${sd1 == null || sd2 == null ? '' : (l?.beatsBeatsGoneMeasured( + metricValue('ms', sd1), metricValue('ms', sd2)) ?? + ' — this night measured ' + 'SD1 ${metricValue('ms', sd1)} ms, ' + 'SD2 ${metricValue('ms', sd2)} ms')}.', icon: LucideIcons.scatterChart, ), ); @@ -319,11 +329,12 @@ class _BeatsState extends State { final axis = AxisSpec.of(d.nn, ticks: 4)!; final dropped = d.rawBeats - d.nn.length; return Section( - 'Every beat against the one before it', + l?.beatsPoincareSection ?? 'Every beat against the one before it', Surface( child: Column(children: [ ChartFrame( - title: 'Each interval, plotted against the previous one', + title: l?.beatsScatterTitle ?? + 'Each interval, plotted against the previous one', unit: 'ms', height: 260, yAxis: axis, @@ -331,9 +342,10 @@ class _BeatsState extends State { // "up 4 ms across 26 747 readings", which is a sentence about a // trend this picture does not draw. The footnote below carries the // meaning instead, and the frame speaks it. - footnote: 'The diagonal is where a beat came out the same length ' - 'as the one before it. Spread across that line is SD1, beat ' - 'to beat; spread along it is SD2, the slower drift.', + footnote: l?.beatsScatterFootnote ?? + 'The diagonal is where a beat came out the same length ' + 'as the one before it. Spread across that line is SD1, ' + 'beat to beat; spread along it is SD2, the slower drift.', child: CustomPaint( size: Size.infinite, painter: Poincare(d.nn, p.on(C.green), axis: axis, grid: p.line), @@ -341,21 +353,26 @@ class _BeatsState extends State { ), const SizedBox(height: S.x4), InlineMetrics([ - if (sd1 != null) ('SD1', '${metricValue('ms', sd1)} ms', C.green), - if (sd2 != null) ('SD2', '${metricValue('ms', sd2)} ms', C.green), - ('Intervals', thousands(d.nn.length), C.green), + if (sd1 != null) + (l?.beatsSd1Label ?? 'SD1', '${metricValue('ms', sd1)} ms', + C.green), + if (sd2 != null) + (l?.beatsSd2Label ?? 'SD2', '${metricValue('ms', sd2)} ms', + C.green), + (l?.beatsIntervalsLabel ?? 'Intervals', thousands(d.nn.length), + C.green), ]), const SizedBox(height: S.x4), _note( p, // The denominator, and the honesty about what the wrist sees. - '${thousands(d.nn.length)} intervals survived correction' - '${dropped <= 0 ? '' : ' — $dropped were rejected as artifact and ' - 'are not in the cloud'}. ' - 'Pulse, not ECG — real and yours, but not the picture an ECG ' - 'draws.' - '${d.deviceFamily == null ? '' : ' Measured on ${d.deviceFamily}; ' - 'straps do not read the same numbers as each other.'}', + '${l?.beatsIntervalsSurvived(d.nn.length) ?? '${thousands(d.nn.length)} intervals survived correction'}' + '${dropped <= 0 ? '' : (l?.beatsDroppedArtifact(dropped) ?? ' — $dropped were rejected as artifact and ' + 'are not in the cloud')}. ' + '${l?.beatsPulseNotEcg ?? 'Pulse, not ECG — real and yours, but ' + 'not the picture an ECG draws.'}' + '${d.deviceFamily == null ? '' : ' ${l?.beatsMeasuredOn(d.deviceFamily!) ?? 'Measured on ${d.deviceFamily}; ' + 'straps do not read the same numbers as each other.'}'}', ), ]), ), @@ -375,16 +392,21 @@ class _BeatsState extends State { // nothing at all, and nothing in this path can tell those apart. Widget _nightCurve(BuildContext c, BeatsData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final present = [for (final b in d.bins) if (b.v != null) b]; if (present.isEmpty) { return Section( - 'Variability across the night', - StatusCard.forMetric('Variability across the night', d.shape, - unit: 'nights', - why: 'No half-hour bin of this night held enough clean beats ' - 'to publish an RMSSD.') ?? - const StatusCard('Variability across the night', - 'No bins were stored for this night.'), + l?.beatsVariabilitySection ?? 'Variability across the night', + StatusCard.forMetric( + l?.beatsVariabilitySection ?? 'Variability across the night', + d.shape, + unit: l?.beatsUnitNights ?? 'nights', + why: l?.beatsVariabilityWhy ?? + 'No half-hour bin of this night held enough clean beats ' + 'to publish an RMSSD.') ?? + StatusCard( + l?.beatsVariabilitySection ?? 'Variability across the night', + l?.beatsNoBinsStored ?? 'No bins were stored for this night.'), ); } @@ -395,11 +417,11 @@ class _BeatsState extends State { final holes = d.bins.length - present.length; return Section( - 'Variability across the night', + l?.beatsVariabilitySection ?? 'Variability across the night', Surface( child: Column(children: [ ChartFrame( - title: 'RMSSD in half-hour bins', + title: l?.beatsRmssdTitle ?? 'RMSSD in half-hour bins', unit: 'ms', height: 150, yAxis: axis, @@ -408,23 +430,26 @@ class _BeatsState extends State { // night had no beats to place an origin on, and never to a // hardcoded pair: an x axis that does not describe the range // actually drawn is worse than none. - xLabels: _nightHours(d), + xLabels: _nightHours(c, d), series: [for (final b in d.bins) b.v], footnote: - 'The bar is how sure we are of the bin, not a range your body ' - 'passed through. The mark inside it is the value.' - '${holes == 0 ? '' : ' $holes ${holes == 1 ? 'bin holds' : 'bins hold'} ' + '${l?.beatsBandFootnote ?? 'The bar is how sure we are of ' + 'the bin, not a range your body passed through. The ' + 'mark inside it is the value.'}' + '${holes == 0 ? '' : (l?.beatsHolesFootnote(holes) ?? ' $holes ${holes == 1 ? 'bin holds' : 'bins hold'} ' 'too few clean beats to publish one, and ' - '${holes == 1 ? 'is' : 'are'} left empty rather than joined ' - 'across.'}', + '${holes == 1 ? 'is' : 'are'} left empty rather than ' + 'joined across.')}', child: _NightBand(d.bins, axis, color: p.on(C.green), empty: p.line), ), if (d.firstThirdMs != null && d.lastThirdMs != null) ...[ const SizedBox(height: S.x4), InlineMetrics([ - ('First third', '${metricValue('ms', d.firstThirdMs!)} ms', C.green), - ('Last third', '${metricValue('ms', d.lastThirdMs!)} ms', C.green), + (l?.beatsFirstThird ?? 'First third', + '${metricValue('ms', d.firstThirdMs!)} ms', C.green), + (l?.beatsLastThird ?? 'Last third', + '${metricValue('ms', d.lastThirdMs!)} ms', C.green), ]), ], // The estimator's own note is NOT rendered here. A present @@ -453,14 +478,17 @@ class _BeatsState extends State { // accent colour on this series would be a verdict. Widget _dc(BuildContext c, BeatsData d) { final p = P.of(c); + final l = AppLocalizations.of(c); if (d.dcPoints.isEmpty) { return Section( - 'Deceleration capacity', - StatusCard.forMetric('Deceleration capacity', d.dc, - unit: 'nights', - why: 'No stored night has produced one yet.') ?? - const StatusCard('Deceleration capacity', - 'No night has produced one yet.'), + l?.beatsDcSection ?? 'Deceleration capacity', + StatusCard.forMetric( + l?.beatsDcSection ?? 'Deceleration capacity', d.dc, + unit: l?.beatsUnitNights ?? 'nights', + why: l?.beatsDcWhy ?? + 'No stored night has produced one yet.') ?? + StatusCard(l?.beatsDcSection ?? 'Deceleration capacity', + l?.beatsDcNoData ?? 'No night has produced one yet.'), ); } @@ -470,15 +498,18 @@ class _BeatsState extends State { final axis = AxisSpec.of(vals, ticks: 3, format: axisFixed); return Section( - 'Deceleration capacity', + l?.beatsDcSection ?? 'Deceleration capacity', Surface( child: Column(children: [ ChartFrame( - title: 'Your own nights, in order', + title: l?.beatsDcChartTitle ?? 'Your own nights, in order', unit: 'ms', height: 130, yAxis: axis, - xLabels: const ['${win - 1} days ago', 'Today'], + xLabels: [ + l?.beatsDaysAgo(win - 1) ?? '${win - 1} days ago', + l?.beatsTodayLabel ?? 'Today', + ], series: series, empty: axis == null ? const NoData() : null, child: CustomPaint( @@ -494,21 +525,24 @@ class _BeatsState extends State { const SizedBox(height: S.x4), InlineMetrics([ if (d.dcAnchors != null) - ('Anchors last night', thousands(d.dcAnchors), C.indigo), + (l?.beatsAnchorsLastNight ?? 'Anchors last night', + thousands(d.dcAnchors), C.indigo), if (d.cleanFraction > 0) - ('Clean beats', '${(d.cleanFraction * 100).toStringAsFixed(1)}%', - C.indigo), + (l?.beatsCleanBeats ?? 'Clean beats', + '${(d.cleanFraction * 100).toStringAsFixed(1)}%', C.indigo), ]), const SizedBox(height: S.x4), _note( p, - 'Yours only. Compare it against your own other nights and nothing ' - 'else — there is no reference band for a wrist.\n\n' - 'It averages the beats around each moment your heart slowed. A ' - 'rising line can be a cleaner signal rather than a different ' - 'heart, so read it beside the anchor count and clean-beat share ' - 'above. If you changed straps inside this window, the two halves ' - 'do not compare.', + l?.beatsDcNote ?? + 'Yours only. Compare it against your own other nights and ' + 'nothing else — there is no reference band for a ' + 'wrist.\n\n' + 'It averages the beats around each moment your heart ' + 'slowed. A rising line can be a cleaner signal rather ' + 'than a different heart, so read it beside the anchor ' + 'count and clean-beat share above. If you changed straps ' + 'inside this window, the two halves do not compare.', ), ]), ), @@ -528,6 +562,7 @@ class _BeatsState extends State { // result, and the only place that can be said is beside the marks. Widget _rhythm(BuildContext c, BeatsData d) { final p = P.of(c); + final l = AppLocalizations.of(c); const win = 30; final flags = denseDays(d.rhythmPoints, win); final screened = flags.where((v) => v != null).length; @@ -546,21 +581,26 @@ class _BeatsState extends State { : null; return Section( - 'Rhythm screen', + l?.beatsRhythmSection ?? 'Rhythm screen', Surface( child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ChartFrame( - title: 'One cell per day', - unit: 'screened / not screened', + title: l?.beatsRhythmChartTitle ?? 'One cell per day', + unit: l?.beatsUnitScreenedNotScreened ?? 'screened / not screened', height: 44, - xLabels: const ['${win - 1} days ago', 'Today'], + xLabels: [ + l?.beatsDaysAgo(win - 1) ?? '${win - 1} days ago', + l?.beatsTodayLabel ?? 'Today', + ], legend: [ - ('Screen did not fire', p.ink3), - ('Screen fired', p.on(C.orange)), - ('Not screened', p.line), + (l?.beatsScreenNotFired ?? 'Screen did not fire', p.ink3), + (l?.beatsScreenFired ?? 'Screen fired', p.on(C.orange)), + (l?.beatsNotScreened ?? 'Not screened', p.line), ], empty: screened == 0 - ? const NoData(message: 'No day in this window was screened') + ? NoData( + message: l?.beatsNoDayScreened ?? + 'No day in this window was screened') : null, child: _RhythmStrip(flags, clear: p.ink3, fired: p.on(C.orange), absent: p.ink3), @@ -568,26 +608,28 @@ class _BeatsState extends State { const SizedBox(height: S.x4), _note( p, - 'A screen, not a test.\n\n' - 'A day the screen did not fire is not a day you were cleared — it ' - 'cannot rule anything out, and it never could. Outlined days were ' - 'not screened at all: too few clean beats, or too much ' - 'movement.\n\n' - // "a clinician can test that properly" is the project's settled - // termination for this whole surface — the same sentence the CVHR - // card ends on. It ends in a person, never in a number. The - // symptom clause is phrased to keep the string "you have" off the - // screen entirely: that is the grammar of a diagnosis, and the - // wiring test greps for it on the neighbouring card. - 'Wrist pulse is not an ECG. If symptoms are what brought you here, ' - 'a clinician can test that properly.', + l?.beatsScreenNote ?? + 'A screen, not a test.\n\n' + 'A day the screen did not fire is not a day you were ' + 'cleared — it cannot rule anything out, and it never ' + 'could. Outlined days were not screened at all: too few ' + 'clean beats, or too much movement.\n\n' + // "a clinician can test that properly" is the project's + // settled termination for this whole surface — the same + // sentence the CVHR card ends on. It ends in a person, + // never in a number. The symptom clause is phrased to + // keep the string "you have" off the screen entirely: + // that is the grammar of a diagnosis, and the wiring + // test greps for it on the neighbouring card. + 'Wrist pulse is not an ECG. If symptoms are what brought ' + 'you here, a clinician can test that properly.', ), const SizedBox(height: S.x3), _note( p, - '$screened of the last $win days were screened' - '${fired == 0 ? '' : '; the screen fired on $fired'}.' - '${note == null ? '' : ' Last night was not screened: $note'}', + '${l?.beatsScreenedSummary(screened, win) ?? '$screened of the last $win days were screened'}' + '${fired == 0 ? '' : (l?.beatsFiredSummary(fired) ?? '; the screen fired on $fired')}.' + '${note == null ? '' : (l?.beatsNotScreenedNote(note) ?? ' Last night was not screened: $note')}', ), ]), ), @@ -597,12 +639,15 @@ class _BeatsState extends State { /// The two ends of the binned night, as wall-clock times when the bundle /// recorded where the first beat sat, and as elapsed time when it did not. /// A bin is half an hour wide, so the last bin ENDS 30 min after it starts. - List _nightHours(BeatsData d) { + List _nightHours(BuildContext c, BeatsData d) { if (d.bins.isEmpty) return const []; final endSec = d.bins.last.startSec + 1800; final origin = d.originMs; if (origin == null) { - return ['Start', metricValue('min', endSec / 60)]; + return [ + AppLocalizations.of(c)?.beatsStart ?? 'Start', + metricValue('min', endSec / 60), + ]; } // Added in ms rather than with a `Duration`: `Duration(` is banned inside // lib/ui2 (it is how animation timings escape `Motion`), and this is a diff --git a/lib/ui2/screens/calm_breathing.dart b/lib/ui2/screens/calm_breathing.dart index 07163fa9..6c9529f7 100644 --- a/lib/ui2/screens/calm_breathing.dart +++ b/lib/ui2/screens/calm_breathing.dart @@ -19,6 +19,7 @@ import 'package:flutter/scheduler.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../../state/app_state.dart'; import '../../stress/breath_phases.dart'; @@ -57,19 +58,74 @@ const kPaceSweepBlockMinutes = 2; /// The profile key holding the winners of past sweeps, oldest first. const kPaceWinsKey = 'breath_pace_wins'; +/// Localized copies of [kBreathPatterns], same order and keys. The consts +/// live in `breath_phases.dart` (which stays free of l10n imports on +/// purpose — it is the pure engine); the localized text is built here, +/// where `AppLocalizations` is already in scope, the same way [paceAt] does +/// it for the custom-paced resonance entry. +List localizedBreathPatterns([AppLocalizations? l]) { + final resonance = kBreathPatternsByKey['resonance']!; + final box = kBreathPatternsByKey['box']!; + final fourSevenEight = kBreathPatternsByKey['four_seven_eight']!; + final extendedExhale = kBreathPatternsByKey['extended_exhale']!; + return [ + BreathPattern( + key: resonance.key, + label: l?.calmBreathingResonanceLabel ?? resonance.label, + description: + l?.calmBreathingResonanceDescription(resonance.rate.toStringAsFixed(1)) ?? + resonance.description, + phases: resonance.phases, + coherenceRated: true, + ), + BreathPattern( + key: box.key, + label: l?.breathPatternBoxName ?? box.label, + description: l?.breathPatternBoxDesc ?? box.description, + phases: box.phases, + ), + BreathPattern( + key: fourSevenEight.key, + label: l?.breathPattern478Name ?? fourSevenEight.label, + description: l?.breathPattern478Desc ?? fourSevenEight.description, + phases: fourSevenEight.phases, + ), + BreathPattern( + key: extendedExhale.key, + label: l?.breathPatternExtendedExhaleName ?? extendedExhale.label, + description: + l?.breathPatternExtendedExhaleDesc ?? extendedExhale.description, + phases: extendedExhale.phases, + ), + ]; +} + +/// The instruction shown on the breathing ring for [kind], localized. +String breathPhaseKindLabel(BreathPhaseKind kind, [AppLocalizations? l]) => + switch (kind) { + BreathPhaseKind.inhale => l?.breathPhaseInhale ?? kind.label, + BreathPhaseKind.holdIn || BreathPhaseKind.holdOut => + l?.breathPhaseHold ?? kind.label, + BreathPhaseKind.exhale => l?.breathPhaseExhale ?? kind.label, + BreathPhaseKind.work => l?.breathPhaseWork ?? kind.label, + BreathPhaseKind.rest => l?.breathPhaseRest ?? kind.label, + }; + /// A resonance pattern at [rate] breaths a minute — even in, even out. /// /// 5.5 returns the SHIPPED resonance pattern rather than a lookalike: two /// patterns at the same pace under two different keys would split her breathing /// history in half for no reason anyone could see. -BreathPattern paceAt(double rate) { +BreathPattern paceAt(double rate, [AppLocalizations? l]) { final shipped = kBreathPatterns.first; - if ((rate - shipped.rate).abs() < 0.05) return shipped; + if ((rate - shipped.rate).abs() < 0.05) { + return localizedBreathPatterns(l).first; + } final half = 30.0 / rate; return BreathPattern( key: 'resonance_${rate.toStringAsFixed(1).replaceAll('.', '_')}', - label: 'Resonance', - description: + label: l?.calmBreathingResonanceLabel ?? 'Resonance', + description: l?.calmBreathingResonanceDescription(rate.toStringAsFixed(1)) ?? 'Even in and out at about ${rate.toStringAsFixed(1)} breaths a ' 'minute. The one with a coherence score.', phases: [ @@ -119,10 +175,13 @@ double? agreedPace(Object? wins) { /// when two sittings have agreed on one, and is the shipped 5.5 otherwise — /// one entry either way, never a personal pace sitting next to the default as /// if they were two different exercises. -List patternsFor(double? yours) => [ - yours == null ? kBreathPatterns.first : paceAt(yours), - ...kBreathPatterns.skip(1), -]; +List patternsFor(double? yours, [AppLocalizations? l]) { + final localized = localizedBreathPatterns(l); + return [ + yours == null ? localized.first : paceAt(yours, l), + ...localized.skip(1), + ]; +} class CalmBreathing extends StatefulWidget { const CalmBreathing({super.key}); @@ -192,19 +251,32 @@ class _CalmBreathingState extends State Duration? get _target => sessionEnd(_pattern, _rounds); int get _rounds => (_minutes * 60 / _pattern.cycleSeconds).round(); + bool _paceRead = false; + @override void initState() { super.initState(); // The pace two sittings agreed on, read once. `read` rather than `watch`: // the pattern is the user's choice from here on, and a profile write mid // session must not silently repace her. - final app = context.read(); - _app = app; - final yours = agreedPace(app.user?[kPaceWinsKey]); - if (yours != null) _pattern = paceAt(yours); + _app = context.read(); unawaited(_loadEffect()); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // AppLocalizations.of(context) may not be called from initState — the + // inherited lookup isn't ready until the widget is fully mounted. Read it + // here instead, but still only once: this picks the starting pattern, not + // a live-relocalized one. + if (_paceRead) return; + _paceRead = true; + final l = AppLocalizations.of(context); + final yours = agreedPace(_app?.user?[kPaceWinsKey]); + _pattern = yours != null ? paceAt(yours, l) : localizedBreathPatterns(l).first; + } + @override void dispose() { _slowTick?.cancel(); @@ -348,11 +420,12 @@ class _CalmBreathingState extends State /// the entire output is a comparison of beat timing and six minutes of /// breathing that cannot produce one is six minutes taken for nothing. Future _startSweep() async { + final l = AppLocalizations.of(context); setState(() { _block = 0; _blockScores.clear(); _minutes = kPaceSweepBlockMinutes; - _pattern = paceAt(kPaceSweepRates.first); + _pattern = paceAt(kPaceSweepRates.first, l); // The sweep is three paced blocks back to back. There is no unpaced // stretch anywhere in it to be a "before", so the windows do not apply. _windows = false; @@ -385,7 +458,7 @@ class _CalmBreathingState extends State if (block + 1 < kPaceSweepRates.length) { setState(() { _block = block + 1; - _pattern = paceAt(kPaceSweepRates[block + 1]); + _pattern = paceAt(kPaceSweepRates[block + 1], AppLocalizations.of(context)); }); await _start(); return; @@ -443,6 +516,7 @@ class _CalmBreathingState extends State // A quiet window holds the same live streams the paced block does, so it // owes the same exit — swiping away during one has to close it, not leave // it running behind a screen that is gone. + final l = AppLocalizations.of(c); final busy = _running || _quiet != null; return PopScope( canPop: !busy, @@ -466,7 +540,8 @@ class _CalmBreathingState extends State Align( alignment: Alignment.centerLeft, child: Pressable( - semanticLabel: 'Close breathing', + semanticLabel: + l?.calmBreathingCloseBreathing ?? 'Close breathing', onTap: () async { if (_running) { await _stop(abort: true); @@ -509,12 +584,16 @@ class _CalmBreathingState extends State ), BigButton( _quiet == 'post' - ? 'Finish now' + ? (l?.calmBreathingFinishNow ?? 'Finish now') : _quiet == 'pre' - ? 'Stop' + ? (l?.calmBreathingStop ?? 'Stop') : _running - ? (_sweeping ? 'Stop' : 'End session') - : (_finished ? 'Done' : 'Begin'), + ? (_sweeping + ? (l?.calmBreathingStop ?? 'Stop') + : (l?.calmBreathingEndSession ?? 'End session')) + : (_finished + ? (l?.actionDone ?? 'Done') + : (l?.calmBreathingBegin ?? 'Begin')), icon: (_running || _quiet != null) ? LucideIcons.square : LucideIcons.play, @@ -563,22 +642,24 @@ class _Setup extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final app = c.watch(); final yours = agreedPace(app.user?[kPaceWinsKey]); return ListView( children: [ const SizedBox(height: S.x4), - Text('Take a breath.', style: F.t1.copyWith(color: p.ink)), + Text(l?.calmBreathingTakeABreath ?? 'Take a breath.', + style: F.t1.copyWith(color: p.ink)), const SizedBox(height: S.x2), // The buzz is band-dependent and this screen does not yet know whether // the band will accept the session, so it is not promised here. The // `!banded` card during the run is where that gets said. Text( - 'The ring leads. Put the phone down.', + l?.calmBreathingRingLeads ?? 'The ring leads. Put the phone down.', style: F.cap.copyWith(color: p.ink2, height: 1.5), ), - for (final b in patternsFor(yours)) + for (final b in patternsFor(yours, l)) Padding( padding: const EdgeInsets.only(top: S.x3), child: Surface( @@ -603,7 +684,7 @@ class _Setup extends StatelessWidget { ), if (b.coherenceRated) ...[ const SizedBox(width: S.x2), - const Pill('Scored', C.domMind), + Pill(l?.calmBreathingScoredPill ?? 'Scored', C.domMind), ], ], ), @@ -625,7 +706,7 @@ class _Setup extends StatelessWidget { ), ), Section( - 'How long', + l?.calmBreathingHowLong ?? 'How long', Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -635,7 +716,8 @@ class _Setup extends StatelessWidget { Expanded( child: Pressable( onTap: () => onMinutes(m), - semanticLabel: '$m minutes', + semanticLabel: + l?.calmBreathingMinutesSemantic(m) ?? '$m minutes', child: Container( padding: const EdgeInsets.symmetric(vertical: S.x3), decoration: BoxDecoration( @@ -644,7 +726,7 @@ class _Setup extends StatelessWidget { ), child: Center( child: Text( - '$m min', + l?.calmBreathingMinutesAbbrev(m) ?? '$m min', style: F.body.copyWith( color: m == minutes ? p.inkOnFill : p.ink2, ), @@ -668,7 +750,8 @@ class _Setup extends StatelessWidget { // and a comparison, which is not what someone who opened this screen // to breathe came for; it lives one tap away rather than as a fourth // thing to read before beginning. - Section('Your own pace', _sweepDoor(c, p, app.isConnected, yours)), + Section(l?.calmBreathingYourOwnPace ?? 'Your own pace', + _sweepDoor(c, p, app.isConnected, yours)), ], ); } @@ -681,12 +764,14 @@ class _Setup extends StatelessWidget { /// is one sentence about a run of sessions — never this session, never a /// delta, never a count that only goes up. Widget _windowRow(BuildContext c, P p, bool connected) { + final l = AppLocalizations.of(c); final e = effect; final on = windows && connected; return Surface( onTap: connected ? () => onWindows(!windows) : null, color: on ? p.wash(C.domMind) : null, - semanticLabel: 'Measure before and after, adds four minutes', + semanticLabel: l?.calmBreathingWindowRowSemantic ?? + 'Measure before and after, adds four minutes', child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -701,7 +786,8 @@ class _Setup extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Measure before and after · adds 4 min', + l?.calmBreathingMeasureBeforeAfter ?? + 'Measure before and after · adds 4 min', style: F.body.copyWith( color: connected ? p.ink : p.ink3, fontWeight: FontWeight.w600, @@ -710,8 +796,9 @@ class _Setup extends StatelessWidget { const SizedBox(height: S.x1), Text( !connected - ? 'Needs the band on — the comparison is made from beat ' - 'timing.' + ? (l?.calmBreathingNeedsBandBeatTiming ?? + 'Needs the band on — the comparison is made from ' + 'beat timing.') : breathingEffectLine(e ?? _noSessionsYet), style: F.cap.copyWith(color: p.ink3, height: 1.4), ), @@ -738,7 +825,9 @@ class _Setup extends StatelessWidget { P p, bool connected, double? yours, - ) => Surface( + ) { + final l = AppLocalizations.of(c); + return Surface( // Not offered without a band: the entire output is a comparison of // beat timing, so an unbanded sweep is six minutes taken for nothing. onTap: connected ? onSweep : null, @@ -749,7 +838,7 @@ class _Setup extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Find the pace your heart follows', + l?.calmBreathingFindYourPace ?? 'Find the pace your heart follows', style: F.body.copyWith( color: connected ? p.ink : p.ink3, fontWeight: FontWeight.w600, @@ -758,17 +847,22 @@ class _Setup extends StatelessWidget { const SizedBox(height: S.x1), Text( !connected - ? 'Needs the band on — the comparison is made from ' - 'beat timing.' + ? (l?.calmBreathingNeedsBandBeatTiming ?? + 'Needs the band on — the comparison is made from ' + 'beat timing.') : yours == null - ? 'Six minutes: ' - '${kPaceSweepRates.map((r) => r.toStringAsFixed(1)).join(', ')} ' - 'breaths a minute, two minutes each. It takes ' - 'two sittings that agree before anything changes.' - : 'Two sittings agreed on ' - '${yours.toStringAsFixed(1)} breaths a minute, ' - 'and Resonance is paced there. Run it again to ' - 'check.', + ? (l?.calmBreathingSweepIntro(kPaceSweepRates + .map((r) => r.toStringAsFixed(1)) + .join(', ')) ?? + 'Six minutes: ' + '${kPaceSweepRates.map((r) => r.toStringAsFixed(1)).join(', ')} ' + 'breaths a minute, two minutes each. It takes ' + 'two sittings that agree before anything changes.') + : (l?.calmBreathingSweepAgreed(yours.toStringAsFixed(1)) ?? + 'Two sittings agreed on ' + '${yours.toStringAsFixed(1)} breaths a minute, ' + 'and Resonance is paced there. Run it again to ' + 'check.'), style: F.cap.copyWith(color: p.ink3, height: 1.4), ), ], @@ -781,6 +875,7 @@ class _Setup extends StatelessWidget { ], ), ); + } } /// What the row says before the history has loaded — the same thing it says @@ -810,6 +905,7 @@ class _Running extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final at = phaseAt(pattern, elapsed); final kind = at?.phase.kind ?? BreathPhaseKind.inhale; @@ -822,26 +918,30 @@ class _Running extends StatelessWidget { children: [ if (block != null) ...[ Text( - 'PACE ${block! + 1} OF ${kPaceSweepRates.length} · ' - '${pattern.rate.toStringAsFixed(1)} BREATHS A MINUTE', + l?.calmBreathingPaceOfRate( + block! + 1, kPaceSweepRates.length, pattern.rate.toStringAsFixed(1)) ?? + 'PACE ${block! + 1} OF ${kPaceSweepRates.length} · ' + '${pattern.rate.toStringAsFixed(1)} BREATHS A MINUTE', textAlign: TextAlign.center, style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x5), ], - BreathCircle(t: t, label: kind.label), + BreathCircle(t: t, label: breathPhaseKindLabel(kind, l)), const SizedBox(height: S.x8), Text(_clock(elapsed), style: F.n34.copyWith(color: p.ink2)), if (target != null) ...[ const SizedBox(height: S.x1), - Text('of ${_clock(target!)}', style: F.cap.copyWith(color: p.ink3)), + Text(l?.calmBreathingOfClock(_clock(target!)) ?? 'of ${_clock(target!)}', + style: F.cap.copyWith(color: p.ink3)), ], if (!banded) ...[ const SizedBox(height: S.x6), - const StatusCard( - 'No coherence score for this session', - 'Scoring needs beat timing from the band. Not connected, so this ' - 'one paces you but is not saved.', + StatusCard( + l?.calmBreathingNoScoreForSession ?? 'No coherence score for this session', + l?.calmBreathingScoringNeedsBand ?? + 'Scoring needs beat timing from the band. Not connected, so ' + 'this one paces you but is not saved.', icon: LucideIcons.bluetoothOff, ), ], @@ -867,6 +967,7 @@ class _Quiet extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final pre = phase == 'pre'; return Column( @@ -874,20 +975,25 @@ class _Quiet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - pre ? 'BEFORE' : 'AFTER', + pre + ? (l?.calmBreathingBeforeLabel ?? 'BEFORE') + : (l?.calmBreathingAfterLabel ?? 'AFTER'), textAlign: TextAlign.center, style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x4), Text( - pre ? 'Sit still for a moment.' : 'Stay sitting.', + pre + ? (l?.calmBreathingSitStill ?? 'Sit still for a moment.') + : (l?.calmBreathingStaySitting ?? 'Stay sitting.'), textAlign: TextAlign.center, style: F.t2.copyWith(color: p.ink), ), const SizedBox(height: S.x3), Text( - 'Breathe however you normally would. Nothing is pacing you and ' - 'nothing is being scored.', + l?.calmBreathingNothingPacingScored ?? + 'Breathe however you normally would. Nothing is pacing you and ' + 'nothing is being scored.', textAlign: TextAlign.center, style: F.cap.copyWith(color: p.ink2, height: 1.5), ), @@ -899,7 +1005,8 @@ class _Quiet extends StatelessWidget { ), const SizedBox(height: S.x1), Text( - 'of ${_clock(kBreathingWindow)}', + l?.calmBreathingOfClock(_clock(kBreathingWindow)) ?? + 'of ${_clock(kBreathingWindow)}', textAlign: TextAlign.center, style: F.cap.copyWith(color: p.ink3), ), @@ -960,6 +1067,7 @@ class _Result extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final app = c.watch(); final raw = app.breathingResult; @@ -975,18 +1083,22 @@ class _Result extends StatelessWidget { ? null : Metric.parse({...raw, 'value': raw['score']}); final absent = StatusCard.forMetric( - 'No coherence score for this session', + l?.calmBreathingNoScoreForSession ?? 'No coherence score for this session', m, why: !rated - ? '${app.breathingPattern.label} is not scored. Resonance is the one ' - 'paced at the rate the score is built for.' + ? (l?.calmBreathingPatternNotScored(app.breathingPattern.label) ?? + '${app.breathingPattern.label} is not scored. Resonance is the ' + 'one paced at the rate the score is built for.') : app.breathingError ?? - 'Too few clean beat timings across the session to score it.', + (l?.calmBreathingTooFewBeatTimings ?? + 'Too few clean beat timings across the session to score ' + 'it.'), ); return ListView( children: [ const SizedBox(height: S.x8), - Text('That is done.', style: F.t1.copyWith(color: p.ink)), + Text(l?.calmBreathingThatIsDone ?? 'That is done.', + style: F.t1.copyWith(color: p.ink)), const SizedBox(height: S.x5), if (absent != null) absent @@ -994,9 +1106,10 @@ class _Result extends StatelessWidget { SignalCard( LucideIcons.wind, C.domMind, - 'Cardiac coherence', + l?.calmBreathingCardiacCoherence ?? 'Cardiac coherence', m!.value!.toStringAsFixed(0), - sub: 'HOW STRONGLY YOUR HEART RATE FOLLOWED THE PACE', + sub: l?.calmBreathingHowStronglyFollowedPace ?? + 'HOW STRONGLY YOUR HEART RATE FOLLOWED THE PACE', ), ], ); @@ -1023,6 +1136,7 @@ class _SweepResult extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final app = c.watch(); final winner = aborted ? null : sweepWinner(kPaceSweepRates, scores); @@ -1031,7 +1145,9 @@ class _SweepResult extends StatelessWidget { children: [ const SizedBox(height: S.x8), Text( - aborted ? 'Stopped there.' : 'That is done.', + aborted + ? (l?.calmBreathingStoppedThere ?? 'Stopped there.') + : (l?.calmBreathingThatIsDone ?? 'That is done.'), style: F.t1.copyWith(color: p.ink), ), const SizedBox(height: S.x5), @@ -1040,7 +1156,8 @@ class _SweepResult extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'HOW STRONGLY YOUR HEART RATE FOLLOWED EACH PACE', + l?.calmBreathingHowStronglyEachPace ?? + 'HOW STRONGLY YOUR HEART RATE FOLLOWED EACH PACE', style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x3), @@ -1051,8 +1168,10 @@ class _SweepResult extends StatelessWidget { children: [ Expanded( child: Text( - '${kPaceSweepRates[i].toStringAsFixed(1)} breaths a ' - 'minute', + l?.calmBreathingBreathsAMinute( + kPaceSweepRates[i].toStringAsFixed(1)) ?? + '${kPaceSweepRates[i].toStringAsFixed(1)} ' + 'breaths a minute', style: F.body.copyWith( color: winner == kPaceSweepRates[i] ? p.ink @@ -1066,9 +1185,10 @@ class _SweepResult extends StatelessWidget { // things, and both of them are sentences. Text( i >= scores.length - ? 'not reached' + ? (l?.calmBreathingNotReached ?? 'not reached') : scores[i] == null - ? 'too few clean beats' + ? (l?.calmBreathingTooFewCleanBeats ?? + 'too few clean beats') : '${scores[i]}', style: (i < scores.length && scores[i] != null @@ -1084,39 +1204,46 @@ class _SweepResult extends StatelessWidget { ), const SizedBox(height: S.x4), Text( - _verdict(winner, agreed), + _verdict(winner, agreed, l), style: F.body.copyWith(color: p.ink, height: 1.4), ), const SizedBox(height: S.x3), Text( - 'A ranking of three paces from one sitting. The blocks run back to ' - 'back, so each pace is measured while you are still settling out of ' - 'the one before. It says which pace your heart rate followed most ' - 'strongly, and nothing else.', + l?.calmBreathingRankingExplainer ?? + 'A ranking of three paces from one sitting. The blocks run back ' + 'to back, so each pace is measured while you are still settling ' + 'out of the one before. It says which pace your heart rate ' + 'followed most strongly, and nothing else.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], ); } - String _verdict(double? winner, double? agreed) { + String _verdict(double? winner, double? agreed, AppLocalizations? l) { if (aborted) { - return 'You stopped part way, so there was nothing to compare. Nothing ' - 'has changed.'; + return l?.calmBreathingVerdictAborted ?? + 'You stopped part way, so there was nothing to compare. Nothing ' + 'has changed.'; } if (winner == null) { return scores.any((s) => s == null) - ? 'At least one pace could not be scored, so there is nothing to ' - 'rank. Nothing has changed.' - : 'Two of the paces scored the same, so this sitting cannot ' - 'separate them. Nothing has changed.'; + ? (l?.calmBreathingVerdictCouldNotScore ?? + 'At least one pace could not be scored, so there is nothing to ' + 'rank. Nothing has changed.') + : (l?.calmBreathingVerdictTied ?? + 'Two of the paces scored the same, so this sitting cannot ' + 'separate them. Nothing has changed.'); } - final w = '${winner.toStringAsFixed(1)} breaths a minute'; + final w = l?.calmBreathingBreathsAMinute(winner.toStringAsFixed(1)) ?? + '${winner.toStringAsFixed(1)} breaths a minute'; return agreed == winner - ? 'Of the paces tested, $w gave your strongest response — and that is ' - 'now two sittings in a row. Resonance is paced there.' - : 'Of the paces tested, $w gave your strongest response. Nothing is ' - 'set yet: the pace only changes when two sittings pick the same ' - 'one.'; + ? (l?.calmBreathingVerdictConfirmed(w) ?? + 'Of the paces tested, $w gave your strongest response — and that ' + 'is now two sittings in a row. Resonance is paced there.') + : (l?.calmBreathingVerdictFirstWin(w) ?? + 'Of the paces tested, $w gave your strongest response. Nothing ' + 'is set yet: the pace only changes when two sittings pick the ' + 'same one.'); } } diff --git a/lib/ui2/screens/circadian_detail.dart b/lib/ui2/screens/circadian_detail.dart index cf850b81..d26c4fbe 100644 --- a/lib/ui2/screens/circadian_detail.dart +++ b/lib/ui2/screens/circadian_detail.dart @@ -23,6 +23,7 @@ import 'package:openstrap_analytics/onehz.dart' as ana; import '../../data/day_label.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../ui2.dart'; import 'home_screen.dart'; @@ -311,25 +312,26 @@ class _CircadianDetailState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d ?? const CircadianData(); final drawn = d.actogram.where((e) => e != null).length; - return detailScaffold(c, 'Body clock', [ + return detailScaffold(c, l?.circadianDetailTitle ?? 'Body clock', [ if (_loading && _d == null) ...[ const SizedBox(height: S.x8), const Center(child: CircularProgressIndicator()), ] else ...[ if (drawn == 0) - const StatusCard( - 'No nights to plot yet', - '0 nights scored.', - fix: 'Wear the band overnight', + StatusCard( + l?.circadianDetailNoNightsTitle ?? 'No nights to plot yet', + l?.circadianDetailNoNightsBody ?? '0 nights scored.', + fix: l?.circadianDetailNoNightsFix ?? 'Wear the band overnight', icon: LucideIcons.calendarClock, ) else Surface( child: ChartFrame( - title: 'Sleep, night by night', + title: l?.circadianDetailSleepTitle ?? 'Sleep, night by night', // Hour of day is what the vertical axis IS. The old card replaced // the axis with a sentence describing it. unit: 'hour of day', @@ -344,9 +346,10 @@ class _CircadianDetailState extends State { xLabels: d.labels.isEmpty ? const [] : [d.labels.first, d.labels.last], - legend: const [('Asleep', C.indigo)], - footnote: '$drawn night${drawn == 1 ? '' : 's'}, one column each. ' - 'Darker is more of that hour asleep.', + legend: [(l?.circadianDetailAsleep ?? 'Asleep', C.indigo)], + footnote: l?.circadianDetailSleepFootnote(drawn) ?? + '$drawn night${drawn == 1 ? '' : 's'}, one column each. ' + 'Darker is more of that hour asleep.', child: CustomPaint( size: Size.infinite, painter: Actogram(d.actogram, p.on(C.indigo)), @@ -357,11 +360,13 @@ class _CircadianDetailState extends State { // SLP-08 rides in this section's action, so the screen gains a tap // rather than two permanent rows. Section( - 'Your rhythm', + l?.circadianDetailYourRhythm ?? 'Your rhythm', _rhythm(c, p, d), action: d.sriPairs.isEmpty || d.regularity.value == null ? null - : (_showNights ? 'Hide' : 'Which nights'), + : (_showNights + ? (l?.circadianDetailHide ?? 'Hide') + : (l?.circadianDetailWhichNights ?? 'Which nights')), onAction: d.sriPairs.isEmpty || d.regularity.value == null ? null : () => setState(() => _showNights = !_showNights), @@ -369,7 +374,8 @@ class _CircadianDetailState extends State { // MIND-11 sits directly under the measured rhythm because it is built // on it — and directly above the battery it borrows the acrophase from. - if (_forecast(c, p, d) case final f?) Section('Today, predicted', f), + if (_forecast(c, p, d) case final f?) + Section(l?.circadianDetailTodayPredicted ?? 'Today, predicted', f), // COLLAPSED BY DEFAULT, and that is how the screen paid for the card // above. Interdaily stability, intradaily variability, relative @@ -378,9 +384,11 @@ class _CircadianDetailState extends State { // Nothing is lost: the section, its title and its own empty state are // unchanged one tap away. Section( - 'Rhythm strength', + l?.circadianDetailRhythmStrength ?? 'Rhythm strength', _showStrength ? _strength(c, p, d) : const SizedBox.shrink(), - action: _showStrength ? 'Hide' : 'Show', + action: _showStrength + ? (l?.circadianDetailHide ?? 'Hide') + : (l?.circadianDetailShow ?? 'Show'), onAction: () => setState(() => _showStrength = !_showStrength), ), @@ -390,7 +398,8 @@ class _CircadianDetailState extends State { // magnitude the card used to assert "later" from — is on the Social // jetlag row itself now, and the night counts are beside it. One card // off, so the hourly row below can go on. - Section('When you are still', _stillness(c, p, d)), + Section(l?.circadianDetailWhenStill ?? 'When you are still', + _stillness(c, p, d)), ], ]); } @@ -414,16 +423,18 @@ class _CircadianDetailState extends State { /// median and never today's, no colour, no band, no verdict, and an hour /// with too few quiet stretches behind it is absent rather than drawn faint. Widget _stillness(BuildContext c, P p, CircadianData d) { + final l = AppLocalizations.of(c); final have = [for (final v in d.hourly) ?v]; if (have.isEmpty) { return StatusCard( - 'No still moments to read yet', + l?.circadianDetailNoStillTitle ?? 'No still moments to read yet', d.hourlyNote?.isNotEmpty == true ? d.hourlyNote! - : 'This reads beat timing only from the seconds you were not ' - 'moving, and the last ' - '${d.hourlyDays} day${d.hourlyDays == 1 ? '' : 's'} had too ' - 'few of them to build an hour from.', + : (l?.circadianDetailNoStillBody(d.hourlyDays) ?? + 'This reads beat timing only from the seconds you were not ' + 'moving, and the last ' + '${d.hourlyDays} day${d.hourlyDays == 1 ? '' : 's'} had ' + 'too few of them to build an hour from.'), icon: LucideIcons.activity, ); } @@ -440,7 +451,8 @@ class _CircadianDetailState extends State { final hi = counts.isEmpty ? 0 : counts.last; return Surface( child: ChartFrame( - title: 'Beat-to-beat variability while still', + title: l?.circadianDetailStillnessTitle ?? + 'Beat-to-beat variability while still', unit: 'ms', height: 120, yAxis: axis, @@ -448,12 +460,13 @@ class _CircadianDetailState extends State { // are wall-clock times rather than positions in an array. xLabels: [clock(0), clock(12 * 60), clock(23 * 60)], series: d.hourly, - footnote: 'Each hour is the middle value of $lo–$hi five-minute ' - 'stretches you were actually still, over the last ' - '${d.hourlyDays} day${d.hourlyDays == 1 ? '' : 's'} — never ' - 'today\'s alone. $drawn of 24 hours had at least three stretches; ' - 'the rest are blank. Not a stress score — sitting up, a warm room ' - 'or a coffee move it just as much.', + footnote: l?.circadianDetailStillnessFootnote(lo, hi, d.hourlyDays, drawn) ?? + 'Each hour is the middle value of $lo–$hi five-minute ' + 'stretches you were actually still, over the last ' + '${d.hourlyDays} day${d.hourlyDays == 1 ? '' : 's'} — never ' + 'today\'s alone. $drawn of 24 hours had at least three ' + 'stretches; the rest are blank. Not a stress score — sitting ' + 'up, a warm room or a coffee move it just as much.', child: CustomPaint( size: Size.infinite, // Uncoloured. A hue here would be a verdict about an hour of your @@ -479,13 +492,15 @@ class _CircadianDetailState extends State { /// or unjudged: the analytics gate refuses rather than assuming eight hours, /// and this returns null rather than explaining an absence nobody asked about. Widget? _forecast(BuildContext c, P p, CircadianData d) { + final l = AppLocalizations.of(c); final v = d.alertness.value; if (v == null) return null; final assumedPhase = d.cosinorV['acrophase_hours'] == null; return Surface( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ ChartFrame( - title: 'How today is likely to run', + title: l?.circadianDetailForecastTitle ?? + 'How today is likely to run', // There is no unit. Saying so is more honest than borrowing one, and // the frame renders it in the slot a unit would have occupied. unit: 'shape only', @@ -497,7 +512,8 @@ class _CircadianDetailState extends State { ], // No `series:`. A shape has no reading; handing the frame one would // have it speak numbers off a curve that deliberately has none. - footnote: 'No scale — the shape is the whole output.', + footnote: l?.circadianDetailForecastFootnote ?? + 'No scale — the shape is the whole output.', child: CustomPaint( size: Size.infinite, // p.ink3, like every other mark on this screen that is not a @@ -508,22 +524,28 @@ class _CircadianDetailState extends State { ), const SizedBox(height: S.x3), Text( - 'The flattest stretch lands in ${v.troughLabel}, around ' - '${_hourClock(v.troughStartHour)}–${_hourClock(v.troughEndHour)}.', + l?.circadianDetailTroughText( + v.troughLabel, + _hourClock(v.troughStartHour), + _hourClock(v.troughEndHour), + ) ?? + 'The flattest stretch lands in ${v.troughLabel}, around ' + '${_hourClock(v.troughStartHour)}–${_hourClock(v.troughEndHour)}.', style: F.body.copyWith(color: p.ink, height: 1.5), ), const SizedBox(height: S.x3), Text( - 'This is a prediction, not a reading. Nothing on the band measures ' - 'how alert you are, and it knows last night and nothing else — a ' - 'nap, coffee, or anything that happens today never reaches it.' - '${assumedPhase ? ' Your own clock peak is not worked out yet, so this uses an average one.' : ''}', + '${l?.circadianDetailPredictionDisclaimer ?? 'This is a prediction, not a reading. Nothing on the band measures ' + 'how alert you are, and it knows last night and nothing else — a ' + 'nap, coffee, or anything that happens today never reaches it.'}' + '${assumedPhase ? ' ${l?.circadianDetailAssumedPhaseNote ?? 'Your own clock peak is not worked out yet, so this uses an average one.'}' : ''}', style: F.cap.copyWith(color: p.ink2, height: 1.6), ), const SizedBox(height: S.x3), Text( - 'It is not a fitness-to-drive check and not a shift-safety tool, and ' - 'it does not say you are impaired.', + l?.circadianDetailNotADrivingCheck ?? + 'It is not a fitness-to-drive check and not a shift-safety ' + 'tool, and it does not say you are impaired.', style: F.cap.copyWith(color: p.ink2, height: 1.6), ), ]), @@ -531,37 +553,42 @@ class _CircadianDetailState extends State { } Widget _rhythm(BuildContext c, P p, CircadianData d) { + final l = AppLocalizations.of(c); final worst = d.sriPairs.isEmpty ? null : d.sriPairs.first; final showNights = _showNights && worst != null; final rows = <(String, String)>[ if (d.chronotypeLabel.isNotEmpty) - ('Chronotype', d.chronotypeLabel), + (l?.circadianDetailChronotype ?? 'Chronotype', d.chronotypeLabel), if (d.midFreeH != null) - ('Mid-sleep, free days', _hourClock(d.midFreeH)), + (l?.circadianDetailMidSleepFree ?? 'Mid-sleep, free days', + _hourClock(d.midFreeH)), if (d.midWorkH != null) - ('Mid-sleep, working days', _hourClock(d.midWorkH)), + (l?.circadianDetailMidSleepWork ?? 'Mid-sleep, working days', + _hourClock(d.midWorkH)), // `abs_hours` is UNSIGNED. Whether the free-day clock runs later or // earlier is the sign of free minus work; the card that used to say // "later" read it off the magnitude. if (d.jetlag.value != null) ( - 'Social jetlag', + l?.circadianDetailSocialJetlag ?? 'Social jetlag', '${_hm(d.jetlag.value!)}' - '${d.midFreeH == null || d.midWorkH == null ? '' : (d.midFreeH! >= d.midWorkH! ? ' later' : ' earlier')}', + '${d.midFreeH == null || d.midWorkH == null ? '' : (d.midFreeH! >= d.midWorkH! ? ' ${l?.circadianDetailLater ?? 'later'}' : ' ${l?.circadianDetailEarlier ?? 'earlier'}')}', ), if (d.nFree != null && d.nWork != null) - ('Free / working nights compared', + (l?.circadianDetailNightsCompared ?? 'Free / working nights compared', '${d.nFree!.round()} / ${d.nWork!.round()}'), if (d.regularity.value != null) - ('Regularity index', '${d.regularity.value!.round()} / 100'), + (l?.circadianDetailRegularityIndex ?? 'Regularity index', + '${d.regularity.value!.round()} / 100'), // SLP-08 — the same arithmetic, one level down. The index above is the // average agreement across every adjacent pair of nights; these two rows // name the pair that agreed least and print it on the same scale. if (showNights) - ('Nights least alike', + (l?.circadianDetailNightsLeastAlike ?? 'Nights least alike', '${_shortDay(worst['prev_date'])} → ${_shortDay(worst['date'])}'), if (showNights) - ('That pair, same scale', '${(worst['sri'] as num).round()} / 100'), + (l?.circadianDetailSamePairScale ?? 'That pair, same scale', + '${(worst['sri'] as num).round()} / 100'), ]; if (rows.isEmpty) { @@ -569,7 +596,9 @@ class _CircadianDetailState extends State { // it is not the one the measured run hit — `no valid epoch pairs` was, // on both gen5 databases, and that note was being overwritten here. return StatusCard.forMetric( - 'Your rhythm is not established yet', d.regularity) ?? + l?.circadianDetailRhythmNotEstablished ?? + 'Your rhythm is not established yet', + d.regularity) ?? const SizedBox.shrink(); } @@ -582,9 +611,11 @@ class _CircadianDetailState extends State { _table(p, rows), const SizedBox(height: S.x3), Text( - 'The pair that matched least, out of ${d.sriPairs.length}. A weekend ' - 'that runs late is a different schedule, not a worse night. Pairs ' - 'where too little of either day was recorded are left out.', + l?.circadianDetailPairFootnote(d.sriPairs.length) ?? + 'The pair that matched least, out of ${d.sriPairs.length}. A ' + 'weekend that runs late is a different schedule, not a worse ' + 'night. Pairs where too little of either day was recorded ' + 'are left out.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ]); @@ -596,33 +627,45 @@ class _CircadianDetailState extends State { /// mean something different because of it — so the footnote is not optional /// decoration, it is the unit. Widget _strength(BuildContext c, P p, CircadianData d) { + final l = AppLocalizations.of(c); final np = d.rhythmV, cos = d.cosinorV; num? n(Map m, String k) => m[k] as num?; final rows = <(String, String)>[ if (n(np, 'IS') != null) - ('Day-to-day stability', n(np, 'IS')!.toStringAsFixed(2)), + (l?.circadianDetailStability ?? 'Day-to-day stability', + n(np, 'IS')!.toStringAsFixed(2)), if (n(np, 'IV') != null) - ('Hour-to-hour fragmentation', n(np, 'IV')!.toStringAsFixed(2)), + (l?.circadianDetailFragmentation ?? 'Hour-to-hour fragmentation', + n(np, 'IV')!.toStringAsFixed(2)), if (n(np, 'RA') != null) - ('Relative amplitude', n(np, 'RA')!.toStringAsFixed(2)), + (l?.circadianDetailAmplitude ?? 'Relative amplitude', + n(np, 'RA')!.toStringAsFixed(2)), if (n(np, 'm10_start_epoch') != null) - ('Highest-HR 10 hours start', _hourClock(n(np, 'm10_start_epoch'))), + (l?.circadianDetailM10Start ?? 'Highest-HR 10 hours start', + _hourClock(n(np, 'm10_start_epoch'))), if (n(np, 'l5_start_epoch') != null) - ('Lowest-HR 5 hours start', _hourClock(n(np, 'l5_start_epoch'))), + (l?.circadianDetailL5Start ?? 'Lowest-HR 5 hours start', + _hourClock(n(np, 'l5_start_epoch'))), if (n(cos, 'acrophase_hours') != null) - ('Rhythm peak', _hourClock(n(cos, 'acrophase_hours'))), + (l?.circadianDetailRhythmPeak ?? 'Rhythm peak', + _hourClock(n(cos, 'acrophase_hours'))), if (n(cos, 'amplitude') != null) - ('Peak-to-mean swing', '${n(cos, 'amplitude')!.toStringAsFixed(1)} bpm'), + (l?.circadianDetailPeakSwing ?? 'Peak-to-mean swing', + '${n(cos, 'amplitude')!.toStringAsFixed(1)} bpm'), if (n(cos, 'r2_adj') != null) - ('Fit to a 24 h curve', n(cos, 'r2_adj')!.toStringAsFixed(2)), + (l?.circadianDetailFitCurve ?? 'Fit to a 24 h curve', + n(cos, 'r2_adj')!.toStringAsFixed(2)), ]; if (rows.isEmpty) { - return StatusCard.forMetric('Rhythm strength is not measured yet', + return StatusCard.forMetric( + l?.circadianDetailStrengthNotMeasured ?? + 'Rhythm strength is not measured yet', d.rhythm, unit: 'days', - why: 'Needs consecutive days with all 24 hours recorded.') ?? + why: l?.circadianDetailStrengthWhy ?? + 'Needs consecutive days with all 24 hours recorded.') ?? const SizedBox.shrink(); } @@ -631,9 +674,15 @@ class _CircadianDetailState extends State { _table(p, rows), const SizedBox(height: S.x3), Text( - 'From ${used == null ? 'a run of' : '$used'} fully-recorded ' - 'day${used == 1 ? '' : 's'} of heart rate. These are your highest and ' - 'lowest heart-rate hours, not your busiest.', + used == null + ? (l?.circadianDetailStrengthFootnoteUnknown ?? + 'From a run of fully-recorded days of heart rate. These are ' + 'your highest and lowest heart-rate hours, not your ' + 'busiest.') + : (l?.circadianDetailStrengthFootnoteKnown(used) ?? + 'From $used fully-recorded day${used == 1 ? '' : 's'} of ' + 'heart rate. These are your highest and lowest ' + 'heart-rate hours, not your busiest.'), style: F.over.copyWith(color: p.ink3, height: 1.5), ), ]); diff --git a/lib/ui2/screens/coach.dart b/lib/ui2/screens/coach.dart index 1af905ad..d9d24cc0 100644 --- a/lib/ui2/screens/coach.dart +++ b/lib/ui2/screens/coach.dart @@ -22,6 +22,7 @@ import 'package:gpt_markdown/gpt_markdown.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; +import '../../l10n/app_localizations.dart'; import '../../ai/briefing.dart' show currentBriefingPeriod; import '../../coach/coach_config.dart'; import '../../coach/coach_engine.dart'; @@ -65,7 +66,9 @@ bool coachReady(BuildContext c) { String? coachSubtitle(BuildContext c) { try { final cfg = c.watch(); - return cfg.configured ? cfg.model : 'Not set up'; + return cfg.configured + ? cfg.model + : (AppLocalizations.of(c)?.coachNotSetUp ?? 'Not set up'); } catch (_) { return null; } @@ -86,14 +89,17 @@ class _CoachScreenState extends State { bool _busy = false; String? _status; - static const _starters = [ - 'How recovered am I today, and why?', - 'Chart my HRV over the last month', - 'How has my sleep been this week?', - 'What did I eat yesterday?', - 'Log 500 ml of water for today', - 'I ran for 40 minutes this morning — log it', - ]; + static List _starters(BuildContext c) { + final l = AppLocalizations.of(c); + return [ + l?.coachStarterRecovery ?? 'How recovered am I today, and why?', + l?.coachStarterHrvChart ?? 'Chart my HRV over the last month', + l?.coachStarterSleep ?? 'How has my sleep been this week?', + l?.coachStarterAteYesterday ?? 'What did I eat yesterday?', + l?.coachStarterLogWater ?? 'Log 500 ml of water for today', + l?.coachStarterLogRun ?? 'I ran for 40 minutes this morning — log it', + ]; + } @override void initState() { @@ -160,7 +166,10 @@ class _CoachScreenState extends State { setState(() { _items.add( CoachItem.error( - e is CoachException ? e.message : 'Something went wrong: $e', + e is CoachException + ? e.message + : (AppLocalizations.of(context)?.coachSomethingWrong('$e') ?? + 'Something went wrong: $e'), ), ); }); @@ -184,6 +193,7 @@ class _CoachScreenState extends State { Future _confirm(ActionRequest req) async { final destructive = req.tool.startsWith('delete_'); final p = P.of(context); + final l = AppLocalizations.of(context); final ok = await showDialog( context: context, builder: (d) => AlertDialog( @@ -200,8 +210,9 @@ class _CoachScreenState extends State { const SizedBox(height: S.x3), Text( destructive - ? 'This removes data from this device and cannot be undone.' - : 'Nothing is written until you tap below.', + ? (l?.coachDestructiveWarning ?? + 'This removes data from this device and cannot be undone.') + : (l?.coachSafeWarning ?? 'Nothing is written until you tap below.'), style: F.cap.copyWith(color: p.ink3), ), ], @@ -209,12 +220,12 @@ class _CoachScreenState extends State { actions: [ TextButton( onPressed: () => Navigator.of(d).pop(false), - child: Text('Cancel', style: F.body.copyWith(color: p.ink2)), + child: Text(l?.actionCancel ?? 'Cancel', style: F.body.copyWith(color: p.ink2)), ), TextButton( onPressed: () => Navigator.of(d).pop(true), child: Text( - destructive ? 'Delete it' : 'Save it', + destructive ? (l?.coachDeleteIt ?? 'Delete it') : (l?.coachSaveIt ?? 'Save it'), style: F.body.copyWith( color: p.on(destructive ? C.red : kCoachAccent), fontWeight: FontWeight.w600, @@ -259,6 +270,7 @@ class _CoachScreenState extends State { Future _menu() async { final engine = _engine; final p = P.of(context); + final l = AppLocalizations.of(context); await showModalBottomSheet( context: context, backgroundColor: p.card, @@ -272,7 +284,7 @@ class _CoachScreenState extends State { children: [ _MenuRow( LucideIcons.plus, - 'New chat', + l?.coachNewChat ?? 'New chat', onTap: () { Navigator.of(sheet).pop(); _newChat(); @@ -287,8 +299,8 @@ class _CoachScreenState extends State { // which is a recovery state rather than a settings entry. _MenuRow( LucideIcons.fileText, - 'Briefing, and what was sent', - sub: 'The exact snapshot that left this device', + l?.coachBriefingMenuTitle ?? 'Briefing, and what was sent', + sub: l?.coachBriefingMenuSub ?? 'The exact snapshot that left this device', onTap: () { Navigator.of(sheet).pop(); go( @@ -302,7 +314,7 @@ class _CoachScreenState extends State { if (engine != null) ...[ const SizedBox(height: S.x4), Text( - 'PAST CHATS', + l?.coachPastChats ?? 'PAST CHATS', style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x2), @@ -312,7 +324,8 @@ class _CoachScreenState extends State { final list = snap.data ?? const []; if (list.isEmpty) { return Text( - 'Nothing yet — this is your first conversation.', + l?.coachNoChatsYet ?? + 'Nothing yet — this is your first conversation.', style: F.cap.copyWith(color: p.ink3), ); } @@ -321,7 +334,7 @@ class _CoachScreenState extends State { for (final s in list.take(20)) _MenuRow( LucideIcons.messageSquare, - s.title.isEmpty ? 'Untitled chat' : s.title, + s.title.isEmpty ? (l?.coachUntitledChat ?? 'Untitled chat') : s.title, sub: s.preview, onTap: () { Navigator.of(sheet).pop(); @@ -349,6 +362,7 @@ class _CoachScreenState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final cfg = c.watch(); return Scaffold( backgroundColor: p.bg, @@ -358,10 +372,10 @@ class _CoachScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), child: NavBar( - 'Coach', - sub: cfg.configured ? cfg.model : 'Not set up', + l?.coachNavTitle ?? 'Coach', + sub: cfg.configured ? cfg.model : (l?.coachNotSetUp ?? 'Not set up'), trailing: Pressable( - semanticLabel: 'Chats and AI settings', + semanticLabel: l?.coachMenuSemantic ?? 'Chats and AI settings', onTap: _menu, child: Icon(LucideIcons.ellipsis, size: 22, color: p.ink), ), @@ -399,6 +413,7 @@ class _CoachScreenState extends State { } Widget _body(BuildContext c, P p, CoachConfig cfg) { + final l = AppLocalizations.of(c); // The key IS saved, this process just could not read it. Showing the setup // wall here would tell the user their key is gone and invite them to paste // it again. @@ -408,10 +423,11 @@ class _CoachScreenState extends State { children: [ const SizedBox(height: S.x4), StatusCard( - 'Your key is still saved', - 'It could not be read from the keychain this time, which happens ' - 'when the app is woken while the phone is locked.', - fix: 'Try again', + l?.coachKeyStillSavedTitle ?? 'Your key is still saved', + l?.coachKeyStillSavedBody ?? + 'It could not be read from the keychain this time, which happens ' + 'when the app is woken while the phone is locked.', + fix: l?.coachTryAgainFix ?? 'Try again', icon: LucideIcons.lock, onFix: () async { await cfg.refreshKeyOnResume(); @@ -427,11 +443,12 @@ class _CoachScreenState extends State { children: [ const SizedBox(height: S.x4), StatusCard( - 'The coach is not set up', - 'It runs on a model you choose — one on your own machine, or any ' - 'OpenAI-compatible provider with your own key. Nothing goes ' - 'through OpenStrap either way.', - fix: 'Choose a model', + l?.coachNotSetUpTitle ?? 'The coach is not set up', + l?.coachNotSetUpBody ?? + 'It runs on a model you choose — one on your own machine, or any ' + 'OpenAI-compatible provider with your own key. Nothing goes ' + 'through OpenStrap either way.', + fix: l?.coachChooseModelFix ?? 'Choose a model', icon: LucideIcons.sparkles, onFix: () => go(c, const CoachSetup()), ), @@ -443,10 +460,11 @@ class _CoachScreenState extends State { padding: pad, children: [ const SizedBox(height: S.x4), - const StatusCard( - 'No data to read yet', - 'The coach answers from your own derived days, and there are none ' - 'on this device yet.', + StatusCard( + l?.coachNoDataTitle ?? 'No data to read yet', + l?.coachNoDataBody ?? + 'The coach answers from your own derived days, and there are none ' + 'on this device yet.', icon: LucideIcons.database, ), ], @@ -472,28 +490,29 @@ class _CoachScreenState extends State { ), const SizedBox(width: S.x2), Text( - 'YOUR DATA, YOUR MODEL', + l?.coachYourDataYourModel ?? 'YOUR DATA, YOUR MODEL', style: F.over.copyWith(color: p.on(kCoachAccent)), ), ], ), const SizedBox(height: S.x3), Text( - 'Ask about anything the app measures, and it can log food, ' - 'water, workouts, doses and how you felt — always asking ' - 'first.', + l?.coachIntroBody ?? + 'Ask about anything the app measures, and it can log food, ' + 'water, workouts, doses and how you felt — always asking ' + 'first.', style: F.body.copyWith(color: p.ink, height: 1.45), ), ], ), ), Section( - 'Try asking', + l?.coachTryAsking ?? 'Try asking', Surface( pad: const EdgeInsets.symmetric(vertical: S.x1), child: Column( children: [ - for (final s in _starters) + for (final s in _starters(c)) Pressable( onTap: () => _send(s), child: Padding( @@ -533,7 +552,9 @@ class _CoachScreenState extends State { ); } - Widget _composer(BuildContext c, P p) => Padding( + Widget _composer(BuildContext c, P p) { + final l = AppLocalizations.of(c); + return Padding( padding: EdgeInsets.fromLTRB( S.x4, S.x2, @@ -555,7 +576,7 @@ class _CoachScreenState extends State { border: Border.all(color: p.line), ), child: Semantics( - label: 'Ask the coach', + label: l?.coachAskLabel ?? 'Ask the coach', textField: true, child: TextField( controller: _input, @@ -569,7 +590,7 @@ class _CoachScreenState extends State { decoration: InputDecoration( isDense: true, border: InputBorder.none, - hintText: 'Ask about your health…', + hintText: l?.coachInputHint ?? 'Ask about your health…', hintStyle: F.body.copyWith(color: p.ink3), ), ), @@ -578,7 +599,7 @@ class _CoachScreenState extends State { ), const SizedBox(width: S.x2), Pressable( - semanticLabel: 'Send', + semanticLabel: l?.coachSendLabel ?? 'Send', onTap: _busy ? null : () => _send(_input.text), child: Container( width: 44, @@ -597,6 +618,7 @@ class _CoachScreenState extends State { ], ), ); + } } /// One transcript entry. The user's turn is a soft bubble; the answer is the @@ -651,7 +673,7 @@ class _Bubble extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: S.x4), child: StatusCard( - 'That did not go through', + AppLocalizations.of(c)?.coachErrorTitle ?? 'That did not go through', item.text ?? '', icon: LucideIcons.triangleAlert, ), @@ -707,7 +729,8 @@ class _MenuRow extends StatelessWidget { ), if (onRemove != null) Pressable( - semanticLabel: 'Delete $title', + semanticLabel: + AppLocalizations.of(c)?.coachDeleteChat(title) ?? 'Delete $title', onTap: onRemove, child: Icon(LucideIcons.trash2, size: 16, color: p.ink3), ), @@ -729,23 +752,17 @@ class _Preset { const _Preset(this.label, this.sub, this.baseUrl, {this.local = false}); } -const _presets = <_Preset>[ - _Preset( - 'Ollama', - 'On this network. Nothing leaves your machine.', - 'http://localhost:11434/v1', - local: true, - ), - _Preset( - 'LM Studio', - 'On this network. Nothing leaves your machine.', - 'http://localhost:1234/v1', - local: true, - ), - _Preset('OpenAI', 'api.openai.com', 'https://api.openai.com/v1'), - _Preset('Anthropic', 'api.anthropic.com', 'https://api.anthropic.com/v1'), - _Preset('OpenRouter', 'openrouter.ai', 'https://openrouter.ai/api/v1'), -]; +List<_Preset> _presets(BuildContext c) { + final local = AppLocalizations.of(c)?.coachLocalSub ?? + 'On this network. Nothing leaves your machine.'; + return <_Preset>[ + _Preset('Ollama', local, 'http://localhost:11434/v1', local: true), + _Preset('LM Studio', local, 'http://localhost:1234/v1', local: true), + _Preset('OpenAI', 'api.openai.com', 'https://api.openai.com/v1'), + _Preset('Anthropic', 'api.anthropic.com', 'https://api.anthropic.com/v1'), + _Preset('OpenRouter', 'openrouter.ai', 'https://openrouter.ai/api/v1'), + ]; +} class CoachSetup extends StatefulWidget { const CoachSetup({super.key}); @@ -803,18 +820,21 @@ class _CoachSetupState extends State { try { final ids = await CoachEngine.fetchModels(_base.text, _key.text); if (!mounted) return; + final l = AppLocalizations.of(context); setState(() { _models = ids; _msg = ids.isEmpty - ? 'That endpoint listed no models. Type one below instead.' - : '${ids.length} models. Tap one.'; + ? (l?.coachNoModelsListed ?? + 'That endpoint listed no models. Type one below instead.') + : (l?.coachModelsFound(ids.length) ?? '${ids.length} models. Tap one.'); }); } catch (e) { if (!mounted) return; + final l = AppLocalizations.of(context); setState( () => _msg = e is CoachException ? e.message - : 'Could not reach that endpoint: $e', + : (l?.coachEndpointUnreachable('$e') ?? 'Could not reach that endpoint: $e'), ); } finally { if (mounted) setState(() => _loading = false); @@ -823,8 +843,9 @@ class _CoachSetupState extends State { Future _save() async { final chosen = _model.isNotEmpty ? _model : _search.text.trim(); + final l = AppLocalizations.of(context); if (chosen.isEmpty) { - setState(() => _msg = 'Pick or type a model first.'); + setState(() => _msg = l?.coachPickModelFirst ?? 'Pick or type a model first.'); return; } final cfg = context.read(); @@ -840,7 +861,10 @@ class _CoachSetupState extends State { model: chosen, ); } catch (e) { - if (mounted) setState(() => _msg = 'The keychain refused the key: $e'); + if (mounted) { + setState(() => + _msg = l?.coachKeychainRefused('$e') ?? 'The keychain refused the key: $e'); + } return; } if (mounted && nav.canPop()) nav.pop(); @@ -849,6 +873,7 @@ class _CoachSetupState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final q = _search.text.trim().toLowerCase(); final shown = q.isEmpty ? _models @@ -861,19 +886,20 @@ class _CoachSetupState extends State { body: SafeArea( child: Column( children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('AI settings', sub: 'Bring your own model'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.coachSetupNavTitle ?? 'AI settings', + sub: l?.coachSetupNavSub ?? 'Bring your own model'), ), Expanded( child: ListView( padding: pad, children: [ Section( - 'Where the model runs', + l?.coachWhereModelRuns ?? 'Where the model runs', Column( children: [ - for (final preset in _presets) + for (final preset in _presets(c)) Padding( padding: const EdgeInsets.only(bottom: S.x2), child: Surface( @@ -930,13 +956,15 @@ class _CoachSetupState extends State { const SizedBox(height: S.x2), OsTextField( controller: _base, - label: 'Base URL', + label: l?.coachBaseUrlLabel ?? 'Base URL', hint: 'http://localhost:11434/v1', ), const SizedBox(height: S.x4), OsTextField( controller: _key, - label: _isLocal ? 'API key (not needed locally)' : 'API key', + label: _isLocal + ? (l?.coachApiKeyLocalLabel ?? 'API key (not needed locally)') + : (l?.coachApiKeyLabel ?? 'API key'), hint: 'sk-…', ), const SizedBox(height: S.x3), @@ -945,16 +973,18 @@ class _CoachSetupState extends State { // in a settings page nobody opens. Text( _isLocal - ? 'Your questions and the rows the coach reads stay on ' - 'your own machine.' - : 'Your questions and the rows the coach reads are sent ' - 'to this endpoint. See exactly what that is on ' - '"What was sent".', + ? (l?.coachLocalDataNote ?? + 'Your questions and the rows the coach reads stay on ' + 'your own machine.') + : (l?.coachCloudDataNote ?? + 'Your questions and the rows the coach reads are sent ' + 'to this endpoint. See exactly what that is on ' + '"What was sent".'), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), const SizedBox(height: S.x4), BigButton( - _loading ? 'Asking…' : 'List models', + _loading ? (l?.coachAsking ?? 'Asking…') : (l?.coachListModels ?? 'List models'), icon: LucideIcons.refreshCw, color: kCoachAccent, soft: true, @@ -967,8 +997,8 @@ class _CoachSetupState extends State { const SizedBox(height: S.x4), OsTextField( controller: _search, - label: 'Model', - hint: 'search, or type an id', + label: l?.coachModelLabel ?? 'Model', + hint: l?.coachModelHint ?? 'search, or type an id', ), const SizedBox(height: S.x1), Builder( @@ -1013,7 +1043,7 @@ class _CoachSetupState extends State { ), const SizedBox(height: S.x4), BigButton( - 'Save', + l?.actionSave ?? 'Save', icon: LucideIcons.check, color: kCoachAccent, onTap: _save, diff --git a/lib/ui2/screens/coach_figures.dart b/lib/ui2/screens/coach_figures.dart index 17c89c14..f3953487 100644 --- a/lib/ui2/screens/coach_figures.dart +++ b/lib/ui2/screens/coach_figures.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; // ── loose parsing (the model is not a schema) ──────────────────────────────── @@ -112,7 +113,7 @@ class CoachFigure extends StatelessWidget { 'kpi_grid' => _kpis(c, p, title), 'heatmap' => _heatmap(c, p, title, unit), 'table' => _table(c, p, title), - _ => _unsupported(title, type), + _ => _unsupported(c, title, type), }; if (note.isEmpty) return body; @@ -126,15 +127,22 @@ class CoachFigure extends StatelessWidget { ); } - Widget _unsupported(String title, String type) => StatusCard( - title.isEmpty ? 'A figure could not be drawn' : title, - type.isEmpty - ? 'The coach sent a figure with no type.' - : 'The coach asked for a "$type" figure, which this app does not draw.', - icon: LucideIcons.chartNoAxesColumn, - ); + Widget _unsupported(BuildContext c, String title, String type) { + final l = AppLocalizations.of(c); + return StatusCard( + title.isEmpty + ? (l?.coachFiguresCouldNotBeDrawn ?? 'A figure could not be drawn') + : title, + type.isEmpty + ? (l?.coachFiguresNoType ?? 'The coach sent a figure with no type.') + : (l?.coachFiguresUnsupportedType(type) ?? + 'The coach asked for a "$type" figure, which this app does not draw.'), + icon: LucideIcons.chartNoAxesColumn, + ); + } Widget _frame( + BuildContext c, String title, String unit, Widget child, { @@ -146,7 +154,7 @@ class CoachFigure extends StatelessWidget { Widget? empty, String? footnote, }) => ChartFrame( - title: title.isEmpty ? 'Figure' : title, + title: title.isEmpty ? (AppLocalizations.of(c)?.coachFiguresFigure ?? 'Figure') : title, unit: unit, height: height, yAxis: yAxis, @@ -167,10 +175,12 @@ class CoachFigure extends StatelessWidget { final series = _seriesOf(spec); final axis = _axisFor(series.map((s) => s.values)); if (axis == null) { - return _frame(title, unit, const SizedBox.shrink(), empty: const NoData()); + return _frame(c, title, unit, const SizedBox.shrink(), empty: const NoData()); } + final l = AppLocalizations.of(c); final inks = _inks(p); return _frame( + c, title, unit, Stack( @@ -199,7 +209,9 @@ class CoachFigure extends StatelessWidget { : [ for (var i = 0; i < series.length; i++) ( - series[i].name.isEmpty ? 'Series ${i + 1}' : series[i].name, + series[i].name.isEmpty + ? (l?.coachFiguresSeriesN(i + 1) ?? 'Series ${i + 1}') + : series[i].name, inks[i % inks.length], ), ], @@ -221,9 +233,10 @@ class CoachFigure extends StatelessWidget { ? null : AxisSpec.of(finite, floor: 0, format: axisFixedOrInt); if (axis == null) { - return _frame(title, unit, const SizedBox.shrink(), empty: const NoData()); + return _frame(c, title, unit, const SizedBox.shrink(), empty: const NoData()); } return _frame( + c, title, unit, CustomPaint( @@ -263,14 +276,16 @@ class CoachFigure extends StatelessWidget { if (s.values.any((v) => v != null && v.isFinite)) s.values, ]; if (lanes.isEmpty) { - return _frame(title, '', const SizedBox.shrink(), empty: const NoData()); + return _frame(c, title, '', const SizedBox.shrink(), empty: const NoData()); } + final loc = AppLocalizations.of(c); final inks = _inks(p); final units = [ for (final k in const ['left', 'right']) if (spec[k] is Map) _str((spec[k] as Map)['unit']), ]; return _frame( + c, title, units.where((u) => u.isNotEmpty).join(' · '), CustomPaint( @@ -290,7 +305,9 @@ class CoachFigure extends StatelessWidget { legend: [ for (var i = 0; i < series.length && i < lanes.length; i++) ( - series[i].name.isEmpty ? 'Lane ${i + 1}' : series[i].name, + series[i].name.isEmpty + ? (loc?.coachFiguresLaneN(i + 1) ?? 'Lane ${i + 1}') + : series[i].name, inks[i % inks.length], ), ], @@ -299,6 +316,9 @@ class CoachFigure extends StatelessWidget { // ── hypnogram ────────────────────────────────────────────────────────────── Widget _hypnogram(BuildContext c, P p, String title) { + final l = AppLocalizations.of(c); + final noSegs = + NoData(message: l?.coachFiguresNoSleepSegments ?? 'No sleep segments'); final segs = _list(spec['segments']).whereType().toList(); // The painter takes one entry per EPOCH, in order — it has no opinion about // time. Expand the segments onto a fixed grid so a 20-minute REM block and @@ -307,22 +327,12 @@ class CoachFigure extends StatelessWidget { final lo = [for (final s in segs) ?_num(s['start'])]; final hi = [for (final s in segs) ?_num(s['end'])]; if (lo.isEmpty || hi.isEmpty) { - return _frame( - title, - 'stage', - const SizedBox.shrink(), - empty: const NoData(message: 'No sleep segments'), - ); + return _frame(c, title, 'stage', const SizedBox.shrink(), empty: noSegs); } final t0 = lo.reduce((a, b) => a < b ? a : b); final t1 = hi.reduce((a, b) => a > b ? a : b); if (t1 <= t0) { - return _frame( - title, - 'stage', - const SizedBox.shrink(), - empty: const NoData(message: 'No sleep segments'), - ); + return _frame(c, title, 'stage', const SizedBox.shrink(), empty: noSegs); } final grid = List.filled(slots, SleepStage.awake); for (final s in segs) { @@ -349,6 +359,7 @@ class CoachFigure extends StatelessWidget { } return _frame( + c, title, 'stage', CustomPaint(size: Size.infinite, painter: Hypnogram(grid, p)), @@ -364,6 +375,7 @@ class CoachFigure extends StatelessWidget { // A per-day stacked version would need a second painter and would be the only // stacked bar in the app. Widget _zones(BuildContext c, P p, String title) { + final l = AppLocalizations.of(c); var z = [ for (final v in _values(spec['zones'] ?? spec['values'])) if (v != null && v.isFinite && v >= 0) v, @@ -378,31 +390,36 @@ class CoachFigure extends StatelessWidget { final total = z.fold(0, (a, v) => a + v); if (z.length < 2 || total <= 0) { return _frame( + c, title, 'min', const SizedBox.shrink(), - empty: const NoData(message: 'No time in zone'), + empty: NoData( + message: l?.coachFiguresNoTimeInZone ?? 'No time in zone'), ); } final fracs = [for (final v in z.take(5)) v / total]; return _frame( + c, title, 'min', CustomPaint(size: Size.infinite, painter: ZoneBar(fracs, p)), height: 56, legend: ZoneBar.legend(p), - footnote: '${total.round()} min total', + footnote: l?.coachFiguresMinTotal(total.round()) ?? + '${total.round()} min total', ); } // ── gauge ────────────────────────────────────────────────────────────────── Widget _gauge(BuildContext c, P p, String title, String unit) { + final l = AppLocalizations.of(c); final v = _num(spec['value']); final lo = _num(spec['min']) ?? 0, hi = _num(spec['max']) ?? 100; if (v == null || hi <= lo) { return StatusCard( - title.isEmpty ? 'Gauge' : title, - 'The coach sent a gauge with no value.', + title.isEmpty ? (l?.coachFiguresGauge ?? 'Gauge') : title, + l?.coachFiguresGaugeNoValue ?? 'The coach sent a gauge with no value.', icon: LucideIcons.gauge, ); } @@ -436,7 +453,9 @@ class CoachFigure extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title.isEmpty ? (label.isEmpty ? 'Gauge' : label) : title, + title.isEmpty + ? (label.isEmpty ? (l?.coachFiguresGauge ?? 'Gauge') : label) + : title, style: F.head.copyWith(color: p.ink), ), const SizedBox(height: S.x1), @@ -460,11 +479,12 @@ class CoachFigure extends StatelessWidget { // number-and-a-picture card invented here would be a card job nobody else // spends. Widget _kpis(BuildContext c, P p, String title) { + final l = AppLocalizations.of(c); final cards = _list(spec['cards']).whereType().toList(); if (cards.isEmpty) { return StatusCard( - title.isEmpty ? 'Summary' : title, - 'The coach sent an empty summary.', + title.isEmpty ? (l?.coachFiguresSummary ?? 'Summary') : title, + l?.coachFiguresEmptySummary ?? 'The coach sent an empty summary.', icon: LucideIcons.layoutGrid, ); } @@ -513,9 +533,10 @@ class CoachFigure extends StatelessWidget { [for (final v in _list(r)) _num(v)], ]; if (weeks.isEmpty || weeks.every((w) => w.every((v) => v == null))) { - return _frame(title, unit, const SizedBox.shrink(), empty: const NoData()); + return _frame(c, title, unit, const SizedBox.shrink(), empty: const NoData()); } return _frame( + c, title, unit, CustomPaint( @@ -530,14 +551,15 @@ class CoachFigure extends StatelessWidget { // ── table ────────────────────────────────────────────────────────────────── Widget _table(BuildContext c, P p, String title) { + final l = AppLocalizations.of(c); final cols = [for (final e in _list(spec['columns'])) _str(e)]; final rows = [ for (final r in _list(spec['rows'])) [for (final e in _list(r)) _str(e)], ]; if (rows.isEmpty) { return StatusCard( - title.isEmpty ? 'Table' : title, - 'The coach sent a table with no rows.', + title.isEmpty ? (l?.coachFiguresTable ?? 'Table') : title, + l?.coachFiguresTableNoRows ?? 'The coach sent a table with no rows.', icon: LucideIcons.table, ); } diff --git a/lib/ui2/screens/custom_journal_field_sheet.dart b/lib/ui2/screens/custom_journal_field_sheet.dart index f0187e0b..c0cd49d0 100644 --- a/lib/ui2/screens/custom_journal_field_sheet.dart +++ b/lib/ui2/screens/custom_journal_field_sheet.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/journal_fields.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'journal_compose.dart' show OsTextField; @@ -86,26 +87,30 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { } void _save() { + final l = AppLocalizations.of(context); final label = _nameCtrl.text.trim(); if (label.isEmpty) { - setState(() => _error = 'Give it a name'); + setState(() => _error = l?.journalFieldErrorNoName ?? 'Give it a name'); return; } final key = customJournalFieldKey(label); // A name that slugs to nothing ("???") would produce a bare `custom_` // key that every other such name also produces. if (key == customJournalFieldKey('')) { - setState(() => _error = 'Use at least one letter or number'); + setState(() => _error = + l?.journalFieldErrorInvalidName ?? 'Use at least one letter or number'); return; } // An amount with no unit renders as a bare number everywhere after — // correlations, findings, CSV. Demand it up front. if (_kind == JournalFieldKind.dose && _unitCtrl.text.trim().isEmpty) { - setState(() => _error = 'Say what it is counted in (mg, ml, cups…)'); + setState(() => _error = + l?.journalFieldErrorNoUnit ?? 'Say what it is counted in (mg, ml, cups…)'); return; } if (widget.existingKeys.contains(key)) { - setState(() => _error = 'You already track something by that name'); + setState(() => _error = + l?.journalFieldErrorDuplicate ?? 'You already track something by that name'); return; } Navigator.pop( @@ -126,6 +131,7 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return SafeArea( top: false, child: ConstrainedBox( @@ -139,7 +145,7 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Track something else', + l?.journalFieldTitle ?? 'Track something else', style: F.head.copyWith(color: p.ink), ), const SizedBox(height: S.x4), @@ -150,21 +156,26 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { children: [ OsTextField( controller: _nameCtrl, - label: 'What do you want to track?', - hint: 'Magnesium, screen time, headache…', + label: l?.journalFieldNameLabel ?? + 'What do you want to track?', + hint: l?.journalFieldNameHint ?? + 'Magnesium, screen time, headache…', ), const SizedBox(height: S.x4), - Text('What kind of number is it?', + Text(l?.journalFieldKindQuestion ?? 'What kind of number is it?', style: F.cap.copyWith(color: p.ink3)), const SizedBox(height: S.x2), Wrap( spacing: S.x2, runSpacing: S.x2, children: [ - for (final (kind, chipLabel) in const [ - (JournalFieldKind.rating, 'A 1–5 rating'), - (JournalFieldKind.dose, 'An amount'), - (JournalFieldKind.duration, 'Minutes'), + for (final (kind, chipLabel) in [ + (JournalFieldKind.rating, + l?.journalFieldKindRating ?? 'A 1–5 rating'), + (JournalFieldKind.dose, + l?.journalFieldKindAmount ?? 'An amount'), + (JournalFieldKind.duration, + l?.journalFieldKindMinutes ?? 'Minutes'), ]) Pressable( onTap: () => _selectKind(kind), @@ -180,11 +191,12 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { const SizedBox(height: S.x4), OsTextField( controller: _unitCtrl, - label: 'Unit', - hint: 'mg, ml, cups…', + label: l?.journalFieldUnitLabel ?? 'Unit', + hint: l?.journalFieldUnitHint ?? 'mg, ml, cups…', ), const SizedBox(height: S.x4), - Text('Step size', style: F.cap.copyWith(color: p.ink3)), + Text(l?.journalFieldStepSize ?? 'Step size', + style: F.cap.copyWith(color: p.ink3)), const SizedBox(height: S.x2), Wrap( spacing: S.x2, @@ -212,7 +224,7 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { ], ), const SizedBox(height: S.x4), - Text('Most you would log in a day', + Text(l?.journalFieldMaxPerDay ?? 'Most you would log in a day', style: F.cap.copyWith(color: p.ink3)), const SizedBox(height: S.x2), Wrap( @@ -239,7 +251,7 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { onTap: () => setState(() => _hasTime = !_hasTime), child: Pill( - 'Ask when the last one was', + l?.journalFieldAskLastTime ?? 'Ask when the last one was', _hasTime ? C.domMind : C.n400, icon: _hasTime ? LucideIcons.check : null, ), @@ -255,7 +267,7 @@ class _CustomFieldSheetState extends State<_CustomFieldSheet> { ), const SizedBox(height: S.x4), BigButton( - 'Start tracking it', + l?.journalFieldStartTracking ?? 'Start tracking it', icon: LucideIcons.check, color: C.domMind, onTap: _save, diff --git a/lib/ui2/screens/cycle_screen.dart b/lib/ui2/screens/cycle_screen.dart index 6d37174d..28235626 100644 --- a/lib/ui2/screens/cycle_screen.dart +++ b/lib/ui2/screens/cycle_screen.dart @@ -49,6 +49,7 @@ import 'package:openstrap_analytics/onehz.dart' show mdc, robustBaseline; import 'package:provider/provider.dart'; import '../../data/day_label.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; import '../ui2.dart'; import '../onboarding/profile_setup.dart' show formatDay; @@ -246,13 +247,16 @@ class _CycleTabState extends State with RevisionReload { /// Confirmed, because it cannot be undone OR redone: the only writer on this /// screen logs TODAY, so a start deleted off an older date has no way back. Future _deleteLog(String date) async { + final l = AppLocalizations.of(context); final ok = await confirmRemove( context, - title: 'Remove ${_short(date)}?', + title: + l?.cycleRemoveLogTitle(_short(date, l)) ?? 'Remove ${_short(date, l)}?', body: + l?.cycleRemoveLogBody ?? 'Cycle day, phase and the predicted next date are all counted from ' - 'the days you log. Only today can be logged, so this one cannot be ' - 'put back.', + 'the days you log. Only today can be logged, so this one cannot be ' + 'put back.', ); if (!ok || !mounted) return; final repo = context.read().repo; @@ -266,6 +270,7 @@ class _CycleTabState extends State with RevisionReload { /// tapped once. Future _pickRepro() async { final p = P.of(context); + final l = AppLocalizations.of(context); final picked = await showModalBottomSheet( context: context, sheetAnimationStyle: sheetMotion(context), @@ -278,14 +283,11 @@ class _CycleTabState extends State with RevisionReload { Padding( padding: const EdgeInsets.all(S.x4), child: Text( - 'What applies to you', + l?.cycleWhatAppliesToYou ?? 'What applies to you', style: F.head.copyWith(color: P.of(c).ink), ), ), - for (final (key, label, why) in [ - ...kReproStates, - ('', 'Prefer not to say', 'The app keeps the phase off.'), - ]) + for (final key in [...kReproStates.map((r) => r.$1), '']) Pressable( onTap: () => Navigator.pop(c, key), child: Padding( @@ -296,9 +298,12 @@ class _CycleTabState extends State with RevisionReload { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: F.body.copyWith(color: P.of(c).ink)), Text( - why, + _reproDisplayLabel(l, key), + style: F.body.copyWith(color: P.of(c).ink), + ), + Text( + _reproDisplayWhy(l, key), style: F.over.copyWith( color: P.of(c).ink3, height: 1.4, @@ -332,14 +337,15 @@ class _CycleTabState extends State with RevisionReload { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d; if (d == null) return const Center(child: CircularProgressIndicator()); if (!d.enabled) { return StatusCard( - 'Cycle tracking is off', - 'It stays on this phone.', - fix: 'Turn on cycle tracking', + l?.cycleTrackingOffTitle ?? 'Cycle tracking is off', + l?.cycleTrackingOffBody ?? 'It stays on this phone.', + fix: l?.cycleTurnOnTracking ?? 'Turn on cycle tracking', icon: LucideIcons.circleDot, onFix: () => _setEnabled(true), ); @@ -350,9 +356,9 @@ class _CycleTabState extends State with RevisionReload { children: [ if (d.cycleDay == null) StatusCard( - 'No period logged yet', - 'Counted from the days you log.', - fix: 'Log a period start today', + l?.cycleNoPeriodTitle ?? 'No period logged yet', + l?.cycleNoPeriodBody ?? 'Counted from the days you log.', + fix: l?.cycleLogPeriodButton ?? 'Log a period start today', icon: LucideIcons.calendarPlus, onFix: _logStart, ) @@ -369,19 +375,25 @@ class _CycleTabState extends State with RevisionReload { // are all look-backs, and the tab itself is for logging today. const SizedBox(height: S.x5), DeepDiveCard( - 'Across your cycles', + l?.cycleAcrossCyclesTitle ?? 'Across your cycles', '${_completedCycles(d)}', - _completedCycles(d) == 1 ? 'complete cycle' : 'complete cycles', - 'Open', + _completedCycles(d) == 1 + ? (l?.cycleUnitCompleteCycle ?? 'complete cycle') + : (l?.cycleUnitCompleteCycles ?? 'complete cycles'), + l?.cycleOpenAction ?? 'Open', C.pink, onTap: () => Navigator.of( c, ).push(MaterialPageRoute(builder: (_) => _CycleHistory(d))), ), - Section('What you noticed today', _symptoms(c, p, d)), + Section( + l?.cycleWhatYouNoticedToday ?? 'What you noticed today', + _symptoms(c, p, d), + ), - if (d.logs.isNotEmpty) Section('Logged days', _logs(c, p, d)), + if (d.logs.isNotEmpty) + Section(l?.cycleLoggedDays ?? 'Logged days', _logs(c, p, d)), // WH-07 — the one control that makes the screen say less. It sits in // this tab's settings gutter, next to the switch that turns the whole @@ -392,17 +404,19 @@ class _CycleTabState extends State with RevisionReload { child: MetricRow( LucideIcons.circleDot, C.pink, - 'What applies to you', - _reproLabel(d.reproState), + l?.cycleWhatAppliesToYou ?? 'What applies to you', + _reproDisplayLabel(l, d.reproState), sub: d.reproState == null - ? 'Optional. Until you say, the app leaves the phase off.' - : 'Only you and this phone. Never exported.', + ? (l?.cycleReproOptionalHint ?? + 'Optional. Until you say, the app leaves the phase off.') + : (l?.cycleReproPrivateHint ?? + 'Only you and this phone. Never exported.'), onTap: _pickRepro, ), ), const SizedBox(height: S.x5), BigButton( - 'Log a period start today', + l?.cycleLogPeriodButton ?? 'Log a period start today', icon: LucideIcons.calendarPlus, color: C.pink, onTap: _logStart, @@ -411,7 +425,7 @@ class _CycleTabState extends State with RevisionReload { Pressable( onTap: () => _setEnabled(false), child: Text( - 'Turn off cycle tracking', + l?.cycleTurnOffTracking ?? 'Turn off cycle tracking', textAlign: TextAlign.center, style: F.cap.copyWith(color: p.ink3), ), @@ -422,35 +436,43 @@ class _CycleTabState extends State with RevisionReload { // ── today ──────────────────────────────────────────────────────────────── - Widget _today(BuildContext c, P p, CycleData d) => Surface( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text('DAY IN THIS CYCLE', style: F.over.copyWith(color: p.ink3)), - const Spacer(), - if (d.phase != 'unknown') Pill(_phaseLabel(d.phase), C.pink), - ], - ), - const SizedBox(height: S.x2), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text('${d.cycleDay}', style: F.n34.copyWith(color: p.ink)), - const SizedBox(width: S.x2), - Text( - d.medianLength == null - ? 'counted from your last logged start' - : 'of about ${d.medianLength!.round()}', - style: F.cap.copyWith(color: p.ink3), - ), - ], - ), - ], - ), - ); + Widget _today(BuildContext c, P p, CycleData d) { + final l = AppLocalizations.of(c); + return Surface( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + l?.cycleDayInThisCycle ?? 'DAY IN THIS CYCLE', + style: F.over.copyWith(color: p.ink3), + ), + const Spacer(), + if (d.phase != 'unknown') Pill(_phaseLabel(l, d.phase), C.pink), + ], + ), + const SizedBox(height: S.x2), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text('${d.cycleDay}', style: F.n34.copyWith(color: p.ink)), + const SizedBox(width: S.x2), + Text( + d.medianLength == null + ? (l?.cycleCountedFromLastStart ?? + 'counted from your last logged start') + : (l?.cycleOfAboutDays(d.medianLength!.round()) ?? + 'of about ${d.medianLength!.round()}'), + style: F.cap.copyWith(color: p.ink3), + ), + ], + ), + ], + ), + ); + } // ── prediction ─────────────────────────────────────────────────────────── @@ -461,14 +483,15 @@ class _CycleTabState extends State with RevisionReload { /// Below two measured gaps there is no spread to state, so it falls back to /// the point and says why it has no width. Widget _prediction(BuildContext c, P p, CycleData d) { + final l = AppLocalizations.of(c); final from = d.predictedFrom, to = d.predictedTo; final ranged = from != null && to != null; final due = _parse(d.predictedNext!); final headline = ranged - ? '${_short(from)} – ${_short(to)}' + ? '${_short(from, l)} – ${_short(to, l)}' : due == null ? d.predictedNext! - : formatDay(due); + : formatDay(due, l); return Surface( child: Row( children: [ @@ -478,8 +501,10 @@ class _CycleTabState extends State with RevisionReload { children: [ Text( ranged - ? 'NEXT PERIOD, EXPECTED BETWEEN' - : 'NEXT PERIOD, EXPECTED AROUND', + ? (l?.cycleNextPeriodBetween ?? + 'NEXT PERIOD, EXPECTED BETWEEN') + : (l?.cycleNextPeriodAround ?? + 'NEXT PERIOD, EXPECTED AROUND'), style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x1), @@ -489,7 +514,7 @@ class _CycleTabState extends State with RevisionReload { ), const SizedBox(height: S.x1), Text( - _when(d), + _when(l, d), style: F.over.copyWith(color: p.ink3, height: 1.4), ), ], @@ -502,12 +527,12 @@ class _CycleTabState extends State with RevisionReload { /// When, and what the number is made of. The evidence rides on the same row /// as the claim — never a date on its own. - String _when(CycleData d) { + String _when(AppLocalizations? l, CycleData d) { final n = d.gapN; if (d.predictedFrom == null || d.predictedTo == null) { final days = d.daysUntilNext?.round(); - return '${_lead(days)}from your one measured gap, which cannot show how ' - 'much your own cycle varies'; + return '${_lead(l, days)}${l?.cycleFromOneMeasuredGap ?? 'from your one measured gap, which cannot show how ' + 'much your own cycle varies'}'; } // Offsets off the SAME `days_until_next` the point case uses, never a // second read of the clock: the repo already resolved "today" once, and a @@ -520,23 +545,23 @@ class _CycleTabState extends State with RevisionReload { if (lo == null || hi == null) { when = ''; } else if (hi < 0) { - when = '${-hi} days past the end of it · '; + when = l?.cyclePastEndOfIt(-hi) ?? '${-hi} days past the end of it · '; } else if (lo <= 0) { - when = 'you are inside it now · '; + when = l?.cycleInsideItNow ?? 'you are inside it now · '; } else { - when = 'in $lo–$hi days · '; + when = l?.cycleInDaysRange(lo, hi) ?? 'in $lo–$hi days · '; } - return '${when}half of your $n measured gaps landed inside a range this ' - 'wide'; + return '$when${l?.cycleHalfOfMeasuredGaps(n) ?? 'half of your $n measured gaps landed inside a range this ' + 'wide'}'; } - String _lead(int? days) => days == null + String _lead(AppLocalizations? l, int? days) => days == null ? '' : days < 0 - ? '${-days} days late · ' + ? (l?.cycleLeadDaysLate(-days) ?? '${-days} days late · ') : days == 0 - ? 'today · ' - : 'in $days days · '; + ? (l?.cycleLeadToday ?? 'today · ') + : (l?.cycleLeadInDays(days) ?? 'in $days days · '); int? _spanDays(String from, String to) { final a = _parse(from), b = _parse(to); @@ -546,6 +571,7 @@ class _CycleTabState extends State with RevisionReload { // ── symptoms ───────────────────────────────────────────────────────────── Widget _symptoms(BuildContext c, P p, CycleData d) { + final l = AppLocalizations.of(c); final on = d.symptoms[_date] ?? const []; return Surface( child: Column( @@ -569,7 +595,7 @@ class _CycleTabState extends State with RevisionReload { borderRadius: R.rPill, ), child: Text( - s, + _symptomLabel(l, s), style: F.cap.copyWith( color: on.contains(s) ? p.on(C.pink) : p.ink2, fontWeight: on.contains(s) @@ -600,20 +626,23 @@ class _CycleTabState extends State with RevisionReload { /// ranked by significance — these tags never go near a correlation. ~20 tags /// against a handful of tagged days is a p-hacking machine at this n. Widget _shape(BuildContext c, P p, CycleData d) { + final l = AppLocalizations.of(c); final s = _symptomShape(d); if (s == null) return const SizedBox.shrink(); + final usuallyNotice = + l?.cycleWhatYouUsuallyNotice ?? 'What you usually notice'; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: S.x3), Pressable( - semanticLabel: 'What you usually notice', + semanticLabel: usuallyNotice, onTap: () => setState(() => _showShape = !_showShape), child: Row( children: [ Expanded( child: Text( - 'What you usually notice', + usuallyNotice, style: F.cap.copyWith(color: p.ink2), ), ), @@ -634,17 +663,22 @@ class _CycleTabState extends State with RevisionReload { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text(e.$1, style: F.cap.copyWith(color: p.ink)), + child: Text(_symptomLabel(l, e.$1), + style: F.cap.copyWith(color: p.ink)), ), Text(e.$2.join(' · '), style: F.n17.copyWith(color: p.ink2)), ], ), ), Text( - 'Four numbers, one per week of the cycle, counted back to your own ' - 'logged starts. You logged something on ${s.daysByWeek.join(', ')} ' - 'days of each week across ${s.cycles} cycles — those are the only ' - 'days in any of this.', + l?.cycleSymptomShapeSummary( + s.daysByWeek.join(', '), + s.cycles, + ) ?? + 'Four numbers, one per week of the cycle, counted back to your own ' + 'logged starts. You logged something on ${s.daysByWeek.join(', ')} ' + 'days of each week across ${s.cycles} cycles — those are the only ' + 'days in any of this.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], @@ -694,6 +728,7 @@ class _CycleTabState extends State with RevisionReload { // ── logs ───────────────────────────────────────────────────────────────── Widget _logs(BuildContext c, P p, CycleData d) { + final l = AppLocalizations.of(c); final recent = d.logs.reversed.take(6).toList(); return Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), @@ -709,17 +744,23 @@ class _CycleTabState extends State with RevisionReload { child: Text( _parse(recent[i]['date'] as String? ?? '') == null ? '${recent[i]['date']}' - : formatDay(_parse(recent[i]['date'] as String)!), + : formatDay(_parse(recent[i]['date'] as String)!, l), style: F.body.copyWith(color: p.ink), ), ), Text( - '${recent[i]['kind']}'.toUpperCase(), + recent[i]['kind'] == 'start' + ? (l?.cycleLogKindStart ?? 'START') + : (l?.cycleLogKindEnd ?? 'END'), style: F.over.copyWith(color: p.ink3), ), const SizedBox(width: S.x3), Pressable( - semanticLabel: 'Remove ${recent[i]['date']}', + semanticLabel: + l?.cycleRemoveLoggedDay( + '${recent[i]['date']}', + ) ?? + 'Remove ${recent[i]['date']}', onTap: () => _deleteLog(recent[i]['date'] as String), child: Icon(LucideIcons.x, size: 18, color: p.ink3), ), @@ -751,23 +792,56 @@ class _Shape { DateTime? _parse(String ymd) => DateTime.tryParse(ymd); -String _short(String ymd) { +String _short(String ymd, [AppLocalizations? l]) { final d = _parse(ymd); - return d == null ? ymd : formatDay(d); + return d == null ? ymd : formatDay(d, l); } -String _reproLabel(String? s) { - for (final (key, label, _) in kReproStates) { - if (key == s) return label; - } - return 'Not set'; -} +/// Localized display label for a `repro_state` key ('' = prefer not to say, +/// null/unrecognised = never set). +String _reproDisplayLabel(AppLocalizations? l, String? key) => switch (key) { + 'cycling' => l?.cycleReproCyclingLabel ?? 'I have natural cycles', + 'contraception' => + l?.cycleReproContraceptionLabel ?? 'Hormonal contraception', + 'none' => + l?.cycleReproNoneLabel ?? 'Pregnant, postpartum, or not cycling', + '' => l?.cyclePreferNotToSay ?? 'Prefer not to say', + _ => l?.cycleReproNotSet ?? 'Not set', +}; + +/// Localized "why" copy shown under a `repro_state` option in the picker. +String _reproDisplayWhy(AppLocalizations? l, String key) => switch (key) { + 'cycling' => + l?.cycleReproCyclingWhy ?? 'Counts a phase from your logged starts.', + 'contraception' => + l?.cycleReproContraceptionWhy ?? + 'No ovulation to count from, so no phase. Bleeds are still logged.', + 'none' => + l?.cycleReproNoneWhy ?? + 'No phase and no predicted next. Your biometrics still show.', + _ => l?.cyclePreferNotToSayWhy ?? 'The app keeps the phase off.', +}; -String _phaseLabel(String p) => switch (p) { - 'menstrual' => 'Menstrual', - 'follicular' => 'Follicular', - 'ovulation' => 'Ovulation window', - 'luteal' => 'Luteal', +/// Localized display label for a `kCycleSymptoms` key. The key itself stays +/// English — it is the storage value posted to the repo — only the label +/// shown on the chip is localized. +String _symptomLabel(AppLocalizations? l, String key) => switch (key) { + 'cramps' => l?.cycleSymptomCramps ?? 'cramps', + 'headache' => l?.cycleSymptomHeadache ?? 'headache', + 'bloating' => l?.cycleSymptomBloating ?? 'bloating', + 'fatigue' => l?.cycleSymptomFatigue ?? 'fatigue', + 'low mood' => l?.cycleSymptomLowMood ?? 'low mood', + 'acne' => l?.cycleSymptomAcne ?? 'acne', + 'tender breasts' => l?.cycleSymptomTenderBreasts ?? 'tender breasts', + 'nausea' => l?.cycleSymptomNausea ?? 'nausea', + _ => key, +}; + +String _phaseLabel(AppLocalizations? l, String p) => switch (p) { + 'menstrual' => l?.cyclePhaseMenstrual ?? 'Menstrual', + 'follicular' => l?.cyclePhaseFollicular ?? 'Follicular', + 'ovulation' => l?.cyclePhaseOvulation ?? 'Ovulation window', + 'luteal' => l?.cyclePhaseLuteal ?? 'Luteal', _ => p, }; @@ -899,11 +973,15 @@ class _CycleHistoryState extends State<_CycleHistory> { @override Widget build(BuildContext c) { - return detailScaffold(c, 'Across your cycles', [ + final l = AppLocalizations.of(c); + return detailScaffold(c, l?.cycleAcrossCyclesTitle ?? 'Across your cycles', [ const SizedBox(height: S.x2), - Section('This cycle', _currentCycleChart(c, d)), - Section('By day of your cycle', _byDay(c)), - Section('How long your cycles have been', _lengths(c)), + Section(l?.cycleThisCycle ?? 'This cycle', _currentCycleChart(c, d)), + Section(l?.cycleByDayOfYourCycle ?? 'By day of your cycle', _byDay(c)), + Section( + l?.cycleHowLongCyclesBeen ?? 'How long your cycles have been', + _lengths(c), + ), ]); } @@ -911,6 +989,7 @@ class _CycleHistoryState extends State<_CycleHistory> { Widget _byDay(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final rhr = byCycleDay(d, 'resting_hr'); final rmssd = byCycleDay(d, 'hrv_rmssd'); // TWO SERIES, NOT THREE. The third would be `skin_temp_idx`, which is the @@ -919,26 +998,28 @@ class _CycleHistoryState extends State<_CycleHistory> { final charts = [ _cycleDayChart( c, - 'Resting heart rate', - 'bpm', + l?.cycleRestingHeartRate ?? 'Resting heart rate', + l?.cycleUnitBpm ?? 'bpm', C.pink, rhr, cycleDayNoise(d, 'resting_hr'), ), _cycleDayChart( c, - 'HRV (RMSSD)', - 'ms', + l?.cycleHrvRmssdTitle ?? 'HRV (RMSSD)', + l?.cycleUnitMs ?? 'ms', C.purple, rmssd, cycleDayNoise(d, 'hrv_rmssd'), ), ]; if (charts.every((w) => w == null)) { - return const StatusCard( - 'Not enough cycles to describe a cycle day yet', - 'Every point here is the middle of the same day across two or more of ' - 'your own cycles. Nothing has two behind it yet.', + return StatusCard( + l?.cycleNotEnoughDescribeDayTitle ?? + 'Not enough cycles to describe a cycle day yet', + l?.cycleNotEnoughDescribeDayBody ?? + 'Every point here is the middle of the same day across two or more of ' + 'your own cycles. Nothing has two behind it yet.', icon: LucideIcons.circleDot, ); } @@ -948,9 +1029,10 @@ class _CycleHistoryState extends State<_CycleHistory> { for (final w in charts) if (w != null) ...[w, const SizedBox(height: S.x3)], Text( - 'Your own past cycles, described. Days that only one cycle reached ' - 'are left empty rather than drawn — one night is not a middle. It ' - 'describes what happened, not what will.', + l?.cycleOwnPastCyclesDescribed ?? + 'Your own past cycles, described. Days that only one cycle reached ' + 'are left empty rather than drawn — one night is not a middle. It ' + 'describes what happened, not what will.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), const SizedBox(height: S.x4), @@ -989,19 +1071,23 @@ class _CycleHistoryState extends State<_CycleHistory> { if (axis == null) return null; final ns = [for (final k in med.keys) byDay[k]!.length]..sort(); final p = P.of(c); + final l = AppLocalizations.of(c); return Surface( child: ChartFrame( title: title, unit: unit, yAxis: axis, - xLabels: ['Day 1', 'Day $last'], + xLabels: [ + l?.cycleDayOneLabel ?? 'Day 1', + l?.cycleDayNLabel(last) ?? 'Day $last', + ], // n, on every point — as the range it actually spans, because thirty // little numbers along a line is not a readable chart and a single // headline n would be false for most of the points under it. Then the // MDC line, which is what stops the shape being read as a finding. footnote: - '${ns.first == ns.last ? 'Middle of ${ns.first} cycles at each day.' : 'Middle of between ${ns.first} and ${ns.last} cycles at each day.'}' - '${_mdcNote(med.values, unit, noise)}', + '${ns.first == ns.last ? (l?.cycleMiddleOfNCycles(ns.first) ?? 'Middle of ${ns.first} cycles at each day.') : (l?.cycleMiddleOfRangeCycles(ns.first, ns.last) ?? 'Middle of between ${ns.first} and ${ns.last} cycles at each day.')}' + '${_mdcNote(l, med.values, unit, noise)}', series: vals, child: CustomPaint( size: Size.infinite, @@ -1030,22 +1116,26 @@ class _CycleHistoryState extends State<_CycleHistory> { /// state is the common one for most of a year. Widget _dayAgainstItself(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final lines = [ - for (final (key, label, dp) in const [ - ('hrv_rmssd', 'HRV', 1), - ('resting_hr', 'Resting heart rate', 1), + for (final (key, label, dp) in [ + ('hrv_rmssd', l?.cycleCompareHrvLabel ?? 'HRV', 1), + ('resting_hr', l?.cycleRestingHeartRate ?? 'Resting heart rate', 1), ]) - ?_compareLine(key, label, dp), + ?_compareLine(l, key, label, dp), ]; if (lines.isEmpty) { final cd = d.cycleDay; return StatusCard( - 'Not enough cycles to compare a day against itself', + l?.cycleNotEnoughCompareTitle ?? + 'Not enough cycles to compare a day against itself', cd == null - ? 'This puts today next to the same day of your own previous ' - 'cycles. It needs three of them that got that far.' - : 'This puts today next to the same day of your own previous ' - 'cycles. It needs three of them that reached day $cd.', + ? (l?.cycleCompareBodyGeneric ?? + 'This puts today next to the same day of your own previous ' + 'cycles. It needs three of them that got that far.') + : (l?.cycleCompareBodyWithDay(cd) ?? + 'This puts today next to the same day of your own previous ' + 'cycles. It needs three of them that reached day $cd.'), icon: LucideIcons.circleDot, ); } @@ -1063,19 +1153,21 @@ class _CycleHistoryState extends State<_CycleHistory> { children: [ if (night.isNotEmpty) ...[ Text( - 'NIGHT OF ${_short(night.last).toUpperCase()}', + l?.cycleNightOfLabel(_short(night.last, l).toUpperCase()) ?? + 'NIGHT OF ${_short(night.last, l).toUpperCase()}', style: F.over.copyWith(color: p.ink3), ), const SizedBox(height: S.x2), ], - for (final l in lines) ...[ - Text(l, style: F.body.copyWith(color: p.ink, height: 1.4)), + for (final line in lines) ...[ + Text(line, style: F.body.copyWith(color: p.ink, height: 1.4)), const SizedBox(height: S.x2), ], Text( - 'A comparison, not a correction. Nothing on your readiness has ' - 'been rescaled by this, and nothing here is a training ' - 'instruction.', + l?.cycleComparisonNotCorrection ?? + 'A comparison, not a correction. Nothing on your readiness has ' + 'been rescaled by this, and nothing here is a training ' + 'instruction.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], @@ -1084,7 +1176,7 @@ class _CycleHistoryState extends State<_CycleHistory> { } /// "HRV −1.2 vs your last 3 weeks, −0.3 vs your last three day-22s." - String? _compareLine(String key, String label, int dp) { + String? _compareLine(AppLocalizations? l, String key, String label, int dp) { final starts = startDates(d); if (starts.isEmpty) return null; // Newest row that carries this key. `overlay` is oldest first. @@ -1119,8 +1211,15 @@ class _CycleHistoryState extends State<_CycleHistory> { final a = _spread(trailing), b = _spread(sameDay); if (a == null || b == null) return null; final z1 = (value - a.mean) / a.sd, z2 = (value - b.mean) / b.sd; - return '$label ${_signed(z1, dp)} vs your last 3 weeks, ' - '${_signed(z2, dp)} vs your last ${sameDay.length} day-${cycleDay}s.'; + return l?.cycleCompareLine( + label, + _signed(z1, dp), + _signed(z2, dp), + sameDay.length, + cycleDay, + ) ?? + '$label ${_signed(z1, dp)} vs your last 3 weeks, ' + '${_signed(z2, dp)} vs your last ${sameDay.length} day-${cycleDay}s.'; } // ── WH-08 ──────────────────────────────────────────────────────────────── @@ -1131,16 +1230,18 @@ class _CycleHistoryState extends State<_CycleHistory> { /// be a verdict. Widget _lengths(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final starts = startDates(d); final gaps = cycleGaps(starts); if (!_lengthReview) { return StatusCard( - 'Your cycle lengths against a published range', - 'Off unless you ask for it. It draws the days between your own logged ' - 'starts next to the range published for an adult cycle, and says ' - 'nothing else about them.', - fix: 'Show it', + l?.cycleLengthsTitle ?? 'Your cycle lengths against a published range', + l?.cycleLengthsBody ?? + 'Off unless you ask for it. It draws the days between your own logged ' + 'starts next to the range published for an adult cycle, and says ' + 'nothing else about them.', + fix: l?.cycleShowIt ?? 'Show it', icon: LucideIcons.ruler, onFix: _app == null ? null @@ -1149,21 +1250,26 @@ class _CycleHistoryState extends State<_CycleHistory> { } if (gaps.length < kCycleLengthReviewMinGaps) { return StatusCard( - 'Not enough logged cycles yet', - 'This needs a long run: ${gaps.length} of ' - '$kCycleLengthReviewMinGaps gaps so far, which is about a year of ' - 'logging every start.', + l?.cycleNotEnoughLoggedTitle ?? 'Not enough logged cycles yet', + l?.cycleNotEnoughLoggedBody( + gaps.length, + kCycleLengthReviewMinGaps, + ) ?? + 'This needs a long run: ${gaps.length} of ' + '$kCycleLengthReviewMinGaps gaps so far, which is about a year of ' + 'logging every start.', icon: LucideIcons.ruler, ); } // REFUSING ON HOLES IS THE FEATURE. if (gaps.any((g) => g > kCycleLengthUnloggableGapDays)) { - return const StatusCard( - 'There is a gap in your logged starts', - 'One of them is more than $kCycleLengthUnloggableGapDays days after ' - 'the one before it. A start you never logged and a cycle that ' - 'genuinely ran that long look the same from here, so nothing is ' - 'drawn.', + return StatusCard( + l?.cycleGapTitle ?? 'There is a gap in your logged starts', + l?.cycleGapBody(kCycleLengthUnloggableGapDays) ?? + 'One of them is more than $kCycleLengthUnloggableGapDays days after ' + 'the one before it. A start you never logged and a cycle that ' + 'genuinely ran that long look the same from here, so nothing is ' + 'drawn.', icon: LucideIcons.ruler, ); } @@ -1185,17 +1291,24 @@ class _CycleHistoryState extends State<_CycleHistory> { children: [ Surface( child: ChartFrame( - title: 'Days between your logged starts', - unit: 'days', + title: l?.cycleDaysBetweenStarts ?? 'Days between your logged starts', + unit: l?.cycleUnitDays ?? 'days', yAxis: axis, - xLabels: [_short(_ymdOf(starts[1])), _short(_ymdOf(starts.last))], + xLabels: [ + _short(_ymdOf(starts[1]), l), + _short(_ymdOf(starts.last), l), + ], legend: [ - ('Your cycles', p.on(C.pink)), - ('Published range', p.ink3), + (l?.cycleLegendYourCycles ?? 'Your cycles', p.on(C.pink)), + (l?.cycleLegendPublishedRange ?? 'Published range', p.ink3), ], footnote: + l?.cycleTwoLinesFootnote( + kPublishedCycleDays.low.round(), + kPublishedCycleDays.high.round(), + ) ?? 'The two lines are ${kPublishedCycleDays.low.round()} and ' - '${kPublishedCycleDays.high.round()} days.', + '${kPublishedCycleDays.high.round()} days.', series: vals, child: Stack( fit: StackFit.expand, @@ -1223,10 +1336,11 @@ class _CycleHistoryState extends State<_CycleHistory> { const SizedBox(height: S.x3), // Non-dismissible, and deliberately not a card that can be closed. Text( - 'Cycle length changes for many reasons — thyroid, stress, weight ' - 'change, contraception, PCOS and others. This is your own logged ' - 'data next to a published range. It is a reason to ask a clinician, ' - 'not an answer from one.', + l?.cycleLengthChangesReasons ?? + 'Cycle length changes for many reasons — thyroid, stress, weight ' + 'change, contraception, PCOS and others. This is your own logged ' + 'data next to a published range. It is a reason to ask a clinician, ' + 'not an answer from one.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), const SizedBox(height: S.x3), @@ -1235,7 +1349,7 @@ class _CycleHistoryState extends State<_CycleHistory> { ? null : () => _app!.updateProfile({'cycle_length_review': false}), child: Text( - 'Hide cycle lengths', + l?.cycleHideLengths ?? 'Hide cycle lengths', textAlign: TextAlign.center, style: F.cap.copyWith(color: p.ink3), ), @@ -1274,16 +1388,26 @@ Widget _currentCycleChart(BuildContext c, CycleData d) { final present = byDay.values.toList(); final axis = present.length < 3 ? null : AxisSpec.of(present); final p = P.of(c); + final l = AppLocalizations.of(c); return Surface( child: ChartFrame( - title: 'Resting heart rate', - unit: 'bpm', + title: l?.cycleRestingHeartRate ?? 'Resting heart rate', + unit: l?.cycleUnitBpm ?? 'bpm', height: 120, yAxis: axis, - xLabels: axis == null ? const [] : ['Day 1', 'Day $last'], - footnote: 'Descriptive only.', + xLabels: axis == null + ? const [] + : [ + l?.cycleDayOneLabel ?? 'Day 1', + l?.cycleDayNLabel(last) ?? 'Day $last', + ], + footnote: l?.cycleDescriptiveOnly ?? 'Descriptive only.', empty: axis == null - ? const NoData(message: 'Not enough derived nights this cycle yet') + ? NoData( + message: + l?.cycleNotEnoughDerivedNights ?? + 'Not enough derived nights this cycle yet', + ) : null, series: vals, child: axis == null @@ -1312,17 +1436,24 @@ Widget _currentCycleChart(BuildContext c, CycleData d) { /// and when the swing is the smaller of the two the sentence says the shape is /// not a shift. Returns '' when there is no MDC to state — an unqualified /// claim is worse than a quiet one, but so is a fabricated threshold. -String _mdcNote(Iterable medians, String unit, double? noise) { +String _mdcNote( + AppLocalizations? l, + Iterable medians, + String unit, + double? noise, +) { if (noise == null || noise <= 0 || medians.isEmpty) return ''; final swing = medians.reduce(math.max) - medians.reduce(math.min); final s = '${swing.toStringAsFixed(1)} $unit'; final n = '${noise.toStringAsFixed(1)} $unit'; return swing < noise - ? ' Every day drawn here is inside your own night-to-night spread: the ' - 'biggest gap between two of them is $s, and $n is the smallest ' - 'change this can tell from noise. A shape, not a shift.' - : ' Your nights vary by $n on their own, so days closer together than ' - 'that are not separated. The biggest gap here is $s.'; + ? (l?.cycleMdcNoteInsideSpread(s, n) ?? + ' Every day drawn here is inside your own night-to-night spread: the ' + 'biggest gap between two of them is $s, and $n is the smallest ' + 'change this can tell from noise. A shape, not a shift.') + : (l?.cycleMdcNoteVaries(n, s) ?? + ' Your nights vary by $n on their own, so days closer together than ' + 'that are not separated. The biggest gap here is $s.'); } /// A z with its sign always printed — "0.3" and "−0.3" are different findings diff --git a/lib/ui2/screens/day_steps.dart b/lib/ui2/screens/day_steps.dart index 998a06f2..ceae920a 100644 --- a/lib/ui2/screens/day_steps.dart +++ b/lib/ui2/screens/day_steps.dart @@ -30,6 +30,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; import '../../data/day_label.dart'; import '../../data/local_repository.dart'; @@ -286,26 +287,28 @@ class _DayStepsDetailState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d ?? const DayStepsData(); // Same rule as Sleep: the stepper names the day, so the nav bar only does // when there is no stepper. - return detailScaffold(c, 'Steps', + return detailScaffold(c, l?.dayStepsTitle ?? 'Steps', sub: d.days.length < 2 ? dayNavLabel(d.day).toUpperCase() : '', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), if (_loading && _d == null) ...[ const SizedBox(height: S.x8), const Center(child: CircularProgressIndicator()), ] else if (d.spans.isEmpty) - _absent(d) + _absent(c, d) else ...[ _chart(c, p, d), - Section('Through the day', _rows(p, d)), + Section(l?.dayStepsThroughDay ?? 'Through the day', _rows(c, p, d)), ], ]); } // ── nothing to place on a clock ──────────────────────────────────────────── - Widget _absent(DayStepsData d) { + Widget _absent(BuildContext c, DayStepsData d) { + final l = AppLocalizations.of(c); // A day CAN carry a step count with no spans behind it: with no windowed // source at all, the day falls back to the strap's on-chip counter, which // is a running total with no times of its own. Saying "no steps" over the @@ -315,22 +318,27 @@ class _DayStepsDetailState extends State { // The day this card is about, named. It used to say "today" on a screen // that could only ever be today; now it can be any day on disk. final when = dayNavLabel(d.day) == 'Today' - ? 'today' - : 'on ${prettyDay(d.day)}'; + ? (l?.dayStepsToday ?? 'today') + : (l?.dayStepsOnDay(prettyDay(d.day, l)) ?? 'on ${prettyDay(d.day, l)}'); return StatusCard( - chip ? 'No times behind the count $when' : 'No steps counted $when', chip - ? 'The ${thousands(d.dayTotal)} steps counted $when came from the ' - 'strap\'s own step counter, which reports a running day total ' - 'and no times. There is nothing to place on a clock.' + ? (l?.dayStepsNoTimesTitle(when) ?? 'No times behind the count $when') + : (l?.dayStepsNoStepsTitle(when) ?? 'No steps counted $when'), + chip + ? (l?.dayStepsStrapCounterBody(thousands(d.dayTotal), when) ?? + 'The ${thousands(d.dayTotal)} steps counted $when came from the ' + 'strap\'s own step counter, which reports a running day total ' + 'and no times. There is nothing to place on a clock.') : whyFromNote(d.note, unit: 'days') ?? - 'Nothing that can count steps recorded $when.', + (l?.dayStepsNothingCounted(when) ?? + 'Nothing that can count steps recorded $when.'), icon: chip ? LucideIcons.watch : LucideIcons.footprints, ); } // ── when they were counted ───────────────────────────────────────────────── Widget _chart(BuildContext c, P p, DayStepsData d) { + final l = AppLocalizations.of(c); final (band, phone) = hourlySteps(d.spans); final totals = [for (var h = 0; h < 24; h++) band[h] ?? phone[h]]; final axis = AxisSpec.of(totals.whereType(), floor: 0); @@ -338,8 +346,8 @@ class _DayStepsDetailState extends State { child: Column( children: [ ChartFrame( - title: 'WHEN THEY WERE COUNTED', - unit: 'steps', + title: l?.dayStepsChartTitle ?? 'WHEN THEY WERE COUNTED', + unit: l?.dayStepsUnit ?? 'steps', height: 150, yAxis: axis, xLabels: const ['00:00', '12:00', '24:00'], @@ -347,9 +355,12 @@ class _DayStepsDetailState extends State { // ONE colour, one key — a day with a single sensor has nothing to // tell apart, and a legend of one is noise. legend: d.mixed - ? [(d.bandLabel, p.on(C.green)), ('Your phone', p.on(C.teal))] + ? [ + (d.bandLabel, p.on(C.green)), + (l?.dayStepsYourPhone ?? 'Your phone', p.on(C.teal)), + ] : const [], - footnote: _honesty(d), + footnote: _honesty(c, d), empty: axis == null ? const NoData() : null, child: Stack( children: [ @@ -378,13 +389,15 @@ class _DayStepsDetailState extends State { InlineMetrics( d.mixed ? [ - ('Counted', thousands(d.total), C.green), + (l?.dayStepsCounted ?? 'Counted', thousands(d.total), C.green), (d.bandLabel, thousands(d.strap), C.green), - ('Your phone', thousands(d.phone), C.teal), + (l?.dayStepsYourPhone ?? 'Your phone', thousands(d.phone), C.teal), ] : [ ( - d.strap > 0 ? d.bandLabel : 'Your phone', + d.strap > 0 + ? d.bandLabel + : (l?.dayStepsYourPhone ?? 'Your phone'), thousands(d.total), d.strap > 0 ? C.green : C.teal, ), @@ -404,44 +417,53 @@ class _DayStepsDetailState extends State { /// over-count — while a trunk-carried counter's error is one-sided, so the /// phone's honest caveat is the steps it never saw rather than the ones it /// invented. - String _honesty(DayStepsData d) => d.mixed - ? 'Counted at your wrist and by your phone, and the two miscount ' - 'differently: a wrist reads a real walk low and can read rhythmic ' - 'hand work as walking, while a phone counts only the steps you had ' - 'it on you for.' - : d.strap > 0 - ? 'Counted at your wrist, where a real walk tends to read low and ' - 'rhythmic hand work can read as walking.' - : 'Counted by your phone, so only the steps you had it on you for ' - 'are here.'; + String _honesty(BuildContext c, DayStepsData d) { + final l = AppLocalizations.of(c); + return d.mixed + ? (l?.dayStepsHonestyMixed ?? + 'Counted at your wrist and by your phone, and the two miscount ' + 'differently: a wrist reads a real walk low and can read rhythmic ' + 'hand work as walking, while a phone counts only the steps you had ' + 'it on you for.') + : d.strap > 0 + ? (l?.dayStepsHonestyStrap ?? + 'Counted at your wrist, where a real walk tends to read low and ' + 'rhythmic hand work can read as walking.') + : (l?.dayStepsHonestyPhone ?? + 'Counted by your phone, so only the steps you had it on you for ' + 'are here.'); + } // ── the stretches themselves ─────────────────────────────────────────────── - Widget _rows(P p, DayStepsData d) => Surface( - child: Column( - children: [ - for (final s in mergeAdjacent(d.spans)) - MetricRow( - // The device is the icon and the colour, and it is said in words - // on the line below — the same three channels the chart uses. - s.fromBand ? LucideIcons.watch : LucideIcons.smartphone, - s.fromBand ? C.green : C.teal, - '${clockOfTs(s.startTs)} – ${clockOfTs(s.endTs)}', - // No `steps` unit on the row. A clock range is already a long - // name, and at 3× text the unit pushed the measurement out of - // the card — the screen is titled Steps and the chart's own unit - // says so, which is the one place it has to be said. - thousands(s.steps), - sub: [ - s.fromBand ? d.bandLabel : 'Your phone', - // The session's own name, when the stretch sat inside one. - // Never invented for a stretch that did not: steps in an hour - // are steps, not a walk we watched. - ?activityByName(s.activity)?.name, - ].join(' · '), - ), - ], - ), - ); + Widget _rows(BuildContext c, P p, DayStepsData d) { + final l = AppLocalizations.of(c); + return Surface( + child: Column( + children: [ + for (final s in mergeAdjacent(d.spans)) + MetricRow( + // The device is the icon and the colour, and it is said in words + // on the line below — the same three channels the chart uses. + s.fromBand ? LucideIcons.watch : LucideIcons.smartphone, + s.fromBand ? C.green : C.teal, + '${clockOfTs(s.startTs)} – ${clockOfTs(s.endTs)}', + // No `steps` unit on the row. A clock range is already a long + // name, and at 3× text the unit pushed the measurement out of + // the card — the screen is titled Steps and the chart's own unit + // says so, which is the one place it has to be said. + thousands(s.steps), + sub: [ + s.fromBand ? d.bandLabel : (l?.dayStepsYourPhone ?? 'Your phone'), + // The session's own name, when the stretch sat inside one. + // Never invented for a stretch that did not: steps in an hour + // are steps, not a walk we watched. + ?activityByName(s.activity)?.name, + ].join(' · '), + ), + ], + ), + ); + } } /// The paired band's name, in the Devices screen's own words — or null when @@ -452,14 +474,16 @@ class _DayStepsDetailState extends State { /// this is the current one's name for both. The alternative is to name none of /// them, which loses the thing the screen exists to say. String bandLabel(BuildContext c) { + final fallback = + AppLocalizations.of(c)?.dayStepsYourBand ?? DayStepsData._defaultBand; try { final app = c.read(); - if (!app.isPaired) return DayStepsData._defaultBand; + if (!app.isPaired) return fallback; // The registry's own label for the band that is paired. A family with no // entry — an import, a pre-stamp row, an adapter this build does not carry // — is NOT a WHOOP 4, which is what the ternary here used to publish. - return bandLabelFor(app.device.generation) ?? DayStepsData._defaultBand; + return bandLabelFor(app.device.generation) ?? fallback; } catch (_) { - return DayStepsData._defaultBand; + return fallback; } } diff --git a/lib/ui2/screens/day_timeline.dart b/lib/ui2/screens/day_timeline.dart index fef82dc1..9482e5f7 100644 --- a/lib/ui2/screens/day_timeline.dart +++ b/lib/ui2/screens/day_timeline.dart @@ -25,6 +25,7 @@ import 'dart:convert' show jsonDecode; import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:provider/provider.dart'; import '../../data/day_label.dart' show localDayEndSec; import '../../data/db.dart'; @@ -32,6 +33,8 @@ import '../../data/journal_fields.dart'; import '../../data/local_repository.dart'; import '../../data/med_store.dart'; import '../../data/nutrition_store.dart'; +import '../../l10n/app_localizations.dart'; +import '../../state/locale_controller.dart'; import '../activity/catalogue.dart' show activityByName; import '../ui2.dart'; import 'home_screen.dart' show clockOfTs, repoOf; @@ -78,14 +81,15 @@ class DayNote { /// Band events worth a line. Everything else the strap emits is about the /// strap — bonds, flash writes, sync bookkeeping — and belongs on Devices, not /// in somebody's day. -const _events = { - 7: ('On the charger', LucideIcons.batteryCharging), - 8: ('Off the charger', LucideIcons.batteryFull), - 14: ('You double-tapped the band', LucideIcons.hand), - 15: ('The band restarted', LucideIcons.rotateCw), - 21: ('Battery pack attached', LucideIcons.batteryCharging), - 22: ('Battery pack removed', LucideIcons.battery), - 57: ('Alarm went off', LucideIcons.alarmClock), +Map _events(AppLocalizations? l) => { + 7: (l?.dayTimelineChargerOn ?? 'On the charger', LucideIcons.batteryCharging), + 8: (l?.dayTimelineChargerOff ?? 'Off the charger', LucideIcons.batteryFull), + 14: (l?.dayTimelineDoubleTap ?? 'You double-tapped the band', LucideIcons.hand), + 15: (l?.dayTimelineRestarted ?? 'The band restarted', LucideIcons.rotateCw), + 21: (l?.dayTimelineBatteryPackAttached ?? 'Battery pack attached', + LucideIcons.batteryCharging), + 22: (l?.dayTimelineBatteryPackRemoved ?? 'Battery pack removed', LucideIcons.battery), + 57: (l?.dayTimelineAlarmWentOff ?? 'Alarm went off', LucideIcons.alarmClock), }; /// An off-wrist gap shorter than this is a dropout, not an event in a day. @@ -115,6 +119,7 @@ List dayMoments({ List<({String label, int at})> doses = const [], Map journal = const {}, List fields = const [], + AppLocalizations? l, }) { final out = []; int? asInt(Object? v) => (v as num?)?.toInt(); @@ -129,7 +134,7 @@ List dayMoments({ out.add(Moment( at: on, until: off, - title: 'Asleep', + title: l?.dayTimelineAsleep ?? 'Asleep', detail: '${_span(on, off)} · ${_dur((off - on) / 60)}', icon: LucideIcons.moon, color: C.blue, @@ -143,7 +148,7 @@ List dayMoments({ out.add(Moment( at: on, until: off, - title: 'Nap', + title: l?.dayTimelineNap ?? 'Nap', detail: '${_span(on, off)} · ${_dur(n['duration_min'] as num?)}', icon: LucideIcons.bedDouble, color: C.indigo, @@ -164,7 +169,10 @@ List dayMoments({ out.add(Moment( at: on, until: asInt(s['end_ts']), - title: act?.name ?? (type ?? 'Workout').replaceAll('_', ' '), + title: act?.name ?? + (type == null + ? (l?.dayTimelineWorkout ?? 'Workout') + : type.replaceAll('_', ' ')), detail: bits.join(' · '), icon: act?.icon ?? LucideIcons.dumbbell, color: act?.color ?? C.orange, @@ -182,7 +190,7 @@ List dayMoments({ out.add(Moment( at: on, until: off, - title: 'Band off your wrist', + title: l?.dayTimelineBandOffWrist ?? 'Band off your wrist', detail: '${_span(on, off)} · ${_dur(len)}', icon: LucideIcons.watch, )); @@ -193,9 +201,9 @@ List dayMoments({ // not support. What it is good for is a time to look at. final highs = timeline['highs']; if (highs is Map) { - for (final e in const [ - ('peak_hr', 'Highest heart rate', LucideIcons.trendingUp), - ('low_hr', 'Lowest heart rate', LucideIcons.trendingDown), + for (final e in [ + ('peak_hr', l?.dayTimelineHighestHr ?? 'Highest heart rate', LucideIcons.trendingUp), + ('low_hr', l?.dayTimelineLowestHr ?? 'Lowest heart rate', LucideIcons.trendingDown), ]) { final h = highs[e.$1]; final t = h is Map ? asInt(h['t']) : null; @@ -204,7 +212,8 @@ List dayMoments({ out.add(Moment( at: t, title: e.$2, - detail: '${v.round()} bpm at ${clockOfTs(t)}', + detail: l?.dayTimelineBpmAt(v.round(), clockOfTs(t)) ?? + '${v.round()} bpm at ${clockOfTs(t)}', icon: e.$3, color: C.red, )); @@ -214,10 +223,11 @@ List dayMoments({ // Events, de-duplicated: the strap delivers the same (id, ts) up to four // times, and a day with the charger on it should not read as four chargers. final seen = {}; + final events = _events(l); for (final e in (timeline['events'] as List?) ?? const []) { if (e is! Map) continue; final id = asInt(e['event_id']), t = asInt(e['ts']); - final def = id == null ? null : _events[id]; + final def = id == null ? null : events[id]; if (def == null || t == null || !seen.add('$id/$t')) continue; out.add(Moment( at: t, @@ -250,7 +260,7 @@ List dayMoments({ out.add(Moment( at: d.at, title: d.label, - detail: 'Taken at ${clockOfTs(d.at)}', + detail: l?.dayTimelineTakenAt(clockOfTs(d.at)) ?? 'Taken at ${clockOfTs(d.at)}', icon: LucideIcons.pill, color: C.purple, )); @@ -272,8 +282,8 @@ List dayMoments({ title: spec?.label ?? key.replaceAll('_', ' '), // "last one at" is the stored meaning, and saying just "at" would turn a // total plus one timestamp into a single event that never happened. - detail: '$n${spec == null || spec.unit.isEmpty ? '' : ' ${spec.unit}'} ' - '· last at ${clockOfTs(dayStart + min * 60)}', + detail: '$n${spec == null || spec.unit.isEmpty ? '' : ' ${spec.unit}'} · ' + '${l?.dayTimelineLastAt(clockOfTs(dayStart + min * 60)) ?? 'last at ${clockOfTs(dayStart + min * 60)}'}', icon: LucideIcons.notebookPen, color: C.domMind, )); @@ -289,6 +299,7 @@ List dayNotes({ Map journal = const {}, List fields = const [], List> journalRows = const [], + AppLocalizations? l, }) { final out = []; for (final r in journalRows) { @@ -303,7 +314,7 @@ List dayNotes({ } catch (_) {/* a malformed row loses its tags, not the note */} if (note.isEmpty && tags.isEmpty) continue; out.add(DayNote( - note.isEmpty ? 'Tagged' : note, + note.isEmpty ? (l?.dayTimelineTaggedTitle ?? 'Tagged') : note, tags.join(' · '), LucideIcons.notebookPen, )); @@ -527,7 +538,11 @@ class TimelineData { final List moments; final List notes; - static Future load(LocalRepository repo, {String? want}) async { + static Future load( + LocalRepository repo, { + String? want, + AppLocalizations? l, + }) async { final days = await repo.availableDays(); final today = await repo.getToday(); final day = pickDay( @@ -566,12 +581,14 @@ class TimelineData { doses: taken, journal: journal, fields: fields, + l: l, ), notes: dayNotes( meals: [for (final m in meals) m.sanitised], journal: journal, fields: fields, journalRows: [for (final r in notes) if (r['date'] == day) r], + l: l, ), ); } @@ -593,6 +610,20 @@ class _DayTimelineScreenState extends State { bool _loading = true; String? _day; + // `TimelineData` bakes `AppLocalizations` strings into `moments`/`notes` at + // load time (see `TimelineData.load`), so a language switch while this + // screen is alive would otherwise leave it showing the old locale until + // something else (a day change, a revision bump) happens to reload it. + // Sentinel so the system-default locale (`code == null`) is not mistaken + // for "never seen yet" on the first pass. + static const Object _localeUnset = Object(); + Object? _seenLocale = _localeUnset; + + /// Bumped on every `_load()` call so an OLDER one that resolves after a + /// NEWER one (a locale change firing while a day-nav load is still in + /// flight) can tell it lost the race and must not overwrite fresher data. + int _loadToken = 0; + @override void initState() { super.initState(); @@ -605,17 +636,41 @@ class _DayTimelineScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) => _load()); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // `code` alone misses a SYSTEM locale change while it is null (no + // in-app override) — see the same fix in RevisionReload. + final Object localeKey; + try { + final code = context.watch().code; + localeKey = code ?? Localizations.localeOf(context); + } catch (_) { + return; + } + if (identical(_seenLocale, _localeUnset)) { + _seenLocale = localeKey; + } else if (_seenLocale != localeKey && widget.data == null) { + _seenLocale = localeKey; + _load(); + } + } + Future _load() async { + final token = ++_loadToken; final repo = repoOf(context); if (repo == null) { - if (mounted) setState(() => _loading = false); + if (mounted && token == _loadToken) setState(() => _loading = false); return; } + final l = AppLocalizations.of(context); try { - final d = await TimelineData.load(repo, want: _day); - if (mounted) setState(() => (_d = d, _loading = false)); + final d = await TimelineData.load(repo, want: _day, l: l); + if (mounted && token == _loadToken) { + setState(() => (_d = d, _loading = false)); + } } catch (_) { - if (mounted) setState(() => _loading = false); + if (mounted && token == _loadToken) setState(() => _loading = false); } } @@ -630,8 +685,9 @@ class _DayTimelineScreenState extends State { @override Widget build(BuildContext c) { final d = _d ?? const TimelineData(); - return detailScaffold(c, 'Breakdown of your day', - sub: 'MIDNIGHT TO MIDNIGHT', [ + final l = AppLocalizations.of(c); + return detailScaffold(c, l?.dayTimelineTitle ?? 'Breakdown of your day', + sub: l?.dayTimelineSub ?? 'MIDNIGHT TO MIDNIGHT', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), if (_loading) ...[ const SizedBox(height: S.x8), @@ -651,6 +707,7 @@ class _DayTimelineScreenState extends State { Widget? dayGraphCard(BuildContext c, DayGraph g) { if (!g.hasCurve) return null; final p = P.of(c); + final l = AppLocalizations.of(c); final n = g.slots; double at(int m) => n <= 0 ? 0 : m / n; final axis = AxisSpec.of([for (final v in g.hr) ?v], ticks: 3); @@ -660,19 +717,24 @@ Widget? dayGraphCard(BuildContext c, DayGraph g) { final gaps = g.unmeasured; return Surface( child: ChartFrame( - title: 'Heart rate', + title: l?.dayTimelineHeartRateTitle ?? 'Heart rate', unit: 'bpm', height: 200, yAxis: axis, // Three, and only three, because ChartFrame lays the first flush left, // the last flush right and the rest centred — which puts a middle label // exactly on the middle of the plot and a five-label row 5 % out. - xLabels: const ['Midnight', 'Noon', 'Midnight'], + xLabels: [ + l?.dayTimelineMidnight ?? 'Midnight', + l?.dayTimelineNoon ?? 'Noon', + l?.dayTimelineMidnight ?? 'Midnight', + ], legend: [ - if (g.rest.isNotEmpty) ('Asleep', asleep), - if (g.work.isNotEmpty) ('Workout', workout), - if (g.movement.any((v) => v != null)) ('Moving', p.on(C.domMove)), - if (gaps.isNotEmpty) ('Not recorded', p.card2), + if (g.rest.isNotEmpty) (l?.dayTimelineAsleep ?? 'Asleep', asleep), + if (g.work.isNotEmpty) (l?.dayTimelineWorkout ?? 'Workout', workout), + if (g.movement.any((v) => v != null)) + (l?.dayTimelineMoving ?? 'Moving', p.on(C.domMove)), + if (gaps.isNotEmpty) (l?.dayTimelineNotRecorded ?? 'Not recorded', p.card2), ], series: g.hr, child: Stack(children: [ @@ -710,21 +772,23 @@ Widget? dayGraphCard(BuildContext c, DayGraph g) { /// state of it without a repository. List timelineBody(BuildContext c, TimelineData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final graph = dayGraphCard(c, d.graph); return [ ?graph, if (d.moments.isEmpty && d.notes.isEmpty && graph == null) - const StatusCard( - 'Nothing was recorded on this day', - 'No sleep, no session, no log and no band event carrying a time. A day ' - 'with nothing on it is usually a day the band was off.', + StatusCard( + l?.dayTimelineNothingRecordedTitle ?? 'Nothing was recorded on this day', + l?.dayTimelineNothingRecordedBody ?? + 'No sleep, no session, no log and no band event carrying a time. A day ' + 'with nothing on it is usually a day the band was off.', icon: LucideIcons.circleSlash, ) else ...[ if (d.moments.isEmpty) - const StatusCard( - 'Nothing on this day carries a time', - 'What was logged for it is below.', + StatusCard( + l?.dayTimelineNoTimeTitle ?? 'Nothing on this day carries a time', + l?.dayTimelineNoTimeBody ?? 'What was logged for it is below.', icon: LucideIcons.clock, ) else @@ -732,7 +796,7 @@ List timelineBody(BuildContext c, TimelineData d) { // that the picture is above it: the graph says when, this says what, // and without a name between them the rows read as a caption. Section( - 'What happened', + l?.dayTimelineWhatHappenedSection ?? 'What happened', Surface( pad: const EdgeInsets.fromLTRB(S.x4, S.x2, S.x4, S.x2), child: Column( @@ -744,7 +808,7 @@ List timelineBody(BuildContext c, TimelineData d) { ), if (d.notes.isNotEmpty) Section( - 'Also logged on this day', + l?.dayTimelineAlsoLoggedSection ?? 'Also logged on this day', Surface( pad: const EdgeInsets.fromLTRB(S.x4, S.x2, S.x4, S.x2), child: Column( @@ -780,16 +844,18 @@ List timelineBody(BuildContext c, TimelineData d) { Padding( padding: const EdgeInsets.only(top: S.x2, left: S.x1), child: Text( - 'These were recorded against the day and carry no time of day, so ' - 'they are not placed on it.', + l?.dayTimelineNoTimeNote ?? + 'These were recorded against the day and carry no time of day, so ' + 'they are not placed on it.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ), ], const SizedBox(height: S.x4), Text( - 'Patterns in your own logs, not causes. Two things next to each other ' - 'here happened near each other, which is all this page claims.', + l?.dayTimelinePatternsNote ?? + 'Patterns in your own logs, not causes. Two things next to each other ' + 'here happened near each other, which is all this page claims.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ]; diff --git a/lib/ui2/screens/driver_breakdown.dart b/lib/ui2/screens/driver_breakdown.dart index b963441f..a9ca98b5 100644 --- a/lib/ui2/screens/driver_breakdown.dart +++ b/lib/ui2/screens/driver_breakdown.dart @@ -27,6 +27,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart' show whyFromNote; import '../ui2.dart'; import 'home_screen.dart' @@ -225,7 +226,8 @@ List driverFacts({ /// /// Null when the numbers are not there, or are not in units a person can read /// (see [DriverFacts.numeric]). Never a partial sentence with a blank in it. -String? driverValueLine(DriverFacts f) { +String? driverValueLine(BuildContext c, DriverFacts f) { + final l = AppLocalizations.of(c); final unit = unitBeside(f.spec.unit); final suffix = unit.isEmpty ? '' : ' $unit'; if (!f.numeric) { @@ -233,18 +235,24 @@ String? driverValueLine(DriverFacts f) { // usual" is the whole honest content of a raw ADC deviation. final d = f.delta; if (d == null || d == 0) return null; - return d > 0 ? 'Higher than your usual' : 'Lower than your usual'; + return d > 0 + ? (l?.driverBreakdownHigherThanUsual ?? 'Higher than your usual') + : (l?.driverBreakdownLowerThanUsual ?? 'Lower than your usual'); } final v = f.value, u = f.usual, d = f.delta; if (v == null) return null; final now = '${metricValue(f.spec.unit, v)}$suffix'; if (u == null || d == null) return now; if (metricValue(f.spec.unit, d.abs()) == metricValue(f.spec.unit, 0)) { - return '$now · right on your usual'; + return l?.driverBreakdownRightOnUsual(now) ?? '$now · right on your usual'; } - return '$now · ${metricValue(f.spec.unit, d.abs())}$suffix ' - '${d > 0 ? 'above' : 'below'} your usual ' - '${metricValue(f.spec.unit, u)}$suffix'; + final deltaStr = '${metricValue(f.spec.unit, d.abs())}$suffix'; + final usualStr = '${metricValue(f.spec.unit, u)}$suffix'; + return d > 0 + ? (l?.driverBreakdownAboveUsual(now, deltaStr, usualStr) ?? + '$now · $deltaStr above your usual $usualStr') + : (l?.driverBreakdownBelowUsual(now, deltaStr, usualStr) ?? + '$now · $deltaStr below your usual $usualStr'); } /// The qualifiers, in `ReadinessDetail`'s own words so the two screens agree. @@ -259,22 +267,28 @@ String? driverValueLine(DriverFacts f) { /// 4. outside it, but still inside measurement noise, /// 5. outside it, with no honest noise estimate — which says nothing rather /// than rounding a missing MDC up into a claim. -List driverQualifiers(DriverFacts f) { +List driverQualifiers(BuildContext c, DriverFacts f) { + final l = AppLocalizations.of(c); final share = f.weightShare; final m = f.mdcMultiples; return [ - if (share != null) '${(share * 100).round()}% weight', - if (!f.used) 'not available', - if (f.used && f.contribution == null) 'contribution not reported', + if (share != null) + l?.driverBreakdownWeightPct((share * 100).round()) ?? + '${(share * 100).round()}% weight', + if (!f.used) l?.driverBreakdownNotAvailable ?? 'not available', + if (f.used && f.contribution == null) + l?.driverBreakdownContributionNotReported ?? 'contribution not reported', // A raw sensor deviation says so, every time it appears. - if (!f.numeric) 'relative, uncalibrated', + if (!f.numeric) + l?.driverBreakdownRelativeUncalibrated ?? 'relative, uncalibrated', if (f.used && !f.beyondUsualSpread) - 'within your usual spread' + l?.driverBreakdownWithinUsualSpread ?? 'within your usual spread' else if (f.used && m != null) m.abs() >= 1 - ? 'bigger than measurement noise' - : 'outside your usual spread, but small enough to be measurement ' - 'noise', + ? (l?.driverBreakdownBiggerThanNoise ?? 'bigger than measurement noise') + : (l?.driverBreakdownSmallerThanNoise ?? + 'outside your usual spread, but small enough to be measurement ' + 'noise'), ]; } @@ -306,6 +320,7 @@ class _DriverBreakdownState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final g = splitDrivers(widget.facts); final rows = []; @@ -327,12 +342,12 @@ class _DriverBreakdownState extends State { } } - group('What helped', g.helped); - group('What held you back', g.held); + group(l?.driverBreakdownWhatHelped ?? 'What helped', g.helped); + group(l?.driverBreakdownWhatHeldYouBack ?? 'What held you back', g.held); // Deliberately last and deliberately not called "what did nothing": an // input that was never measured did not fail to move your score, it was // absent, and the row underneath says which. - group('Neither', g.neither); + group(l?.driverBreakdownNeither ?? 'Neither', g.neither); return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Surface( @@ -344,10 +359,11 @@ class _DriverBreakdownState extends State { elevation: 0, color: p.card2, child: Text( - 'Each input is ranked against your own history — a parallel view of ' - 'the same inputs, not slices of the score itself. "Measurement noise" ' - 'is how far a reading can move on its own without anything having ' - 'changed. Patterns in your own logs, not causes.', + l?.driverBreakdownFooter ?? + 'Each input is ranked against your own history — a parallel view of ' + 'the same inputs, not slices of the score itself. "Measurement ' + 'noise" is how far a reading can move on its own without ' + 'anything having changed. Patterns in your own logs, not causes.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ), @@ -365,9 +381,10 @@ class _DriverTile extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final contribution = f.used ? f.contribution : null; - final value = driverValueLine(f); - final quals = driverQualifiers(f).join(' · '); + final value = driverValueLine(c, f); + final quals = driverQualifiers(c, f).join(' · '); // The pipeline's own reason, never a written-here one. `need_baseline: // have=2,need=7` becomes "Need 5 more nights"; anything it cannot read // comes back null and the row simply says "not available" above. @@ -413,8 +430,10 @@ class _DriverTile extends StatelessWidget { Pressable( onTap: onTap, semanticLabel: open - ? '${f.label}, hide its history' - : '${f.label}, show its history', + ? (l?.driverBreakdownHideHistory(f.label) ?? + '${f.label}, hide its history') + : (l?.driverBreakdownShowHistory(f.label) ?? + '${f.label}, show its history'), child: head, ), if (open) ...[ @@ -432,6 +451,7 @@ class _DriverTile extends StatelessWidget { /// and the band is the pipeline's own `|z| ≤ 1` — not a second definition of /// normal invented for a chart. Widget _chart(BuildContext c, P p, DriverFacts f) { + final l = AppLocalizations.of(c); final win = _window(f.series); final band = f.band; final ink = p.on(f.spec.color); @@ -454,13 +474,21 @@ Widget _chart(BuildContext c, P p, DriverFacts f) { yAxis: axis, xLabels: win.length < 2 ? const [] - : ['${win.length - 1} day${win.length == 2 ? '' : 's'} ago', 'Today'], + : [ + l?.driverBreakdownDaysAgo(win.length - 1) ?? + '${win.length - 1} day${win.length == 2 ? '' : 's'} ago', + l?.driverBreakdownToday ?? 'Today', + ], series: win, footnote: band == null ? null - : 'Your usual range ${metricValue(f.spec.unit, band.$1)}–' - '${metricValue(f.spec.unit, band.$2)}' - '${unit.isEmpty ? '' : ' $unit'}', + : (l?.driverBreakdownUsualRange( + metricValue(f.spec.unit, band.$1), + metricValue(f.spec.unit, band.$2), + unit.isEmpty ? '' : ' $unit') ?? + 'Your usual range ${metricValue(f.spec.unit, band.$1)}–' + '${metricValue(f.spec.unit, band.$2)}' + '${unit.isEmpty ? '' : ' $unit'}'), empty: axis == null ? const NoData() : null, child: axis == null ? const SizedBox.shrink() @@ -524,29 +552,35 @@ class _Band extends StatelessWidget { /// carries its own note, or nothing recorded says why — and the third case /// says exactly that instead of borrowing the cold-start sentence, which is a /// wrong answer to "the numbers exist and we will not stand behind them". -StatusCard driverAbsenceCard({ +StatusCard driverAbsenceCard( + BuildContext c, { Map? stale, String? note, VoidCallback? onSync, }) { + final l = AppLocalizations.of(c); if (stale != null) { return StatusCard( - 'No breakdown to show', + l?.driverBreakdownAbsenceTitle ?? 'No breakdown to show', switch (stale['kind']) { - 'algo_version' => 'How readiness is worked out changed with the last ' - 'update, and it is being rebuilt.', - 'stale' => 'The last rollup is too old to stand behind.', - _ => 'The stored rollup carries no version stamp.', + 'algo_version' => l?.driverBreakdownAbsenceAlgoVersion ?? + 'How readiness is worked out changed with the last ' + 'update, and it is being rebuilt.', + 'stale' => l?.driverBreakdownAbsenceStale ?? + 'The last rollup is too old to stand behind.', + _ => l?.driverBreakdownAbsenceNoVersion ?? + 'The stored rollup carries no version stamp.', }, - fix: onSync == null ? '' : 'Sync the band', + fix: onSync == null ? '' : (l?.driverBreakdownSyncTheBand ?? 'Sync the band'), onFix: onSync, icon: LucideIcons.refreshCw, ); } return StatusCard( - 'No breakdown to show', + l?.driverBreakdownAbsenceTitle ?? 'No breakdown to show', whyFromNote(note) ?? - 'Nothing recorded says why last night has no breakdown.', + (l?.driverBreakdownAbsenceNoReason ?? + 'Nothing recorded says why last night has no breakdown.'), icon: LucideIcons.listTree, ); } diff --git a/lib/ui2/screens/findings_log.dart b/lib/ui2/screens/findings_log.dart index 8198235a..f161ac04 100644 --- a/lib/ui2/screens/findings_log.dart +++ b/lib/ui2/screens/findings_log.dart @@ -30,6 +30,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../compute/findings.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'home_screen.dart' show prettyDay; import 'metric_detail.dart' show detailScaffold; @@ -56,6 +57,7 @@ class FindingsLog extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); // Already newest-first out of findingsHistory; grouped here only so a day // with three findings reads as one morning rather than three events. final byDay = >{}; @@ -65,14 +67,15 @@ class FindingsLog extends StatelessWidget { return detailScaffold( c, - 'Observations', + l?.findingsLogTitle ?? 'Observations', [ if (findings.isEmpty) - const StatusCard( - 'Nothing has stood out', - 'The watches for illness, unusual overnight physiology, skin ' - 'temperature and a shift in your resting heart rate have all ' - 'been quiet. That is an outcome, not an empty screen.', + StatusCard( + l?.findingsLogEmptyTitle ?? 'Nothing has stood out', + l?.findingsLogEmptyBody ?? + 'The watches for illness, unusual overnight physiology, skin ' + 'temperature and a shift in your resting heart rate have ' + 'all been quiet. That is an outcome, not an empty screen.', icon: LucideIcons.check, ) else ...[ @@ -105,9 +108,10 @@ class FindingsLog extends StatelessWidget { // written down when they fired, so a day whose data was later // re-derived changes here with it — including out of existence. Text( - 'Worked out from your own days each time this opens, not written ' - 'down when it happened — so if a day is re-analysed, what it ' - 'says here changes with it.', + l?.findingsLogDerivedNote ?? + 'Worked out from your own days each time this opens, not ' + 'written down when it happened — so if a day is ' + 're-analysed, what it says here changes with it.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], diff --git a/lib/ui2/screens/health_screen.dart b/lib/ui2/screens/health_screen.dart index 92104025..c5cfad02 100644 --- a/lib/ui2/screens/health_screen.dart +++ b/lib/ui2/screens/health_screen.dart @@ -15,6 +15,7 @@ import '../../data/day_label.dart'; import '../../data/db.dart'; import '../../data/lab_catalogue.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../ui2.dart'; import 'circadian_detail.dart'; @@ -377,6 +378,44 @@ const _catalogue = <_Cat>[ ]), ]; +/// Catalogue category titles and row blurbs are read off a top-level `const` +/// list, which cannot call `AppLocalizations.of(context)` itself — so the +/// lookup happens here, at render time, keyed off the same literal English +/// text/row key the const list already carries as its fallback. +String _catTitle(AppLocalizations? l, String title) => switch (title) { + 'Heart & rhythm' => l?.healthCatHeartRhythm ?? title, + 'Sleep' => l?.healthRowSleep ?? title, + 'Breathing' => l?.healthCatBreathing ?? title, + 'Movement & load' => l?.healthCatMovementLoad ?? title, + 'Body & wear' => l?.healthCatBodyWear ?? title, + _ => title, + }; + +String _rowBlurb(AppLocalizations? l, String key, String blurb) => + switch (key) { + 'resting_hr' => l?.healthBlurbRestingHr ?? blurb, + 'hrv' => l?.healthBlurbHrv ?? blurb, + 'hrv_cv' => l?.healthBlurbHrvCv ?? blurb, + 'lf_hf' => l?.healthBlurbLfHf ?? blurb, + 'dip' => l?.healthBlurbDip ?? blurb, + 'hrr' => l?.healthBlurbHrr ?? blurb, + 'sleep' => l?.healthBlurbSleep ?? blurb, + 'efficiency' => l?.healthBlurbEfficiency ?? blurb, + 'deep' => l?.healthBlurbDeep ?? blurb, + 'rem' => l?.healthBlurbRem ?? blurb, + 'nap_min' => l?.healthBlurbNapMin ?? blurb, + 'resp_rate' => l?.healthBlurbRespRate ?? blurb, + 'brv' => l?.healthBlurbBrv ?? blurb, + 'steps' => l?.healthBlurbSteps ?? blurb, + 'active_min' => l?.healthBlurbActiveMin ?? blurb, + 'calories' => l?.healthBlurbCalories ?? blurb, + 'strain' => l?.healthBlurbStrain ?? blurb, + 'trimp' => l?.healthBlurbTrimp ?? blurb, + 'skin_temp' => l?.healthBlurbSkinTemp ?? blurb, + 'wear' => l?.healthBlurbWear ?? blurb, + _ => blurb, + }; + class ExploreData { /// Non-null `metric_series` rows per key — used ONLY as has / hasn't. /// @@ -424,7 +463,13 @@ class _HealthScreenState extends State with RevisionReload { // discoverability failure this tab exists to fix. Labs takes the clip instead // — it is the manual-entry tab, the one a user goes looking for on purpose, // and the only one here that holds numbers this app did not measure. - static const _tabs = ['Overview', 'Explore', 'Trends', 'Vitals', 'Labs']; + List _tabsOf(AppLocalizations? l) => [ + l?.healthTabOverview ?? 'Overview', + l?.healthTabExplore ?? 'Explore', + l?.healthTabTrends ?? 'Trends', + l?.healthTabVitals ?? 'Vitals', + l?.healthTabLabs ?? 'Labs', + ]; late int _tab = widget.tab; HealthData? _d; @@ -532,14 +577,18 @@ class _HealthScreenState extends State with RevisionReload { /// The one card both failed reads render. Not "nothing logged yet" — a read /// that went wrong and an empty table are different states. - StatusCard _readFailed(String what, VoidCallback retry) => StatusCard( - 'Could not read your $what', - 'The stored rows failed to load. Nothing was deleted — this is a read ' - 'that went wrong.', - fix: 'Try again', - icon: LucideIcons.databaseZap, - onFix: retry, - ); + StatusCard _readFailed(String what, VoidCallback retry) { + final l = AppLocalizations.of(context); + return StatusCard( + l?.healthCouldNotRead(what) ?? 'Could not read your $what', + l?.healthReadFailedBody ?? + 'The stored rows failed to load. Nothing was deleted — this is a ' + 'read that went wrong.', + fix: l?.healthTryAgain ?? 'Try again', + icon: LucideIcons.databaseZap, + onFix: retry, + ); + } Future _loadExplore() async { if (_e != null) return; @@ -562,9 +611,10 @@ class _HealthScreenState extends State with RevisionReload { @override Widget build(BuildContext c) { final d = _d ?? const HealthData(); + final l = AppLocalizations.of(c); return ListView(padding: pad, children: [ - const ScreenTitle('Health'), - SubTabs(_tabs, _tab, _select, color: C.blue), + ScreenTitle(l?.healthTitle ?? 'Health'), + SubTabs(_tabsOf(l), _tab, _select, color: C.blue), const SizedBox(height: S.x5), if (_loading && _d == null) const Padding( @@ -585,6 +635,7 @@ class _HealthScreenState extends State with RevisionReload { // ─────────────── OVERVIEW ─────────────── Widget _overview(BuildContext c, HealthData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final rows = []; final gaps = []; @@ -603,7 +654,10 @@ class _HealthScreenState extends State with RevisionReload { bool overnight = true, Rising rising = Rising.neither}) { if (m.isEmpty) { - final s = StatusCard.forMetric('No ${name.toLowerCase()}', m, + final s = StatusCard.forMetric( + l?.healthNoMetric(name.toLowerCase()) ?? + 'No ${name.toLowerCase()}', + m, why: whyAbsent ?? '', gap: overnight ? d.nightGap : null); if (s != null) gaps.add(s); return; @@ -627,7 +681,8 @@ class _HealthScreenState extends State with RevisionReload { final sleepMin = d.sleepMin; final rhr = d.daily('resting_hr'); - row(rhr, LucideIcons.heart, C.red, 'Resting heart rate', ofNight('Overnight'), + row(rhr, LucideIcons.heart, C.red, l?.healthRowRestingHr ?? 'Resting heart rate', + ofNight(l?.healthSubOvernight ?? 'Overnight'), rhr.value == null ? '' : '${rhr.value!.round()}', 'bpm', d.spark('resting_hr', 24), 'resting_hr', // Sleep duration and nocturnal RHR are gated separately, so "no night @@ -639,28 +694,31 @@ class _HealthScreenState extends State with RevisionReload { // watches for a RISE, and what readiness scores as `lowerIsBetter`. rising: Rising.bad, whyAbsent: sleepMin.isEmpty - ? 'Read from sleep, and no night was scored.' + ? (l?.healthWhyReadFromSleep ?? + 'Read from sleep, and no night was scored.') : ''); final hrvMetric = d.hrv; - row(hrvMetric, LucideIcons.activity, C.green, 'HRV', - ofNight('RMSSD, asleep'), + row(hrvMetric, LucideIcons.activity, C.green, l?.healthRowHrv ?? 'HRV', + ofNight(l?.healthSubRmssdAsleep ?? 'RMSSD, asleep'), hrvMetric.value == null ? '' : '${hrvMetric.value!.round()}', 'ms', d.spark('hrv', 24), 'hrv', rising: Rising.good, // Blaming signal quality unconditionally told a day-one user their // sensor produced dirty data on a night that never happened. whyAbsent: sleepMin.isEmpty - ? 'Read only from sleep, and no night was scored.' + ? (l?.healthWhyReadOnlyFromSleep ?? + 'Read only from sleep, and no night was scored.') : ''); - row(sleepMin, LucideIcons.moon, C.blue, 'Sleep', - night == null ? 'Last night' : prettyDay(night), + row(sleepMin, LucideIcons.moon, C.blue, l?.healthRowSleep ?? 'Sleep', + night == null ? (l?.healthSubLastNight ?? 'Last night') : prettyDay(night), hm(sleepMin.value), '', d.spark('sleep', 24), 'sleep', // More sleep is the direction this app coaches towards — `sleepNeed` // exists to say you are short of it, never over it. rising: Rising.good, - whyAbsent: 'No sleep period long enough to score was recorded.'); + whyAbsent: l?.healthWhySleepNotLongEnough ?? + 'No sleep period long enough to score was recorded.'); final stressBlock = d.today['stress']; final stressScore = @@ -669,9 +727,9 @@ class _HealthScreenState extends State with RevisionReload { d.stress, LucideIcons.brain, C.purple, - 'Stress', + l?.healthRowStress ?? 'Stress', ofNight((stressBlock is Map ? stressBlock['level']?.toString() : null) ?? - 'Stress'), + (l?.healthRowStress ?? 'Stress')), stressScore == null ? '' : '${stressScore.round()}', // 0–100, and the scale has to be on the row. Wellness has always shown // it for the same number. @@ -682,12 +740,13 @@ class _HealthScreenState extends State with RevisionReload { // Was 'No resting stretch long enough last night.' — one of several // gates stress abstains on, asserted for all of them. whyAbsent: sleepMin.isEmpty - ? 'Read from the night, and no night was scored.' + ? (l?.healthWhyReadFromNight ?? + 'Read from the night, and no night was scored.') : ''); final respMetric = d.resp; - row(respMetric, LucideIcons.wind, C.teal, 'Respiratory rate', - ofNight('Asleep'), + row(respMetric, LucideIcons.wind, C.teal, l?.healthRowRespRate ?? 'Respiratory rate', + ofNight(l?.healthSubAsleep ?? 'Asleep'), respMetric.value == null ? '' : respMetric.value!.toStringAsFixed(1), 'br/min', d.spark('resp_rate', 24), 'resp_rate', @@ -704,8 +763,10 @@ class _HealthScreenState extends State with RevisionReload { whyAbsent: respMetric.note?.isNotEmpty == true ? respMetric.note! : (sleepMin.isEmpty - ? 'Read only from sleep, and no night was scored.' - : 'No reading from last night.')); + ? (l?.healthWhyReadOnlyFromSleep ?? + 'Read only from sleep, and no night was scored.') + : (l?.healthWhyNoReadingLastNight ?? + 'No reading from last night.'))); final illness = d.today['illness']; final state = illness is Map ? illness['state']?.toString() : null; @@ -726,21 +787,35 @@ class _HealthScreenState extends State with RevisionReload { ? null : Observation( state == 'red' - ? 'Several nights in a row are away from your normal' + ? (l?.healthIllnessRedTitle ?? + 'Several nights in a row are away from your normal') : (illnessBehind == null || illnessBehind <= 0 - ? 'Last night sat outside your normal range' - : '${prettyDay(illnessDay)} sat outside your normal range'), + ? (l?.healthIllnessLastNightTitle ?? + 'Last night sat outside your normal range') + : (l?.healthIllnessDayTitle(prettyDay(illnessDay)) ?? + '${prettyDay(illnessDay)} sat outside your normal range')), // The RUN is what is above baseline — the accumulator only clears // after two nights back under. The stored z is the LATEST night's // own deviation and can be negative while the run is still up, // which read as "tracking above your own baseline, 1.3 deviations // below it". - 'Your nocturnal resting heart rate has been running above your own ' - 'baseline${illnessZ == null ? '' : '; that night sat ' - '${illnessZ.abs().toStringAsFixed(1)} standard deviations ' - '${illnessZ >= 0 ? 'above' : 'below'} it'}. This watches ' - 'one signal only. It names a pattern, not a cause.', - advice: 'Worth noting if it continues past a couple of days.', + illnessZ == null + ? (l?.healthIllnessBodyNoZ ?? + 'Your nocturnal resting heart rate has been running above ' + 'your own baseline. This watches one signal only. It ' + 'names a pattern, not a cause.') + : (l?.healthIllnessBodyWithZ( + illnessZ.abs().toStringAsFixed(1), + illnessZ >= 0 + ? (l.healthDirectionAbove) + : (l.healthDirectionBelow)) ?? + 'Your nocturnal resting heart rate has been running above ' + 'your own baseline; that night sat ' + '${illnessZ.abs().toStringAsFixed(1)} standard deviations ' + '${illnessZ >= 0 ? 'above' : 'below'} it. This watches ' + 'one signal only. It names a pattern, not a cause.'), + advice: l?.healthIllnessAdvice ?? + 'Worth noting if it continues past a couple of days.', ); return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -768,7 +843,7 @@ class _HealthScreenState extends State with RevisionReload { if (illnessCard != null || d.findings.isNotEmpty) ...[ const SizedBox(height: S.x4), Section( - 'Observations', + l?.healthObservationsTitle ?? 'Observations', illnessCard ?? // No live illness, but the log is not empty: the newest entry in // place and the rest one tap away. ONE row — a wall of findings @@ -777,7 +852,7 @@ class _HealthScreenState extends State with RevisionReload { onTap: () => go(c, FindingsLog(d.findings)), child: FindingRow(d.findings.first), ), - action: d.findings.isEmpty ? null : 'See all', + action: d.findings.isEmpty ? null : (l?.healthSeeAll ?? 'See all'), onAction: d.findings.isEmpty ? null : () => go(c, FindingsLog(d.findings)), ), @@ -787,7 +862,7 @@ class _HealthScreenState extends State with RevisionReload { // section is here on a day with no naps too, because the door to logging // one has to exist on exactly the day the detector found nothing. Section( - 'Naps', + l?.healthNapsTitle ?? 'Naps', d.napCount == null // `napDay` defaults to '' and `prettyDay` returns '' for anything // it cannot parse, so this printed "No nap reading for" with the @@ -795,10 +870,13 @@ class _HealthScreenState extends State with RevisionReload { // when there is one to name. ? StatusCard( prettyDay(d.napDay).isEmpty - ? 'No nap reading' - : 'No nap reading for ${prettyDay(d.napDay)}', - 'Naps come off the same second-by-second recording as the rest ' - 'of the day, and this day does not have enough of it.', + ? (l?.healthNoNapReading ?? 'No nap reading') + : (l?.healthNoNapReadingFor(prettyDay(d.napDay)) ?? + 'No nap reading for ${prettyDay(d.napDay)}'), + l?.healthNapsBody ?? + 'Naps come off the same second-by-second recording as the ' + 'rest of the day, and this day does not have enough of ' + 'it.', icon: LucideIcons.sun, ) : Surface( @@ -806,18 +884,20 @@ class _HealthScreenState extends State with RevisionReload { child: MetricRow( LucideIcons.sun, C.indigo, - 'Daytime sleep', + l?.healthDaytimeSleep ?? 'Daytime sleep', // A MEASURED zero, not a dash: the day was judged and held // no nap. The two are different answers and read as two. - d.napCount == 0 ? 'None' : hm(d.napMin), + d.napCount == 0 ? (l?.healthValueNone ?? 'None') : hm(d.napMin), sub: d.napCount == 0 - ? 'None detected · ${prettyDay(d.napDay)}' - : '${d.napCount} nap${d.napCount == 1 ? '' : 's'} · ' + ? (l?.healthNoneDetectedOn(prettyDay(d.napDay)) ?? + 'None detected · ${prettyDay(d.napDay)}') + : '${l?.healthNapCountLabel(d.napCount!) ?? '${d.napCount} ' + 'nap${d.napCount == 1 ? '' : 's'}'} · ' '${prettyDay(d.napDay)}', onTap: () => go(c, NapsScreen(day: d.napDay)), ), ), - action: 'Add or correct', + action: l?.healthAddOrCorrect ?? 'Add or correct', onAction: () => go(c, NapsScreen(day: d.napDay)), ), @@ -864,6 +944,7 @@ class _HealthScreenState extends State with RevisionReload { // ─────────────── TRENDS ─────────────── Widget _trends(BuildContext c, HealthData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final cd = d.insights; final chrono = envValue(cd['chronotype']) ?? const {}; final sjl = envValue(cd['social_jetlag']) ?? const {}; @@ -890,8 +971,8 @@ class _HealthScreenState extends State with RevisionReload { final s = valuesOf(pts); if (s.isEmpty) { return StatusCard( - 'No $label trend yet', - '0 days stored.', + l?.healthNoTrendYet(label) ?? 'No $label trend yet', + l?.healthZeroDaysStored ?? '0 days stored.', icon: LucideIcons.chartLine, ); } @@ -905,22 +986,25 @@ class _HealthScreenState extends State with RevisionReload { final base = against ?? mean; final window = against != null ? (againstLabel ?? '') - : 'vs your ${prior.length}-day average'; + : (l?.healthVsDayAverage(prior.length) ?? + 'vs your ${prior.length}-day average'); final delta = base == null ? 0.0 : s.last - base; final win = denseDays(pts, 30); final metricKey = key == 'sleep' ? 'sleep' : key; // The hero number is the newest STORED point, which after a sync gap is // not today's. Say when it is from rather than let the card imply now. final behind = daysBehind(pts.last.t) ?? 0; - final asOf = behind <= 0 ? '' : ' · as of ${axisDay(pts.last.t)}'; + final asOf = behind <= 0 + ? '' + : (l?.healthAsOf(axisDay(pts.last.t)) ?? ' · as of ${axisDay(pts.last.t)}'); return TrendCard( label, key == 'sleep' ? hm(s.last) : metricValue(unit, s.last), key == 'sleep' ? '' : unit, base == null - ? 'no baseline' + ? (l?.healthNoBaseline ?? 'no baseline') : (key == 'sleep' ? hm(delta.abs()) : metricValue(unit, delta.abs())), - '${base == null ? 'first readings' : window}$asOf', + '${base == null ? (l?.healthFirstReadings ?? 'first readings') : window}$asOf', win, col, up: delta >= 0, @@ -932,34 +1016,37 @@ class _HealthScreenState extends State with RevisionReload { } return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - trend('resting_hr', 'Resting heart rate', 'bpm', C.red, - higherBetter: false), + trend('resting_hr', l?.healthRowRestingHr ?? 'Resting heart rate', 'bpm', + C.red, higherBetter: false), const SizedBox(height: S.x3), - trend('hrv', 'HRV', 'ms', C.green), + trend('hrv', l?.healthRowHrv ?? 'HRV', 'ms', C.green), const SizedBox(height: S.x3), if (d.need.value == null) - trend('sleep', 'Time asleep', '', C.blue) + trend('sleep', l?.healthTimeAsleep ?? 'Time asleep', '', C.blue) else // `need` here is `crossday.sleep_coach.need` — the COMPUTED need. It is // never `sleep.need_min`, which is a hardcoded 480. - trend('sleep', 'Time asleep', '', C.blue, + trend('sleep', l?.healthTimeAsleep ?? 'Time asleep', '', C.blue, against: d.need.value!.toDouble(), - againstLabel: 'vs your ${hm(d.need.value)} need'), + againstLabel: l?.healthVsNeed(hm(d.need.value)) ?? + 'vs your ${hm(d.need.value)} need'), // Chronotype, jetlag and regularity ALL come out of the cross-day // rollup. When it is withheld, the section says why rather than showing // the cold-start "it takes a few weeks" line, which would be a lie. if (stale != null) - Section('Body clock', stale) + Section(l?.healthBodyClockTitle ?? 'Body clock', stale) else Section( - 'Body clock', + l?.healthBodyClockTitle ?? 'Body clock', Surface( onTap: () => go(c, const CircadianDetail()), child: Column(children: [ Row(children: [ Expanded( - child: Text('Chronotype, jetlag and regularity', + child: Text( + l?.healthChronotypeJetlagRegularity ?? + 'Chronotype, jetlag and regularity', style: F.cap.copyWith(color: p.ink2)), ), Icon(LucideIcons.chevronRight, size: 16, color: p.ink3), @@ -968,28 +1055,32 @@ class _HealthScreenState extends State with RevisionReload { const SizedBox(height: S.x4), InlineMetrics([ if (chrono['type_label'] != null) - ('CHRONOTYPE', chrono['type_label'].toString(), C.indigo), + (l?.healthChronotypeLabel ?? 'CHRONOTYPE', + chrono['type_label'].toString(), C.indigo), if (sjlH != null) - ('SOCIAL JETLAG', _hoursHm(sjlH), C.orange), + (l?.healthSocialJetlagLabel ?? 'SOCIAL JETLAG', + _hoursHm(sjlH), C.orange), if (sri != null) - ('REGULARITY', '${sri.round()} / 100', C.green), + (l?.healthRegularityLabel ?? 'REGULARITY', + '${sri.round()} / 100', C.green), ]), ], ]), ), - action: 'Explore', + action: l?.healthTabExplore ?? 'Explore', onAction: () => go(c, const CircadianDetail()), ), Section( - 'Consistency', + l?.healthConsistencyTitle ?? 'Consistency', Surface( child: Consistency( // Already windowed to the last 30 calendar days by `HealthData.load` // — the clamp is a floor for a bad count, not the window. d.daysWithData.clamp(0, 30), 30, - 'Days with a derived record in the last 30 days', + l?.healthDaysWithRecord ?? + 'Days with a derived record in the last 30 days', C.domHealth, ), ), @@ -1005,10 +1096,11 @@ class _HealthScreenState extends State with RevisionReload { // ─────────────── VITALS ─────────────── Widget _vitals(BuildContext c, HealthData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final v = _v; if (v == null) { return _vFailed - ? _readFailed('vitals', () { + ? _readFailed(l?.healthWhatVitals ?? 'vitals', () { setState(() => _vFailed = false); _loadVitals(); }) @@ -1021,7 +1113,8 @@ class _HealthScreenState extends State with RevisionReload { // WHICH DAY this tab is showing. Every row here used to say "Today" for a // day the loader had fallen back to, which after a sync gap is days ago. final behind = _behind(v.day); - final dayWord = behind == null || behind <= 0 ? 'Today' : prettyDay(v.day); + final isToday = behind == null || behind <= 0; + final dayWord = isToday ? (l?.healthToday ?? 'Today') : prettyDay(v.day); // Skin temperature comes off the latest OVERNIGHT bundle, not the day the // other three rows describe, so it gets its own night when they differ. final tempNight = heldOverNightOf(d.today); @@ -1047,37 +1140,42 @@ class _HealthScreenState extends State with RevisionReload { // list in the app already puts it. final rows = [ if (lo != null && hi != null) - MetricRow(LucideIcons.heart, C.red, 'Heart rate', + MetricRow(LucideIcons.heart, C.red, l?.healthRowHeartRate ?? 'Heart rate', '${lo.round()} – ${hi.round()}', sub: dayWord, unit: 'bpm'), if (resp != null) - MetricRow(LucideIcons.wind, C.teal, 'Respiratory rate', + MetricRow(LucideIcons.wind, C.teal, l?.healthRowRespRate ?? 'Respiratory rate', resp.toStringAsFixed(1), - sub: 'Asleep', unit: 'br/min'), + sub: l?.healthSubAsleep ?? 'Asleep', unit: 'br/min'), if (skinTemp.value != null) // NAME THE QUANTITY. This is `skin_temp_z` — standard deviations from // the user's own baseline. It printed signed and unitless beside a // heart rate in bpm, so it read as °C; and the sleep scrub's // "temperature" is a THIRD quantity again (raw ADC minus that day's // median), which is why neither may go unlabelled. - MetricRow(LucideIcons.thermometer, C.orange, 'Skin temperature', + MetricRow(LucideIcons.thermometer, C.orange, + l?.healthRowSkinTemp ?? 'Skin temperature', '${skinTemp.value! >= 0 ? '+' : '−'}' '${skinTemp.value!.abs().toStringAsFixed(2)}', sub: tempNight == null - ? 'vs your own nights' - : 'vs your own nights · ${prettyDay(tempNight)}', + ? (l?.healthVsOwnNights ?? 'vs your own nights') + : (l?.healthVsOwnNightsOn(prettyDay(tempNight)) ?? + 'vs your own nights · ${prettyDay(tempNight)}'), unit: 'SD', // Both this row and the wear row below it carry a FULL, written, // cited spec in `metric_detail.dart` that no tap in the app opened. // The number was on screen and its method was unreachable. onTap: () => go(c, const MetricDetail('skin_temp'))), if (worn != null) - MetricRow(LucideIcons.watch, C.green, 'Wear time', hm(worn), + MetricRow(LucideIcons.watch, C.green, l?.healthRowWearTime ?? 'Wear time', + hm(worn), // `83.33333333333333% of the day` shipped. It is a percentage. sub: coverage == null ? dayWord - : '${coverage.round()}% of ' - '${dayWord == 'Today' ? 'the day' : dayWord}', + : (l?.healthCoverageOf(coverage.round(), + isToday ? (l.healthTheDay) : dayWord) ?? + '${coverage.round()}% of ' + '${isToday ? 'the day' : dayWord}'), onTap: () => go(c, const MetricDetail('wear'))), ]; @@ -1085,9 +1183,9 @@ class _HealthScreenState extends State with RevisionReload { ...dayNavRow(_vDay ?? v.day, v.days, _goVitalsDay), if (rows.isEmpty) StatusCard( - 'Nothing measured for this day', - 'No band recordings reached this day.', - fix: syncOf(c) == null ? '' : 'Sync the band', + l?.healthNothingMeasuredDay ?? 'Nothing measured for this day', + l?.healthNoBandRecordings ?? 'No band recordings reached this day.', + fix: syncOf(c) == null ? '' : (l?.healthSyncTheBand ?? 'Sync the band'), icon: LucideIcons.watch, onFix: syncOf(c), ) @@ -1112,9 +1210,13 @@ class _HealthScreenState extends State with RevisionReload { // a feature. if (rmssd != null) Section( - 'Deep dives', - DeepDiveCard('Heart rate variability', '${rmssd.round()}', 'ms', - 'Time, frequency and non-linear', C.green, + l?.healthDeepDivesTitle ?? 'Deep dives', + DeepDiveCard( + l?.healthHeartRateVariability ?? 'Heart rate variability', + '${rmssd.round()}', + 'ms', + l?.healthTimeFrequencyNonLinear ?? 'Time, frequency and non-linear', + C.green, preview: _hrvPreview(c, d), onTap: () => go(c, const Investigate('hrv'))), ), @@ -1135,16 +1237,22 @@ class _HealthScreenState extends State with RevisionReload { final have = [for (final v in win) ?v]; final axis = AxisSpec.of(have, ticks: 2); final p = P.of(c); + final l = AppLocalizations.of(c); return ChartFrame( - title: 'RMSSD, ${have.length} of the last $days nights', + title: l?.healthRmssdOfLastNights(have.length, days) ?? + 'RMSSD, ${have.length} of the last $days nights', unit: 'ms', height: 48, yAxis: axis, xLabels: have.length < 2 ? const [] - : ['${days - 1} nights ago', 'Last night'], + : [ + l?.healthNightsAgo(days - 1) ?? '${days - 1} nights ago', + l?.healthSubLastNight ?? 'Last night', + ], empty: have.length < 2 - ? const NoData(message: 'One night is not a trend yet') + ? NoData( + message: l?.healthOneNightNotTrend ?? 'One night is not a trend yet') : null, series: win, child: CustomPaint( @@ -1157,10 +1265,11 @@ class _HealthScreenState extends State with RevisionReload { // ─────────────── EXPLORE ─────────────── Widget _explore(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final e = _e; if (e == null) { return _eFailed - ? _readFailed('measures', () { + ? _readFailed(l?.healthMeasuresUnit ?? 'measures', () { setState(() => _eFailed = false); _loadExplore(); }) @@ -1181,21 +1290,25 @@ class _HealthScreenState extends State with RevisionReload { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Surface( child: Consistency(have, total, - 'Measures with stored history on this device', C.domHealth, - unit: 'measures'), + l?.healthMeasuresWithHistory ?? + 'Measures with stored history on this device', + C.domHealth, + unit: l?.healthMeasuresUnit ?? 'measures'), ), const SizedBox(height: S.x3), // Not a promise of insight — a statement of what a tap gets you. Every // row below opens the same drill-down: the chart, your own range, the // method in full, and the paper it came from. Text( - 'Each one opens its chart, your own range, and how it is worked out.', + l?.healthEachOneOpens ?? + 'Each one opens its chart, your own range, and how it is worked out.', style: F.over.copyWith(color: p.ink3, height: 1.6)), for (final f in _catalogue) _family(c, p, f, e.counts), ]); } Widget _family(BuildContext c, P p, _Cat f, Map counts) { + final l = AppLocalizations.of(c); final have = [ for (final r in f.rows) if ((counts[r.series] ?? 0) > 0) r, @@ -1206,7 +1319,7 @@ class _HealthScreenState extends State with RevisionReload { ]; return Section( - f.title, + _catTitle(l, f.title), Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (have.isNotEmpty) Surface( @@ -1234,7 +1347,7 @@ class _HealthScreenState extends State with RevisionReload { // below. Has / hasn't is the only thing an index owes you, // and the layout already says it. return MetricRow(s.icon, s.color, s.title, '', - sub: r.blurb, + sub: _rowBlurb(l, r.key, r.blurb), onTap: () => go(c, MetricDetail(r.key))); }), ], @@ -1243,13 +1356,16 @@ class _HealthScreenState extends State with RevisionReload { if (none.isNotEmpty) ...[ if (have.isNotEmpty) const SizedBox(height: S.x3), StatusCard( - have.isEmpty ? 'Nothing measured here yet' : 'Not measured yet', + have.isEmpty + ? (l?.healthNothingMeasuredHere ?? 'Nothing measured here yet') + : (l?.healthNotMeasuredYet ?? 'Not measured yet'), // No cause is named, because none is known here: this screen reads // a row count, and a count of zero says the day never produced one // — never why. No `fix:` either; there is no button that makes a // derive happen for a night that has already been scored. '${none.map((r) => specOf(r.key).title).join(' · ')}. ' - 'No day on this device has produced one yet.', + '${l?.healthNoDayProduced ?? 'No day on this device has ' + 'produced one yet.'}', icon: LucideIcons.chartLine, ), ], @@ -1260,10 +1376,11 @@ class _HealthScreenState extends State with RevisionReload { // ─────────────── LABS ─────────────── Widget _labs(BuildContext c) { final p = P.of(c); + final loc = AppLocalizations.of(c); final l = _l; if (l == null) { return _lFailed - ? _readFailed('lab results', () { + ? _readFailed(loc?.healthWhatLabResults ?? 'lab results', () { setState(() => _lFailed = false); _loadLabs(); }) @@ -1299,9 +1416,10 @@ class _HealthScreenState extends State with RevisionReload { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (rows.isEmpty) StatusCard( - 'No lab results', - 'Nothing logged. Anything you add here stays on this device, and ' - 'anything you remove is gone from it.', + loc?.healthNoLabResults ?? 'No lab results', + loc?.healthNoLabResultsBody ?? + 'Nothing logged. Anything you add here stays on this device, ' + 'and anything you remove is gone from it.', icon: LucideIcons.testTube, ) else ...[ @@ -1317,12 +1435,14 @@ class _HealthScreenState extends State with RevisionReload { ]), ), const SizedBox(height: S.x3), - Text('Last panel ${lastDraw ?? ''} · logged by hand', + Text( + loc?.healthLastPanel(lastDraw?.toString() ?? '') ?? + 'Last panel ${lastDraw ?? ''} · logged by hand', style: F.over.copyWith(color: p.ink3)), ], if (mine.isNotEmpty) _myMarkers(p, mine, counts), const SizedBox(height: S.x4), - BigButton('Add a result', + BigButton(loc?.healthAddAResult ?? 'Add a result', icon: LucideIcons.plus, color: C.blue, soft: true, @@ -1331,13 +1451,15 @@ class _HealthScreenState extends State with RevisionReload { // The app never prints "abnormal" anywhere, so it does not need to say // it does not. What the user cannot know without being told is that the // range shown here is not the range their own lab used. - Text('Ranges differ by lab. Use the one on your report.', + Text(loc?.healthRangesDifferByLab ?? + 'Ranges differ by lab. Use the one on your report.', style: F.over.copyWith(color: p.ink3, height: 1.6)), ]); } Widget _lab(P p, LabMarker? m, Map r, String? sex, VoidCallback onRemove) { + final l = AppLocalizations.of(context); final v = (r['value'] as num?)?.toDouble(); final unit = (r['unit'] ?? m?.unit ?? '').toString(); final range = m?.rangeFor(sex); @@ -1348,7 +1470,8 @@ class _HealthScreenState extends State with RevisionReload { // the difference between a row that fits and one that overflows. return Pressable( onTap: onRemove, - semanticLabel: + semanticLabel: l?.healthRemoveMarkerFrom( + (m?.label ?? r['marker']).toString(), r['taken_on'].toString()) ?? 'Remove ${m?.label ?? r['marker']} from ${r['taken_on']}', child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), @@ -1371,9 +1494,12 @@ class _HealthScreenState extends State with RevisionReload { style: F.body.copyWith(color: p.ink)), Text( range == null - ? 'No reference interval · ${r['taken_on']}' - : 'Typical ${_num(range.low)}–${_num(range.high)} · ' - '${r['taken_on']}', + ? (l?.healthNoReferenceInterval(r['taken_on'].toString()) ?? + 'No reference interval · ${r['taken_on']}') + : (l?.healthTypicalRange(_num(range.low), _num(range.high), + r['taken_on'].toString()) ?? + 'Typical ${_num(range.low)}–${_num(range.high)} · ' + '${r['taken_on']}'), style: F.over.copyWith(color: p.ink3)), ]), ), @@ -1402,6 +1528,7 @@ class _HealthScreenState extends State with RevisionReload { /// blood results is how the wrong one is lost. Future _removeResult( LabMarker? m, Map r, LabsData l) async { + final loc = AppLocalizations.of(context); final marker = r['marker'].toString(); final takenOn = r['taken_on'].toString(); final label = m?.label ?? marker; @@ -1417,10 +1544,15 @@ class _HealthScreenState extends State with RevisionReload { final ok = await confirmRemove( context, - title: 'Remove $label from $takenOn?', - body: 'The ${m?.format(v) ?? _num(v)} $unit you logged for that draw. ' - 'It leaves this device and there is no undo.' - '${older == null ? '' : ' Your $older draw stays, and shows here instead.'}', + title: loc?.healthRemoveLabelFrom(label, takenOn) ?? + 'Remove $label from $takenOn?', + body: (loc?.healthRemoveLabBody(m?.format(v) ?? _num(v), unit) ?? + 'The ${m?.format(v) ?? _num(v)} $unit you logged for that draw. ' + 'It leaves this device and there is no undo.') + + (older == null + ? '' + : (loc?.healthRemoveLabOlderNote(older) ?? + ' Your $older draw stays, and shows here instead.')), ); if (!ok || !mounted) return; await LocalDb.deleteLabResult(marker, takenOn); @@ -1429,51 +1561,60 @@ class _HealthScreenState extends State with RevisionReload { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(older == null - ? 'Removed $label from $takenOn. No $label results left.' - : 'Removed $label from $takenOn. Showing your $older draw now.'), + ? (loc?.healthRemovedNoneLeft(label, takenOn) ?? + 'Removed $label from $takenOn. No $label results left.') + : (loc?.healthRemovedShowingOlder(label, takenOn, older) ?? + 'Removed $label from $takenOn. Showing your $older draw now.')), )); } /// Markers the user named. Only the DEFINITION is theirs to remove here — /// see [_removeMarker] for why one holding results is refused. - Widget _myMarkers(P p, List mine, Map counts) => - Section( - 'Markers you named', - Surface( - pad: const EdgeInsets.symmetric(horizontal: S.x4), - child: Column(children: [ - for (var i = 0; i < mine.length; i++) ...[ - if (i > 0) Divider(color: p.line, height: 1), - Pressable( - semanticLabel: 'Remove the ${mine[i].label} marker', - onTap: () => _removeMarker(mine[i], counts[mine[i].key] ?? 0), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: S.x3), - child: Row(children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(mine[i].label, - style: F.body.copyWith(color: p.ink)), - Text( - switch (counts[mine[i].key] ?? 0) { - 0 => 'Nothing logged under it', - 1 => '1 result · ${mine[i].unit}', - final n => '$n results · ${mine[i].unit}', - }, - style: F.over.copyWith(color: p.ink3)), - ]), - ), - const SizedBox(width: S.x2), - Icon(LucideIcons.trash2, size: 16, color: p.ink3), - ]), - ), + Widget _myMarkers(P p, List mine, Map counts) { + final l = AppLocalizations.of(context); + return Section( + l?.healthMarkersYouNamed ?? 'Markers you named', + Surface( + pad: const EdgeInsets.symmetric(horizontal: S.x4), + child: Column(children: [ + for (var i = 0; i < mine.length; i++) ...[ + if (i > 0) Divider(color: p.line, height: 1), + Pressable( + semanticLabel: l?.healthRemoveTheMarker(mine[i].label) ?? + 'Remove the ${mine[i].label} marker', + onTap: () => _removeMarker(mine[i], counts[mine[i].key] ?? 0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: S.x3), + child: Row(children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(mine[i].label, + style: F.body.copyWith(color: p.ink)), + Text( + (counts[mine[i].key] ?? 0) == 0 + ? (l?.healthNothingLoggedUnderIt ?? + 'Nothing logged under it') + : (l?.healthResultsCount( + counts[mine[i].key] ?? 0, + mine[i].unit) ?? + '${counts[mine[i].key]} ' + '${(counts[mine[i].key] ?? 0) == 1 ? 'result' : 'results'} · ' + '${mine[i].unit}'), + style: F.over.copyWith(color: p.ink3)), + ]), + ), + const SizedBox(width: S.x2), + Icon(LucideIcons.trash2, size: 16, color: p.ink3), + ]), ), - ], - ]), - ), - ); + ), + ], + ]), + ), + ); + } /// Removing a marker DEFINITION, which is not the same act as removing its /// readings — `deleteLabMarkerDef` deliberately leaves those alone, because @@ -1487,19 +1628,22 @@ class _HealthScreenState extends State with RevisionReload { /// a marker that still holds results is refused, and says how to proceed — /// the results are one screen up, each removable on its own. Future _removeMarker(LabMarker m, int results) async { + final l = AppLocalizations.of(context); if (results > 0) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('${m.label} still holds $results ' - '${results == 1 ? 'result' : 'results'}. Remove those first — the ' - 'marker is what labels them.'), + content: Text(l?.healthStillHoldsResults(results, m.label) ?? + '${m.label} still holds $results ' + '${results == 1 ? 'result' : 'results'}. Remove those first — ' + 'the marker is what labels them.'), )); return; } final ok = await confirmRemove( context, - title: 'Remove ${m.label}?', - body: 'It leaves the marker list, so you can no longer log it. Nothing ' - 'measured goes with it — you have no results under it.', + title: l?.healthRemoveMarkerQ(m.label) ?? 'Remove ${m.label}?', + body: l?.healthRemoveMarkerBody ?? + 'It leaves the marker list, so you can no longer log it. Nothing ' + 'measured goes with it — you have no results under it.', ); if (!ok || !mounted) return; await LocalDb.deleteLabMarkerDef(m.key); @@ -1513,6 +1657,7 @@ class _HealthScreenState extends State with RevisionReload { /// Deliberately plain. Entering blood work is a rare, careful act; it does /// not need a designed flow, it needs the marker, the number and the date. Future _addLab(BuildContext c, LabsData l) async { + final loc = AppLocalizations.of(c); var marker = l.markers.first; final value = TextEditingController(); final now = DateTime.now(); @@ -1525,13 +1670,13 @@ class _HealthScreenState extends State with RevisionReload { context: c, builder: (dc) => StatefulBuilder( builder: (dc, setLocal) => AlertDialog( - title: const Text('Add a result'), + title: Text(loc?.healthAddAResult ?? 'Add a result'), content: SingleChildScrollView( child: Column(mainAxisSize: MainAxisSize.min, children: [ // Unlabelled it announces only its current value — a marker // name, with no statement of what the field is. Semantics( - label: 'Marker', + label: loc?.healthMarkerLabel ?? 'Marker', child: DropdownButton( isExpanded: true, value: marker, @@ -1546,22 +1691,24 @@ class _HealthScreenState extends State with RevisionReload { controller: value, keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration(labelText: 'Value (${marker.unit})'), + decoration: InputDecoration( + labelText: + loc?.healthValueUnit(marker.unit) ?? 'Value (${marker.unit})'), ), TextField( controller: takenOn, - decoration: - const InputDecoration(labelText: 'Date drawn (YYYY-MM-DD)'), + decoration: InputDecoration( + labelText: loc?.healthDateDrawn ?? 'Date drawn (YYYY-MM-DD)'), ), ]), ), actions: [ TextButton( onPressed: () => Navigator.of(dc).pop(false), - child: const Text('Cancel')), + child: Text(loc?.actionCancel ?? 'Cancel')), TextButton( onPressed: () => Navigator.of(dc).pop(true), - child: const Text('Save')), + child: Text(loc?.actionSave ?? 'Save')), ], ), ), @@ -1577,9 +1724,11 @@ class _HealthScreenState extends State with RevisionReload { if (v.value == null || DateTime.tryParse(date) == null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(v.value == null - ? 'The value needs to be a number on its own, without the unit. ' - 'Nothing was saved.' - : 'The date needs to be YYYY-MM-DD. Nothing was saved.'), + ? (loc?.healthValueMustBeNumber ?? + 'The value needs to be a number on its own, without the unit. ' + 'Nothing was saved.') + : (loc?.healthDateFormatError ?? + 'The date needs to be YYYY-MM-DD. Nothing was saved.')), )); return; } @@ -1592,8 +1741,9 @@ class _HealthScreenState extends State with RevisionReload { ); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Could not save it: $e'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + loc?.healthCouldNotSaveIt(e.toString()) ?? 'Could not save it: $e'))); } return; } diff --git a/lib/ui2/screens/home_screen.dart b/lib/ui2/screens/home_screen.dart index bfbe6a42..b4d7de1e 100644 --- a/lib/ui2/screens/home_screen.dart +++ b/lib/ui2/screens/home_screen.dart @@ -33,6 +33,7 @@ import 'package:provider/provider.dart'; import '../../data/db.dart' show DbRebuild; import '../../data/journal_fields.dart' show formatMinuteOfDay; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../../state/app_state.dart'; import '../../state/units_controller.dart'; @@ -141,14 +142,14 @@ Metric metricOf(Object? raw) => Metric.parse(raw); /// this must never blur is strap versus phone — a card that lets the phone's /// count read as the wrist's, or the other way round, defeats the whole point /// of resolving the day per window. -String? stepSensorLabel(Metric m) { +String? stepSensorLabel(Metric m, [AppLocalizations? l]) { final used = m.inputsUsed; final strap = used.contains('band_pedometer_100hz') || used.contains('band_step_counter'); final phone = used.contains('phone_pedometer'); - if (strap && phone) return 'Strap + phone'; - if (strap) return 'Strap'; - if (phone) return 'Phone'; + if (strap && phone) return l?.homeStepSensorStrapPhone ?? 'Strap + phone'; + if (strap) return l?.homeStepSensorStrap ?? 'Strap'; + if (phone) return l?.homeStepSensorPhone ?? 'Phone'; return null; } @@ -198,12 +199,13 @@ String? heldOverNightOf(Map today) { /// /// Prose, not a `key:arg` token, so `whyFromNote` passes it through as the /// sentence it already is. -String? staleOvernightNote(Map today) { +String? staleOvernightNote(Map today, [AppLocalizations? l]) { if (heldOverNightOf(today) == null) return null; final st = today['status']; return (st is Map ? st['overnight_state']?.toString() : null) == 'building' - ? 'Last night is still being worked out.' - : 'Nothing from last night has reached the app yet.'; + ? l?.homeOvernightBuilding ?? 'Last night is still being worked out.' + : l?.homeOvernightNothingYet ?? + 'Nothing from last night has reached the app yet.'; } /// An overnight envelope, REFUSED when the night behind it is not today's. @@ -220,8 +222,9 @@ String? staleOvernightNote(Map today) { /// So the numbers stop here and the reason travels in their place. The night /// itself is not lost — [heldOverNightOf] still names it, and the screens that /// are ABOUT a dated night still open it. -Metric overnightMetric(Map today, Object? raw) { - final why = staleOvernightNote(today); +Metric overnightMetric(Map today, Object? raw, + [AppLocalizations? l]) { + final why = staleOvernightNote(today, l); return why == null ? metricOf(raw) : Metric(note: why); } @@ -357,18 +360,19 @@ Map? staleReasonOf(Map insights) => /// The counts are stated per table rather than summed. "1,204 rows recovered" /// reads as reassurance; "nutrition 0" is the sentence that actually tells /// someone their food log is gone. -StatusCard? dbRebuiltCard(DbRebuild? r) { +StatusCard? dbRebuiltCard(DbRebuild? r, [AppLocalizations? l]) { if (r == null) return null; final saved = r.salvaged.entries.where((e) => e.value > 0).toList() ..sort((a, b) => b.value.compareTo(a.value)); final lost = r.salvaged.entries.where((e) => e.value == 0).toList(); + final savedList = saved.map((e) => '${e.key} ${thousands(e.value)}').join(' · '); + final lostList = lost.map((e) => e.key).join(' · '); return StatusCard( - 'Your database was rebuilt to start the app', + l?.homeDbRebuiltTitle ?? 'Your database was rebuilt to start the app', '${r.cause}\n\n' - '${saved.isEmpty ? 'Nothing could be read back.' : 'Recovered: ${saved.map((e) => '${e.key} ${thousands(e.value)}').join(' · ')}.'}' - '${lost.isEmpty ? '' : ' Empty: ${lost.map((e) => e.key).join(' · ')}.'}' - '\n\nThe original file is kept at ${r.quarantinePath} — nothing was ' - 'deleted.', + '${saved.isEmpty ? (l?.homeDbRebuiltNothingRecovered ?? 'Nothing could be read back.') : (l?.homeDbRebuiltRecovered(savedList) ?? 'Recovered: $savedList.')}' + '${lost.isEmpty ? '' : ' ${l?.homeDbRebuiltEmpty(lostList) ?? 'Empty: $lostList.'}'}' + '\n\n${l?.homeDbRebuiltKept(r.quarantinePath) ?? 'The original file is kept at ${r.quarantinePath} — nothing was deleted.'}', icon: LucideIcons.databaseBackup, ); } @@ -380,30 +384,33 @@ StatusCard? dbRebuiltCard(DbRebuild? r) { /// completes and changes nothing on this screen. The true remedy is finishing /// the session, and its bar is pinned right below this card, so the card /// points there rather than duplicating the door. -StatusCard workoutHoldCard() => const StatusCard( - 'A workout is still running', - 'Today is on hold while a workout is live: the band keeps recording, ' - 'but the numbers are computed once the session ends. Finish the workout ' - 'from the bar below and today fills in — syncing will not.', +StatusCard workoutHoldCard([AppLocalizations? l]) => StatusCard( + l?.homeWorkoutHoldTitle ?? 'A workout is still running', + l?.homeWorkoutHoldBody ?? + 'Today is on hold while a workout is live: the band keeps recording, ' + 'but the numbers are computed once the session ends. Finish the workout ' + 'from the bar below and today fills in — syncing will not.', icon: LucideIcons.timer, ); StatusCard? staleInsightsCard( - Map? reason, VoidCallback? onSync) { + Map? reason, VoidCallback? onSync, [AppLocalizations? l]) { final s = reason; if (s == null) return null; final built = s['built_for_day']?.toString(); return StatusCard( - 'Your cross-day insights are being rebuilt', + l?.homeInsightsRebuildingTitle ?? 'Your cross-day insights are being rebuilt', switch (s['kind']) { - 'algo_version' => - 'How these are computed changed with the last update.', - 'stale' => 'The last rollup was built ' - '${built == null || built.isEmpty ? 'over a week ago' : 'on ${prettyDay(built)}'}' - ', which is too old to stand behind.', - _ => 'The stored rollup carries no version stamp.', + 'algo_version' => l?.homeInsightsRebuildingAlgoVersion ?? + 'How these are computed changed with the last update.', + 'stale' => built == null || built.isEmpty + ? (l?.homeInsightsStaleOverWeek ?? + 'The last rollup was built over a week ago, which is too old to stand behind.') + : (l?.homeInsightsStaleOnDay(prettyDay(built, l)) ?? + 'The last rollup was built on ${prettyDay(built, l)}, which is too old to stand behind.'), + _ => l?.homeInsightsNoVersionStamp ?? 'The stored rollup carries no version stamp.', }, - fix: onSync == null ? '' : 'Sync the band', + fix: onSync == null ? '' : (l?.homeSyncBand ?? 'Sync the band'), icon: LucideIcons.refreshCw, onFix: onSync, ); @@ -490,11 +497,59 @@ const _weekdays = [ 'Friday', 'Saturday', 'Sunday', ]; +String monthName(int month, AppLocalizations? l) { + if (l == null) return _months[month - 1]; + return [ + l.homeMonthJanuary, l.homeMonthFebruary, l.homeMonthMarch, + l.homeMonthApril, l.homeMonthMay, l.homeMonthJune, + l.homeMonthJuly, l.homeMonthAugust, l.homeMonthSeptember, + l.homeMonthOctober, l.homeMonthNovember, l.homeMonthDecember, + ][month - 1]; +} + +const _monthsShort = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', +]; + +/// Abbreviated month for "Thu 4 Sep" date chips. +String monthShortName(int month, AppLocalizations? l) { + if (l == null) return _monthsShort[month - 1]; + return [ + l.homeMonthJanuaryShort, l.homeMonthFebruaryShort, l.homeMonthMarchShort, + l.homeMonthAprilShort, l.homeMonthMayShort, l.homeMonthJuneShort, + l.homeMonthJulyShort, l.homeMonthAugustShort, l.homeMonthSeptemberShort, + l.homeMonthOctoberShort, l.homeMonthNovemberShort, l.homeMonthDecemberShort, + ][month - 1]; +} + +String _weekdayName(int weekday, AppLocalizations? l) { + if (l == null) return _weekdays[weekday - 1]; + return [ + l.homeWeekdayMonday, l.homeWeekdayTuesday, l.homeWeekdayWednesday, + l.homeWeekdayThursday, l.homeWeekdayFriday, l.homeWeekdaySaturday, + l.homeWeekdaySunday, + ][weekday - 1]; +} + +const _weekdaysShort = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + +/// Abbreviated weekday for `DateTime.weekday` (1 = Monday), e.g. "Thu 4 Sep" +/// date chips. Reuses the `wellness*` short-day keys — they're already +/// translated everywhere and mean the same three letters here. +String weekdayShortName(int weekday, AppLocalizations? l) { + if (l == null) return _weekdaysShort[weekday - 1]; + return [ + l.wellnessMon, l.wellnessTue, l.wellnessWed, + l.wellnessThu, l.wellnessFri, l.wellnessSat, l.wellnessSun, + ][weekday - 1]; +} + /// 'YYYY-MM-DD' → "Saturday, 20 May". -String prettyDay(String? dayId) { +String prettyDay(String? dayId, [AppLocalizations? l]) { final d = dayId == null ? null : DateTime.tryParse(dayId); if (d == null) return ''; - return '${_weekdays[d.weekday - 1]}, ${d.day} ${_months[d.month - 1]}'; + return '${_weekdayName(d.weekday, l)}, ${d.day} ${monthName(d.month, l)}'; } /// The readiness band. `readiness_glassbox` carries no label of its own, so the @@ -534,12 +589,21 @@ String prettyDay(String? dayId) { /// person's four inputs are, and it moves with how many of them are present. /// Re-derive it from a real `metric_series` readiness distribution when there /// is one long enough to measure; do not nudge the cut-offs by feel. -({String label, Color color, int tier}) readinessBand(num? v) { - if (v == null) return (label: 'Not scored', color: C.n400, tier: -1); - if (v >= 61) return (label: 'Good to go', color: C.green, tier: 3); - if (v >= 37) return (label: 'Steady', color: C.green, tier: 2); - if (v >= 26) return (label: 'Take it easy', color: C.orange, tier: 1); - return (label: 'Rest today', color: C.red, tier: 0); +({String label, Color color, int tier}) readinessBand(num? v, + [AppLocalizations? l]) { + if (v == null) { + return (label: l?.homeReadinessNotScored ?? 'Not scored', color: C.n400, tier: -1); + } + if (v >= 61) { + return (label: l?.homeReadinessGoodToGo ?? 'Good to go', color: C.green, tier: 3); + } + if (v >= 37) { + return (label: l?.homeReadinessSteady ?? 'Steady', color: C.green, tier: 2); + } + if (v >= 26) { + return (label: l?.homeReadinessTakeItEasy ?? 'Take it easy', color: C.orange, tier: 1); + } + return (label: l?.homeReadinessRestToday ?? 'Rest today', color: C.red, tier: 0); } /// Glass-box driver keys are the pipeline's own short names. @@ -553,9 +617,15 @@ const driverLabels = { /// A pipeline key the map does not cover is HUMANISED, never printed raw. The /// glass-box emits whatever inputs the composite used, so a new one used to /// surface on Home as `resp_rate_slope`. -String driverLabel(Object? key) { +String driverLabel(Object? key, [AppLocalizations? l]) { final k = key?.toString() ?? ''; - final known = driverLabels[k]; + final known = switch (k) { + 'hrv' => l?.homeDriverHrv ?? driverLabels['hrv'], + 'rhr' => l?.homeDriverRhr ?? driverLabels['rhr'], + 'resp' => l?.homeDriverResp ?? driverLabels['resp'], + 'temp' => l?.homeDriverTemp ?? driverLabels['temp'], + _ => null, + }; if (known != null) return known; if (k.isEmpty) return ''; final words = k.replaceAll('_', ' ').trim(); @@ -599,12 +669,13 @@ class RingTrio extends StatelessWidget { /// Whether ANY of the three has something to draw. When none do, the screen /// owes the user one written absence, not three empty circles. static bool has(HomeData d) => - HomeRingKind.values.any((k) => _ringOf(k, d).why == null); + HomeRingKind.values.any((k) => _ringOf(k, d, null).why == null); @override Widget build(BuildContext c) { final p = P.of(c); - final rings = [for (final k in HomeRingKind.values) _ringOf(k, d)]; + final l = AppLocalizations.of(c); + final rings = [for (final k in HomeRingKind.values) _ringOf(k, d, l)]; final gaps = rings.where((r) => r.why != null).toList(); // THERE IS NO "THESE TWO ARE FROM SATURDAY" LINE ANY MORE, and there is // nothing left for one to explain. Recovery and sleep used to be served @@ -651,13 +722,13 @@ class RingTrio extends StatelessWidget { // Top-aligned: at an accessibility size the driver list is three // lines and "Why?" was centred against the middle of them. child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Why?', style: F.cap.copyWith(color: p.ink3)), + Text(l?.homeWhyLabel ?? 'Why?', style: F.cap.copyWith(color: p.ink3)), const SizedBox(width: S.x2), Expanded( child: Text( d.drivers .take(3) - .map((e) => driverLabel(e['label'])) + .map((e) => driverLabel(e['label'], l)) .join(' · '), style: F.cap.copyWith(color: p.ink2), ), @@ -724,36 +795,41 @@ class _RingState { ].join('. '); } -_RingState _ringOf(HomeRingKind k, HomeData d) { +_RingState _ringOf(HomeRingKind k, HomeData d, AppLocalizations? l) { switch (k) { case HomeRingKind.recovery: final v = d.readiness.value; - final band = readinessBand(v); + final band = readinessBand(v, l); return v == null - ? _gap(k, 'Recovery', LucideIcons.batteryCharging, C.green, - d.readiness, 'Not scored') - : _RingState(k, 'Recovery', LucideIcons.batteryCharging, band.color, + ? _gap(k, l?.homeRingRecovery ?? 'Recovery', LucideIcons.batteryCharging, + C.green, d.readiness, l?.homeReadinessNotScored ?? 'Not scored', l) + : _RingState(k, l?.homeRingRecovery ?? 'Recovery', + LucideIcons.batteryCharging, band.color, value: '${v.round()}', sub: band.label, frac: v / 100); case HomeRingKind.strain: final v = d.strain.value; // 0–21 is the scale's own ceiling, not a target invented here. return v == null - ? _gap(k, 'Strain', LucideIcons.zap, C.purple, d.strain, 'No strain', - unit: 'days') - : _RingState(k, 'Strain', LucideIcons.zap, C.purple, - value: v.toStringAsFixed(1), sub: 'of 21', frac: v / 21); + ? _gap(k, l?.homeRingStrain ?? 'Strain', LucideIcons.zap, C.purple, + d.strain, l?.homeRingNoStrain ?? 'No strain', l, unit: 'days') + : _RingState(k, l?.homeRingStrain ?? 'Strain', LucideIcons.zap, C.purple, + value: v.toStringAsFixed(1), sub: l?.homeStrainOf21 ?? 'of 21', frac: v / 21); case HomeRingKind.sleep: final v = d.sleepMin.value; final need = d.sleepNeedMin.value; return v == null - ? _gap(k, 'Sleep', LucideIcons.moon, C.blue, d.sleepMin, 'No sleep', - fallbackWhy: 'No night long enough to score was recorded.') - : _RingState(k, 'Sleep', LucideIcons.moon, C.blue, + ? _gap(k, l?.homeRingSleep ?? 'Sleep', LucideIcons.moon, C.blue, + d.sleepMin, l?.homeRingNoSleep ?? 'No sleep', l, + fallbackWhy: l?.homeSleepGapFallback ?? + 'No night long enough to score was recorded.') + : _RingState(k, l?.homeRingSleep ?? 'Sleep', LucideIcons.moon, C.blue, value: hm(v), // No computed need means no denominator. The hardcoded 480 in // the sleep bundle is not this user's need and must never be // shown as one, so the ring stays open and says so. - sub: need == null ? 'No target yet' : 'of ${hm(need)}', + sub: need == null + ? (l?.homeSleepNoTarget ?? 'No target yet') + : (l?.homeOfSpan(hm(need)) ?? 'of ${hm(need)}'), frac: need == null || need <= 0 ? null : v / need); } } @@ -761,13 +837,17 @@ _RingState _ringOf(HomeRingKind k, HomeData d) { /// The absent half: calibrating when the note says the gate is a baseline /// still filling, otherwise the absence and its reason. _RingState _gap(HomeRingKind k, String label, IconData icon, Color color, - Metric m, String word, + Metric m, String word, AppLocalizations? l, {String unit = 'nights', String fallbackWhy = ''}) { final counts = baselineCountsFromNote(m.note); if (counts != null) { return _RingState(k, label, icon, color, - value: 'Calibrating', - sub: '${counts.have} of ${counts.need} $unit', + value: l?.homeCalibrating ?? 'Calibrating', + sub: unit == 'days' + ? (l?.homeCalibratingDays(counts.have, counts.need) ?? + '${counts.have} of ${counts.need} days') + : (l?.homeCalibratingNights(counts.have, counts.need) ?? + '${counts.have} of ${counts.need} nights'), frac: (counts.have / counts.need).clamp(0.0, 1.0), calibrating: true); } @@ -779,7 +859,7 @@ _RingState _gap(HomeRingKind k, String label, IconData icon, Color color, why: whyFromNote(m.note, unit: unit) ?? (fallbackWhy.isNotEmpty ? fallbackWhy - : 'Nothing recorded says why this is missing.')); + : (l?.homeGapNoReason ?? 'Nothing recorded says why this is missing.'))); } /// The dial itself. An empty [frac] draws the track and nothing else — which is @@ -1024,7 +1104,7 @@ class HomeData { insightsStale: insightsStale, ); - static Future load(LocalRepository repo) async { + static Future load(LocalRepository repo, [AppLocalizations? l]) async { final today = await repo.getToday(); final cd = await repo.getInsights(); final profile = await repo.getProfile(); @@ -1062,14 +1142,14 @@ class HomeData { // is not today's cannot arrive wearing today's clothes — see // [overnightMetric]. Steps, active energy and strain are today's own and // are read straight. - readiness: overnightMetric(today, d('readiness')), + readiness: overnightMetric(today, d('readiness'), l), drivers: [ for (final e in (gbDrivers is List ? gbDrivers : const [])) if (e is Map) e.cast(), ], strain: metricOf(d('strain')), - sleepMin: overnightMetric(today, s('duration_min')), - rhr: overnightMetric(today, d('resting_hr')), + sleepMin: overnightMetric(today, s('duration_min'), l), + rhr: overnightMetric(today, d('resting_hr'), l), steps: metricOf(d('steps')), calories: metricOf(d('calories')), caloriesTotal: metricOf(d('calories_total')), @@ -1147,7 +1227,7 @@ class _HomeScreenState extends State with RevisionReload { } final t = beginRead(#home); try { - final d = await HomeData.load(repo); + final d = await HomeData.load(repo, AppLocalizations.of(context)); if (stillNewest(#home, t)) { setState(() => (_d = d, _loading = false, _failed = false)); } @@ -1158,22 +1238,25 @@ class _HomeScreenState extends State with RevisionReload { /// Morning / afternoon / evening / night. One split at 18:00 greeted 00:30 /// and 15:40 alike with "Good morning" beside a sun. - ({String word, IconData icon, Color color}) _greeting(int h) { - if (h < 5) return (word: 'Still up', icon: LucideIcons.moon, color: C.indigo); + ({String word, IconData icon, Color color}) _greeting(int h, AppLocalizations? l) { + if (h < 5) { + return (word: l?.homeGreetingStillUp ?? 'Still up', icon: LucideIcons.moon, color: C.indigo); + } if (h < 12) { - return (word: 'Good morning', icon: LucideIcons.sun, color: C.yellow); + return (word: l?.homeGreetingMorning ?? 'Good morning', icon: LucideIcons.sun, color: C.yellow); } if (h < 18) { - return (word: 'Good afternoon', icon: LucideIcons.sun, color: C.orange); + return (word: l?.homeGreetingAfternoon ?? 'Good afternoon', icon: LucideIcons.sun, color: C.orange); } - return (word: 'Good evening', icon: LucideIcons.moon, color: C.indigo); + return (word: l?.homeGreetingEvening ?? 'Good evening', icon: LucideIcons.moon, color: C.indigo); } @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d; - final g = _greeting(widget.hour ?? DateTime.now().hour); + final g = _greeting(widget.hour ?? DateTime.now().hour, l); if (d == null) { return _refreshable(ListView(padding: pad, children: [ @@ -1182,10 +1265,11 @@ class _HomeScreenState extends State with RevisionReload { const Center(child: CircularProgressIndicator()) else if (_failed) StatusCard( - 'Today could not be read', - 'The stored day failed to load. Nothing was deleted — this is a ' + l?.homeLoadFailedTitle ?? 'Today could not be read', + l?.homeLoadFailedBody ?? + 'The stored day failed to load. Nothing was deleted — this is a ' 'read that went wrong, not missing data.', - fix: 'Try again', + fix: l?.homeTryAgain ?? 'Try again', icon: LucideIcons.databaseZap, onFix: () { setState(() => (_loading = true, _failed = false)); @@ -1194,9 +1278,9 @@ class _HomeScreenState extends State with RevisionReload { ) else StatusCard( - 'Nothing derived yet', - 'No band recordings processed yet.', - fix: syncOf(c) == null ? '' : 'Sync the band', + l?.homeNothingDerivedTitle ?? 'Nothing derived yet', + l?.homeNothingDerivedBody ?? 'No band recordings processed yet.', + fix: syncOf(c) == null ? '' : (l?.homeSyncBand ?? 'Sync the band'), icon: LucideIcons.watch, onFix: syncOf(c), ), @@ -1220,10 +1304,10 @@ class _HomeScreenState extends State with RevisionReload { d.steps.value == null && d.calories.isEmpty; - final stale = staleInsightsCard(d.insightsStale, syncOf(c)); + final stale = staleInsightsCard(d.insightsStale, syncOf(c), l); // Above the greeting, not below it: if the app had to rebuild the database // to start, that outranks anything else this screen has to say today. - final rebuilt = dbRebuiltCard(dbRebuildOf(c)); + final rebuilt = dbRebuiltCard(dbRebuildOf(c), l); return _refreshable(ListView(padding: pad, children: [ if (rebuilt != null) ...[const SizedBox(height: S.x3), rebuilt], @@ -1258,7 +1342,7 @@ class _HomeScreenState extends State with RevisionReload { Icon(g.icon, size: 17, color: p.on(g.color)), ]), const SizedBox(height: 2), - Text(prettyDay(d.dayId), style: F.cap.copyWith(color: p.ink3)), + Text(prettyDay(d.dayId, l), style: F.cap.copyWith(color: p.ink3)), ]), ), const SizedBox(width: S.x3), @@ -1273,7 +1357,7 @@ class _HomeScreenState extends State with RevisionReload { // Setting the coach up is a setting, and it lives in Profile now. if (coachReady(c)) ...[ Pressable( - semanticLabel: 'Ask the coach', + semanticLabel: l?.homeAskCoach ?? 'Ask the coach', onTap: () => go(c, const CoachScreen()), child: Container( width: 40, @@ -1287,7 +1371,7 @@ class _HomeScreenState extends State with RevisionReload { const SizedBox(width: S.x2), ], Pressable( - semanticLabel: 'Profile and settings', + semanticLabel: l?.homeProfileSettings ?? 'Profile and settings', onTap: () => go(c, const ProfileHome()), child: Container( width: 40, @@ -1304,16 +1388,17 @@ class _HomeScreenState extends State with RevisionReload { // A live workout holds derivation, so a bare day with a session open // is the hold at work, not a sync problem — see [workoutHoldCard]. (widget.workoutLive ?? workoutLiveOf(c)) - ? workoutHoldCard() + ? workoutHoldCard(l) : StatusCard( d.heldOverNight == null - ? 'Nothing derived yet' - : 'Nothing recorded for today', + ? (l?.homeNothingDerivedTitle ?? 'Nothing derived yet') + : (l?.homeNothingTodayTitle ?? 'Nothing recorded for today'), d.heldOverNight == null - ? 'No band recordings processed yet.' - : 'The last night this app scored was ' - '${prettyDay(d.heldOverNight)}. Nothing has reached it since.', - fix: syncOf(c) == null ? '' : 'Sync the band', + ? (l?.homeNothingDerivedBody ?? 'No band recordings processed yet.') + : (l?.homeNothingTodayBody(prettyDay(d.heldOverNight, l)) ?? + 'The last night this app scored was ' + '${prettyDay(d.heldOverNight, l)}. Nothing has reached it since.'), + fix: syncOf(c) == null ? '' : (l?.homeSyncBand ?? 'Sync the band'), icon: LucideIcons.watch, onFix: syncOf(c), ) @@ -1344,16 +1429,17 @@ class _HomeScreenState extends State with RevisionReload { Builder(builder: (c) { final need = needMessageFromNote(d.readiness.note); return StatusCard( - 'Readiness is not scored today', + l?.homeReadinessNotScoredTitle ?? 'Readiness is not scored today', need != null - ? '$need to know what normal looks like for you.' + ? (l?.homeReadinessNeedBody(need) ?? + '$need to know what normal looks like for you.') // Was "Needs a night of beat-to-beat data, plus your own // history to compare it to" — a cause, stated for every // absence the note convention did not cover. The door below // is what actually answers it. : whyFromNote(d.readiness.note) ?? - 'Nothing recorded says why.', - fix: 'See what was missing', + (l?.homeReadinessNoReason ?? 'Nothing recorded says why.'), + fix: l?.homeSeeWhatWasMissing ?? 'See what was missing', icon: LucideIcons.batteryCharging, onFix: () => go(c, const ReadinessDetail()), ); @@ -1363,10 +1449,10 @@ class _HomeScreenState extends State with RevisionReload { if (stale != null) ...[const SizedBox(height: S.x3), stale], // ── at a glance ── - Section('At a glance', _glance(c, d)), + Section(l?.homeAtAGlance ?? 'At a glance', _glance(c, d)), // ── today's plan: only what the app can actually stand behind ── - Section("Today's plan", _plan(c, p, d)), + Section(l?.homeTodaysPlan ?? "Today's plan", _plan(c, p, d)), // ── the way into the whole day ── // @@ -1376,8 +1462,10 @@ class _HomeScreenState extends State with RevisionReload { // Home decides; the day view is where you go to look, and until this // row existed the only ways in were two screens deep. const SizedBox(height: S.x5), - detailLinkRow(c, LucideIcons.chartGantt, 'Breakdown of your day', - 'Hour by hour', () => go(c, const DayTimelineScreen())), + detailLinkRow(c, LucideIcons.chartGantt, + l?.homeBreakdownTitle ?? 'Breakdown of your day', + l?.homeBreakdownSubtitle ?? 'Hour by hour', + () => go(c, const DayTimelineScreen())), ], ])); } @@ -1397,23 +1485,37 @@ class _HomeScreenState extends State with RevisionReload { static List? _bodyWatch(BuildContext c, HomeData d) { final state = d.illnessState; if (state == null || state == 'green') return null; + final l = AppLocalizations.of(c); final sameNight = d.illnessDay == null || d.illnessDay == d.dayId; final z = d.illnessZ; + final zAbs = z == null ? '' : z.abs().toStringAsFixed(1); return [ Observation( state == 'red' - ? 'Several nights in a row are away from your normal' + ? (l?.homeIllnessRedTitle ?? 'Several nights in a row are away from your normal') : sameNight - ? 'Last night sat outside your normal range' - : '${prettyDay(d.illnessDay)} sat outside your normal range', - 'Your nocturnal resting heart rate has been running above your own ' - 'baseline${z == null ? '' : '; that night sat ' - '${z.abs().toStringAsFixed(1)} standardised deviations ' - '${z >= 0 ? 'above' : 'below'} it'}. This reads one signal. It ' - 'names a pattern, and it does not name a cause.', - advice: 'Worth noting if it continues past a couple of days.', + ? (l?.homeIllnessAmberSameNight ?? 'Last night sat outside your normal range') + : (l?.homeIllnessAmberOtherNight(prettyDay(d.illnessDay, l)) ?? + '${prettyDay(d.illnessDay, l)} sat outside your normal range'), + z == null + ? (l?.homeIllnessBodyNoZ ?? + 'Your nocturnal resting heart rate has been running above your own ' + 'baseline. This reads one signal. It names a pattern, and it does ' + 'not name a cause.') + : (z >= 0 + ? (l?.homeIllnessBodyAbove(zAbs) ?? + 'Your nocturnal resting heart rate has been running above your own ' + 'baseline; that night sat $zAbs standardised deviations above it. ' + 'This reads one signal. It names a pattern, and it does not name ' + 'a cause.') + : (l?.homeIllnessBodyBelow(zAbs) ?? + 'Your nocturnal resting heart rate has been running above your own ' + 'baseline; that night sat $zAbs standardised deviations below it. ' + 'This reads one signal. It names a pattern, and it does not name ' + 'a cause.')), + advice: l?.homeIllnessAdvice ?? 'Worth noting if it continues past a couple of days.', onTap: () => go(c, const MetricDetail('resting_hr')), ), const SizedBox(height: S.x3), @@ -1427,6 +1529,7 @@ class _HomeScreenState extends State with RevisionReload { RefreshIndicator(onRefresh: _load, child: list); Widget _glance(BuildContext c, HomeData d) { + final l = AppLocalizations.of(c); final cards = []; final absent = []; @@ -1450,10 +1553,10 @@ class _HomeScreenState extends State with RevisionReload { // duration was measured against. add( d.rhr, - () => SignalCard(LucideIcons.heart, C.red, 'Heart rate', + () => SignalCard(LucideIcons.heart, C.red, l?.homeHeartRate ?? 'Heart rate', '${d.rhr.value!.round()}', unit: 'bpm', - sub: 'Resting', + sub: l?.homeRestingSub ?? 'Resting', onTap: () => go(c, const MetricDetail('resting_hr'))), // "no sleep was recorded" was stated as fact, unconditionally — and it // was rendered directly beside a Sleep card showing that night's @@ -1465,10 +1568,10 @@ class _HomeScreenState extends State with RevisionReload { // resting rate, picked by a human writing copy. Only the branch the // screen can actually see is stated; the other defers to the note, or to // saying it does not know. - () => StatusCard.forMetric('No resting heart rate', d.rhr, + () => StatusCard.forMetric(l?.homeNoRestingHr ?? 'No resting heart rate', d.rhr, why: d.sleepMin.isEmpty - ? 'Resting heart rate is read from sleep, and no sleep was ' - 'recorded.' + ? (l?.homeNoRestingHrWhy ?? + 'Resting heart rate is read from sleep, and no sleep was recorded.') : ''), ); // Steps keeps its tile whether or not a counter reported. Zero steps is a @@ -1479,34 +1582,37 @@ class _HomeScreenState extends State with RevisionReload { cards.add(SignalCard( LucideIcons.footprints, C.green, - 'Steps', - d.steps.value == null ? 'None' : thousands(d.steps.value), + l?.homeSteps ?? 'Steps', + d.steps.value == null ? (l?.homeStepsNone ?? 'None') : thousands(d.steps.value), // The sensor rides the line that is already there rather than adding a // row: the day is resolved per window now, so "8,412" can be the strap's // count, the phone's, or both, and the card has to say which. The split // behind a mixed day is on Nerd stats, one tap down. sub: d.steps.value == null - ? 'NOT RECORDED' + ? (l?.homeStepsNotRecorded ?? 'NOT RECORDED') : [ if (d.stepGoal > 0) - '${((d.steps.value! / d.stepGoal) * 100).clamp(0, 999).round()}% of goal', - ?stepSensorLabel(d.steps), + l?.homeStepsPercentGoal( + ((d.steps.value! / d.stepGoal) * 100).clamp(0, 999).round()) ?? + '${((d.steps.value! / d.stepGoal) * 100).clamp(0, 999).round()}% of goal', + ?stepSensorLabel(d.steps, l), ].join(' · '), onTap: () => go(c, const MetricDetail('steps')), )); add( d.calories, - () => SignalCard(LucideIcons.flame, C.orange, 'Active energy', + () => SignalCard(LucideIcons.flame, C.orange, l?.homeActiveEnergy ?? 'Active energy', thousands(d.calories.value), unit: 'kcal', sub: d.caloriesTotal.value == null - ? 'Estimated' - : '${thousands(d.caloriesTotal.value)} total', + ? (l?.homeCaloriesEstimated ?? 'Estimated') + : (l?.homeCaloriesTotal(thousands(d.caloriesTotal.value)) ?? + '${thousands(d.caloriesTotal.value)} total'), onTap: () => go(c, const MetricDetail('calories'))), // No `why:`. It said "Needs your weight and age" — and the measured run // printed that to a profile carrying both, because energy had gone absent // for an entirely different reason that the card never asked for. - () => StatusCard.forMetric('No energy estimate', d.calories), + () => StatusCard.forMetric(l?.homeNoEnergyEstimate ?? 'No energy estimate', d.calories), ); return Column(children: [ @@ -1536,18 +1642,25 @@ class _HomeScreenState extends State with RevisionReload { Widget _plan(BuildContext c, P p, HomeData d) { + final l = AppLocalizations.of(c); final rows = []; final stepsLeft = d.steps.value == null ? null : (d.stepGoal - d.steps.value!).round(); if (stepsLeft != null && stepsLeft > 0) { - rows.add(_row(p, LucideIcons.footprints, C.green, - '${thousands(stepsLeft)} steps left', 'Movement', - 'Goal ${thousands(d.stepGoal)}', false)); + rows.add(_row( + p, + LucideIcons.footprints, + C.green, + l?.homeStepsLeft(thousands(stepsLeft)) ?? '${thousands(stepsLeft)} steps left', + l?.homeMovement ?? 'Movement', + l?.homeGoalSteps(thousands(d.stepGoal)) ?? 'Goal ${thousands(d.stepGoal)}', + false)); } else if (stepsLeft != null) { - rows.add(_row(p, LucideIcons.footprints, C.green, 'Step goal met', - 'Movement', 'Done', true)); + rows.add(_row(p, LucideIcons.footprints, C.green, + l?.homeStepGoalMet ?? 'Step goal met', + l?.homeMovement ?? 'Movement', l?.actionDone ?? 'Done', true)); } final target = d.strainTarget; @@ -1561,10 +1674,13 @@ class _HomeScreenState extends State with RevisionReload { p, LucideIcons.zap, C.purple, - met ? 'Strain target met' : 'Aim for ${aim.toStringAsFixed(1)} strain', - 'Training', met - ? 'Done' + ? (l?.homeStrainTargetMet ?? 'Strain target met') + : (l?.homeAimForStrain(aim.toStringAsFixed(1)) ?? + 'Aim for ${aim.toStringAsFixed(1)} strain'), + l?.homeTraining ?? 'Training', + met + ? (l?.actionDone ?? 'Done') : '${(target['low'] as num?)?.toStringAsFixed(1) ?? ''}–' '${(target['high'] as num?)?.toStringAsFixed(1) ?? ''}', met)); @@ -1576,19 +1692,21 @@ class _HomeScreenState extends State with RevisionReload { p, LucideIcons.bedDouble, C.blue, - '${hm(need)} of sleep', - 'Tonight', - d.bedtime.value == null ? 'Need' : 'Bed ${clock(d.bedtime.value)}', + l?.homeSleepNeedRow(hm(need)) ?? '${hm(need)} of sleep', + l?.homeTonight ?? 'Tonight', + d.bedtime.value == null + ? (l?.homeNeed ?? 'Need') + : (l?.homeBedTime(clock(d.bedtime.value)) ?? 'Bed ${clock(d.bedtime.value)}'), false)); } if (rows.isEmpty) { - return StatusCard.forMetric('No plan for today yet', d.sleepNeedMin, + return StatusCard.forMetric(l?.homeNoPlanTitle ?? 'No plan for today yet', d.sleepNeedMin, // "none are established yet" is the COLD-START reason, and it is // a wrong answer when the baselines exist and are being withheld. why: d.insightsStale != null - ? 'The cross-day rollup they come from is being rebuilt.' - : 'None are established yet.') ?? + ? (l?.homeNoPlanWhyStale ?? 'The cross-day rollup they come from is being rebuilt.') + : (l?.homeNoPlanWhyNone ?? 'None are established yet.')) ?? const SizedBox.shrink(); } diff --git a/lib/ui2/screens/investigate.dart b/lib/ui2/screens/investigate.dart index 83769d55..c645c0b1 100644 --- a/lib/ui2/screens/investigate.dart +++ b/lib/ui2/screens/investigate.dart @@ -30,6 +30,7 @@ import 'package:openstrap_analytics/onehz.dart' as ana; import '../../data/day_label.dart'; import '../../data/db.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'day_timeline.dart'; import 'home_screen.dart'; @@ -264,11 +265,13 @@ class _InvestigateState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final spec = specOf(widget.metricKey); final d = _d ?? const InvestigateData(); final hrvish = widget.metricKey == 'hrv'; - return detailScaffold(c, spec.title, sub: 'NERD STATS', [ + return detailScaffold(c, spec.title, + sub: l?.investigateNerdStatsLabel ?? 'NERD STATS', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), if (_loading) ...[ const SizedBox(height: S.x8), @@ -280,19 +283,22 @@ class _InvestigateState extends State { if (widget.metricKey == 'sleep') ..._stagePanels(d), if (widget.metricKey == 'steps') ..._stepSourcePanels(d), const SizedBox(height: S.x3), - MonoTable('Provenance', [ - ('Day', d.day ?? '—'), - ('Coverage', d.coveragePct == null ? '—' : '${d.coveragePct} %'), + MonoTable(l?.investigateProvenanceLabel ?? 'Provenance', [ + (l?.investigateDayLabel ?? 'Day', d.day ?? '—'), + (l?.investigateCoverageLabel ?? 'Coverage', + d.coveragePct == null ? '—' : '${d.coveragePct} %'), if (d.windowStart != null) - ('Sleep window', + (l?.investigateSleepWindowLabel ?? 'Sleep window', '${clockOfTs(d.windowStart)} – ${clockOfTs(d.windowEnd)}'), // Asserted as "wrist optical · this device" for every day, including // days that were read out of somebody else's export. - ('Source', + (l?.investigateSourceLabel ?? 'Source', d.importedFrom == null - ? 'Band records · derived on this phone' - : 'Imported · ${d.importedFrom}'), - ('Algorithm version', + ? (l?.investigateSourceOnDevice ?? + 'Band records · derived on this phone') + : (l?.investigateSourceImported(d.importedFrom!) ?? + 'Imported · ${d.importedFrom}')), + (l?.investigateAlgoVersionLabel ?? 'Algorithm version', d.algoVersion == null ? '—' : 'v${d.algoVersion}'), ]), // The arithmetic is above; this is the other question a person has in @@ -304,8 +310,9 @@ class _InvestigateState extends State { detailLinkRow( c, LucideIcons.listOrdered, - 'What happened that day', - 'Sleep, sessions, meals and logs in time order', + l?.investigateWhatHappenedTitle ?? 'What happened that day', + l?.investigateWhatHappenedSub ?? + 'Sleep, sessions, meals and logs in time order', () => go(c, DayTimelineScreen(day: d.day)), ), ], @@ -328,16 +335,20 @@ class _InvestigateState extends State { // phone won still gets to say what the wrist thought, and a gen4 day drops // the row entirely because that hardware cannot count steps at all. List _stepSourcePanels(InvestigateData d) { + final l = AppLocalizations.of(context); final by = d.steps['by_source']; final split = by is Map ? by : const {}; String n(Object? v) => v is num ? thousands(v) : '—'; return [ - MonoTable('Which sensor counted', [ - ('strap · 100 Hz pedometer', n(split['strap'])), - ('strap · on-chip counter', n(split['strap_counter'])), - ('phone · pedometer', n(split['phone'])), - ('day total', n(d.steps['value'])), - ('strap chip reported', n(d.steps['band_measured'])), + MonoTable(l?.investigateWhichSensorCounted ?? 'Which sensor counted', [ + (l?.investigateStrapPedometer ?? 'strap · 100 Hz pedometer', + n(split['strap'])), + (l?.investigateStrapOnChipCounter ?? 'strap · on-chip counter', + n(split['strap_counter'])), + (l?.investigatePhonePedometer ?? 'phone · pedometer', n(split['phone'])), + (l?.investigateDayTotal ?? 'day total', n(d.steps['value'])), + (l?.investigateStrapChipReported ?? 'strap chip reported', + n(d.steps['band_measured'])), ]), const SizedBox(height: S.x3), ]; @@ -345,6 +356,7 @@ class _InvestigateState extends State { // ── HRV: time, frequency, non-linear ── List _hrvPanels(BuildContext c, InvestigateData d) { + final l = AppLocalizations.of(c); final time = envValue(d.hrv['hrv_time']) ?? const {}; final freq = envValue(d.hrv['hrv_freq']) ?? const {}; final freqNote = metricOf(d.hrv['hrv_freq']).note; @@ -374,15 +386,16 @@ class _InvestigateState extends State { final beats = time['n_beats'] ?? irr['n_beats'] ?? irr24['n_beats']; return [ - MonoTable('Time domain', [ - ('RMSSD', ms(time['rmssd_ms'] ?? d.hrv['rmssd'])), - ('SDNN', ms(time['sdnn_ms'] ?? d.hrv['sdnn'])), - ('SDANN', ms(time['sdann_ms'])), - ('SDNN index', ms(time['sdnn_index_ms'])), - ('pNN50', pct(time['pnn50_pct'])), - ('ln RMSSD', plain(d.hrv['ln_rmssd'])), - ('Your baseline RMSSD', ms(d.hrv['baseline'])), - ('Stability (CV)', pct(cv)), + MonoTable(l?.investigateTimeDomain ?? 'Time domain', [ + (l?.investigateRmssd ?? 'RMSSD', ms(time['rmssd_ms'] ?? d.hrv['rmssd'])), + (l?.investigateSdnn ?? 'SDNN', ms(time['sdnn_ms'] ?? d.hrv['sdnn'])), + (l?.investigateSdann ?? 'SDANN', ms(time['sdann_ms'])), + (l?.investigateSdnnIndex ?? 'SDNN index', ms(time['sdnn_index_ms'])), + (l?.investigatePnn50 ?? 'pNN50', pct(time['pnn50_pct'])), + (l?.investigateLnRmssd ?? 'ln RMSSD', plain(d.hrv['ln_rmssd'])), + (l?.investigateBaselineRmssd ?? 'Your baseline RMSSD', + ms(d.hrv['baseline'])), + (l?.investigateStabilityCv ?? 'Stability (CV)', pct(cv)), ]), const SizedBox(height: S.x3), // ms² is correct AGAIN: `lombScargle` now returns physical PSD (ms²/Hz) @@ -392,27 +405,30 @@ class _InvestigateState extends State { // small. Every row here self-drops when its key is absent — ULF and a // too-short session now return null rather than a fabricated zero, and // MonoTable removes the row rather than dashing it. - MonoTable('Frequency domain', [ - ('ULF power', ms2(freq['ulf'])), - ('VLF power', ms2(freq['vlf'])), - ('LF power', ms2(freq['lf'])), - ('HF power', ms2(freq['hf'])), - ('Total power', ms2(freq['total'])), - ('LF / HF', plain(freq['lf_hf'])), - ('LF, normalised', plain(freq['nu_lf'])), - ('HF, normalised', plain(freq['nu_hf'])), + MonoTable(l?.investigateFrequencyDomain ?? 'Frequency domain', [ + (l?.investigateUlfPower ?? 'ULF power', ms2(freq['ulf'])), + (l?.investigateVlfPower ?? 'VLF power', ms2(freq['vlf'])), + (l?.investigateLfPower ?? 'LF power', ms2(freq['lf'])), + (l?.investigateHfPower ?? 'HF power', ms2(freq['hf'])), + (l?.investigateTotalPower ?? 'Total power', ms2(freq['total'])), + (l?.investigateLfHf ?? 'LF / HF', plain(freq['lf_hf'])), + (l?.investigateLfNormalised ?? 'LF, normalised', plain(freq['nu_lf'])), + (l?.investigateHfNormalised ?? 'HF, normalised', plain(freq['nu_hf'])), // An ABSENT spectrum is not an ungated one. `envValue` returns null for // an absent envelope, so `== true ? : 'no'` collapsed "never computed" // into a confident negative — and it left a one-row table reading // "HF gated no" directly above the card saying there is no spectrum. - ('HF gated', freq['hf_gated'] == null + (l?.investigateHfGated ?? 'HF gated', freq['hf_gated'] == null ? '—' - : (freq['hf_gated'] == true ? 'yes' : 'no')), + : (freq['hf_gated'] == true + ? (l?.investigateYes ?? 'yes') + : (l?.investigateNo ?? 'no'))), ]), const SizedBox(height: S.x3), if (freq['total'] == null) StatusCard( - 'No frequency-domain spectrum for this night', + l?.investigateNoFrequencySpectrum ?? + 'No frequency-domain spectrum for this night', // THE ESTIMATOR'S OWN REASON. `total` goes null for three different // reasons and the envelope note distinguishes them; "too short" was // printed for all three, so a full 8 h night that failed the artifact @@ -420,30 +436,45 @@ class _InvestigateState extends State { // their strap fit — contradicting the "HF gated yes" row above it. freqNote?.isNotEmpty == true ? freqNote! - : 'The recording was too short to resolve the bands.', + : (l?.investigateRecordingTooShort ?? + 'The recording was too short to resolve the bands.'), ), const SizedBox(height: S.x3), - MonoTable('Non-linear', [ - ('SD1, sleep', ms(irr['sd1'])), - ('SD2, sleep', ms(irr['sd2'])), - ('SD1, 24 h', ms(irr24['sd1_ms'])), - ('SD2, 24 h', ms(irr24['sd2_ms'])), - ('SD1 / SD2, 24 h', plain(irr24['sd1_sd2'])), + MonoTable(l?.investigateNonLinear ?? 'Non-linear', [ + (l?.investigateSd1Sleep ?? 'SD1, sleep', ms(irr['sd1'])), + (l?.investigateSd2Sleep ?? 'SD2, sleep', ms(irr['sd2'])), + (l?.investigateSd124h ?? 'SD1, 24 h', ms(irr24['sd1_ms'])), + (l?.investigateSd224h ?? 'SD2, 24 h', ms(irr24['sd2_ms'])), + (l?.investigateSd1Sd224h ?? 'SD1 / SD2, 24 h', + plain(irr24['sd1_sd2'])), // The screen's own threshold, stated rather than baked into a label // the stored key cannot confirm. - ('Successive intervals over 70 ms', pct(irr24['pnn_pct'])), + (l?.investigateSuccessiveIntervalsOver70ms ?? + 'Successive intervals over 70 ms', pct(irr24['pnn_pct'])), // A screen that never RAN is not a screen that ran and found nothing. // `irregularBeatScreen` abstains below 500 clean beats or over 30% // artifact — the common case for a barely-worn day — and both rows // printed "clear" for it, i.e. a negative arrhythmia screen for a day // the screen was explicitly suppressed. MonoTable drops the em-dash. - ('Irregular-rhythm flag, sleep', - irr['flag'] == null ? '—' : (irr['flag'] == true ? 'raised' : 'clear')), - ('Irregular-rhythm flag, 24 h', - irr24['flag'] == null ? '—' : (irr24['flag'] == true ? 'raised' : 'clear')), - ('Deceleration capacity', ms(dc['capacity_ms'])), - ('Acceleration capacity', ms(ac['capacity_ms'])), - ('DC anchors', dc['anchors'] == null ? '—' : thousands(dc['anchors'] as num)), + (l?.investigateIrregularRhythmFlagSleep ?? + 'Irregular-rhythm flag, sleep', + irr['flag'] == null + ? '—' + : (irr['flag'] == true + ? (l?.investigateFlagRaised ?? 'raised') + : (l?.investigateFlagClear ?? 'clear'))), + (l?.investigateIrregularRhythmFlag24h ?? 'Irregular-rhythm flag, 24 h', + irr24['flag'] == null + ? '—' + : (irr24['flag'] == true + ? (l?.investigateFlagRaised ?? 'raised') + : (l?.investigateFlagClear ?? 'clear'))), + (l?.investigateDecelerationCapacity ?? 'Deceleration capacity', + ms(dc['capacity_ms'])), + (l?.investigateAccelerationCapacity ?? 'Acceleration capacity', + ms(ac['capacity_ms'])), + (l?.investigateDcAnchors ?? 'DC anchors', + dc['anchors'] == null ? '—' : thousands(dc['anchors'] as num)), ]), if (d.dcPoints.isNotEmpty) ...[ const SizedBox(height: S.x3), @@ -457,10 +488,13 @@ class _InvestigateState extends State { // Beats analysed is the only MEASURED row this table ever had; the other // three were constants sitting under a heading that made them look // measured. They are in the method note at the bottom of the screen. - MonoTable('Signal quality', [ - ('Beats analysed', beats == null ? '—' : thousands(beats as num)), - ('Beats analysed, 24 h', - irr24['n_beats'] == null ? '—' : thousands(irr24['n_beats'] as num)), + MonoTable(l?.investigateSignalQuality ?? 'Signal quality', [ + (l?.investigateBeatsAnalysed ?? 'Beats analysed', + beats == null ? '—' : thousands(beats as num)), + (l?.investigateBeatsAnalysed24h ?? 'Beats analysed, 24 h', + irr24['n_beats'] == null + ? '—' + : thousands(irr24['n_beats'] as num)), ]), ..._shapePanels(c, d), ]; @@ -490,6 +524,7 @@ class _InvestigateState extends State { /// hours between adjacent nights on identical physiology. The analytics /// refuses to compute it; this refuses to ask. List _shapePanels(BuildContext c, InvestigateData d) { + final l = AppLocalizations.of(c); final raw = d.hrv['night_shape']; // No key at all: a bundle derived before the pipeline emitted this. Silence // is right — there is nothing to explain about a night nobody measured it @@ -507,10 +542,11 @@ class _InvestigateState extends State { return [ const SizedBox(height: S.x3), StatusCard( - 'No shape for this night', + l?.investigateNoShapeForNight ?? 'No shape for this night', note?.isNotEmpty == true ? note! - : 'The night carried too few clean beats to bin.', + : (l?.investigateTooFewBeatsToBin ?? + 'The night carried too few clean beats to bin.'), icon: LucideIcons.activity, ), ]; @@ -556,17 +592,18 @@ class _InvestigateState extends State { const SizedBox(height: S.x3), Surface( child: ChartFrame( - title: 'Shape of the night', + title: l?.investigateShapeOfTheNight ?? 'Shape of the night', unit: 'ms', height: 130, yAxis: axis, xLabels: origin == null ? const [] : [at(0), at(bins.length - 1)], legend: [ - ('Bin RMSSD', p.on(C.green)), - ('Sampling range', p.ink3), + (l?.investigateBinRmssd ?? 'Bin RMSSD', p.on(C.green)), + (l?.investigateSamplingRange ?? 'Sampling range', p.ink3), ], series: mid, - footnote: '$drawn of ${bins.length} bins carried enough beats to ' + footnote: l?.investigateShapeFootnote(drawn, bins.length) ?? + '$drawn of ${bins.length} bins carried enough beats to ' 'read; the rest are gaps, not zeroes. The outer pair is the ' "estimator's own sampling spread, not a range you were in. This " 'describes the night and cannot explain it — a low first third ' @@ -593,15 +630,15 @@ class _InvestigateState extends State { ), ), const SizedBox(height: S.x3), - MonoTable('Night shape', [ - ('Bin width', + MonoTable(l?.investigateNightShape ?? 'Night shape', [ + (l?.investigateBinWidth ?? 'Bin width', widthMin == null ? '—' : '${widthMin.round()} min'), - ('Bins read', '$drawn of ${bins.length}'), - ('First third', ms(v['first_third_ms'])), - ('Last third', ms(v['last_third_ms'])), + (l?.investigateBinsRead ?? 'Bins read', '$drawn of ${bins.length}'), + (l?.investigateFirstThird ?? 'First third', ms(v['first_third_ms'])), + (l?.investigateLastThird ?? 'Last third', ms(v['last_third_ms'])), // A ratio, printed as a ratio. No adjective, no direction word, no // colour: "1.32" is the fact and "recovered well" is not one. - ('Last third ÷ first', + (l?.investigateLastThirdOverFirst ?? 'Last third ÷ first', ratio is num ? ratio.toStringAsFixed(2) : '—'), ]), ]; @@ -615,22 +652,32 @@ class _InvestigateState extends State { /// cleaner-signal line. No colour, no threshold, no reference range, ever. /// The beat count goes on the card because the artifact gate is load-bearing. Widget _dcTrend(BuildContext c, InvestigateData d, Object? beats) { + final l = AppLocalizations.of(c); final p = P.of(c); final win = denseDays(d.dcPoints, 30); final vals = [for (final v in win) ?v]; final axis = AxisSpec.of(vals, ticks: 3, format: axisFixed); return Surface( child: ChartFrame( - title: 'Deceleration capacity', + title: l?.investigateDecelerationCapacity ?? 'Deceleration capacity', unit: 'ms', height: 110, yAxis: axis, - xLabels: const ['29 days ago', 'Today'], + xLabels: [ + l?.investigate29DaysAgo ?? '29 days ago', + l?.investigateToday ?? 'Today', + ], series: win, - footnote: 'Your own nights only — no reference range, and none exists ' - 'for pulse arrivals. Night-to-night signal quality moves this line ' - 'on its own' - '${beats is num ? ', and last night was ${thousands(beats)} beats' : ''}.', + footnote: beats is num + ? (l?.investigateDcFootnoteWithBeats(thousands(beats)) ?? + 'Your own nights only — no reference range, and none exists ' + 'for pulse arrivals. Night-to-night signal quality moves ' + 'this line on its own, and last night was ' + '${thousands(beats)} beats.') + : (l?.investigateDcFootnote ?? + 'Your own nights only — no reference range, and none exists ' + 'for pulse arrivals. Night-to-night signal quality moves ' + 'this line on its own.'), child: CustomPaint( size: Size.infinite, // p.ink3, not an accent. A colour here would be a verdict. @@ -655,6 +702,7 @@ class _InvestigateState extends State { /// abnormal beats here and no AF, PVC or ectopy vocabulary anywhere near it. /// The footnote is permanent, not a tooltip: a clear strip means nothing. Widget _rhythmStrip(BuildContext c, InvestigateData d) { + final l = AppLocalizations.of(c); final p = P.of(c); const weeks = 12; final grid = _weekGrid(d.rhythmPoints, weeks); @@ -662,12 +710,16 @@ class _InvestigateState extends State { final raised = d.rhythmPoints.where((e) => e.v >= 1).length; return Surface( child: ChartFrame( - title: 'Irregular-rhythm screen', - unit: 'one square per day', + title: l?.investigateIrregularRhythmScreen ?? 'Irregular-rhythm screen', + unit: l?.investigateOneSquarePerDay ?? 'one square per day', height: 96, - xLabels: const ['12 weeks ago', 'This week'], - legend: [('Screen ran', p.on(C.purple))], - footnote: 'Ran on $ran day${ran == 1 ? '' : 's'}, raised its flag on ' + xLabels: [ + l?.investigate12WeeksAgo ?? '12 weeks ago', + l?.investigateThisWeek ?? 'This week', + ], + legend: [(l?.investigateScreenRan ?? 'Screen ran', p.on(C.purple))], + footnote: l?.investigateRhythmStripFootnote(ran, raised) ?? + 'Ran on $ran day${ran == 1 ? '' : 's'}, raised its flag on ' '$raised. An outlined square is a day it did not run. A clear ' 'strip is not a negative result: this is a screen on pulse ' 'timing, and it cannot tell an ectopic beat from a dropped beat ' @@ -712,17 +764,20 @@ class _InvestigateState extends State { // nocturnal breathing rate has its own row on Vitals and its own trend; this // one exists to answer the same question without waiting for a night. List _restingBreathPanels(InvestigateData d) { + final l = AppLocalizations.of(context); final line = d.timeline['resp']; if (line is! List || line.isEmpty) return const []; final t0 = d.windowStart, t1 = d.windowEnd; - const empty = [ - SizedBox(height: S.x3), + final empty = [ + const SizedBox(height: S.x3), StatusCard( - 'No resting breathing rate away from sleep', - 'This reads breathing only from three-minute stretches where the band ' - 'saw you almost completely still, outside the sleep window. Most ' - 'days have none — a day with none is a day you were moving, not a ' - 'day anything went wrong.', + l?.investigateNoRestingBreathingRate ?? + 'No resting breathing rate away from sleep', + l?.investigateNoRestingBreathingRateBody ?? + 'This reads breathing only from three-minute stretches where the ' + 'band saw you almost completely still, outside the sleep ' + 'window. Most days have none — a day with none is a day you ' + 'were moving, not a day anything went wrong.', icon: LucideIcons.wind, ), ]; @@ -742,25 +797,30 @@ class _InvestigateState extends State { return [ const SizedBox(height: S.x3), - MonoTable('Breathing at rest, awake', [ - ('Still stretches outside sleep', '${awake.length}'), - ('Lowest', '${awake.first.toStringAsFixed(1)} br/min'), + MonoTable(l?.investigateBreathingAtRestAwake ?? 'Breathing at rest, awake', [ + (l?.investigateStillStretchesOutsideSleep ?? + 'Still stretches outside sleep', '${awake.length}'), + (l?.investigateLowest ?? 'Lowest', + '${awake.first.toStringAsFixed(1)} br/min'), // The next one up, so the lowest is readable as one of several rather // than as a lone reading. Not a median of the day — these windows are // the stillest slices of it, not a sample of it. - ('Next lowest', '${awake[1].toStringAsFixed(1)} br/min'), - ('Highest of them', '${awake.last.toStringAsFixed(1)} br/min'), + (l?.investigateNextLowest ?? 'Next lowest', + '${awake[1].toStringAsFixed(1)} br/min'), + (l?.investigateHighestOfThem ?? 'Highest of them', + '${awake.last.toStringAsFixed(1)} br/min'), ]), const SizedBox(height: S.x3), Surface( color: P.of(context).card2, elevation: 0, child: Text( - 'A floor, not a rate for the day. Only stretches where you were ' - 'almost completely still can be read at all, so these are the ' - 'stillest few minutes the band saw outside your sleep — nothing ' - 'here describes the rest of your day, and breathing while you move ' - 'cannot be recovered from beat timing.', + l?.investigateFloorNotRateBody ?? + 'A floor, not a rate for the day. Only stretches where you were ' + 'almost completely still can be read at all, so these are the ' + 'stillest few minutes the band saw outside your sleep — nothing ' + 'here describes the rest of your day, and breathing while you ' + 'move cannot be recovered from beat timing.', style: F.cap.copyWith(color: P.of(context).ink2, height: 1.6), ), ), @@ -778,6 +838,7 @@ class _InvestigateState extends State { // only. Nothing here is a sleep-apnea finding and no copy anywhere near it // names a breathing disorder or a mechanism. List _cvhrPanels(InvestigateData d) { + final l = AppLocalizations.of(context); final v = envValue(d.cvhr); final note = metricOf(d.cvhr).note; if (v == null) { @@ -787,10 +848,12 @@ class _InvestigateState extends State { return [ const SizedBox(height: S.x3), StatusCard( - 'The cycle screen did not run for this night', + l?.investigateCycleScreenDidNotRun ?? + 'The cycle screen did not run for this night', note?.isNotEmpty == true ? note! - : 'Not enough clean beats to run it.', + : (l?.investigateNotEnoughCleanBeats ?? + 'Not enough clean beats to run it.'), icon: LucideIcons.wind, ), // The across-nights view survives a night that abstained — that is the @@ -810,24 +873,28 @@ class _InvestigateState extends State { final hours = v['analyzed_hours'] as num?; return [ const SizedBox(height: S.x3), - MonoTable('Heart-rate cycles', [ - ('Cycles counted', v['cycle_count'] == null - ? '—' - : thousands(v['cycle_count'] as num)), - ('Observed hours analysed', + MonoTable(l?.investigateHeartRateCycles ?? 'Heart-rate cycles', [ + (l?.investigateCyclesCounted ?? 'Cycles counted', + v['cycle_count'] == null ? '—' : thousands(v['cycle_count'] as num)), + (l?.investigateObservedHoursAnalysed ?? 'Observed hours analysed', hours == null ? '—' : '${hours.toStringAsFixed(2)} h'), - ('Cycles per observed hour', v['cvhr_per_hour'] == null - ? '—' - : (v['cvhr_per_hour'] as num).toStringAsFixed(2)), - ('Mean cycle length', v['mean_width_sec'] == null - ? '—' - : '${(v['mean_width_sec'] as num).toStringAsFixed(1)} s'), - ('Mean dip depth', v['mean_depth_ms'] == null - ? '—' - : '${(v['mean_depth_ms'] as num).toStringAsFixed(1)} ms'), + (l?.investigateCyclesPerObservedHour ?? 'Cycles per observed hour', + v['cvhr_per_hour'] == null + ? '—' + : (v['cvhr_per_hour'] as num).toStringAsFixed(2)), + (l?.investigateMeanCycleLength ?? 'Mean cycle length', + v['mean_width_sec'] == null + ? '—' + : '${(v['mean_width_sec'] as num).toStringAsFixed(1)} s'), + (l?.investigateMeanDipDepth ?? 'Mean dip depth', + v['mean_depth_ms'] == null + ? '—' + : '${(v['mean_depth_ms'] as num).toStringAsFixed(1)} ms'), // p25 · p50 · p75 of the SAME per-cycle lists the means come from. - ('Cycle length, quartiles', q(v['width_quartiles_sec'], 's', 1)), - ('Dip depth, quartiles', q(v['depth_quartiles_ms'], 'ms', 1)), + (l?.investigateCycleLengthQuartiles ?? 'Cycle length, quartiles', + q(v['width_quartiles_sec'], 's', 1)), + (l?.investigateDipDepthQuartiles ?? 'Dip depth, quartiles', + q(v['depth_quartiles_ms'], 'ms', 1)), ]), ..._cvhrDistribution(d), ]; @@ -863,6 +930,7 @@ class _InvestigateState extends State { // screen that fires on atrial fibrillation, on altitude and on any broken-up // night turns into a diagnosis in somebody's head. List _cvhrDistribution(InvestigateData d) { + final l = AppLocalizations.of(context); final m = d.cvhrDist; if (m == null) return const []; final p = P.of(context); @@ -872,7 +940,8 @@ class _InvestigateState extends State { return [ const SizedBox(height: S.x3), StatusCard( - 'Not enough nights for the across-nights view', + l?.investigateNotEnoughNightsAcross ?? + 'Not enough nights for the across-nights view', // The screen's OWN reason, VERBATIM, including which gate dropped // which night. `need_baseline:` and `nights=A/B` are the pipeline's // machine spellings and they stay: this is the surface where the @@ -880,7 +949,8 @@ class _InvestigateState extends State { // paraphrase of it, and the owner asked for it that way. m.note?.isNotEmpty == true ? m.note! - : 'This needs several nights with a few observed hours each.', + : (l?.investigateNeedsSeveralNights ?? + 'This needs several nights with a few observed hours each.'), icon: LucideIcons.wind, ), ]; @@ -889,10 +959,12 @@ class _InvestigateState extends State { final n = v.nightsUsed; final dropped = [ if (v.nightsExcludedIrregular > 0) - '${v.nightsExcludedIrregular} left out because the irregular-rhythm ' - 'screen flagged them', + l?.investigateDroppedIrregular(v.nightsExcludedIrregular) ?? + '${v.nightsExcludedIrregular} left out because the ' + 'irregular-rhythm screen flagged them', if (v.nightsExcludedThin > 0) - '${v.nightsExcludedThin} left out for too few observed hours', + l?.investigateDroppedThin(v.nightsExcludedThin) ?? + '${v.nightsExcludedThin} left out for too few observed hours', ]; return [ @@ -901,40 +973,47 @@ class _InvestigateState extends State { color: p.card2, elevation: 0, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('ACROSS $n OF YOUR OWN NIGHTS', + Text(l?.investigateAcrossNOwnNights(n) ?? 'ACROSS $n OF YOUR OWN NIGHTS', style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), Text( v.aboveOwnUsual - ? 'Over your most recent nights, the heart-rate cycling this ' - 'screen counts has been running higher than across the $n ' - 'nights behind it.' - : 'Over your most recent nights, the heart-rate cycling this ' - 'screen counts has stayed inside the range of the $n ' - 'nights behind it.', + ? (l?.investigateCvhrAboveUsual(n) ?? + 'Over your most recent nights, the heart-rate cycling ' + 'this screen counts has been running higher than ' + 'across the $n nights behind it.') + : (l?.investigateCvhrInsideUsual(n) ?? + 'Over your most recent nights, the heart-rate cycling ' + 'this screen counts has stayed inside the range of ' + 'the $n nights behind it.'), style: F.body.copyWith(color: p.ink, height: 1.5), ), const SizedBox(height: S.x3), Text( - 'It is a pattern in your pulse, not a measurement of your ' - 'breathing, and it is not a test for anything. The same cycling ' - 'comes from an irregular rhythm, from being at altitude, and from ' - 'any broken-up night — and beta-blockers, diabetes and nerve ' - 'conditions flatten it, so genuinely disturbed breathing often ' - 'leaves nothing here at all.', + l?.investigateCvhrExplainer ?? + 'It is a pattern in your pulse, not a measurement of your ' + 'breathing, and it is not a test for anything. The same ' + 'cycling comes from an irregular rhythm, from being at ' + 'altitude, and from any broken-up night — and beta-blockers, ' + 'diabetes and nerve conditions flatten it, so genuinely ' + 'disturbed breathing often leaves nothing here at all.', style: F.cap.copyWith(color: p.ink2, height: 1.6), ), const SizedBox(height: S.x3), Text( - 'So nothing here is a negative result and nothing here clears ' - 'anything, and none of it says anything about any one night — a ' - 'single night’s count moves for a dozen reasons on its own.', + l?.investigateCvhrNotNegativeResult ?? + 'So nothing here is a negative result and nothing here ' + 'clears anything, and none of it says anything about any one ' + 'night — a single night’s count moves for a dozen reasons on ' + 'its own.', style: F.cap.copyWith(color: p.ink2, height: 1.6), ), const SizedBox(height: S.x3), Text( - 'If you snore, wake unrefreshed, or someone has seen you stop ' - 'breathing in your sleep, a clinician can test that properly.', + l?.investigateCvhrSeeClinicianIfSymptoms ?? + 'If you snore, wake unrefreshed, or someone has seen you ' + 'stop breathing in your sleep, a clinician can test that ' + 'properly.', style: F.cap.copyWith(color: p.ink2, height: 1.6), ), if (dropped.isNotEmpty) ...[ @@ -954,14 +1033,15 @@ class _InvestigateState extends State { // somewhere the item names. Both are on the row, so the width is legible as a // property of the night rather than as a house style. List _stagePanels(InvestigateData d) { + final l = AppLocalizations.of(context); final n = d.night; int? min(String k) => (n[k] as num?)?.round(); - final l = min('light_min'), dp = min('deep_min'), r = min('rem_min'); + final light = min('light_min'), dp = min('deep_min'), r = min('rem_min'); final tst = min('duration_min'); - if (l == null || dp == null || r == null || tst == null) return const []; + if (light == null || dp == null || r == null || tst == null) return const []; final conf = (n['stages_confidence'] as num?)?.toDouble(); final iv = ana.stageIntervals( - lightSec: l * 60, + lightSec: light * 60, deepSec: dp * 60, remSec: r * 60, tstSec: tst * 60, @@ -972,16 +1052,19 @@ class _InvestigateState extends State { '${(i.hiSec / 60).round()} min'; return [ const SizedBox(height: S.x3), - MonoTable('Stage minutes, as counted', [ - ('Light', row(l, iv.light)), - ('Deep', row(dp, iv.deep)), - ('REM', row(r, iv.rem)), - ('Awake', min('awake_min') == null ? '—' : '${min('awake_min')} min'), - ('Total sleep', '$tst min'), + MonoTable(l?.investigateStageMinutesAsCounted ?? 'Stage minutes, as counted', [ + (l?.investigateLight ?? 'Light', row(light, iv.light)), + (l?.investigateDeep ?? 'Deep', row(dp, iv.deep)), + (l?.investigateRem ?? 'REM', row(r, iv.rem)), + (l?.investigateAwake ?? 'Awake', + min('awake_min') == null ? '—' : '${min('awake_min')} min'), + (l?.investigateTotalSleep ?? 'Total sleep', '$tst min'), // The width of every interval above is a function of this one number // and nothing else, so it goes on the same table. - ('Segmentation confidence', - conf == null ? 'not published' : conf.toStringAsFixed(2)), + (l?.investigateSegmentationConfidence ?? 'Segmentation confidence', + conf == null + ? (l?.investigateNotPublished ?? 'not published') + : conf.toStringAsFixed(2)), ]), ]; } @@ -989,9 +1072,12 @@ class _InvestigateState extends State { // ── anything else: what the series itself looks like ── List _genericPanels( BuildContext c, MetricSpec spec, InvestigateData d) { + final l = AppLocalizations.of(c); if (spec.suppress != null) { return [ - StatusCard('Nothing computed for this key', spec.suppress!, + StatusCard( + l?.investigateNothingComputedForKey ?? 'Nothing computed for this key', + spec.suppress!, icon: spec.icon), ]; } @@ -999,8 +1085,9 @@ class _InvestigateState extends State { if (s.isEmpty) { return [ StatusCard( - 'No stored series', - 'Nothing stored for ${spec.title.toLowerCase()} yet.', + l?.investigateNoStoredSeries ?? 'No stored series', + l?.investigateNothingStoredYet(spec.title.toLowerCase()) ?? + 'Nothing stored for ${spec.title.toLowerCase()} yet.', icon: spec.icon, ), ]; @@ -1016,32 +1103,38 @@ class _InvestigateState extends State { : v.toStringAsFixed(2); return [ - MonoTable('Series', [ + MonoTable(l?.investigateSeries ?? 'Series', [ // DERIVED days, not calendar days. `metric_series` gets a row only on a // day that derives, so "Days stored 40" under "one value per calendar // day" read as 40 days of continuous coverage. - ('Days derived', '${s.length}'), - ('Latest', n(s.last)), - ('Mean', n(mean)), - ('Median', n(sorted[sorted.length ~/ 2])), - ('SD', n(sd)), - ('Min', n(sorted.first)), - ('Max', n(sorted.last)), - ('Unit', spec.unit.isEmpty ? 'unitless' : spec.unit), - ('Storage', 'one value per derived day'), + (l?.investigateDaysDerived ?? 'Days derived', '${s.length}'), + (l?.investigateLatest ?? 'Latest', n(s.last)), + (l?.investigateMean ?? 'Mean', n(mean)), + (l?.investigateMedian ?? 'Median', n(sorted[sorted.length ~/ 2])), + (l?.investigateSd ?? 'SD', n(sd)), + (l?.investigateMin ?? 'Min', n(sorted.first)), + (l?.investigateMax ?? 'Max', n(sorted.last)), + (l?.investigateUnit ?? 'Unit', + spec.unit.isEmpty ? (l?.investigateUnitless ?? 'unitless') : spec.unit), + (l?.investigateStorage ?? 'Storage', + l?.investigateOneValuePerDerivedDay ?? 'one value per derived day'), ]), ]; } Widget _method(BuildContext c, MetricSpec spec) { + final l = AppLocalizations.of(c); final p = P.of(c); return Surface( elevation: 0, color: p.card2, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('METHOD', style: F.over.copyWith(color: p.ink3)), + Text(l?.investigateMethodLabel ?? 'METHOD', style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x2), - Text(spec.method.isEmpty ? 'Not documented.' : spec.method, + Text( + spec.method.isEmpty + ? (l?.investigateNotDocumented ?? 'Not documented.') + : spec.method, style: F.cap.copyWith(color: p.ink2, height: 1.6)), if (spec.citation.isNotEmpty) ...[ const SizedBox(height: S.x3), diff --git a/lib/ui2/screens/journal_compose.dart b/lib/ui2/screens/journal_compose.dart index 01a00dc5..ad04ef16 100644 --- a/lib/ui2/screens/journal_compose.dart +++ b/lib/ui2/screens/journal_compose.dart @@ -19,6 +19,7 @@ import '../../ai/journal_ai.dart' show kJournalPresetTags; import '../../data/day_label.dart'; import '../../data/db.dart'; import '../../data/journal_fields.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; import '../../state/units_controller.dart'; import '../ui2.dart'; @@ -108,8 +109,11 @@ class _JournalComposeState extends State { // No repo means the sheet should never have been reachable; say so rather // than eating the tap. if (repo == null) { + final l = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Not ready yet — open the app first.')), + SnackBar( + content: Text(l?.journalComposeNotReady ?? + 'Not ready yet — open the app first.')), ); return; } @@ -120,9 +124,11 @@ class _JournalComposeState extends State { if (!mounted) return; // A failed persist must not read as success — the field would vanish // from the list while the user believes it saved. + final l = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Could not save it — check storage and retry.')), + SnackBar( + content: Text(l?.journalComposeSaveFailed ?? + 'Could not save it — check storage and retry.')), ); return; } @@ -141,6 +147,7 @@ class _JournalComposeState extends State { Future _setTime(String key) async { final cur = _values[key]; if (cur == null) return; + final l = AppLocalizations.of(context); final at = await showTimePicker( context: context, initialTime: cur.atMinuteOfDay == null @@ -149,7 +156,7 @@ class _JournalComposeState extends State { hour: cur.atMinuteOfDay! ~/ 60, minute: cur.atMinuteOfDay! % 60, ), - helpText: 'When was the last one?', + helpText: l?.journalComposeWhenWasLastOne ?? 'When was the last one?', ); if (at == null || !mounted) return; setState(() { @@ -172,12 +179,14 @@ class _JournalComposeState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column( children: [ - NavBar('Journal', sub: _date, onBack: () => Navigator.of(c).pop()), + NavBar(l?.journalComposeTitle ?? 'Journal', + sub: _date, onBack: () => Navigator.of(c).pop()), Expanded( child: _loading ? const Center(child: CircularProgressIndicator()) @@ -189,7 +198,7 @@ class _JournalComposeState extends State { onChanged: (v) => _set('mood', v?.toDouble()), ), Section( - 'Today', + l?.journalComposeTodaySection ?? 'Today', Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: Column( @@ -234,7 +243,9 @@ class _JournalComposeState extends State { Icon(LucideIcons.plusCircle, size: 18, color: p.ink3), const SizedBox(width: S.x2), - Text('Track something else', + Text( + l?.journalComposeTrackSomethingElse ?? + 'Track something else', style: F.body.copyWith(color: p.ink3)), ], @@ -245,7 +256,9 @@ class _JournalComposeState extends State { ), ), Section( - 'What happened', + // Same English text as day_timeline's section — key + // shared across the two files, not duplicated. + l?.dayTimelineWhatHappenedSection ?? 'What happened', Wrap( spacing: S.x2, runSpacing: S.x2, @@ -271,13 +284,16 @@ class _JournalComposeState extends State { const SizedBox(height: S.x5), OsTextField( controller: _note, - label: 'Anything else', - hint: 'A line about the day.', + label: l?.journalComposeAnythingElseLabel ?? 'Anything else', + hint: l?.journalComposeAnythingElseHint ?? + 'A line about the day.', lines: 4, ), const SizedBox(height: S.x6), BigButton( - _saving ? 'Saving' : 'Save', + _saving + ? (l?.journalComposeSavingLabel ?? 'Saving') + : (l?.actionSave ?? 'Save'), icon: LucideIcons.check, color: C.domMind, onTap: _saving ? null : _save, @@ -319,19 +335,21 @@ class MoodPicker extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'How are you feeling?', + l?.journalComposeHowAreYouFeeling ?? 'How are you feeling?', style: F.body.copyWith(color: p.ink, fontWeight: FontWeight.w600), ), const SizedBox(height: S.x1), Text( value == null - ? 'Not answered yet' - : 'Mood $value of 5 · tap it again to clear', + ? (l?.journalComposeNotAnsweredYet ?? 'Not answered yet') + : (l?.journalComposeMoodOfFive(value!) ?? + 'Mood $value of 5 · tap it again to clear'), style: F.cap.copyWith(color: p.ink3), ), const SizedBox(height: S.x4), @@ -341,8 +359,10 @@ class MoodPicker extends StatelessWidget { Expanded( child: Pressable( semanticLabel: value == i + 1 - ? 'Mood ${i + 1} of 5, selected. Activate to clear.' - : 'Mood ${i + 1} of 5', + ? (l?.journalComposeMoodOfFiveSelected(i + 1) ?? + 'Mood ${i + 1} of 5, selected. Activate to clear.') + : (l?.journalComposeMoodOfFiveLabel(i + 1) ?? + 'Mood ${i + 1} of 5'), onTap: () => onChanged(value == i + 1 ? null : i + 1), child: AnimatedContainer( duration: motion(c, Motion.base), @@ -409,6 +429,7 @@ class FieldStepper extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final v = value; return Padding( padding: const EdgeInsets.symmetric(vertical: S.x2), @@ -420,17 +441,22 @@ class FieldStepper extends StatelessWidget { children: [ Text(spec.label, style: F.body.copyWith(color: p.ink)), Text( - v == null ? 'Not logged' : spec.formatWithUnit(v), + v == null + ? (l?.journalComposeNotLogged ?? 'Not logged') + : spec.formatWithUnit(v), style: F.over.copyWith(color: p.ink3), ), if (v != null && v > 0 && onTime != null) Pressable( - semanticLabel: 'When was the last ${spec.label}', + semanticLabel: l?.journalComposeWhenWasLastField(spec.label) ?? + 'When was the last ${spec.label}', onTap: onTime, child: Text( atMin == null - ? 'Add the time of the last one' - : 'Last at ${formatMinuteOfDay(atMin!)}', + ? (l?.journalComposeAddTimeOfLastOne ?? + 'Add the time of the last one') + : (l?.journalComposeLastAt(formatMinuteOfDay(atMin!)) ?? + 'Last at ${formatMinuteOfDay(atMin!)}'), style: F.over.copyWith(color: p.on(C.blue)), ), ), @@ -471,8 +497,11 @@ class _Step extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Pressable( - semanticLabel: icon == LucideIcons.plus ? 'Increase' : 'Decrease', + semanticLabel: icon == LucideIcons.plus + ? (l?.journalComposeIncrease ?? 'Increase') + : (l?.journalComposeDecrease ?? 'Decrease'), onTap: enabled ? onTap : null, child: Container( width: 32, @@ -584,6 +613,7 @@ class _WeightRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final u = unitsOf(c); final v = kg; return Padding( @@ -597,18 +627,21 @@ class _WeightRow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Weight', style: F.body.copyWith(color: p.ink)), + Text(l?.journalComposeWeightLabel ?? 'Weight', + style: F.body.copyWith(color: p.ink)), Text( v == null - ? 'Not entered' - : '${u == null ? '${v.toStringAsFixed(1)} kg' : u.weight(v)} · entered, not measured', + ? (l?.journalComposeNotEntered ?? 'Not entered') + : (l?.journalComposeEnteredNotMeasured( + u == null ? '${v.toStringAsFixed(1)} kg' : u.weight(v)) ?? + '${u == null ? '${v.toStringAsFixed(1)} kg' : u.weight(v)} · entered, not measured'), style: F.over.copyWith(color: p.ink3), ), ], ), ), Pressable( - semanticLabel: 'Enter weight', + semanticLabel: l?.journalComposeEnterWeight ?? 'Enter weight', onTap: () async { final next = await _askWeight(c, u, v); if (next != null) onChanged(next.value); @@ -620,7 +653,9 @@ class _WeightRow extends StatelessWidget { ), decoration: BoxDecoration(color: p.card2, borderRadius: R.rSm), child: Text( - v == null ? 'Enter' : 'Change', + v == null + ? (l?.journalComposeEnter ?? 'Enter') + : (l?.journalComposeChange ?? 'Change'), style: F.cap.copyWith(color: p.ink2), ), ), @@ -628,12 +663,12 @@ class _WeightRow extends StatelessWidget { ], ), Pressable( - semanticLabel: 'See the weight trend', + semanticLabel: l?.journalComposeSeeWeightTrend ?? 'See the weight trend', onTap: () => Navigator.of(c).push( MaterialPageRoute(builder: (_) => const _WeightTrend()), ), child: Text( - 'See the trend', + l?.journalComposeSeeTheTrend ?? 'See the trend', style: F.over.copyWith(color: p.on(C.blue)), ), ), @@ -652,12 +687,13 @@ class _WeightRow extends StatelessWidget { ) { final imperial = u?.isImperial ?? false; final ctrl = TextEditingController(text: u?.weightField(kg) ?? ''); + final l = AppLocalizations.of(c); return showDialog<({double? value})>( context: c, builder: (dc) => AlertDialog( backgroundColor: P.of(dc).card, title: Text( - 'Weight today', + l?.journalComposeWeightToday ?? 'Weight today', style: F.head.copyWith(color: P.of(dc).ink), ), content: Column( @@ -666,13 +702,14 @@ class _WeightRow extends StatelessWidget { children: [ OsTextField( controller: ctrl, - label: u?.weightLabel ?? 'Weight (kg)', + label: u?.weightLabel ?? (l?.journalComposeWeightKgLabel ?? 'Weight (kg)'), hint: imperial ? '154' : '70.0', keyboard: const TextInputType.numberWithOptions(decimal: true), ), const SizedBox(height: S.x3), Text( - 'What you or your scale read. The band does not measure this.', + l?.journalComposeWeightScaleNote ?? + 'What you or your scale read. The band does not measure this.', style: F.over.copyWith(color: P.of(dc).ink3, height: 1.4), ), ], @@ -682,14 +719,14 @@ class _WeightRow extends StatelessWidget { TextButton( onPressed: () => Navigator.of(dc).pop((value: null)), child: Text( - 'Clear', + l?.journalComposeClear ?? 'Clear', style: F.body.copyWith(color: P.of(dc).ink2), ), ), TextButton( onPressed: () => Navigator.of(dc).pop(), child: Text( - 'Cancel', + l?.actionCancel ?? 'Cancel', style: F.body.copyWith(color: P.of(dc).ink2), ), ), @@ -698,7 +735,8 @@ class _WeightRow extends StatelessWidget { // A typo is not a blank. Nothing is saved from an unreadable // field, and the form says which one rather than storing a hole. if (Typed.of(ctrl.text).bad) { - sayUnreadable(dc, [u?.weightLabel ?? 'Weight']); + sayUnreadable( + dc, [u?.weightLabel ?? (l?.journalComposeWeightLabel ?? 'Weight')]); return; } final kgIn = u == null @@ -707,7 +745,7 @@ class _WeightRow extends StatelessWidget { Navigator.of(dc).pop((value: kgIn)); }, child: Text( - 'Save', + l?.actionSave ?? 'Save', style: F.body.copyWith(color: P.of(dc).on(C.blue)), ), ), @@ -769,8 +807,10 @@ class _WeightTrendState extends State<_WeightTrend> { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); + final title = l?.journalComposeWeightLabel ?? 'Weight'; if (_loading) { - return detailScaffold(c, 'Weight', const [ + return detailScaffold(c, title, const [ SizedBox(height: S.x8), Center(child: CircularProgressIndicator()), ]); @@ -779,12 +819,13 @@ class _WeightTrendState extends State<_WeightTrend> { final u = unitsOf(c); final trend = weightTrendEwma(_byDay); if (trend.length < 2) { - return detailScaffold(c, 'Weight', const [ - SizedBox(height: S.x2), + return detailScaffold(c, title, [ + const SizedBox(height: S.x2), StatusCard( - 'Not enough entries for a trend', - 'The line is a seven-day average through what you entered, so it ' - 'needs at least two days. Nothing is filled in between them.', + l?.journalComposeNotEnoughEntriesTitle ?? 'Not enough entries for a trend', + l?.journalComposeNotEnoughEntriesBody ?? + 'The line is a seven-day average through what you entered, so it ' + 'needs at least two days. Nothing is filled in between them.', icon: LucideIcons.scale, ), ]); @@ -809,16 +850,17 @@ class _WeightTrendState extends State<_WeightTrend> { final present = [for (final v in vals) ?v]; final axis = AxisSpec.of(present, format: axisFixed); - return detailScaffold(c, 'Weight', [ + return detailScaffold(c, title, [ const SizedBox(height: S.x2), Surface( child: ChartFrame( - title: 'Seven-day trend', + title: l?.journalComposeSevenDayTrend ?? 'Seven-day trend', unit: u?.isImperial == true ? 'lb' : 'kg', height: 140, yAxis: axis, xLabels: [days.first, days.last], - footnote: 'Entered by you. Days with no entry are left empty.', + footnote: l?.journalComposeTrendFootnote ?? + 'Entered by you. Days with no entry are left empty.', series: vals, empty: axis == null ? const NoData() : null, child: axis == null @@ -839,11 +881,12 @@ class _WeightTrendState extends State<_WeightTrend> { ), const SizedBox(height: S.x4), Text( - 'Entered by you or your scale — the band does not measure weight. What ' - 'is drawn is a seven-day average, because a scale moves one to two ' - 'kilos on water and food alone and the raw readings would show that as ' - 'something happening to your body. ${trend.length} ' - '${trend.length == 1 ? 'day' : 'days'} entered.', + l?.journalComposeWeightTrendExplainer(trend.length) ?? + 'Entered by you or your scale — the band does not measure weight. What ' + 'is drawn is a seven-day average, because a scale moves one to two ' + 'kilos on water and food alone and the raw readings would show that as ' + 'something happening to your body. ${trend.length} ' + '${trend.length == 1 ? 'day' : 'days'} entered.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ]); diff --git a/lib/ui2/screens/log_food.dart b/lib/ui2/screens/log_food.dart index 1630cdd8..8b173b17 100644 --- a/lib/ui2/screens/log_food.dart +++ b/lib/ui2/screens/log_food.dart @@ -32,6 +32,7 @@ import '../../data/db.dart'; import '../../data/day_label.dart'; import '../../data/nutrition_store.dart'; import '../../data/off_lookup.dart'; +import '../../l10n/app_localizations.dart'; import '../profile/profile.dart' show SetRow; import '../ui2.dart'; import 'journal_compose.dart' show OsTextField; @@ -233,38 +234,43 @@ class _LogFoodSheetState extends State { /// Every branch ends in the same place on purpose: type the numbers off the /// pack. That is the fallback, it always was, and none of these are errors /// the user did anything to cause. - StatusCard? get _lookupProblem { + StatusCard? _lookupProblem(BuildContext c) { final o = _outcome; if (o == null) return null; // A product whose numbers all survived needs no card; the filled boxes // are the answer. if (o == OffOutcome.ok && _scanned?.isBare != true) return null; + final l = AppLocalizations.of(c); return switch (o) { - OffOutcome.ok => const StatusCard( - 'No numbers for this one', - 'Open Food Facts has the product but nothing usable on its ' - 'nutrition — or what it had did not survive a sanity check.', + OffOutcome.ok => StatusCard( + l?.logFoodNoNumbersTitle ?? 'No numbers for this one', + l?.logFoodNoNumbersBody ?? + 'Open Food Facts has the product but nothing usable on its ' + 'nutrition — or what it had did not survive a sanity check.', icon: LucideIcons.scanBarcode, ), - OffOutcome.notFound => const StatusCard( - 'Not in Open Food Facts', - 'Nobody has added this barcode yet.', + OffOutcome.notFound => StatusCard( + l?.logFoodNotFoundTitle ?? 'Not in Open Food Facts', + l?.logFoodNotFoundBody ?? 'Nobody has added this barcode yet.', icon: LucideIcons.scanBarcode, ), - OffOutcome.flagged => const StatusCard( - 'This record is flagged as wrong', - 'Open Food Facts marks this product as containing errors, so none ' - 'of its numbers were filled in.', + OffOutcome.flagged => StatusCard( + l?.logFoodFlaggedTitle ?? 'This record is flagged as wrong', + l?.logFoodFlaggedBody ?? + 'Open Food Facts marks this product as containing errors, so none ' + 'of its numbers were filled in.', icon: LucideIcons.triangleAlert, ), - OffOutcome.unreachable => const StatusCard( - 'No answer from Open Food Facts', - 'The lookup could not reach openfoodfacts.org.', + OffOutcome.unreachable => StatusCard( + l?.logFoodUnreachableTitle ?? 'No answer from Open Food Facts', + l?.logFoodUnreachableBody ?? + 'The lookup could not reach openfoodfacts.org.', icon: LucideIcons.cloudOff, ), - OffOutcome.refused => const StatusCard( - 'Barcode lookup is off', - 'Nothing was sent. You can turn it on in Settings › Privacy.', + OffOutcome.refused => StatusCard( + l?.logFoodRefusedTitle ?? 'Barcode lookup is off', + l?.logFoodRefusedBody ?? + 'Nothing was sent. You can turn it on in Settings › Privacy.', icon: LucideIcons.scanBarcode, ), }; @@ -273,13 +279,15 @@ class _LogFoodSheetState extends State { /// What basis the boxes are on. The pack's own serving is named when the /// record states one, because "33 g" beside a 400 g jar is the difference /// between a spoonful and a fortnight. - String _portionNote(OffProduct p) { - final base = 'Open Food Facts lists this per 100 g. Change the portion and ' - 'the numbers follow.'; + String _portionNote(BuildContext c, OffProduct p) { + final l = AppLocalizations.of(c); + final base = l?.logFoodPortionNoteBase ?? + 'Open Food Facts lists this per 100 g. Change the portion and ' + 'the numbers follow.'; if (p.servingLabel.isEmpty && p.servingG == null) return base; final serving = p.servingLabel.isNotEmpty ? p.servingLabel : '${_plain(p.servingG)} g'; - return '$base The pack’s own serving is $serving.'; + return l?.logFoodPortionNoteServing(serving) ?? '$base The pack’s own serving is $serving.'; } void _fillFrom(OffProduct p, double grams) { @@ -292,21 +300,25 @@ class _LogFoodSheetState extends State { } /// The number fields that were typed into and cannot be read. - List get _unreadable => [ - for (final (name, ctl) in [ - ('Energy', _kcal), - ('Protein', _protein), - ('Carbs', _carbs), - ('Fat', _fat), - ('Fibre', _fibre), - if (_scanned != null) ('Portion', _portion), - ]) - if (Typed.of(ctl.text).bad) name, - ]; + List _unreadable(BuildContext c) { + final l = AppLocalizations.of(c); + return [ + for (final (name, ctl) in [ + (l?.logFoodEnergyLabel ?? 'Energy', _kcal), + (l?.logFoodProteinLabel ?? 'Protein', _protein), + (l?.logFoodCarbsLabel ?? 'Carbs', _carbs), + (l?.logFoodFatLabel ?? 'Fat', _fat), + (l?.logFoodFibreLabel ?? 'Fibre', _fibre), + if (_scanned != null) (l?.logFoodPortionLabel ?? 'Portion', _portion), + ]) + if (Typed.of(ctl.text).bad) name, + ]; + } @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Padding( padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(c).bottom), child: SafeArea( @@ -317,11 +329,11 @@ class _LogFoodSheetState extends State { Row( children: [ Expanded( - child: Text('Log an eating occasion', + child: Text(l?.logFoodTitle ?? 'Log an eating occasion', style: F.t2.copyWith(color: p.ink)), ), Pressable( - semanticLabel: 'Close', + semanticLabel: l?.logFoodClose ?? 'Close', onTap: () => Navigator.of(c).pop(), child: Icon(LucideIcons.x, size: 20, color: p.ink3), ), @@ -334,7 +346,7 @@ class _LogFoodSheetState extends State { Expanded( child: Pressable( onTap: () => setState(() => _meal = m), - semanticLabel: m, + semanticLabel: _mealLabel(c, m), child: Container( padding: const EdgeInsets.symmetric(vertical: S.x3), decoration: BoxDecoration( @@ -343,7 +355,7 @@ class _LogFoodSheetState extends State { ), child: Center( child: Text( - _mealLabel(m), + _mealLabel(c, m), style: F.cap.copyWith( color: m == _meal ? p.inkOnFill : p.ink2, ), @@ -358,14 +370,15 @@ class _LogFoodSheetState extends State { ), const SizedBox(height: S.x5), BigButton( - 'I ate ${_mealLabel(_meal).toLowerCase()}', + l?.logFoodIAte(_mealLabel(c, _meal)) ?? + 'I ate ${_mealLabel(c, _meal).toLowerCase()}', icon: LucideIcons.check, color: C.domFood, - onTap: () => _write(_base(label: _mealLabel(_meal))), + onTap: () => _write(_base(label: _mealLabel(c, _meal))), ), if (_recent.isNotEmpty) Section( - 'Again', + l?.logFoodAgain ?? 'Again', Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: Column( @@ -412,7 +425,7 @@ class _LogFoodSheetState extends State { const SizedBox(width: S.x2), Expanded( child: Text( - 'Add the numbers', + l?.logFoodAddNumbers ?? 'Add the numbers', style: F.body.copyWith(color: p.ink2), ), ), @@ -426,65 +439,73 @@ class _LogFoodSheetState extends State { child: SetRow( LucideIcons.scanBarcode, C.domFood, - 'Scan a barcode', + l?.logFoodScanBarcode ?? 'Scan a barcode', sub: offLookupAllowed - ? 'Asks openfoodfacts.org about the barcode, and fills ' - 'in what it can stand behind' - : 'Looks the pack up online. Asks first', + ? (l?.logFoodScanSubOn ?? + 'Asks openfoodfacts.org about the barcode, and fills ' + 'in what it can stand behind') + : (l?.logFoodScanSubOff ?? 'Looks the pack up online. Asks first'), chevron: false, onTap: _looking ? null : _scan, ), ), if (_looking) ...[ const SizedBox(height: S.x3), - const StatusCard( - 'Looking it up', - 'The boxes fill as soon as the answer is here.', + StatusCard( + l?.logFoodLookingUpTitle ?? 'Looking it up', + l?.logFoodLookingUpBody ?? + 'The boxes fill as soon as the answer is here.', icon: LucideIcons.scanBarcode, ), - ] else if (_lookupProblem != null) ...[ + ] else if (_lookupProblem(c) != null) ...[ const SizedBox(height: S.x3), - _lookupProblem!, + _lookupProblem(c)!, ], const SizedBox(height: S.x4), OsTextField( controller: _label, - label: 'What', - hint: 'Chicken and rice', + label: l?.logFoodWhatLabel ?? 'What', + hint: l?.logFoodWhatHint ?? 'Chicken and rice', ), if (_scanned != null) ...[ const SizedBox(height: S.x4), _NumberRow( - fields: [('Portion', 'g', _portion)], + fields: [(l?.logFoodPortionLabel ?? 'Portion', 'g', _portion)], hint: '100', ), const SizedBox(height: S.x2), Text( - _portionNote(_scanned!), + _portionNote(c, _scanned!), style: F.cap.copyWith(color: p.ink3, height: 1.45), ), ], const SizedBox(height: S.x4), _NumberRow( fields: [ - ('Energy', 'kcal', _kcal), - ('Protein', 'g', _protein), + (l?.logFoodEnergyLabel ?? 'Energy', 'kcal', _kcal), + (l?.logFoodProteinLabel ?? 'Protein', 'g', _protein), ], + hint: l?.logFoodUnknownHint ?? 'unknown', ), const SizedBox(height: S.x4), _NumberRow( fields: [ - ('Carbs', 'g', _carbs), - ('Fat', 'g', _fat), + (l?.logFoodCarbsLabel ?? 'Carbs', 'g', _carbs), + (l?.logFoodFatLabel ?? 'Fat', 'g', _fat), ], + hint: l?.logFoodUnknownHint ?? 'unknown', ), const SizedBox(height: S.x4), // The week card has always averaged fibre; nothing could enter // it, so the row read "Not recorded" forever. - _NumberRow(fields: [('Fibre', 'g', _fibre)]), + _NumberRow( + fields: [(l?.logFoodFibreLabel ?? 'Fibre', 'g', _fibre)], + hint: l?.logFoodUnknownHint ?? 'unknown', + ), const SizedBox(height: S.x3), Text( - 'A blank number stays blank. Only "What" is needed.', + l?.logFoodBlankHint ?? + 'A blank number stays blank. Only "What" is needed.', style: F.cap.copyWith(color: p.ink3, height: 1.45), ), // ODbL asks for attribution "reasonably calculated" to make a @@ -496,7 +517,7 @@ class _LogFoodSheetState extends State { ], const SizedBox(height: S.x4), BigButton( - 'Save', + l?.actionSave ?? 'Save', color: C.domFood, soft: true, onTap: () { @@ -505,12 +526,13 @@ class _LogFoodSheetState extends State { // button: nothing happened, nothing was said, and the typed // numbers were thrown away by the only control that reacted. if (label.isEmpty) { - ScaffoldMessenger.of(c).showSnackBar(const SnackBar( - content: Text('Say what it was first.'), + ScaffoldMessenger.of(c).showSnackBar(SnackBar( + content: Text( + l?.logFoodSayWhatFirst ?? 'Say what it was first.'), )); return; } - final bad = _unreadable; + final bad = _unreadable(c); if (bad.isNotEmpty) { sayUnreadable(c, bad); return; @@ -584,6 +606,7 @@ Future _askLookupConsent(BuildContext c) => showModalBottomSheet( ), builder: (s) { final p = P.of(s); + final l = AppLocalizations.of(s); return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(S.x5, S.x5, S.x5, S.x6), @@ -591,36 +614,39 @@ Future _askLookupConsent(BuildContext c) => showModalBottomSheet( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Look barcodes up online?', + Text(l?.logFoodConsentTitle ?? 'Look barcodes up online?', style: F.t2.copyWith(color: p.ink)), const SizedBox(height: S.x4), Text( - 'A scan sends the barcode to openfoodfacts.org, a free, ' - 'open food database. They see the barcode and your IP ' - 'address. Nothing about you, your meals or your health ' - 'leaves this phone, and a barcode you have scanned before ' - 'is answered from your own copy without asking them again.', + l?.logFoodConsentBody1 ?? + 'A scan sends the barcode to openfoodfacts.org, a free, ' + 'open food database. They see the barcode and your IP ' + 'address. Nothing about you, your meals or your health ' + 'leaves this phone, and a barcode you have scanned before ' + 'is answered from your own copy without asking them again.', style: F.body.copyWith(color: p.ink2, height: 1.5), ), const SizedBox(height: S.x3), Text( - 'Their numbers are typed in by the public and a fair few of ' - 'them are wrong, so anything that fails a sanity check is ' - 'left blank rather than filled in. Everything it does fill ' - 'in is yours to edit before you save.', + l?.logFoodConsentBody2 ?? + 'Their numbers are typed in by the public and a fair few of ' + 'them are wrong, so anything that fails a sanity check is ' + 'left blank rather than filled in. Everything it does fill ' + 'in is yours to edit before you save.', style: F.body.copyWith(color: p.ink2, height: 1.5), ), const SizedBox(height: S.x3), Text( - 'You can turn this back off in Settings › Privacy. Typing ' - 'the numbers off the pack works either way.', + l?.logFoodConsentBody3 ?? + 'You can turn this back off in Settings › Privacy. Typing ' + 'the numbers off the pack works either way.', style: F.cap.copyWith(color: p.ink3, height: 1.45), ), const SizedBox(height: S.x5), - BigButton('Allow lookups', + BigButton(l?.logFoodAllowLookups ?? 'Allow lookups', color: C.domFood, onTap: () => Navigator.of(s).pop(true)), const SizedBox(height: S.x3), - BigButton('Not now', + BigButton(l?.logFoodNotNow ?? 'Not now', color: C.domFood, soft: true, onTap: () => Navigator.of(s).pop(false)), @@ -668,7 +694,8 @@ class _Link extends StatelessWidget { @override Widget build(BuildContext c) => Pressable( - semanticLabel: '$label, opens in your browser', + semanticLabel: AppLocalizations.of(c)?.logFoodOpensInBrowser(label) ?? + '$label, opens in your browser', onTap: () => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication), child: Text( @@ -681,12 +708,15 @@ class _Link extends StatelessWidget { ); } -String _mealLabel(String m) => switch (m) { - 'breakfast' => 'Breakfast', - 'lunch' => 'Lunch', - 'dinner' => 'Dinner', - _ => 'Snack', -}; +String _mealLabel(BuildContext c, String m) { + final l = AppLocalizations.of(c); + return switch (m) { + 'breakfast' => l?.logFoodBreakfast ?? 'Breakfast', + 'lunch' => l?.logFoodLunch ?? 'Lunch', + 'dinner' => l?.logFoodDinner ?? 'Dinner', + _ => l?.logFoodSnack ?? 'Snack', + }; +} /// One logged entry. The provenance line is not decoration: a manufacturer /// panel and a typed guess are different claims, and the row says which it is. @@ -705,9 +735,10 @@ class FoodRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Pressable( onTap: onTap, - semanticLabel: '${entry.label}. ${_detail(entry)}', + semanticLabel: '${entry.label}. ${_detail(c, entry)}', child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row( @@ -725,7 +756,7 @@ class FoodRow extends StatelessWidget { children: [ Text(entry.label, style: F.body.copyWith(color: p.ink)), Text( - _detail(entry), + _detail(c, entry), style: F.over.copyWith(color: p.ink3), ), ], @@ -737,7 +768,9 @@ class FoodRow extends StatelessWidget { // everything else here is the user's own. const SizedBox(width: S.x2), Pill( - entry.source == FoodSource.barcode ? 'Open Food Facts' : 'Yours', + entry.source == FoodSource.barcode + ? (l?.logFoodPillOpenFoodFacts ?? 'Open Food Facts') + : (l?.logFoodPillYours ?? 'Yours'), C.n400, ), if (trailing != null) ...[ @@ -750,10 +783,13 @@ class FoodRow extends StatelessWidget { ); } - static String _detail(FoodEntry e) { + static String _detail(BuildContext c, FoodEntry e) { // No photo pipeline exists, so `FoodSource.photo` is never written and the // branch that read it could never run. - if (e.isBareOccasion) return 'LOGGED · ENERGY NOT RECORDED'; + if (e.isBareOccasion) { + return AppLocalizations.of(c)?.logFoodBareOccasion ?? + 'LOGGED · ENERGY NOT RECORDED'; + } final parts = ['${e.kcal!.round()} kcal']; if (e.proteinG != null) parts.add('${e.proteinG!.round()}P'); if (e.carbsG != null) parts.add('${e.carbsG!.round()}C'); diff --git a/lib/ui2/screens/log_workout.dart b/lib/ui2/screens/log_workout.dart index e20b61c3..c7de00ac 100644 --- a/lib/ui2/screens/log_workout.dart +++ b/lib/ui2/screens/log_workout.dart @@ -36,12 +36,13 @@ import '../../compute/manual_session.dart'; import '../../data/db.dart'; import '../../data/journal_fields.dart' show formatMinuteOfDay; import '../../health/health_export.dart'; +import '../../l10n/app_localizations.dart'; import '../../notify/notification_prefs.dart'; import '../../state/app_state.dart'; import '../activity/catalogue.dart'; import '../profile/profile.dart' show SetRow, settingsGroup; import '../ui2.dart'; -import 'home_screen.dart' show repoOf; +import 'home_screen.dart' show monthShortName, repoOf, weekdayShortName; /// One detected bout, as this screen needs it. Built straight off a /// `workout_suggestions` row. @@ -165,6 +166,7 @@ class _WorkoutSuggestionScreenState extends State { Future _confirm(Suggestion s) async { final repo = repoOf(context); if (repo == null || _busy) return; + final l = AppLocalizations.of(context); setState(() => _busy = true); var message = ''; try { @@ -186,7 +188,7 @@ class _WorkoutSuggestionScreenState extends State { await LocalDb.dismissWorkoutSuggestion(s.id); } catch (_) {/* the reason is already on screen */} } catch (_) { - message = 'Could not log this one — try again.'; + message = l?.logWorkoutCouldNotLog ?? 'Could not log this one — try again.'; } if (!mounted) return; setState(() => _busy = false); @@ -196,11 +198,15 @@ class _WorkoutSuggestionScreenState extends State { Future _dismiss(Suggestion s) async { if (_busy) return; + final l = AppLocalizations.of(context); setState(() => _busy = true); try { await LocalDb.dismissWorkoutSuggestion(s.id); } catch (_) { - if (mounted) _say('Could not dismiss this one — try again.'); + if (mounted) { + _say(l?.logWorkoutCouldNotDismiss ?? + 'Could not dismiss this one — try again.'); + } } if (!mounted) return; setState(() => _busy = false); @@ -211,12 +217,13 @@ class _WorkoutSuggestionScreenState extends State { /// session they actually did, then save that instead. Future _adjust(Suggestion s) async { final nav = Navigator.of(context); + final l = AppLocalizations.of(context); final saved = await nav.push(MaterialPageRoute( builder: (_) => LogWorkout( start: DateTime.fromMillisecondsSinceEpoch(s.startTs * 1000), end: DateTime.fromMillisecondsSinceEpoch(s.endTs * 1000), activity: s.activity, - title: 'Adjust the times', + title: l?.logWorkoutAdjustTimes ?? 'Adjust the times', ), )); if (saved == true) await _afterAction(); @@ -239,14 +246,16 @@ class _WorkoutSuggestionScreenState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final items = _items; return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar('Detected activity', sub: 'YOURS TO CONFIRM'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: S.x4), + child: NavBar(l?.logWorkoutDetectedActivityTitle ?? 'Detected activity', + sub: l?.logWorkoutYoursToConfirmSub ?? 'YOURS TO CONFIRM'), ), Expanded( child: ListView( @@ -254,19 +263,24 @@ class _WorkoutSuggestionScreenState extends State { children: [ if (_failed) StatusCard( - 'Could not read your detected activity', - 'The store did not answer. Nothing has been logged or ' - 'dismissed.', - fix: 'Try again', + l?.logWorkoutReadFailedTitle ?? + 'Could not read your detected activity', + l?.logWorkoutReadFailedBody ?? + 'The store did not answer. Nothing has been logged or ' + 'dismissed.', + fix: l?.logWorkoutTryAgain ?? 'Try again', icon: LucideIcons.refreshCw, onFix: _load, ) else if (items == null) - const NoData(message: 'Reading what the band spotted…') + NoData( + message: l?.logWorkoutReadingSpotted ?? + 'Reading what the band spotted…') else if (items.isEmpty) - const StatusCard( - 'Nothing to review', - 'This one may already have been logged or dismissed.', + StatusCard( + l?.logWorkoutNothingToReviewTitle ?? 'Nothing to review', + l?.logWorkoutNothingToReviewBody ?? + 'This one may already have been logged or dismissed.', icon: LucideIcons.circleCheck, ) else @@ -280,11 +294,13 @@ class _WorkoutSuggestionScreenState extends State { const SizedBox(height: S.x3), ], const SizedBox(height: S.x3), - const StatusCard( - 'These are the hard minutes, not the whole session', - 'Detection reports the sustained effort it could see, so a ' - 'warm-up and the rest between sets fall outside it. ' - 'Adjust the times before logging if the window is short.', + StatusCard( + l?.logWorkoutHardMinutesTitle ?? + 'These are the hard minutes, not the whole session', + l?.logWorkoutHardMinutesBody ?? + 'Detection reports the sustained effort it could see, so a ' + 'warm-up and the rest between sets fall outside it. ' + 'Adjust the times before logging if the window is short.', icon: LucideIcons.scissors, ), ], @@ -311,6 +327,7 @@ class _SuggestionCard extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final a = s.activity; final colour = a?.color ?? C.purple; return Surface( @@ -328,10 +345,12 @@ class _SuggestionCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${s.durationMin} min of effort', + Text( + l?.logWorkoutMinutesOfEffort(s.durationMin) ?? + '${s.durationMin} min of effort', style: F.body .copyWith(color: p.ink, fontWeight: FontWeight.w600)), - Text(windowLabel(s.startTs, s.endTs), + Text(windowLabel(s.startTs, s.endTs, l), style: F.over.copyWith(color: p.ink3)), ]), ), @@ -342,16 +361,20 @@ class _SuggestionCard extends StatelessWidget { // window is finally saved, and printing one here would be a number // this screen made up. InlineMetrics([ - if (s.avgBpm != null) ('Avg HR', '${s.avgBpm} bpm', p.on(C.red)), - if (s.peakBpm != null) ('Peak HR', '${s.peakBpm} bpm', p.on(C.orange)), - if (a != null) ('Looks like', a.name, p.on(colour)), + if (s.avgBpm != null) + (l?.logWorkoutAvgHr ?? 'Avg HR', '${s.avgBpm} bpm', p.on(C.red)), + if (s.peakBpm != null) + (l?.logWorkoutPeakHr ?? 'Peak HR', '${s.peakBpm} bpm', + p.on(C.orange)), + if (a != null) (l?.logWorkoutLooksLike ?? 'Looks like', a.name, p.on(colour)), ]), const SizedBox(height: S.x4), - BigButton('Log it', icon: LucideIcons.check, onTap: onConfirm), + BigButton(l?.logWorkoutLogIt ?? 'Log it', + icon: LucideIcons.check, onTap: onConfirm), const SizedBox(height: S.x2), Row(children: [ Expanded( - child: BigButton('Adjust the times', + child: BigButton(l?.logWorkoutAdjustTimes ?? 'Adjust the times', icon: LucideIcons.clock, color: C.blue, soft: true, @@ -359,7 +382,7 @@ class _SuggestionCard extends StatelessWidget { ), const SizedBox(width: S.x2), Expanded( - child: BigButton('Not a workout', + child: BigButton(l?.logWorkoutNotAWorkout ?? 'Not a workout', icon: LucideIcons.x, color: C.red, soft: true, onTap: onDismiss), ), ]), @@ -371,28 +394,23 @@ class _SuggestionCard extends StatelessWidget { /// "Today · 6:30 PM – 7:31 PM". The WINDOW, never just the start — the whole /// reason someone opens this screen is to check whether the detector clipped /// it, and a start time alone cannot show that. -String windowLabel(int startTs, int endTs) { +String windowLabel(int startTs, int endTs, [AppLocalizations? l]) { final s = DateTime.fromMillisecondsSinceEpoch(startTs * 1000); final e = DateTime.fromMillisecondsSinceEpoch(endTs * 1000); - return '${dayLabel(s)} · ${formatMinuteOfDay(s.hour * 60 + s.minute)} – ' + return '${dayLabel(s, l: l)} · ${formatMinuteOfDay(s.hour * 60 + s.minute)} – ' '${formatMinuteOfDay(e.hour * 60 + e.minute)}'; } /// Today / Yesterday / "Mon 11 Aug", against the real calendar day rather than /// a 24-hour subtraction — the day after a spring-forward is 23 hours long. -String dayLabel(DateTime at, {DateTime? now}) { +String dayLabel(DateTime at, {DateTime? now, AppLocalizations? l}) { final n = now ?? DateTime.now(); final today = DateTime(n.year, n.month, n.day); final d = DateTime(at.year, at.month, at.day); final diff = today.difference(d).inDays; - if (diff == 0) return 'Today'; - if (diff == 1) return 'Yesterday'; - const wd = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - const mo = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', - ]; - return '${wd[d.weekday - 1]} ${d.day} ${mo[d.month - 1]}'; + if (diff == 0) return l?.logWorkoutToday ?? 'Today'; + if (diff == 1) return l?.logWorkoutYesterday ?? 'Yesterday'; + return '${weekdayShortName(d.weekday, l)} ${d.day} ${monthShortName(d.month, l)}'; } // ══════════════════ THE FORM ══════════════════ @@ -413,7 +431,7 @@ class LogWorkout extends StatefulWidget { this.start, this.end, this.activity, - this.title = 'Log a past workout', + this.title, this.spans, this.now, }); @@ -421,7 +439,11 @@ class LogWorkout extends StatefulWidget { final String? sessionId; final DateTime? start, end; final Activity? activity; - final String title; + + /// Null means "use the default heading" — kept null rather than defaulted + /// in the constructor so the default can be localized with a BuildContext, + /// which a `const` field initializer does not have. + final String? title; /// The windows already in the log, for the live overlap check. Injected in /// tests; null means read them from the repo. @@ -553,6 +575,7 @@ class _LogWorkoutState extends State { if (repo == null || _saving || _invalid != null) return; final nav = Navigator.of(context); final app = appOf(context); + final l = AppLocalizations.of(context); setState(() { _saving = true; _wrote = null; @@ -576,9 +599,10 @@ class _LogWorkoutState extends State { if (!mounted) return; setState(() { _saving = false; - _wrote = 'Saved. No heart rate was recorded over that window, so it ' - 'has no strain and no calorie figure — the times are all this ' - 'one carries.'; + _wrote = l?.logWorkoutUnscoredSaved ?? + 'Saved. No heart rate was recorded over that window, so it ' + 'has no strain and no calorie figure — the times are all this ' + 'one carries.'; }); return; } @@ -589,7 +613,7 @@ class _LogWorkoutState extends State { if (mounted) { setState(() { _saving = false; - _wrote = 'Could not save that — try again.'; + _wrote = l?.logWorkoutCouldNotSave ?? 'Could not save that — try again.'; }); } } @@ -598,63 +622,80 @@ class _LogWorkoutState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final bad = _invalid; final mins = _end.difference(_start).inMinutes; final retime = widget.sessionId != null; + final title = widget.title ?? (l?.logWorkoutDefaultTitle ?? 'Log a past workout'); return Scaffold( backgroundColor: p.bg, body: SafeArea( child: Column(children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: S.x4), - child: NavBar(widget.title, - sub: retime ? 'THE WINDOW, RE-SCORED' : 'YOUR OWN TIMES'), + child: NavBar(title, + sub: retime + ? (l?.logWorkoutWindowRescoredSub ?? 'THE WINDOW, RE-SCORED') + : (l?.logWorkoutYourOwnTimesSub ?? 'YOUR OWN TIMES')), ), Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(S.x4, 0, S.x4, S.x10), children: [ - settingsGroup(c, 'When', [ + settingsGroup(c, l?.logWorkoutWhenGroup ?? 'When', [ if (!retime) - SetRow(_activity.icon, _activity.color, 'Activity', + SetRow(_activity.icon, _activity.color, + l?.logWorkoutActivityLabel ?? 'Activity', value: _activity.name, onTap: _pickActivity), - SetRow(LucideIcons.calendar, C.blue, 'Date', - value: dayLabel(_start, now: widget.now), + SetRow(LucideIcons.calendar, C.blue, + l?.logWorkoutDateLabel ?? 'Date', + value: dayLabel(_start, now: widget.now, l: l), onTap: _pickDate), - SetRow(LucideIcons.play, C.green, 'Started', + SetRow(LucideIcons.play, C.green, + l?.logWorkoutStartedLabel ?? 'Started', value: formatMinuteOfDay(_start.hour * 60 + _start.minute), onTap: () => _pickTime(isStart: true)), - SetRow(LucideIcons.square, C.orange, 'Ended', + SetRow(LucideIcons.square, C.orange, + l?.logWorkoutEndedLabel ?? 'Ended', value: formatMinuteOfDay(_end.hour * 60 + _end.minute), - sub: _end.day != _start.day ? 'the next morning' : '', + sub: _end.day != _start.day + ? (l?.logWorkoutNextMorningSub ?? 'the next morning') + : '', onTap: () => _pickTime(isStart: false)), - SetRow(LucideIcons.timer, C.purple, 'Length', + SetRow(LucideIcons.timer, C.purple, + l?.logWorkoutLengthLabel ?? 'Length', value: mins > 0 ? '$mins min' : '—', chevron: false), ]), const SizedBox(height: S.x4), if (bad != null) - StatusCard('That window will not save', bad.message, + StatusCard( + l?.logWorkoutWindowInvalidTitle ?? 'That window will not save', + bad.message, icon: LucideIcons.triangleAlert) else if (_wrote != null) - StatusCard(retime ? 'Times updated' : 'Workout logged', + StatusCard( + retime + ? (l?.logWorkoutTimesUpdatedTitle ?? 'Times updated') + : (l?.logWorkoutLoggedTitle ?? 'Workout logged'), _wrote!, icon: LucideIcons.circleCheck) else StatusCard( - 'Scored from what the band recorded', - 'Strain and calories come from the 1-second heart rate ' - 'inside these times, through the same method the day ' - 'uses. Nothing is estimated from the duration.', + l?.logWorkoutScoredTitle ?? 'Scored from what the band recorded', + l?.logWorkoutScoredBody ?? + 'Strain and calories come from the 1-second heart rate ' + 'inside these times, through the same method the day ' + 'uses. Nothing is estimated from the duration.', icon: LucideIcons.heartPulse, ), const SizedBox(height: S.x4), BigButton( _saving - ? 'Saving…' + ? (l?.logWorkoutSaving ?? 'Saving…') : retime - ? 'Save the new times' - : 'Log it', + ? (l?.logWorkoutSaveNewTimes ?? 'Save the new times') + : (l?.logWorkoutLogIt ?? 'Log it'), icon: LucideIcons.check, onTap: bad == null && !_saving ? _save : null, ), @@ -681,6 +722,7 @@ class _TypeSheetState extends State<_TypeSheet> { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final q = _q.trim().toLowerCase(); final items = q.isEmpty ? allActivities @@ -699,7 +741,7 @@ class _TypeSheetState extends State<_TypeSheet> { style: F.body.copyWith(color: p.ink), onChanged: (v) => setState(() => _q = v), decoration: InputDecoration( - hintText: 'Search activities', + hintText: l?.logWorkoutSearchActivities ?? 'Search activities', hintStyle: F.body.copyWith(color: p.ink3), filled: true, fillColor: p.card2, @@ -712,9 +754,11 @@ class _TypeSheetState extends State<_TypeSheet> { ), Flexible( child: items.isEmpty - ? const Padding( - padding: EdgeInsets.all(S.x6), - child: NoData(message: 'No activity by that name'), + ? Padding( + padding: const EdgeInsets.all(S.x6), + child: NoData( + message: l?.logWorkoutNoActivityByName ?? + 'No activity by that name'), ) : ListView.builder( shrinkWrap: true, diff --git a/lib/ui2/screens/metric_detail.dart b/lib/ui2/screens/metric_detail.dart index b1b0d690..39aaeebb 100644 --- a/lib/ui2/screens/metric_detail.dart +++ b/lib/ui2/screens/metric_detail.dart @@ -14,6 +14,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/day_label.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'beats.dart'; import 'day_steps.dart'; @@ -418,7 +419,17 @@ class _MetricDetailState extends State { // is it right now" and "what has it been lately" are different questions, // and a range list that starts at 7 days made the first one unanswerable. static const _windows = [1, 7, 30, 182, 365]; - static const _labels = ['Today', '7 days', '30 days', '6 months', 'Year']; + + List _labelsOf(BuildContext c) { + final l = AppLocalizations.of(c); + return [ + l?.metricDetailToday ?? 'Today', + l?.metricDetailRange7Days ?? '7 days', + l?.metricDetailRange30Days ?? '30 days', + l?.metricDetailRange6Months ?? '6 months', + l?.metricDetailRangeYear ?? 'Year', + ]; + } /// TODAY. A tile on Home shows today's number, so the screen behind that tap /// opens on today's number — anything else is a different question than the @@ -455,19 +466,22 @@ class _MetricDetailState extends State { } /// The reason the next range up is not there yet, in its own words. - String? _lockedNote(MetricData d) { + String? _lockedNote(BuildContext c, MetricData d) { final n = _offered(d); if (n >= _windows.length) return null; - return '${_labels[n]} needs ${_windows[n]} days of history. ' - 'You have ${d.daysAvailable}.'; + final l = AppLocalizations.of(c); + final label = _labelsOf(c)[n]; + return l?.metricDetailLockedNote(label, _windows[n], d.daysAvailable) ?? + '$label needs ${_windows[n]} days of history. ' + 'You have ${d.daysAvailable}.'; } Widget _ranges(BuildContext c, MetricData d, Color color) { final p = P.of(c); final n = _offered(d); - final note = _lockedNote(d); + final note = _lockedNote(c, d); return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SubTabs(_labels.sublist(0, n), _range.clamp(0, n - 1), + SubTabs(_labelsOf(c).sublist(0, n), _range.clamp(0, n - 1), (i) => setState(() => (_range = i, _pick = null)), color: color), if (note != null) ...[ @@ -504,6 +518,7 @@ class _MetricDetailState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final spec = specOf(widget.metricKey); final d = _d ?? const MetricData(); @@ -531,7 +546,7 @@ class _MetricDetailState extends State { if (spec.suppress != null) ...[ const SizedBox(height: S.x2), StatusCard( - 'Not shown as a trend', + l?.metricDetailNotShownTitle ?? 'Not shown as a trend', spec.suppress!, fix: spec.suppressFix ?? '', icon: spec.icon, @@ -546,19 +561,27 @@ class _MetricDetailState extends State { else StatusCard( win == 1 - ? 'Nothing recorded today' - : 'No history for ${spec.title.toLowerCase()} yet', + ? (l?.metricDetailNothingRecordedToday ?? + 'Nothing recorded today') + : (l?.metricDetailNoHistoryYet(spec.title.toLowerCase()) ?? + 'No history for ${spec.title.toLowerCase()} yet'), win == 1 ? (all.isEmpty - ? 'Today has not produced a value yet.' - : 'Today has not produced a value yet. The wider ranges ' - 'above hold the days that did.') - : 'No day in this window produced a value.', + ? (l?.metricDetailNoValueYet ?? + 'Today has not produced a value yet.') + : (l?.metricDetailNoValueYetWiderRanges ?? + 'Today has not produced a value yet. The wider ranges ' + 'above hold the days that did.')) + : (l?.metricDetailNoValueInWindow ?? + 'No day in this window produced a value.'), // Today opens first now, so this card is what someone with months // of history sees on a morning before the derive lands. Telling // them to wear the band is a promise that cannot change anything — // they already did, and the days are one tab away. - fix: all.isEmpty ? 'Wear the band overnight to start the series' : '', + fix: all.isEmpty + ? (l?.metricDetailWearBandFix ?? + 'Wear the band overnight to start the series') + : '', icon: spec.icon, ), const SizedBox(height: S.x5), @@ -572,11 +595,12 @@ class _MetricDetailState extends State { // property of your history, not of the window — so on Today it reads // the whole series. Section( - 'Your normal range', + l?.metricDetailNormalRangeSection ?? 'Your normal range', _range3(c, spec, win == 1 ? valuesOf(all) : vals, d.percentile, all.isEmpty ? null : all.last.t)), if (d.movers.isNotEmpty) - Section('What moves it', _movers(c, d.movers)), + Section(l?.metricDetailWhatMovesItSection ?? 'What moves it', + _movers(c, d.movers)), const SizedBox(height: S.x5), // Steps are the one metric assembled from SPANS of the day, each // counted by a different sensor. That breakdown is a day's worth of @@ -592,8 +616,12 @@ class _MetricDetailState extends State { // Wording, not a gate: this door opens the newest night and Beats // carries its own day stepper, so it is honest under any range — but // "behind this number" was not, with a 30-day average as the number. - detailLinkRow(c, LucideIcons.heartPulse, 'Beats', - 'The intervals a night is made of, drawn', + detailLinkRow( + c, + LucideIcons.heartPulse, + l?.metricDetailBeatsLinkTitle ?? 'Beats', + l?.metricDetailBeatsLinkSub ?? + 'The intervals a night is made of, drawn', () => go(c, const Beats())), const SizedBox(height: S.x3), ], @@ -608,8 +636,12 @@ class _MetricDetailState extends State { // content, and it read as a phrase rather than a place. A doorway // wants the plainest noun that is still true. if (widget.metricKey == 'steps' && win == 1) ...[ - detailLinkRow(c, LucideIcons.footprints, 'Breakdown', - 'Each stretch of today, and what counted it', + detailLinkRow( + c, + LucideIcons.footprints, + l?.metricDetailBreakdownLinkTitle ?? 'Breakdown', + l?.metricDetailBreakdownLinkSub ?? + 'Each stretch of today, and what counted it', () => go(c, const DayStepsDetail())), const SizedBox(height: S.x3), ], @@ -635,6 +667,7 @@ class _MetricDetailState extends State { List series, List vals, int win, List wear, List algoBreaks) { final p = P.of(c); + final l = AppLocalizations.of(c); final mean = vals.reduce((a, b) => a + b) / vals.length; final latest = vals.last; // WHICH DAY the newest reading is from. `metric_series` gets a row only on @@ -661,8 +694,9 @@ class _MetricDetailState extends State { alignment: Alignment.centerLeft, child: Text( win == 1 - ? 'Today' - : 'Daily average · ${vals.length} of $win days', + ? (l?.metricDetailToday ?? 'Today') + : (l?.metricDetailDailyAverage(vals.length, win) ?? + 'Daily average · ${vals.length} of $win days'), style: F.cap.copyWith(color: p.ink3), ), ), @@ -674,7 +708,9 @@ class _MetricDetailState extends State { Align( alignment: Alignment.centerLeft, child: Text( - 'Latest ${_fmt(spec, latest)} ${unitBeside(spec.unit)} · $asOf' + (l?.metricDetailLatestReading( + _fmt(spec, latest), unitBeside(spec.unit), asOf) ?? + 'Latest ${_fmt(spec, latest)} ${unitBeside(spec.unit)} · $asOf') .replaceAll(' ', ' '), style: F.cap.copyWith(color: p.ink3)), ), @@ -722,13 +758,14 @@ class _MetricDetailState extends State { // provenance, not an event that happened to the user. footnote: marks.isEmpty ? null - : marks.length == 1 - ? 'The dotted line is a change in how these days were ' - 'computed. Readings either side of it came from ' - 'different versions.' - : 'The dotted lines are changes in how these days were ' - 'computed. Readings either side of one came from ' - 'different versions.', + : (l?.metricDetailAlgoBreakFootnote(marks.length) ?? + (marks.length == 1 + ? 'The dotted line is a change in how these days were ' + 'computed. Readings either side of it came from ' + 'different versions.' + : 'The dotted lines are changes in how these days were ' + 'computed. Readings either side of one came from ' + 'different versions.')), // The window IS the span now: `series` has one slot per calendar // day whether or not that day derived, so both edges are dates // rather than array positions. It used to read the length of a @@ -737,8 +774,9 @@ class _MetricDetailState extends State { // Slot 0 is `length - 1` days behind today, not `length` — the // last slot IS today. A 30-slot window spans 29 days of distance. xLabels: [ - '${series.length - 1} day${series.length == 2 ? '' : 's'} ago', - 'Today', + l?.metricDetailDaysAgoLabel(series.length - 1) ?? + '${series.length - 1} day${series.length == 2 ? '' : 's'} ago', + l?.metricDetailToday ?? 'Today', ], // The dots are already beside the big number two rows up; twice on // one card reads as two different claims. @@ -758,7 +796,8 @@ class _MetricDetailState extends State { value: _pick == null ? null : _slotAt01(_pick!, series.length), step: 1 / (series.length - 1), label: spec.title, - describe: (v) => _slotSays(spec, series, _slotAt(v, series.length)), + describe: (v) => + _slotSays(c, spec, series, _slotAt(v, series.length)), onChanged: (v) => setState(() => _pick = _slotAt(v, series.length)), child: CustomPaint( @@ -801,14 +840,15 @@ class _MetricDetailState extends State { return Padding( padding: const EdgeInsets.only(top: S.x4), child: ChartFrame( - title: 'Worn', - unit: 'h a day', + title: l?.metricDetailWornChartTitle ?? 'Worn', + unit: l?.metricDetailHoursADayUnit ?? 'h a day', height: 56, yAxis: axis, series: hrs, - footnote: '${have.length} of these $win days have a wear ' - 'record. The rest are gaps in both charts — the line above ' - 'is not carried across one.', + footnote: l?.metricDetailWearFootnote(have.length, win) ?? + '${have.length} of these $win days have a wear ' + 'record. The rest are gaps in both charts — the line above ' + 'is not carried across one.', child: CustomPaint( size: Size.infinite, painter: Bars(hrs, p.ink3, axis: axis), @@ -840,12 +880,16 @@ class _MetricDetailState extends State { } /// What the slider reads out. The value, or the fact that the day is a hole. - String _slotSays(MetricSpec spec, List series, int i) { - final day = prettyDay(_dayOfSlot(i, series.length)); + String _slotSays(BuildContext c, MetricSpec spec, List series, int i) { + final l = AppLocalizations.of(c); + final day = prettyDay(_dayOfSlot(i, series.length), l); final v = series[i]; return v == null - ? '$day, no record' - : '$day, ${_fmt(spec, v)} ${unitBeside(spec.unit)}'.trimRight(); + ? (l?.metricDetailSlotNoRecord(day) ?? '$day, no record') + : (l?.metricDetailSlotWithValue( + day, _fmt(spec, v), unitBeside(spec.unit)) ?? + '$day, ${_fmt(spec, v)} ${unitBeside(spec.unit)}') + .trimRight(); } /// The touched day, and the door into it. @@ -855,6 +899,7 @@ class _MetricDetailState extends State { /// button is a promise, and there is no screen behind an empty day. Widget _picked(BuildContext c, MetricSpec spec, List series) { final p = P.of(c); + final l = AppLocalizations.of(c); final i = _pick!.clamp(0, series.length - 1); final day = _dayOfSlot(i, series.length); final v = series[i]; @@ -865,8 +910,10 @@ class _MetricDetailState extends State { elevation: 0, onTap: v == null ? null : () => go(c, _dayScreen(widget.metricKey, day)), semanticLabel: v == null - ? '${prettyDay(day)}, no record' - : 'Open ${prettyDay(day)}', + ? (l?.metricDetailSlotNoRecord(prettyDay(day, l)) ?? + '${prettyDay(day, l)}, no record') + : (l?.metricDetailOpenDay(prettyDay(day, l)) ?? + 'Open ${prettyDay(day, l)}'), child: Row(children: [ Expanded( child: Text(dayNavLabel(day), @@ -878,7 +925,7 @@ class _MetricDetailState extends State { const SizedBox(width: S.x3), Text( v == null - ? 'No record' + ? (l?.metricDetailNoRecordLabel ?? 'No record') : '${_fmt(spec, v)} ${unitBeside(spec.unit)}'.trimRight(), style: v == null ? F.cap.copyWith(color: p.ink3) @@ -914,35 +961,79 @@ class _MetricDetailState extends State { Widget _range3(BuildContext c, MetricSpec spec, List win, Map? pct, int? latestTs) { final p = P.of(c); + final l = AppLocalizations.of(c); final sorted = [...win]..sort(); final lo = sorted.first, hi = sorted.last; final mid = sorted[sorted.length ~/ 2]; final band = pct?['label']?.toString(); final rank = (pct?['percentile_of_you'] as num?); + final isToday = (daysBehind(latestTs) ?? 0) <= 0; + final ordinal = rank == null ? '' : _ordinal(rank.round(), l); return Surface( child: Column(children: [ Row(children: [ - Expanded(child: _stat(p, _fmt(spec, lo), 'Lowest')), - Expanded(child: _stat(p, _fmt(spec, mid), 'Typical')), - Expanded(child: _stat(p, _fmt(spec, hi), 'Highest')), + Expanded( + child: _stat(p, _fmt(spec, lo), l?.metricDetailLowest ?? 'Lowest')), + Expanded( + child: + _stat(p, _fmt(spec, mid), l?.metricDetailTypical ?? 'Typical')), + Expanded( + child: _stat( + p, _fmt(spec, hi), l?.metricDetailHighest ?? 'Highest')), ]), const SizedBox(height: S.x4), Text( rank == null - ? 'From ${win.length} of your own days.' - : '${(daysBehind(latestTs) ?? 0) <= 0 ? 'Today' : 'Your reading from ${axisDay(latestTs)}'}' - ' sits at the ${_ordinal(rank.round())} percentile of your ' - 'own history${band == null ? '' : ' — $band'}.', + ? (l?.metricDetailFromDaysCount(win.length) ?? + 'From ${win.length} of your own days.') + : (isToday + ? (band == null + ? (l?.metricDetailPercentileTodayNoBand(ordinal) ?? + 'Today sits at the $ordinal percentile of your own ' + 'history.') + : (l?.metricDetailPercentileTodayBand(ordinal, band) ?? + 'Today sits at the $ordinal percentile of your own ' + 'history — $band.')) + : (band == null + ? (l?.metricDetailPercentileFromNoBand( + axisDay(latestTs), ordinal) ?? + 'Your reading from ${axisDay(latestTs)} sits at the ' + '$ordinal percentile of your own history.') + : (l?.metricDetailPercentileFromBand( + axisDay(latestTs), ordinal, band) ?? + 'Your reading from ${axisDay(latestTs)} sits at the ' + '$ordinal percentile of your own history — ' + '$band.'))), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ]), ); } - String _ordinal(int n) { - if (n % 100 >= 11 && n % 100 <= 13) return '${n}th'; - return '$n${const ['th', 'st', 'nd', 'rd'][n % 10 < 4 ? n % 10 : 0]}'; + /// [n]th, localized. `{ordinal}` gets substituted whole into an ARB + /// sentence, so this is the one place the suffix has to match the reader's + /// language — an English "12th" inside a French sentence reads as broken, + /// not translated. + String _ordinal(int n, AppLocalizations? l) { + switch (l?.localeName.split('_').first) { + case 'fr': + return n == 1 ? '1er' : '${n}e'; + case 'de': + return '$n.'; + case 'es': + return '$nº'; + case 'hi': + case 'zh': + // Neither language marks the ordinal with a suffix here — the + // surrounding ARB sentence already carries the "the Nth" framing + // (Hindi's postposition, Chinese's 第 prefix), so a bare number is + // the correct rendering, not a fallback. + return '$n'; + default: + if (n % 100 >= 11 && n % 100 <= 13) return '${n}th'; + return '$n${const ['th', 'st', 'nd', 'rd'][n % 10 < 4 ? n % 10 : 0]}'; + } } Widget _stat(P p, String v, String l) => Column(children: [ @@ -956,6 +1047,7 @@ class _MetricDetailState extends State { /// "because". Widget _movers(BuildContext c, List> movers) { final p = P.of(c); + final l = AppLocalizations.of(c); final rows = movers.take(5).toList(); return Column(children: [ Surface( @@ -973,8 +1065,11 @@ class _MetricDetailState extends State { Text(rows[i]['tag']?.toString() ?? '', style: F.body.copyWith(color: p.ink)), Text( - '${rows[i]['n_with'] ?? 0} days with · ' - '${rows[i]['n_without'] ?? 0} without', + l?.metricDetailDaysWithWithout( + (rows[i]['n_with'] as num? ?? 0).toInt(), + (rows[i]['n_without'] as num? ?? 0).toInt()) ?? + '${rows[i]['n_with'] ?? 0} days with · ' + '${rows[i]['n_without'] ?? 0} without', style: F.over.copyWith(color: p.ink3)), ]), ), @@ -991,7 +1086,8 @@ class _MetricDetailState extends State { ), const SizedBox(height: S.x3), Text( - 'Patterns in your own logs, not causes.', + l?.metricDetailPatternsNotCauses ?? + 'Patterns in your own logs, not causes.', style: F.over.copyWith(color: p.ink3, height: 1.5)), ]); } @@ -1083,7 +1179,7 @@ Future chooseDay( firstDate: first, lastDate: last, selectableDayPredicate: (d) => have.contains(dayLabelOf(d)), - helpText: 'Choose a day', + helpText: AppLocalizations.of(c)?.metricDetailChooseDayHelp ?? 'Choose a day', ); return picked == null ? null : dayLabelOf(picked); } @@ -1110,6 +1206,7 @@ class DayNav extends StatelessWidget { Widget build(BuildContext c) { if (days.length < 2) return const SizedBox.shrink(); final p = P.of(c); + final l = AppLocalizations.of(c); final i = days.indexOf(day ?? ''); // days is newest first: the OLDER day is further down the list. final older = i < 0 ? days.first : (i + 1 < days.length ? days[i + 1] : null); @@ -1127,14 +1224,16 @@ class DayNav extends StatelessWidget { return Container( decoration: BoxDecoration(color: p.card2, borderRadius: R.rMd), child: Row(children: [ - arrow(LucideIcons.chevronLeft, 'Previous day', older), + arrow(LucideIcons.chevronLeft, l?.metricDetailPreviousDay ?? 'Previous day', + older), Expanded( child: Pressable( onTap: () async { final picked = await chooseDay(c, days, day); if (picked != null && picked != day) onDay(picked); }, - semanticLabel: 'Choose a day. Showing ${dayNavLabel(day)}', + semanticLabel: l?.metricDetailChooseDayShowing(dayNavLabel(day)) ?? + 'Choose a day. Showing ${dayNavLabel(day)}', child: Text( dayNavLabel(day), textAlign: TextAlign.center, @@ -1144,7 +1243,7 @@ class DayNav extends StatelessWidget { ), ), ), - arrow(LucideIcons.chevronRight, 'Next day', newer), + arrow(LucideIcons.chevronRight, l?.metricDetailNextDay ?? 'Next day', newer), ]), ); } @@ -1199,11 +1298,12 @@ Widget detailLinkRow(BuildContext c, IconData icon, String title, String sub, Widget investigateRow(BuildContext c, VoidCallback onTap) => detailLinkRow( c, LucideIcons.cpu, - 'Nerd stats', + AppLocalizations.of(c)?.metricDetailNerdStatsTitle ?? 'Nerd stats', // One line at 1x. A subtitle that wraps makes this row taller than every // other `detailLinkRow` in the app, which is a layout change dressed up as // a copy change — keep it at or under the old string's length. - 'The figures behind the picture', + AppLocalizations.of(c)?.metricDetailNerdStatsSub ?? + 'The figures behind the picture', onTap); /// A two-column legend. Used by the hypnogram and the overnight stack. diff --git a/lib/ui2/screens/month_grid.dart b/lib/ui2/screens/month_grid.dart index 0179e315..da70c9be 100644 --- a/lib/ui2/screens/month_grid.dart +++ b/lib/ui2/screens/month_grid.dart @@ -25,6 +25,7 @@ import 'package:flutter/material.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'home_screen.dart' show ChartPoint, denseDays, pointsOf; import 'metric_detail.dart' show MetricSpec, specOf; @@ -132,6 +133,7 @@ class MonthGrid extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final shaded = [for (final r in rows) if (r.shaded) r]; final waiting = [for (final r in rows) if (!r.shaded) r]; return Column( @@ -158,14 +160,18 @@ class MonthGrid extends StatelessWidget { const SizedBox(width: S.x2), // Coverage, never a run. "23 of 30" costs a missed day // one day; a streak costs it everything. - Text('${r.have} of $kGridDays days', + Text( + l?.monthGridCoverage(r.have, kGridDays) ?? + '${r.have} of $kGridDays days', style: F.over.copyWith(color: p.ink3)), ], ), ), Semantics( - label: '${r.spec.title}: ${r.have} of $kGridDays days have ' - 'a value. Shaded against your own range.', + label: l?.monthGridSemanticsLabel( + r.spec.title, r.have, kGridDays) ?? + '${r.spec.title}: ${r.have} of $kGridDays days have ' + 'a value. Shaded against your own range.', child: SizedBox( height: 22, child: CustomPaint( @@ -182,18 +188,23 @@ class MonthGrid extends StatelessWidget { ], Row( children: [ - Text('30 days ago', style: F.over.copyWith(color: p.ink3)), + Text( + l?.monthGridDaysAgo(kGridDays - 1) ?? + '${kGridDays - 1} days ago', + style: F.over.copyWith(color: p.ink3)), const Spacer(), - Text('Today', style: F.over.copyWith(color: p.ink3)), + Text(l?.monthGridToday ?? 'Today', + style: F.over.copyWith(color: p.ink3)), ], ), const SizedBox(height: S.x3), Text( - 'One cell per day. Darker is further up YOUR own range — the ' - '10th to 90th percentile of every day you have stored — and ' - 'an outlined cell is a day with no value, not a low one. ' - 'More strain is not better strain and longer sleep is not ' - 'healthier sleep; this says where a day sat, not how it went.', + l?.monthGridFootnote ?? + 'One cell per day. Darker is further up YOUR own range — the ' + '10th to 90th percentile of every day you have stored — and ' + 'an outlined cell is a day with no value, not a low one. ' + 'More strain is not better strain and longer sleep is not ' + 'healthier sleep; this says where a day sat, not how it went.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], @@ -203,10 +214,12 @@ class MonthGrid extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: S.x3), child: StatusCard( - '${r.spec.title} is not shaded yet', - 'A shade is where a day sits in your own range, and ' - '${r.historyDays} day${r.historyDays == 1 ? '' : 's'} is not ' - 'a range. It appears at $kGridMinHistory.', + l?.monthGridNotShadedYetTitle(r.spec.title) ?? + '${r.spec.title} is not shaded yet', + l?.monthGridNotShadedYetBody(r.historyDays, kGridMinHistory) ?? + 'A shade is where a day sits in your own range, and ' + '${r.historyDays} day${r.historyDays == 1 ? '' : 's'} is not ' + 'a range. It appears at $kGridMinHistory.', ), ), ], diff --git a/lib/ui2/screens/naps.dart b/lib/ui2/screens/naps.dart index 5bc6b0d6..ba26002c 100644 --- a/lib/ui2/screens/naps.dart +++ b/lib/ui2/screens/naps.dart @@ -32,6 +32,7 @@ import '../../compute/nap_edits.dart'; import '../../data/day_label.dart'; import '../../data/db.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart' show whyFromNote; import '../../state/app_state.dart'; import '../ui2.dart'; @@ -158,9 +159,11 @@ class _NapsScreenState extends State { if (!mounted) return; final after = _d?.naps.map((n) => n['start']).toList(); if (failed != null || '$before' == '$after') { + final l = AppLocalizations.of(context); setState(() => _failed = failed ?? - 'The day was not re-analysed — another analysis was already ' - 'running. Your edit is saved and will apply next time.'); + l?.napsNotReanalysed ?? + 'The day was not re-analysed — another analysis was already ' + 'running. Your edit is saved and will apply next time.'); } } @@ -186,16 +189,17 @@ class _NapsScreenState extends State { Future _log(String day) async { final base = DateTime.tryParse(day); if (base == null) return; + final l = AppLocalizations.of(context); final from = await showTimePicker( context: context, initialTime: const TimeOfDay(hour: 14, minute: 0), - helpText: 'WHEN YOU FELL ASLEEP', + helpText: l?.napsFellAsleepHelp ?? 'WHEN YOU FELL ASLEEP', ); if (from == null || !mounted) return; final to = await showTimePicker( context: context, initialTime: TimeOfDay(hour: (from.hour + 1) % 24, minute: from.minute), - helpText: 'WHEN YOU WOKE UP', + helpText: l?.napsWokeUpHelp ?? 'WHEN YOU WOKE UP', ); if (to == null || !mounted) return; @@ -212,14 +216,16 @@ class _NapsScreenState extends State { // Both refusals are the shared rules, not a second copy written here. if (!manualNapWindowIsValid(s, e)) { - setState(() => _failed = 'A nap is between 5 minutes and 6 hours. ' - 'Anything longer is a sleep, and it belongs in the night where the ' - 'stages can be read.'); + setState(() => _failed = l?.napsInvalidWindow ?? + 'A nap is between 5 minutes and 6 hours. ' + 'Anything longer is a sleep, and it belongs in the night where the ' + 'stages can be read.'); return; } if (napOverlapsExisting(s, e, _d?.naps ?? const [])) { - setState(() => _failed = 'That overlaps a nap already on this day. ' - 'Remove that one first, rather than counting the same hour twice.'); + setState(() => _failed = l?.napsOverlap ?? + 'That overlaps a nap already on this day. ' + 'Remove that one first, rather than counting the same hour twice.'); return; } await _edit(() => LocalDb.putNapEdit( @@ -229,9 +235,10 @@ class _NapsScreenState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d; if (d == null) { - return detailScaffold(c, 'Naps', const [ + return detailScaffold(c, l?.napsTitle ?? 'Naps', const [ Padding( padding: EdgeInsets.only(top: S.x8), child: Center(child: CircularProgressIndicator()), @@ -242,7 +249,7 @@ class _NapsScreenState extends State { return detailScaffold( c, - 'Naps', + l?.napsTitle ?? 'Naps', [ ...dayNavRow(day, d.days, _goDay), if (!d.judged) @@ -250,17 +257,19 @@ class _NapsScreenState extends State { // to assess naps', 'nap detection failed for this day'. The sentence // below is only for a day that recorded no reason at all. StatusCard( - 'No nap reading for this day', + l?.napsNoReadingTitle ?? 'No nap reading for this day', whyFromNote(d.note, unit: 'days') ?? - 'Naps are worked out from the same 1 Hz recording the rest of ' - 'the day is, and this day does not have enough of it.', + l?.napsNoReadingBody ?? + 'Naps are worked out from the same 1 Hz recording the rest of ' + 'the day is, and this day does not have enough of it.', icon: LucideIcons.circleHelp, ) else if (d.naps.isEmpty) - const StatusCard( - 'No naps on this day', - 'Nothing on this day was still enough, for long enough, with the ' - 'heart-rate dip that goes with sleeping through it.', + StatusCard( + l?.napsEmptyTitle ?? 'No naps on this day', + l?.napsEmptyBody ?? + 'Nothing on this day was still enough, for long enough, with the ' + 'heart-rate dip that goes with sleeping through it.', icon: LucideIcons.sun, ) else ...[ @@ -285,26 +294,30 @@ class _NapsScreenState extends State { // The number that MOVES, said plainly, because that is why the // edit is a recompute: these minutes come off tonight's sleep // need and your sleep debt one for one. - '${hm(d.napMin)} of nap counts toward tonight’s sleep need.', + l?.napsCountsToward(hm(d.napMin)) ?? + '${hm(d.napMin)} of nap counts toward tonight’s sleep need.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], ], if (_failed != null) ...[ const SizedBox(height: S.x3), - StatusCard('That has not been applied', _failed!, + StatusCard(l?.napsNotAppliedTitle ?? 'That has not been applied', + _failed!, icon: LucideIcons.triangleAlert), ], const SizedBox(height: S.x4), BigButton( - _busy ? 'Working…' : 'Log a nap', + _busy + ? (l?.napsWorking ?? 'Working…') + : (l?.napsLogANap ?? 'Log a nap'), icon: LucideIcons.plus, color: C.domHealth, onTap: _busy || day == null ? null : () => _log(day), ), if (d.rejected.isNotEmpty) ...[ Section( - 'Removed', + l?.napsRemovedSection ?? 'Removed', Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -323,8 +336,9 @@ class _NapsScreenState extends State { ? null : () => _restore( day!, (r['start_ts'] as num).toInt()), - semanticLabel: 'Put this nap back', - child: Text('Put it back', + semanticLabel: + l?.napsPutBackSemantic ?? 'Put this nap back', + child: Text(l?.napsPutBackLabel ?? 'Put it back', style: F.cap.copyWith( color: p.on(C.blue), fontWeight: FontWeight.w600)), @@ -337,8 +351,9 @@ class _NapsScreenState extends State { ), const SizedBox(height: S.x2), Text( - 'A removal is kept as a window rather than an id, so it still ' - 'applies after the detector’s edges move.', + l?.napsRemovalKept ?? + 'A removal is kept as a window rather than an id, so it still ' + 'applies after the detector’s edges move.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], @@ -347,6 +362,7 @@ class _NapsScreenState extends State { } Widget _nap(BuildContext c, P p, String day, Map nap) { + final l = AppLocalizations.of(c); final mine = nap['source'] == 'manual'; final mins = nap['duration_min'] as int?; return Row( @@ -372,10 +388,14 @@ class _NapsScreenState extends State { // `hm(null)` is the empty string, and a row whose only caption // is ' · detected' is the silent nothing this app does not do. mins == null - ? (mine ? 'You logged this' : 'Detected') + ? (mine + ? (l?.napsYouLoggedThis ?? 'You logged this') + : (l?.napsDetected ?? 'Detected')) : (mine - ? '${hm(mins)} · you logged this' - : '${hm(mins)} asleep · detected'), + ? (l?.napsLoggedWithMins(hm(mins)) ?? + '${hm(mins)} · you logged this') + : (l?.napsDetectedWithMins(hm(mins)) ?? + '${hm(mins)} asleep · detected')), style: F.cap.copyWith(color: p.ink3), ), ]), @@ -383,8 +403,13 @@ class _NapsScreenState extends State { const SizedBox(width: S.x2), Pressable( onTap: _busy ? null : () => _remove(day, nap), - semanticLabel: mine ? 'Delete this nap' : 'This was not a nap', - child: Text(mine ? 'Delete' : 'Not a nap', + semanticLabel: mine + ? (l?.napsDeleteSemantic ?? 'Delete this nap') + : (l?.napsNotANapSemantic ?? 'This was not a nap'), + child: Text( + mine + ? (l?.napsDeleteLabel ?? 'Delete') + : (l?.napsNotANapLabel ?? 'Not a nap'), style: F.cap .copyWith(color: p.on(C.blue), fontWeight: FontWeight.w600)), ), diff --git a/lib/ui2/screens/nutrition_screen.dart b/lib/ui2/screens/nutrition_screen.dart index 990f0a78..64c00fba 100644 --- a/lib/ui2/screens/nutrition_screen.dart +++ b/lib/ui2/screens/nutrition_screen.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; +import '../../l10n/app_localizations.dart'; import '../../data/db.dart'; import '../../data/day_label.dart'; import '../../data/nutrition_store.dart'; @@ -38,7 +39,14 @@ class NutritionScreen extends StatefulWidget { class _NutritionScreenState extends State with RevisionReload { int _tab = 0; - static const _tabs = ['Today', 'Week', 'Goals']; + static List _tabs(BuildContext c) { + final l = AppLocalizations.of(c); + return [ + l?.nutritionTabToday ?? 'Today', + l?.nutritionTabWeek ?? 'Week', + l?.nutritionTabGoals ?? 'Goals', + ]; + } /// Read on every use, never captured once: the shell keeps this tab alive in /// its IndexedStack, so a field initialiser would still be yesterday after @@ -162,11 +170,13 @@ class _NutritionScreenState extends State with RevisionReload { /// Removing a log is destructive and there is no undo, so the entry is named /// back before it goes. Future _confirmDelete(FoodEntry e) async { + final l = AppLocalizations.of(context); final ok = await confirmRemove( context, - title: 'Remove ${e.label}?', - body: 'It leaves the day and every average that counted it. There is no ' - 'undo.', + title: l?.nutritionRemoveTitle(e.label) ?? 'Remove ${e.label}?', + body: l?.nutritionRemoveBody ?? + 'It leaves the day and every average that counted it. There is no ' + 'undo.', ); if (!ok) return; await NutritionDb.delete(await LocalDb.instance, e.id); @@ -175,13 +185,14 @@ class _NutritionScreenState extends State with RevisionReload { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); return ListView( padding: const EdgeInsets.fromLTRB(S.x4, S.x4, S.x4, S.x16), children: [ ScreenTitle( - 'Nutrition', + l?.nutritionTitle ?? 'Nutrition', trailing: Pressable( - semanticLabel: 'Log food', + semanticLabel: l?.nutritionLogFood ?? 'Log food', onTap: _logFood, child: Icon( LucideIcons.circlePlus, @@ -190,7 +201,7 @@ class _NutritionScreenState extends State with RevisionReload { ), ), ), - SubTabs(_tabs, _tab, (i) => setState(() => _tab = i), color: C.domFood), + SubTabs(_tabs(c), _tab, (i) => setState(() => _tab = i), color: C.domFood), const SizedBox(height: S.x5), if (_loading) const Center(child: CircularProgressIndicator()) @@ -203,6 +214,7 @@ class _NutritionScreenState extends State with RevisionReload { // ── TODAY ──────────────────────────────────────────────────────────────── Widget _todayTab(BuildContext c) { + final l = AppLocalizations.of(c); final day = _today; final w = _week; return Column( @@ -210,16 +222,16 @@ class _NutritionScreenState extends State with RevisionReload { children: [ if (day == null || !day.logged) StatusCard( - 'Nothing logged today', - 'One tap is a complete log.', - fix: 'Log an eating occasion', + l?.nutritionEmptyTodayTitle ?? 'Nothing logged today', + l?.nutritionEmptyTodayBody ?? 'One tap is a complete log.', + fix: l?.nutritionLogOccasionFix ?? 'Log an eating occasion', icon: LucideIcons.utensils, onFix: _logFood, ) else DayEnergyCard(day: day, burned: _burned), Section( - 'Occasions', + l?.nutritionOccasionsSection ?? 'Occasions', Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: Column( @@ -247,7 +259,7 @@ class _NutritionScreenState extends State with RevisionReload { ], ), ), - action: 'Add', + action: l?.nutritionAddAction ?? 'Add', onAction: _logFood, ), const SizedBox(height: S.x4), @@ -268,11 +280,12 @@ class _NutritionScreenState extends State with RevisionReload { if (day != null && day.logged && day.kcal.isFloor) ...[ const SizedBox(height: S.x4), StatusCard( - 'Today\'s energy is a floor, not a total', - '${day.kcal.unknown} of ${day.entries.length} occasions were ' - 'logged without an energy figure, so the number above is the ' - 'least you ate rather than what you ate.', - fix: 'Add the numbers to an occasion', + l?.nutritionFloorTitle ?? 'Today\'s energy is a floor, not a total', + l?.nutritionFloorBody(day.kcal.unknown, day.entries.length) ?? + '${day.kcal.unknown} of ${day.entries.length} occasions were ' + 'logged without an energy figure, so the number above is ' + 'the least you ate rather than what you ate.', + fix: l?.nutritionAddNumbersFix ?? 'Add the numbers to an occasion', icon: LucideIcons.circleDashed, onFix: _logFood, ), @@ -280,8 +293,11 @@ class _NutritionScreenState extends State with RevisionReload { if (w != null && w.daysExcluded > 0) ...[ const SizedBox(height: S.x4), Observation( - '${w.daysExcluded} of the last ${w.span} days could not be counted', - 'A day counts once every occasion carries an energy figure.', + l?.nutritionDaysNotCounted(w.daysExcluded, w.span) ?? + '${w.daysExcluded} of the last ${w.span} days could not be ' + 'counted', + l?.nutritionDayCountsRule ?? + 'A day counts once every occasion carries an energy figure.', ), ], ], @@ -291,6 +307,7 @@ class _NutritionScreenState extends State with RevisionReload { // ── WEEK ───────────────────────────────────────────────────────────────── Widget _weekTab(BuildContext c) { + final l = AppLocalizations.of(c); final w = _week; if (w == null) return const SizedBox.shrink(); final counted = w.counted.length; @@ -306,53 +323,62 @@ class _NutritionScreenState extends State with RevisionReload { w.daysLogged, w.span, partial == 0 - ? 'Days with something logged' - : '$partial logged but partial, so excluded from ' - 'every average below', + ? (l?.nutritionDaysLoggedLabel ?? 'Days with something logged') + : (l?.nutritionPartialExcluded(partial) ?? + '$partial logged but partial, so excluded from ' + 'every average below'), C.domFood, ), ), - Section('Energy, day by day', _weekChart(c, w)), + Section(l?.nutritionEnergyByDay ?? 'Energy, day by day', _weekChart(c, w)), Section( - 'Seven-day average', + l?.nutritionSevenDayAvg ?? 'Seven-day average', counted == 0 - ? const StatusCard( - 'No complete day to average yet', - 'You have none.', + ? StatusCard( + l?.nutritionNoCompleteDayTitle ?? 'No complete day to average yet', + l?.nutritionNoCompleteDayBody ?? 'You have none.', icon: LucideIcons.chartNoAxesColumn, ) : Surface( child: Column( children: [ - _Mean('Energy', w.meanKcal, 'kcal', C.domFood), - _Mean('Protein', w.meanProtein, 'g', C.red), - _Mean('Carbs', w.meanCarbs, 'g', C.orange), - _Mean('Fat', w.meanFat, 'g', C.yellow), - _Mean('Fibre', w.meanFibre, 'g', C.green), + _Mean(l?.nutritionLabelEnergy ?? 'Energy', w.meanKcal, + 'kcal', C.domFood), + _Mean(l?.nutritionLabelProtein ?? 'Protein', + w.meanProtein, 'g', C.red), + _Mean(l?.nutritionLabelCarbs ?? 'Carbs', w.meanCarbs, + 'g', C.orange), + _Mean(l?.nutritionLabelFat ?? 'Fat', w.meanFat, 'g', + C.yellow), + _Mean(l?.nutritionLabelFibre ?? 'Fibre', w.meanFibre, + 'g', C.green), ], ), ), ), if (counted > 0 && w.meanKcal.value != null && _burned?.value != null) Section( - 'Energy balance', + l?.nutritionEnergyBalance ?? 'Energy balance', Surface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InlineMetrics([ - ('EATEN', '${w.meanKcal.value!.round()} kcal', C.domFood), - ('BURNED', '${_burned!.value!.round()} kcal', C.purple), + (l?.nutritionLabelEaten ?? 'EATEN', + '${w.meanKcal.value!.round()} kcal', C.domFood), + (l?.nutritionLabelBurned ?? 'BURNED', + '${_burned!.value!.round()} kcal', C.purple), ( - 'BALANCE', + l?.nutritionLabelBalance ?? 'BALANCE', '${(w.meanKcal.value! - _burned!.value!).round()} kcal', C.teal, ), ]), const SizedBox(height: S.x3), Text( - 'Eaten is the mean of ${w.meanKcal.days} complete days. ' - 'Burned is today only.', + l?.nutritionEatenMeanNote(w.meanKcal.days) ?? + 'Eaten is the mean of ${w.meanKcal.days} complete ' + 'days. Burned is today only.', style: F.cap.copyWith(color: P.of(c).ink3, height: 1.45), ), ], @@ -373,6 +399,7 @@ class _NutritionScreenState extends State with RevisionReload { /// day is an absence. A PARTIAL day still draws, because its total is a real /// floor; the footnote says it is one and that the mean below excluded it. Widget _weekChart(BuildContext c, NutritionWindow w) { + final l = AppLocalizations.of(c); // `?? 0.0` here was the whole absence-versus-zero bug in one operator: an // unlogged day became a real zero, and `Bars`' 2 pt visibility floor drew // it as a measured day with almost nothing in it. Null is a hole. @@ -382,7 +409,7 @@ class _NutritionScreenState extends State with RevisionReload { final axis = drawn == 0 ? null : AxisSpec.of(real, floor: 0); return Surface( child: ChartFrame( - title: 'Energy logged', + title: l?.nutritionEnergyLoggedTitle ?? 'Energy logged', unit: 'kcal', height: 120, yAxis: axis, @@ -390,18 +417,22 @@ class _NutritionScreenState extends State with RevisionReload { // the range actually drawn rather than a hardcoded guess. xLabels: drawn == 0 ? const [] - : [_dayShort(w.days.first.date), 'Today'], + : [ + _dayShort(w.days.first.date, l), + l?.nutritionTabToday ?? 'Today', + ], footnote: _partialDays(w) == 0 ? null - : '${_partialDays(w)} partial, left out of the averages below.', + : l?.nutritionPartialFootnote(_partialDays(w)) ?? + '${_partialDays(w)} partial, left out of the averages below.', // The bars are ENERGY. A week of one-tap occasions is a fully logged // week with no energy in it, and "Nothing logged yet" called the user // a liar directly under a card counting those same days. empty: axis == null ? NoData( message: w.daysLogged == 0 - ? 'Nothing logged yet' - : 'No energy figures yet') + ? (l?.nutritionNothingLoggedYet ?? 'Nothing logged yet') + : (l?.nutritionNoEnergyFiguresYet ?? 'No energy figures yet')) : null, series: vals, child: axis == null @@ -420,10 +451,13 @@ class _NutritionScreenState extends State with RevisionReload { /// A target the user TYPES. Adaptive targets need weight history and ~21 /// complete days and stay deferred; a typed one needs no science at all, and /// the tab is named Goals. - static const _goalSpecs = <(String, String, String, Color)>[ - ('kcal_target', 'Daily energy', 'kcal', C.domFood), - ('protein_target', 'Daily protein', 'g', C.red), - ]; + static List<(String, String, String, Color)> _goalSpecs(BuildContext c) { + final l = AppLocalizations.of(c); + return [ + ('kcal_target', l?.nutritionDailyEnergy ?? 'Daily energy', 'kcal', C.domFood), + ('protein_target', l?.nutritionDailyProtein ?? 'Daily protein', 'g', C.red), + ]; + } double? _target(String key) => (_profile[key] as num?)?.toDouble(); @@ -433,8 +467,9 @@ class _NutritionScreenState extends State with RevisionReload { key == 'kcal_target' ? _week?.meanKcal : _week?.meanProtein; Future _editTargets() async { + final specs = _goalSpecs(context); final ctrls = { - for (final g in _goalSpecs) + for (final g in specs) g.$1: TextEditingController(text: _target(g.$1)?.round().toString() ?? ''), }; final saved = await showModalBottomSheet( @@ -445,47 +480,51 @@ class _NutritionScreenState extends State with RevisionReload { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(R.xxl)), ), - builder: (s) => Padding( - padding: EdgeInsets.only( - left: S.x5, - right: S.x5, - top: S.x5, - bottom: MediaQuery.of(s).viewInsets.bottom + S.x5), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('Your targets', style: F.head.copyWith(color: P.of(s).ink)), - const SizedBox(height: S.x4), - for (final g in _goalSpecs) ...[ - OsTextField( - controller: ctrls[g.$1]!, - label: '${g.$2} (${g.$3})', - hint: 'none', - keyboard: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: S.x3), + builder: (s) { + final l = AppLocalizations.of(s); + return Padding( + padding: EdgeInsets.only( + left: S.x5, + right: S.x5, + top: S.x5, + bottom: MediaQuery.of(s).viewInsets.bottom + S.x5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(l?.nutritionYourTargetsSection ?? 'Your targets', + style: F.head.copyWith(color: P.of(s).ink)), + const SizedBox(height: S.x4), + for (final g in specs) ...[ + OsTextField( + controller: ctrls[g.$1]!, + label: '${g.$2} (${g.$3})', + hint: l?.nutritionHintNone ?? 'none', + keyboard: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: S.x3), + ], + const SizedBox(height: S.x2), + BigButton(l?.actionSave ?? 'Save', + color: C.domFood, onTap: () => Navigator.of(s).pop(true)), ], - const SizedBox(height: S.x2), - BigButton('Save', - color: C.domFood, onTap: () => Navigator.of(s).pop(true)), - ], - ), - ), + ), + ); + }, ); // Blank clears the target; a typo does NOT. "2,000" used to clear it and // the sheet closed as if it had saved. final typed = { - for (final g in _goalSpecs) g.$1: Typed.of(ctrls[g.$1]!.text), + for (final g in specs) g.$1: Typed.of(ctrls[g.$1]!.text), }; final fields = { - for (final g in _goalSpecs) g.$1: typed[g.$1]!.value, + for (final g in specs) g.$1: typed[g.$1]!.value, }; for (final ctrl in ctrls.values) { ctrl.dispose(); } if (saved != true || !mounted) return; - final bad = [for (final g in _goalSpecs) if (typed[g.$1]!.bad) g.$2]; + final bad = [for (final g in specs) if (typed[g.$1]!.bad) g.$2]; if (bad.isNotEmpty) { sayUnreadable(context, bad); return; @@ -495,33 +534,35 @@ class _NutritionScreenState extends State with RevisionReload { } Widget _goalsTab(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); - final set = [for (final g in _goalSpecs) if (_target(g.$1) != null) g]; + final specs = _goalSpecs(c); + final set = [for (final g in specs) if (_target(g.$1) != null) g]; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (set.isEmpty) StatusCard( - 'No targets set', - 'A target here is one you type.', - fix: 'Set a target', + l?.nutritionNoTargetsTitle ?? 'No targets set', + l?.nutritionNoTargetsBody ?? 'A target here is one you type.', + fix: l?.nutritionSetTargetFix ?? 'Set a target', icon: LucideIcons.target, onFix: _editTargets, ) else Section( - 'Your targets', + l?.nutritionYourTargetsSection ?? 'Your targets', Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final g in set) ...[ - _goalCard(g), + _goalCard(c, g), const SizedBox(height: S.x3), ], ], ), - action: 'Edit', + action: l?.nutritionEditAction ?? 'Edit', onAction: _editTargets, ), const SizedBox(height: S.x4), @@ -535,7 +576,7 @@ class _NutritionScreenState extends State with RevisionReload { const SizedBox(width: S.x2), Expanded( child: Text( - 'What your body spent today', + l?.nutritionBodySpentToday ?? 'What your body spent today', style: F.body.copyWith( color: p.ink, fontWeight: FontWeight.w600, @@ -548,10 +589,12 @@ class _NutritionScreenState extends State with RevisionReload { MetricRow( LucideIcons.flame, C.purple, - 'Estimated expenditure', - _burned?.value == null ? 'Not measured' : '${_burned!.value!.round()}', + l?.nutritionEstimatedExpenditure ?? 'Estimated expenditure', + _burned?.value == null + ? (l?.nutritionNotMeasured ?? 'Not measured') + : '${_burned!.value!.round()}', unit: _burned?.value == null ? '' : 'kcal', - sub: 'TODAY, FROM HEART RATE AND YOUR PROFILE', + sub: l?.nutritionExpenditureSub ?? 'TODAY, FROM HEART RATE AND YOUR PROFILE', ), ], ), @@ -563,13 +606,21 @@ class _NutritionScreenState extends State with RevisionReload { /// Current → target → the rate between them. The "current" is the mean of /// COMPLETE days only, the same denominator the Week tab uses, so the goal /// and the average can never disagree about what was counted. - Widget _goalCard((String, String, String, Color) g) { + Widget _goalCard(BuildContext c, (String, String, String, Color) g) { + final l = AppLocalizations.of(c); final target = _target(g.$1)!; final m = _meanFor(g.$1); final mean = m?.value; + // A stable per-goal noun, not `g.$2`'s last word: the label is a full + // localized phrase ("Énergie quotidienne" in French puts the adjective + // AFTER the noun), so slicing it grabs "quotidienne", not "énergie". + final nutrient = g.$1 == 'kcal_target' + ? (l?.nutritionEnergyWord ?? 'energy') + : (l?.nutritionProteinWord ?? 'protein'); if (mean == null || target <= 0) { return StatusCard( - 'Nothing to measure ${g.$2.toLowerCase()} against yet', + l?.nutritionNothingToMeasure(nutrient) ?? + 'Nothing to measure $nutrient against yet', // Two different absences, and the difference matters: the day never // qualified, or it qualified on energy while this nutrient was only a // floor. Progress against a floor would read low every single day. @@ -577,19 +628,23 @@ class _NutritionScreenState extends State with RevisionReload { // nutrient was never typed at all, and blaming that on day // completeness is a sentence the user can see is false. (m?.floorDays ?? 0) > 0 - ? 'Every complete day had an occasion logged without a ' - '${g.$2.split(' ').last.toLowerCase()} figure, so the average ' - 'would only be a lower bound.' + ? (l?.nutritionFloorAverageBody(nutrient) ?? + 'Every complete day had an occasion logged without a ' + '$nutrient figure, so the average would only be a lower ' + 'bound.') : (_week?.counted.length ?? 0) > 0 - ? '${_week!.counted.length} of the last ${_week!.span} days ' - 'counted, but none of them carried a ' - '${g.$2.split(' ').last.toLowerCase()} figure.' - : 'A day counts once every occasion carries a figure and the ' - 'log reaches the evening. None of the last ' - '${_week?.span ?? 7} days has.', + ? (l?.nutritionCountedNoFigureBody( + _week!.counted.length, _week!.span, nutrient) ?? + '${_week!.counted.length} of the last ${_week!.span} days ' + 'counted, but none of them carried a $nutrient ' + 'figure.') + : (l?.nutritionDayCountsRuleFull(_week?.span ?? 7) ?? + 'A day counts once every occasion carries a figure and ' + 'the log reaches the evening. None of the last ' + '${_week?.span ?? 7} days has.'), fix: (m?.floorDays ?? 0) > 0 || (_week?.counted.length ?? 0) > 0 - ? 'Add the numbers to an occasion' - : 'Log an eating occasion', + ? (l?.nutritionAddNumbersFix ?? 'Add the numbers to an occasion') + : (l?.nutritionLogOccasionFix ?? 'Log an eating occasion'), icon: LucideIcons.target, onFix: _logFood, ); @@ -598,13 +653,20 @@ class _NutritionScreenState extends State with RevisionReload { // protein can be measured on fewer days than energy was. final days = m!.days; final diff = mean - target; + final rate = diff.abs() < 1 + ? (l?.nutritionOnTarget ?? 'On target') + : (diff > 0 + ? (l?.nutritionRateAbove(diff.abs().round(), g.$3) ?? + '${diff.abs().round()} ${g.$3}/day above') + : (l?.nutritionRateBelow(diff.abs().round(), g.$3) ?? + '${diff.abs().round()} ${g.$3}/day below')); + final meanNote = l?.nutritionMeanOfDays(days) ?? + 'mean of $days complete day${days == 1 ? '' : 's'}'; return GoalTrajectory( g.$2, '${mean.round()} ${g.$3}', '${target.round()} ${g.$3}', - '${diff.abs() < 1 ? 'On target' : '${diff.abs().round()} ${g.$3}/day ' - '${diff > 0 ? 'above' : 'below'}'} · mean of $days complete ' - 'day${days == 1 ? '' : 's'}', + '$rate · $meanNote', (mean / target).clamp(0, 1).toDouble(), g.$4, rateDown: diff > 0, @@ -613,9 +675,9 @@ class _NutritionScreenState extends State with RevisionReload { } /// "Thu 4 Sep" from a `YYYY-MM-DD` day label, for an axis end-label. -String _dayShort(String ymd) { +String _dayShort(String ymd, [AppLocalizations? l]) { final d = DateTime.tryParse(ymd); - return d == null ? ymd : formatDay(d); + return d == null ? ymd : formatDay(d, l); } // ── components ───────────────────────────────────────────────────────────── @@ -631,6 +693,7 @@ class DayEnergyCard extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final k = day.kcal; return Surface( @@ -641,11 +704,14 @@ class DayEnergyCard extends StatelessWidget { children: [ Expanded( child: Text( - k.value == null ? 'LOGGED TODAY' : 'EATEN TODAY', + k.value == null + ? (l?.nutritionLoggedToday ?? 'LOGGED TODAY') + : (l?.nutritionEatenToday ?? 'EATEN TODAY'), style: F.over.copyWith(color: p.ink3), ), ), - if (k.isFloor) const Flexible(child: Pill('At least', C.yellow)), + if (k.isFloor) + Flexible(child: Pill(l?.nutritionAtLeast ?? 'At least', C.yellow)), ], ), const SizedBox(height: S.x2), @@ -668,14 +734,16 @@ class DayEnergyCard extends StatelessWidget { ), Text( k.value == null - ? 'occasion${day.entries.length == 1 ? '' : 's'}' + ? (l?.nutritionOccasionsUnit(day.entries.length) ?? + 'occasion${day.entries.length == 1 ? '' : 's'}') : 'kcal', style: F.cap.copyWith(color: p.ink3), ), if (k.value != null) Text( - '· ${day.entries.length} occasion' - '${day.entries.length == 1 ? '' : 's'}', + '· ${l?.nutritionOccasionsCount(day.entries.length) ?? + '${day.entries.length} occasion' + '${day.entries.length == 1 ? '' : 's'}'}', style: F.cap.copyWith(color: p.ink2), ), ], @@ -683,14 +751,17 @@ class DayEnergyCard extends StatelessWidget { if (burned?.value != null) ...[ const SizedBox(height: S.x4), InlineMetrics([ - ('BURNED', '${burned!.value!.round()} kcal', C.purple), + (l?.nutritionLabelBurned ?? 'BURNED', + '${burned!.value!.round()} kcal', C.purple), if (k.value != null) ( // Eaten is a FLOOR when occasions were logged without an // energy figure, so eaten minus burned is a floor too: the // balance is AT LEAST this, never at most. The pill on this // same card says "At least". - k.isFloor ? 'BALANCE AT LEAST' : 'BALANCE', + k.isFloor + ? (l?.nutritionLabelBalanceAtLeast ?? 'BALANCE AT LEAST') + : (l?.nutritionLabelBalance ?? 'BALANCE'), '${(k.value! - burned!.value!).round()} kcal', C.teal, ), @@ -725,6 +796,7 @@ class MealRow extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final known = [ for (final e in entries) @@ -734,7 +806,7 @@ class MealRow extends StatelessWidget { final anyUnknown = entries.any((e) => e.kcal == null); return Pressable( onTap: onTap, - semanticLabel: _mealLabel(meal), + semanticLabel: _mealLabel(c, meal), child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row( @@ -755,15 +827,16 @@ class MealRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _mealLabel(meal), + _mealLabel(c, meal), style: F.body.copyWith(color: p.ink), ), Text( entries.isEmpty - ? 'Not logged' + ? (l?.nutritionNotLogged ?? 'Not logged') : total == null - ? '${entries.length} logged · energy not recorded' - : '${anyUnknown ? 'at least ' : ''}' + ? (l?.nutritionLoggedNoEnergy(entries.length) ?? + '${entries.length} logged · energy not recorded') + : '${anyUnknown ? (l?.nutritionAtLeastPrefix ?? 'at least ') : ''}' '${total.round()} kcal', style: F.over.copyWith(color: p.ink3), ), @@ -784,12 +857,15 @@ class MealRow extends StatelessWidget { } } -String _mealLabel(String m) => switch (m) { - 'breakfast' => 'Breakfast', - 'lunch' => 'Lunch', - 'dinner' => 'Dinner', - _ => 'Snacks', -}; +String _mealLabel(BuildContext c, String m) { + final l = AppLocalizations.of(c); + return switch (m) { + 'breakfast' => l?.nutritionMealBreakfast ?? 'Breakfast', + 'lunch' => l?.nutritionMealLunch ?? 'Lunch', + 'dinner' => l?.nutritionMealDinner ?? 'Dinner', + _ => l?.nutritionMealSnacks ?? 'Snacks', + }; +} /// One nutrient's seven-day mean, with its own denominator. /// @@ -809,6 +885,7 @@ class _Mean extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final n = mean.days; final floors = mean.floorDays; return MetricRow( @@ -816,16 +893,24 @@ class _Mean extends StatelessWidget { color, label, mean.value == null - ? (floors > 0 ? 'Not counted' : 'Not recorded') + ? (floors > 0 + ? (l?.nutritionNotCounted ?? 'Not counted') + : (l?.nutritionNotRecorded ?? 'Not recorded')) : mean.value!.round().toString(), unit: mean.value == null ? '' : unit, sub: mean.value == null ? (floors > 0 - ? 'EVERY COMPLETE DAY HAD AN OCCASION WITH NO ' - '${label.toUpperCase()} FIGURE' - : 'NO COMPLETE DAY RECORDED ${label.toUpperCase()}') - : 'MEAN OF $n COMPLETE DAY${n == 1 ? '' : 'S'}' - '${floors == 0 ? '' : ' · $floors LEFT OUT AS A FLOOR'}', + ? (l?.nutritionEveryDayNoFigure(label.toUpperCase()) ?? + 'EVERY COMPLETE DAY HAD AN OCCASION WITH NO ' + '${label.toUpperCase()} FIGURE') + : (l?.nutritionNoDayRecorded(label.toUpperCase()) ?? + 'NO COMPLETE DAY RECORDED ${label.toUpperCase()}')) + : (l?.nutritionMeanOfCompleteDaysCaps(n) ?? + 'MEAN OF $n COMPLETE DAY${n == 1 ? '' : 'S'}') + + (floors == 0 + ? '' + : (l?.nutritionLeftOutAsFloor(floors) ?? + ' · $floors LEFT OUT AS A FLOOR')), ); } } @@ -845,6 +930,7 @@ class _WaterRow extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); return Surface( child: Row(children: [ @@ -852,9 +938,11 @@ class _WaterRow extends StatelessWidget { const SizedBox(width: S.x3), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Water', style: F.body.copyWith(color: p.ink)), + Text(l?.nutritionWaterLabel ?? 'Water', style: F.body.copyWith(color: p.ink)), Text( - ml == null ? 'Not logged' : 'Tap − or + to change', + ml == null + ? (l?.nutritionNotLogged ?? 'Not logged') + : (l?.nutritionTapToChange ?? 'Tap − or + to change'), style: F.over.copyWith(color: p.ink3), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -869,7 +957,9 @@ class _WaterRow extends StatelessWidget { child: Text( // NEVER a bare em-dash. An absent value says the word; a dash is a // shrug the reader has to interpret, and the suite pins this. - ml == null ? 'None yet' : '${(ml! / 1000).toStringAsFixed(1)} L', + ml == null + ? (l?.nutritionNoneYet ?? 'None yet') + : '${(ml! / 1000).toStringAsFixed(1)} L', textAlign: TextAlign.center, style: ml == null ? F.cap.copyWith(color: p.ink3) @@ -890,11 +980,14 @@ class _WaterStep extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final p = P.of(c); final on = onTap != null; return Pressable( onTap: onTap, - semanticLabel: icon == LucideIcons.plus ? 'Add water' : 'Remove water', + semanticLabel: icon == LucideIcons.plus + ? (l?.nutritionAddWater ?? 'Add water') + : (l?.nutritionRemoveWater ?? 'Remove water'), child: Container( width: S.tap, height: S.tap, diff --git a/lib/ui2/screens/readiness_detail.dart b/lib/ui2/screens/readiness_detail.dart index 7c986f6a..1b3a955d 100644 --- a/lib/ui2/screens/readiness_detail.dart +++ b/lib/ui2/screens/readiness_detail.dart @@ -12,6 +12,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/db.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../ui2.dart'; import 'home_screen.dart'; @@ -154,14 +155,15 @@ class _ReadinessDetailState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final d = _d ?? const ReadinessData(); final v = d.readiness.value; - final band = readinessBand(v); + final band = readinessBand(v, l); // No date in the nav bar. It named the held-over night, and the headline // can no longer BE that night — a date up here now would be labelling // today's number with somebody else's day. - return detailScaffold(c, 'Readiness', [ + return detailScaffold(c, l?.readinessDetailTitle ?? 'Readiness', [ if (_loading && _d == null) ...[ const SizedBox(height: S.x8), const Center(child: CircularProgressIndicator()), @@ -171,17 +173,23 @@ class _ReadinessDetailState extends State { // day it does, and the "What was missing" section directly below is // built from that record — a sentence written here was competing // with the real answer one line down and winning. - StatusCard.forMetric('Readiness is not scored', d.readiness, + StatusCard.forMetric( + l?.readinessDetailNotScoredTitle ?? + 'Readiness is not scored', + d.readiness, // Where the data stops, appended to whatever the pipeline // said. Not a substitute for the reason and not a reading — // "the last one was Saturday" is a fact about coverage. gap: d.heldOverNight == null ? null - : 'The last night scored was ' - '${prettyDay(d.heldOverNight)}.') ?? + : (l?.readinessDetailLastNightScored( + prettyDay(d.heldOverNight, l)) ?? + 'The last night scored was ' + '${prettyDay(d.heldOverNight, l)}.')) ?? const SizedBox.shrink(), if (d.absentDiag != null) - Section('What was missing', _absence(c, p, d.absentDiag!)), + Section(l?.readinessDetailWhatWasMissing ?? 'What was missing', + _absence(c, p, d.absentDiag!)), ] else Surface( child: Column(children: [ @@ -205,7 +213,8 @@ class _ReadinessDetailState extends State { ), if (d.breakdown.isNotEmpty) ...[ - Section('What went into it', _breakdown(c, p, d)), + Section(l?.readinessDetailWhatWentIntoIt ?? 'What went into it', + _breakdown(c, p, d)), const SizedBox(height: S.x4), Surface( elevation: 0, @@ -213,9 +222,11 @@ class _ReadinessDetailState extends State { child: Row(children: [ Expanded( child: Text( - '${d.inputsUsed}/${d.breakdown.length} inputs. Each one is ' - 'ranked against your own history — a parallel view of the ' - 'same inputs, not slices of the number above.', + l?.readinessDetailInputsFooter( + d.inputsUsed, d.breakdown.length) ?? + '${d.inputsUsed}/${d.breakdown.length} inputs. Each one is ' + 'ranked against your own history — a parallel view of the ' + 'same inputs, not slices of the number above.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ), @@ -223,11 +234,12 @@ class _ReadinessDetailState extends State { ), ] else if (v != null) Section( - 'What went into it', - const StatusCard( - 'No breakdown yet', - 'Ranking each input against your own history takes about two ' - 'weeks of nights.', + l?.readinessDetailWhatWentIntoIt ?? 'What went into it', + StatusCard( + l?.readinessDetailNoBreakdownTitle ?? 'No breakdown yet', + l?.readinessDetailNoBreakdownBody ?? + 'Ranking each input against your own history takes about two ' + 'weeks of nights.', icon: LucideIcons.listTree, ), ), @@ -235,12 +247,12 @@ class _ReadinessDetailState extends State { // The header used to say "Last 90 days" over a chart of five points. // It says what is drawn. Section( - _historyTitle(d), + _historyTitle(c, d), !d.series.any((v) => v != null) - ? const StatusCard( - 'No readiness history', - '0 days scored.', - fix: 'Wear the band overnight', + ? StatusCard( + l?.readinessDetailNoHistoryTitle ?? 'No readiness history', + l?.readinessDetailNoHistoryBody ?? '0 days scored.', + fix: l?.readinessDetailWearOvernight ?? 'Wear the band overnight', icon: LucideIcons.chartLine, ) : Surface(child: _history(c, d)), @@ -259,9 +271,13 @@ class _ReadinessDetailState extends State { return first <= 0 ? d.series : d.series.sublist(first); } - String _historyTitle(ReadinessData d) { + String _historyTitle(BuildContext c, ReadinessData d) { + final l = AppLocalizations.of(c); final n = d.series.any((v) => v != null) ? _window(d).length : 0; - return n == 0 ? 'History' : 'Last $n day${n == 1 ? '' : 's'}'; + return n == 0 + ? (l?.readinessDetailHistoryTitle ?? 'History') + : (l?.readinessDetailLastNDays(n) ?? + 'Last $n day${n == 1 ? '' : 's'}'); } Widget _history(BuildContext c, ReadinessData d) { @@ -270,17 +286,19 @@ class _ReadinessDetailState extends State { // 71-to-76 week into a chart that looked like a collapse and a recovery. const axis = AxisSpec(min: 0, max: 100, ticks: 3, format: axisInt); final p = P.of(c); + final l = AppLocalizations.of(c); return ChartFrame( - title: 'Readiness', - unit: '/100', + title: l?.readinessDetailTitle ?? 'Readiness', + unit: l?.readinessDetailUnit ?? '/100', height: 120, yAxis: axis, // Slot 0 is `length - 1` days behind today, not `length` — the last slot // IS today. MetricDetail draws the same `recovery` series and already // counts it this way; the two screens dated one chart differently. xLabels: [ - '${win.length - 1} day${win.length == 2 ? '' : 's'} ago', - 'Today', + l?.readinessDetailDaysAgo(win.length - 1) ?? + '${win.length - 1} day${win.length == 2 ? '' : 's'} ago', + l?.readinessDetailToday ?? 'Today', ], series: win, child: CustomPaint( @@ -298,15 +316,16 @@ class _ReadinessDetailState extends State { /// and it never turns a night count into a date, because nothing in the /// pipeline knows when you will next wear the band. Widget _absence(BuildContext c, P p, Map diag) { + final l = AppLocalizations.of(c); final rows = <(String, String)>[]; for (final k in const ['hrv', 'rhr', 'resp', 'temp']) { final e = diag[k]; if (e is! Map) continue; final n = (e['baseline_n'] as num?)?.toInt() ?? 0; rows.add(( - driverLabel(k), - '${e['value'] == true ? 'Measured' : 'Not measured'} · ' - '$n night${n == 1 ? '' : 's'} of your own history', + driverLabel(k, l), + '${e['value'] == true ? (l?.readinessDetailMeasured ?? 'Measured') : (l?.readinessDetailNotMeasured ?? 'Not measured')} · ' + '${l?.readinessDetailNightsOfHistory(n) ?? '$n night${n == 1 ? '' : 's'} of your own history'}', )); } final note = diag['note']?.toString(); @@ -335,12 +354,14 @@ class _ReadinessDetailState extends State { const SizedBox(height: S.x3), Text( need != null - ? '$need. Each input is ranked against your own nights, so the ' - 'score cannot start before there are enough of them.' + ? (l?.readinessDetailNeedSuffix(need) ?? + '$need. Each input is ranked against your own nights, so the ' + 'score cannot start before there are enough of them.') : (note != null && note.isNotEmpty ? note - : 'Everything above was present, and the comparison against ' - 'your own history still could not be made.'), + : (l?.readinessDetailNoNoteFallback ?? + 'Everything above was present, and the comparison against ' + 'your own history still could not be made.')), style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ]); @@ -360,13 +381,14 @@ class _ReadinessDetailState extends State { child: Column(children: [ for (var i = 0; i < rows.length; i++) ...[ if (i > 0) Divider(color: p.line, height: 1), - _row(p, rows[i], wsum), + _row(c, p, rows[i], wsum), ], ]), ); } - Widget _row(P p, Map r, double wsum) { + Widget _row(BuildContext c, P p, Map r, double wsum) { + final l = AppLocalizations.of(c); final key = r['label']?.toString() ?? ''; final raw = (r['weight'] as num?)?.toDouble(); final contribution = (r['weighted_contribution'] as num?); @@ -377,15 +399,20 @@ class _ReadinessDetailState extends State { final share = !used || raw == null || wsum <= 0 ? null : raw / wsum; final parts = [ - if (share != null) '${(share * 100).round()}% weight', - if (!used) 'not available', - if (used && contribution == null) 'contribution not reported', + if (share != null) + l?.readinessDetailWeightPercent((share * 100).round()) ?? + '${(share * 100).round()}% weight', + if (!used) l?.readinessDetailNotAvailable ?? 'not available', + if (used && contribution == null) + l?.readinessDetailContributionNotReported ?? 'contribution not reported', // The temperature input is a raw sensor deviation, not a calibrated // temperature. It gets said, every time. - if (key == 'temp') 'relative, uncalibrated', + if (key == 'temp') + l?.readinessDetailRelativeUncalibrated ?? 'relative, uncalibrated', // An unlabelled glyph is not an explanation. This is the // smallest-worthwhile-change gate, so it says what it means. - if (used && !pastMdc) 'within your usual spread', + if (used && !pastMdc) + l?.readinessDetailWithinSpread ?? 'within your usual spread', ]; return Padding( diff --git a/lib/ui2/screens/rough_night.dart b/lib/ui2/screens/rough_night.dart index dd56d7d1..3b04f272 100644 --- a/lib/ui2/screens/rough_night.dart +++ b/lib/ui2/screens/rough_night.dart @@ -38,6 +38,7 @@ import 'package:openstrap_analytics/onehz.dart' as ana; import 'package:provider/provider.dart'; import '../../ai/journal_ai.dart' show kJournalPresetTags; +import '../../l10n/app_localizations.dart'; import '../../data/db.dart'; import '../../data/journal_fields.dart' show formatMinuteOfDay; import '../../data/local_repository.dart'; @@ -85,6 +86,7 @@ class RoughNight { required this.descriptor, required this.moved, required this.knows, + this.illnessFlagged = false, }); final String day; @@ -105,6 +107,11 @@ class RoughNight { /// than asks. Each is one finished sentence. final List knows; + /// Whether the illness watch already flagged this night — set alongside the + /// matching sentence in [knows], and read here rather than by matching + /// English words inside a sentence that is localized. See [ask]. + final bool illnessFlagged; + bool get rough => signs >= 2; /// Tags worth offering, once attribution is opted into: the shared preset @@ -113,9 +120,7 @@ class RoughNight { /// asking them to grade the sensor. List get ask => [ for (final t in kJournalPresetTags) - if (t != 'poor sleep' && - !(t == 'sick' && knows.any((k) => k.contains('illness')))) - t, + if (t != 'poor sleep' && !(t == 'sick' && illnessFlagged)) t, ]; } @@ -130,11 +135,12 @@ class RoughNight { /// the published foundations, the same two `alcoholNightFlag` uses. A sign that /// cannot clear its own minimal detectable change does not count, and a /// degenerate scale yields no MDC and therefore no sign — never a claim. -({int signs, List moved})? roughNightSignCount( +({int signs, List moved, bool tempMoved})? roughNightSignCount( String day, Map> series, { int minNights = kRoughNightMinNights, int window = kRoughNightWindow, + AppLocalizations? l, }) { /// The [window] most recent values for [key] strictly BEFORE [day]. Strictly: /// a night compared against a window containing itself pulls its own baseline @@ -175,21 +181,25 @@ class RoughNight { var signs = 0; if (fired(rhr, rhrHist, 1)) { signs++; - moved.add('your resting heart rate ran higher'); + moved.add(l?.roughNightSignRhr ?? 'your resting heart rate ran higher'); } if (fired(rmssd, rmssdHist, -1)) { signs++; - moved.add('your HRV ran lower'); + moved.add(l?.roughNightSignHrv ?? 'your HRV ran lower'); } if (fired(tonight(_kDip), history(_kDip), -1)) { signs++; - moved.add('your heart rate dropped less overnight than it usually does'); + moved.add( + l?.roughNightSignDip ?? + 'your heart rate dropped less overnight than it usually does', + ); } - if (fired(tonight(_kTempZ), history(_kTempZ), 1)) { + final tempMoved = fired(tonight(_kTempZ), history(_kTempZ), 1); + if (tempMoved) { signs++; - moved.add('your skin ran warmer'); + moved.add(l?.roughNightSignTemp ?? 'your skin ran warmer'); } - return (signs: signs, moved: moved); + return (signs: signs, moved: moved, tempMoved: tempMoved); } /// Read [day]'s state, plus everything the app can state about it instead of @@ -199,7 +209,12 @@ class RoughNight { /// FOUR indexed series reads and three read-seam calls, no compute. `getToday` /// is not among them: [day] is passed in because the caller already knows which /// night it is looking at. -Future loadRoughNight(LocalRepository repo, String day) async { +Future loadRoughNight( + LocalRepository repo, + String day, { + BuildContext? c, +}) async { + final l = c == null ? null : AppLocalizations.of(c); // MEASURED ONLY. This is a detection, not a chart: the night is called // rough by comparing it against the spread of the days behind it, and a // day another vendor's algorithm derived is not the same measurement. @@ -220,7 +235,7 @@ Future loadRoughNight(LocalRepository repo, String day) async { r['date'] as String: (r['value'] as num).toDouble(), }; } - final counted = roughNightSignCount(day, series); + final counted = roughNightSignCount(day, series, l: l); if (counted == null || counted.signs < 2) return null; final knows = []; @@ -242,20 +257,26 @@ Future loadRoughNight(LocalRepository repo, String day) async { } if (latest != null) { final at = formatMinuteOfDay(latest.hour * 60 + latest.minute); - knows.add('You trained until $at, which often does this on its own.'); + knows.add( + l?.roughNightLateTraining(at) ?? + 'You trained until $at, which often does this on its own.', + ); } } } catch (_) {/* a knowable that could not be read is simply not stated */} // ILLNESS — the same night's CUSUM state, from the rollup that already ran. + var illnessFlagged = false; try { final recent = (await repo.getInsights())['recent']; if (recent is List) { for (final r in recent) { if (r is Map && r['date'] == day && r['illness'] == true) { + illnessFlagged = true; knows.add( - 'The illness watch flagged this night too — a sustained rise ' - 'against your own baseline, not a diagnosis.', + l?.roughNightIllness ?? + 'The illness watch flagged this night too — a sustained rise ' + 'against your own baseline, not a diagnosis.', ); break; } @@ -269,8 +290,9 @@ Future loadRoughNight(LocalRepository repo, String day) async { final cycle = await repo.getCycle(); if (cycle['enabled'] == true && cycle['phase'] == 'luteal') { knows.add( - 'You are in the luteal phase, which lifts resting heart rate and ' - 'skin temperature by itself.', + l?.roughNightLuteal ?? + 'You are in the luteal phase, which lifts resting heart rate and ' + 'skin temperature by itself.', ); } } catch (_) {/* cycle tracking off, or nothing logged */} @@ -278,9 +300,10 @@ Future loadRoughNight(LocalRepository repo, String day) async { // WARM ROOM — only when the temp sign is one of the ones that fired. The // channel is relative ADC, so this is "warmer than your usual", never a // temperature. - if (counted.moved.any((m) => m.contains('skin'))) { + if (counted.tempMoved) { knows.add( - 'Your skin ran warmer than your usual — a warm room does this too.', + l?.roughNightWarmRoom ?? + 'Your skin ran warmer than your usual — a warm room does this too.', ); } @@ -309,6 +332,7 @@ Future loadRoughNight(LocalRepository repo, String day) async { .descriptor, moved: counted.moved, knows: knows, + illnessFlagged: illnessFlagged, ); } @@ -390,6 +414,7 @@ class _RoughNightCardState extends State { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final n = widget.night; // "a rougher night than usual for you — your body worked harder overnight" // splits at the dash into a headline and its own explanation. @@ -411,7 +436,8 @@ class _RoughNightCardState extends State { // Sentence case from the analytics wording, which is written // lower-case for use mid-sentence. head.isEmpty - ? 'A rougher night than usual' + ? (l?.roughNightDefaultHeadline ?? + 'A rougher night than usual') : head[0].toUpperCase() + head.substring(1), style: F.head.copyWith( color: p.ink, @@ -422,7 +448,7 @@ class _RoughNightCardState extends State { ), const SizedBox(width: S.x2), Pressable( - semanticLabel: 'Dismiss', + semanticLabel: l?.roughNightDismiss ?? 'Dismiss', onTap: _dismiss, child: Icon(LucideIcons.x, size: 16, color: p.ink3), ), @@ -430,8 +456,9 @@ class _RoughNightCardState extends State { ), const SizedBox(height: S.x3), Text( - '${_sentence(n.moved)}, against your own nights. ' - 'This is a measurement of the night, not a verdict on you.', + l?.roughNightSummary(_sentence(l, n.moved)) ?? + '${_sentence(l, n.moved)}, against your own nights. ' + 'This is a measurement of the night, not a verdict on you.', style: F.cap.copyWith(color: p.ink2, height: 1.5), ), // WHAT THE APP ALREADY KNOWS. Stated, never asked — a screen that @@ -462,25 +489,33 @@ class _RoughNightCardState extends State { /// The half nobody sees until they ask for it. The action names nothing that /// is in the vocabulary behind it. - List _invite(BuildContext c, P p) => [ - if (_ask != 'never') - BigButton( - 'Tell it what happened', - icon: LucideIcons.messageSquarePlus, - color: C.indigo, - soft: true, - onTap: () => _setAsk('on'), - ) - else - Text( - 'Nothing to answer — this card only reports the night.', - style: F.over.copyWith(color: p.ink3, height: 1.5), - ), - ]; + List _invite(BuildContext c, P p) { + final l = AppLocalizations.of(c); + return [ + if (_ask != 'never') + BigButton( + l?.roughNightTellWhatHappened ?? 'Tell it what happened', + icon: LucideIcons.messageSquarePlus, + color: C.indigo, + soft: true, + onTap: () => _setAsk('on'), + ) + else + Text( + l?.roughNightNothingToAnswer ?? + 'Nothing to answer — this card only reports the night.', + style: F.over.copyWith(color: p.ink3, height: 1.5), + ), + ]; + } - List _question(BuildContext c, P p, RoughNight n) => [ + List _question(BuildContext c, P p, RoughNight n) { + final l = AppLocalizations.of(c); + return [ Text( - n.knows.isEmpty ? 'What else was going on?' : 'Anything else?', + n.knows.isEmpty + ? (l?.roughNightWhatElse ?? 'What else was going on?') + : (l?.roughNightAnythingElse ?? 'Anything else?'), style: F.body.copyWith(color: p.ink, fontWeight: FontWeight.w600), ), const SizedBox(height: S.x3), @@ -503,7 +538,9 @@ class _RoughNightCardState extends State { ), const SizedBox(height: S.x4), BigButton( - _saving ? 'Saving' : 'Log it for that night', + _saving + ? (l?.roughNightSaving ?? 'Saving') + : (l?.roughNightLogIt ?? 'Log it for that night'), icon: LucideIcons.check, color: C.domMind, onTap: _saving || _picked.isEmpty ? null : _save, @@ -516,7 +553,7 @@ class _RoughNightCardState extends State { MaterialPageRoute(builder: (_) => JournalCompose(date: n.day)), ), child: Text( - 'Add how much', + l?.roughNightAddHowMuch ?? 'Add how much', style: F.cap.copyWith(color: p.on(C.domMind)), ), ), @@ -524,15 +561,19 @@ class _RoughNightCardState extends State { Pressable( onTap: () => _setAsk('never'), child: Text( - 'Do not ask again', + l?.roughNightDoNotAskAgain ?? 'Do not ask again', style: F.over.copyWith(color: p.ink3), ), ), ]; + } /// "a, b and c" — the moved measurements as one clause. - static String _sentence(List parts) { - if (parts.isEmpty) return 'Several overnight measurements moved together'; + static String _sentence(AppLocalizations? l, List parts) { + if (parts.isEmpty) { + return l?.roughNightSeveralMoved ?? + 'Several overnight measurements moved together'; + } final s = parts.length == 1 ? parts.first : '${parts.sublist(0, parts.length - 1).join(', ')} and ${parts.last}'; diff --git a/lib/ui2/screens/scan_barcode.dart b/lib/ui2/screens/scan_barcode.dart index 119357ab..12746bc3 100644 --- a/lib/ui2/screens/scan_barcode.dart +++ b/lib/ui2/screens/scan_barcode.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; /// Read one barcode. Resolves the digits, or null if the sheet was closed, @@ -71,6 +72,7 @@ class _ScanSheetState extends State<_ScanSheet> { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(S.x5, S.x4, S.x5, S.x6), @@ -81,11 +83,11 @@ class _ScanSheetState extends State<_ScanSheet> { Row( children: [ Expanded( - child: Text('Scan the barcode', + child: Text(l?.scanBarcodeTitle ?? 'Scan the barcode', style: F.t2.copyWith(color: p.ink)), ), Pressable( - semanticLabel: 'Close', + semanticLabel: l?.scanBarcodeClose ?? 'Close', onTap: () => Navigator.of(c).pop(), child: Icon(LucideIcons.x, size: 20, color: p.ink3), ), @@ -105,8 +107,9 @@ class _ScanSheetState extends State<_ScanSheet> { ), const SizedBox(height: S.x4), Text( - 'Hold the barcode inside the frame. Nothing is recorded — the ' - 'digits are all this reads.', + l?.scanBarcodeInstructions ?? + 'Hold the barcode inside the frame. Nothing is recorded — ' + 'the digits are all this reads.', style: F.cap.copyWith(color: p.ink3, height: 1.45), ), ], @@ -124,17 +127,22 @@ class _CameraProblem extends StatelessWidget { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final denied = error.errorCode == MobileScannerErrorCode.permissionDenied; return Center( child: Padding( padding: const EdgeInsets.all(S.x4), child: StatusCard( - denied ? 'No camera access' : 'The camera did not start', denied - ? 'Scanning needs the camera, and this app has not been given ' - 'it.' - : 'This device would not open its camera for the scanner.', - fix: 'Type the numbers instead', + ? (l?.scanBarcodeNoAccessTitle ?? 'No camera access') + : (l?.scanBarcodeCameraFailedTitle ?? 'The camera did not start'), + denied + ? (l?.scanBarcodeNoAccessBody ?? + 'Scanning needs the camera, and this app has not been ' + 'given it.') + : (l?.scanBarcodeCameraFailedBody ?? + 'This device would not open its camera for the scanner.'), + fix: l?.scanBarcodeTypeInstead ?? 'Type the numbers instead', icon: LucideIcons.cameraOff, onFix: () => Navigator.of(c).pop(), ), diff --git a/lib/ui2/screens/sleep_detail.dart b/lib/ui2/screens/sleep_detail.dart index 1c9ce8f3..08086d35 100644 --- a/lib/ui2/screens/sleep_detail.dart +++ b/lib/ui2/screens/sleep_detail.dart @@ -21,7 +21,9 @@ import 'package:provider/provider.dart'; import '../../data/day_label.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../../state/app_state.dart'; +import '../../state/locale_controller.dart'; import '../../state/prefs.dart'; import '../../models/metric.dart'; import '../ui2.dart'; @@ -59,20 +61,24 @@ SleepStage? _stageOf(Object? raw) => switch (raw?.toString()) { /// 15-minute bands. The stager sees a wrist, so "you fell asleep in 7 minutes" /// is a precision nobody measured. -String _solBand(double m) { - if (m < 15) return 'under 15 minutes'; - if (m >= 60) return 'over an hour'; +String _solBand(BuildContext c, double m) { + final l = AppLocalizations.of(c); + if (m < 15) return l?.sleepDetailSolUnder15 ?? 'under 15 minutes'; + if (m >= 60) return l?.sleepDetailSolOverHour ?? 'over an hour'; final lo = (m ~/ 15) * 15; - return '$lo–${lo + 15} minutes'; + return l?.sleepDetailSolRange(lo, lo + 15) ?? '$lo–${lo + 15} minutes'; } -/// Two call sites drew this byte-identical card; one const so they cannot +/// Two call sites drew this byte-identical card; one function so they cannot /// drift apart. -const _noOvernightLines = StatusCard( - 'No overnight signal lines', - 'No overnight recordings reached this day.', - icon: LucideIcons.activity, -); +Widget _noOvernightLines(BuildContext c) { + final l = AppLocalizations.of(c); + return StatusCard( + l?.sleepDetailNoOvernightTitle ?? 'No overnight signal lines', + l?.sleepDetailNoOvernightBody ?? 'No overnight recordings reached this day.', + icon: LucideIcons.activity, + ); +} /// Local noon of a 'YYYY-MM-DD' day, in epoch seconds — the stamp `getChart` /// puts on that day's stored scalar. Used to cut the history at last night, so @@ -337,10 +343,44 @@ class _SleepDetailState extends State { WidgetsBinding.instance.addPostFrameCallback((_) => _load()); } + // `_rough` bakes `AppLocalizations` strings into `knows`/`moved` at load + // time (see `loadRoughNight`), so a language switch while this screen is + // alive would otherwise leave the rough-night card showing the old locale + // until the user steps to another day. Sentinel so the system-default + // locale (`code == null`) is not mistaken for "never seen yet" on the first + // pass — same fix as `RevisionReload`/`DayTimelineScreen`. + static const Object _localeUnset = Object(); + Object? _seenLocale = _localeUnset; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final Object localeKey; + try { + final code = context.watch().code; + localeKey = code ?? Localizations.localeOf(context); + } catch (_) { + return; + } + if (identical(_seenLocale, _localeUnset)) { + _seenLocale = localeKey; + } else if (_seenLocale != localeKey && widget.data == null) { + _seenLocale = localeKey; + _load(); + } + } + + // A locale change and `_goDay` can each kick off a `_load()` while a prior + // one is still in flight; whichever resolves last would otherwise win and + // could paint the wrong night. Stamp each call and only apply the result + // still holding the latest stamp. + int _loadGen = 0; + Future _load() async { + final gen = ++_loadGen; final repo = repoOf(context); if (repo == null) { - if (mounted) setState(() => _loading = false); + if (mounted && gen == _loadGen) setState(() => _loading = false); return; } try { @@ -352,11 +392,13 @@ class _SleepDetailState extends State { // stronger half of the reason. final rough = d.day == todayLabel() && Prefs.getString(kRoughNightDismissed, '') != d.day - ? await loadRoughNight(repo, d.day!) + ? await loadRoughNight(repo, d.day!, c: mounted ? context : null) : null; - if (mounted) setState(() => (_d = d, _rough = rough, _loading = false)); + if (mounted && gen == _loadGen) { + setState(() => (_d = d, _rough = rough, _loading = false)); + } } catch (_) { - if (mounted) setState(() => _loading = false); + if (mounted && gen == _loadGen) setState(() => _loading = false); } } @@ -374,26 +416,30 @@ class _SleepDetailState extends State { @override Widget build(BuildContext c) { final d = _d ?? const SleepData(); + final l = AppLocalizations.of(c); + final title = l?.sleepDetailNavTitle ?? 'Sleep'; if (_loading && _d == null) { - return detailScaffold(c, 'Sleep', const [ + return detailScaffold(c, title, const [ SizedBox(height: S.x8), Center(child: CircularProgressIndicator()), ]); } if (!d.hasNight) { - return detailScaffold(c, 'Sleep', [ + return detailScaffold(c, title, [ ...dayNavRow(_day ?? d.day, d.days, _goDay), const SizedBox(height: S.x2), // A day CAN be in `availableDays` and still hold no night — the band // was worn through the day and off overnight. Stepping onto one of // those says so and leaves the stepper above it, so it is a day you // walk off rather than a dead end. - const StatusCard( - 'No night to show', - 'No stretch of band recordings long enough to score.', - fix: 'Wear the band overnight and sync in the morning', + StatusCard( + l?.sleepDetailNoNightTitle ?? 'No night to show', + l?.sleepDetailNoNightBody ?? + 'No stretch of band recordings long enough to score.', + fix: l?.sleepDetailNoNightFix ?? + 'Wear the band overnight and sync in the morning', icon: LucideIcons.moon, ), ]); @@ -406,7 +452,7 @@ class _SleepDetailState extends State { // The stepper names the night, so the nav bar does not say it twice. With // one night on disk there is no stepper, and then the subtitle is the only // thing that dates the screen. - return detailScaffold(c, 'Sleep', + return detailScaffold(c, title, sub: d.days.length < 2 ? (d.day ?? '').toUpperCase() : '', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), @@ -422,11 +468,11 @@ class _SleepDetailState extends State { ...?_windowCard(c, p, d, n), // ── 3 · WHAT IT WAS MADE OF ── - Section('Stages', _stages(c, p, n)), + Section(l?.sleepDetailStagesSection ?? 'Stages', _stages(c, p, n)), // ── 4 · AGAINST THE USER'S OWN NIGHTS ── if (_versusUsual(c, p, d, n) case final versus?) - Section('Against your usual', versus), + Section(l?.sleepDetailVersusUsualSection ?? 'Against your usual', versus), // ── 5 · WHAT STOOD OUT ── // Named after the night the nav bar is already showing. "Unusual last @@ -435,15 +481,17 @@ class _SleepDetailState extends State { if (unusual != null) Section( (daysBehind(_noonOf(d.day)) ?? 0) <= 0 - ? 'Unusual last night' - : 'Unusual on ${prettyDay(d.day)}', + ? (l?.sleepDetailUnusualLastNight ?? 'Unusual last night') + : (l?.sleepDetailUnusualOnDay(prettyDay(d.day, l)) ?? + 'Unusual on ${prettyDay(d.day, l)}'), unusual), // ── 6 · THE SIGNALS UNDERNEATH ── - Section('Overnight signals', _overnight(c, p, d)), + Section(l?.sleepDetailOvernightSection ?? 'Overnight signals', + _overnight(c, p, d)), // ── 7 · ONE TAKEAWAY ── - Section('Tonight', _tonight(c, p, d)), + Section(l?.sleepDetailTonightSection ?? 'Tonight', _tonight(c, p, d)), const SizedBox(height: S.x5), // The night this screen is steered to, not the newest one. Dropping it @@ -458,6 +506,7 @@ class _SleepDetailState extends State { /// Total sleep, when it ran, and the two ratios that qualify it. Everything /// here is measured; nothing is a judgement. Widget _answer(BuildContext c, P p, SleepData d, Map n) { + final l = AppLocalizations.of(c); final tst = n['duration_min'] as num?; final eff = n['efficiency'] as num?; final inBed = n['in_bed_min'] as num?; @@ -474,7 +523,8 @@ class _SleepDetailState extends State { child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(hm(tst), style: F.n48.copyWith(color: p.ink)), const SizedBox(height: S.x1), - Text('Total sleep', style: F.cap.copyWith(color: p.ink3)), + Text(l?.sleepDetailTotalSleep ?? 'Total sleep', + style: F.cap.copyWith(color: p.ink3)), if (from.isNotEmpty && to.isNotEmpty) ...[ const SizedBox(height: S.x4), Row(children: [ @@ -490,19 +540,23 @@ class _SleepDetailState extends State { if (inBed != null || eff != null) ...[ const SizedBox(height: S.x4), InlineMetrics([ - if (inBed != null) ('IN BED', hm(inBed), C.indigo), - if (watched != null) ('WATCHED', hm(watched), C.sky), + if (inBed != null) (l?.sleepDetailInBed ?? 'IN BED', hm(inBed), C.indigo), + if (watched != null) + (l?.sleepDetailWatched ?? 'WATCHED', hm(watched), C.sky), if (eff != null) - (watched == null ? 'ASLEEP OF THAT' : 'ASLEEP', + (watched == null + ? (l?.sleepDetailAsleepOfThat ?? 'ASLEEP OF THAT') + : (l?.sleepDetailAsleep ?? 'ASLEEP'), _pct(eff * 100), C.green), ]), ], if (watched != null) ...[ const SizedBox(height: S.x3), Text( - 'We watched ${hm(watched)} of your ${hm(inBed!)} in bed; the rest ' - 'is not a measurement. Asleep, and the stage shares below, are out ' - 'of the time we watched.', + l?.sleepDetailWatchedExplain(hm(watched), hm(inBed!)) ?? + 'We watched ${hm(watched)} of your ${hm(inBed!)} in bed; the rest ' + 'is not a measurement. Asleep, and the stage shares below, are out ' + 'of the time we watched.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], @@ -526,6 +580,7 @@ class _SleepDetailState extends State { /// It needs a writer first. List? _windowCard( BuildContext c, P p, SleepData d, Map n) { + final l = AppLocalizations.of(c); final day = d.day; final t0 = (n['onset_ts'] as num?)?.round(); final t1 = (n['wake_ts'] as num?)?.round(); @@ -546,10 +601,12 @@ class _SleepDetailState extends State { Expanded( child: Text( mine - ? 'You set this window' + ? (l?.sleepDetailWindowMine ?? 'You set this window') : fallback - ? 'This window was inferred from heart rate' - : 'This window was staged from the signals', + ? (l?.sleepDetailWindowFallback ?? + 'This window was inferred from heart rate') + : (l?.sleepDetailWindowAuto ?? + 'This window was staged from the signals'), style: F.body.copyWith(color: p.ink), ), ), @@ -557,8 +614,9 @@ class _SleepDetailState extends State { if (fallback) ...[ const SizedBox(height: S.x2), Text( - 'Staging could not find the edges, so the times are a best ' - 'guess.', + l?.sleepDetailWindowFallbackBody ?? + 'Staging could not find the edges, so the times are a best ' + 'guess.', style: F.cap.copyWith(color: p.ink3), ), ], @@ -572,8 +630,9 @@ class _SleepDetailState extends State { if (mine && d.solMin != null) ...[ const SizedBox(height: S.x2), Text( - 'From the start of your window to asleep: ' - '${_solBand(d.solMin!)}.', + l?.sleepDetailWindowSol(_solBand(c, d.solMin!)) ?? + 'From the start of your window to asleep: ' + '${_solBand(c, d.solMin!)}.', style: F.cap.copyWith(color: p.ink3), ), ], @@ -582,27 +641,31 @@ class _SleepDetailState extends State { if (fallback) TextButton( onPressed: busy ? null : () => _confirmWindow(day), - child: const Text('These times are right'), + child: Text(l?.sleepDetailConfirmTimes ?? 'These times are right'), ), TextButton( onPressed: busy ? null : () => _editWindow(day, t0, t1), - child: Text(mine ? 'Change the times' : 'Set the times myself'), + child: Text(mine + ? (l?.sleepDetailChangeTimes ?? 'Change the times') + : (l?.sleepDetailSetTimesMyself ?? 'Set the times myself')), ), if (mine) TextButton( onPressed: busy ? null : () => _clearWindow(day), - child: const Text('Back to automatic'), + child: + Text(l?.sleepDetailBackToAutomatic ?? 'Back to automatic'), ), ]), if (busy) ...[ const SizedBox(height: S.x2), - Text('Re-analysing the night…', + Text(l?.sleepDetailReanalysing ?? 'Re-analysing the night…', style: F.cap.copyWith(color: p.ink3)), ], if (!busy && _overrideFailed != null) ...[ const SizedBox(height: S.x3), StatusCard( - 'That correction has not been applied', + l?.sleepDetailCorrectionFailedTitle ?? + 'That correction has not been applied', _overrideFailed!, icon: LucideIcons.triangleAlert, ), @@ -625,16 +688,17 @@ class _SleepDetailState extends State { Future _editWindow(String day, int t0, int t1) async { final onset = DateTime.fromMillisecondsSinceEpoch(t0 * 1000); final wake = DateTime.fromMillisecondsSinceEpoch(t1 * 1000); + final l = AppLocalizations.of(context); final bed = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(onset), - helpText: 'WHEN YOU GOT INTO BED', + helpText: l?.sleepDetailBedTimeHelp ?? 'WHEN YOU GOT INTO BED', ); if (bed == null || !mounted) return; final up = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(wake), - helpText: 'WHEN YOU GOT UP', + helpText: l?.sleepDetailWakeTimeHelp ?? 'WHEN YOU GOT UP', ); if (up == null || !mounted) return; final newOnset = @@ -681,7 +745,9 @@ class _SleepDetailState extends State { // window's source changes on all three actions, so an unchanged source // means nothing was restaged. if (failed != null || _source == before) { + final l = AppLocalizations.of(context); setState(() => _overrideFailed = failed ?? + l?.sleepDetailReanalyseFailed ?? 'The night was not re-analysed — another re-analysis was already ' 'running, or it failed. The times you set are saved; ' 'Re-analyze everything on Your data applies them.'); @@ -696,11 +762,13 @@ class _SleepDetailState extends State { /// cycle count rides underneath it because it is a property of this shape, /// not a section of its own. Widget _night(BuildContext c, P p, SleepData d, Map n) { + final l = AppLocalizations.of(c); final stages = d.stages; if (stages.isEmpty) { - return const StatusCard( - 'No hypnogram for this night', - 'Staging needs movement and beat timing. One was missing.', + return StatusCard( + l?.sleepDetailNoHypnogramTitle ?? 'No hypnogram for this night', + l?.sleepDetailNoHypnogramBody ?? + 'Staging needs movement and beat timing. One was missing.', icon: LucideIcons.chartNoAxesColumn, ); } @@ -711,8 +779,8 @@ class _SleepDetailState extends State { return Surface( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ ChartFrame( - title: 'Through the night', - unit: 'stage', + title: l?.sleepDetailThroughTheNight ?? 'Through the night', + unit: l?.sleepDetailUnitStage ?? 'stage', height: 132, xLabels: [ clockOfTs(t0), @@ -731,12 +799,19 @@ class _SleepDetailState extends State { const SizedBox(height: S.x2), Text( cycles > 0 - ? 'Tap or drag the chart for any moment. $cycles cycles' - '${mean == null ? '' : ', ${hm(mean)} on average'}.' - : 'Tap or drag the chart for any moment of the night.', + ? (mean == null + ? (l?.sleepDetailTapDragCycles(cycles) ?? + 'Tap or drag the chart for any moment. $cycles ' + '${cycles == 1 ? 'cycle' : 'cycles'}.') + : (l?.sleepDetailTapDragCyclesAvg(cycles, hm(mean)) ?? + 'Tap or drag the chart for any moment. $cycles ' + '${cycles == 1 ? 'cycle' : 'cycles'}, ${hm(mean)} ' + 'on average.')) + : (l?.sleepDetailTapDragNone ?? + 'Tap or drag the chart for any moment of the night.'), style: F.over.copyWith(color: p.ink3, height: 1.5), ), - if (_shape(d) case final shape?) ...[ + if (_shape(c, d) case final shape?) ...[ const SizedBox(height: S.x2), Text(shape, style: F.over.copyWith(color: p.ink3, height: 1.5)), ], @@ -752,17 +827,22 @@ class _SleepDetailState extends State { /// true number is higher than this one. Five minutes is a choice, not /// physiology, so it is stated rather than assumed. No arousal index, no /// explanation, only the shape. - String? _shape(SleepData d) { + String? _shape(BuildContext c, SleepData d) { + final l = AppLocalizations.of(c); final w = d.awakenings?.round(); final longest = d.longestSleepMin; final parts = [ if (w != null) w == 0 - ? 'No wake-ups of 5 minutes or more; shorter ones are invisible to ' - 'a wrist.' - : 'At least $w wake-up${w == 1 ? '' : 's'} of 5 minutes or more; ' - 'shorter ones are invisible to a wrist.', - if (longest != null) 'Longest unbroken stretch ${hm(longest)}.', + ? (l?.sleepDetailNoWakeups ?? + 'No wake-ups of 5 minutes or more; shorter ones are invisible to ' + 'a wrist.') + : (l?.sleepDetailAtLeastWakeups(w) ?? + 'At least $w wake-up${w == 1 ? '' : 's'} of 5 minutes or more; ' + 'shorter ones are invisible to a wrist.'), + if (longest != null) + l?.sleepDetailLongestStretch(hm(longest)) ?? + 'Longest unbroken stretch ${hm(longest)}.', ]; return parts.isEmpty ? null : parts.join(' '); } @@ -799,17 +879,22 @@ class _SleepDetailState extends State { Scrubber( value: _scrub, onChanged: (v) => setState(() => _scrub = v), - label: 'Hypnogram', + label: AppLocalizations.of(c)?.sleepDetailHypnogramLabel ?? 'Hypnogram', describe: (v) { + final l = AppLocalizations.of(c); final t0 = (n['onset_ts'] as num?)?.toInt(); final t1 = (n['wake_ts'] as num?)?.toInt(); final st = stages.isEmpty ? null : stages[(v * (stages.length - 1)).round().clamp(0, stages.length - 1)]; final at = (t0 == null || t1 == null || t1 <= t0) - ? '${(v * 100).round()}% through the night' + ? (l?.sleepDetailPercentThroughNight((v * 100).round()) ?? + '${(v * 100).round()}% through the night') : clockOfTs(t0 + ((t1 - t0) * v).round()); - return '$at, ${st == null ? 'not measured' : _stageName(st)}'; + final stageName = st == null + ? (l?.sleepDetailNotMeasured ?? 'not measured') + : _stageName(c, st); + return l?.sleepDetailScrubAt(at, stageName) ?? '$at, $stageName'; }, child: SizedBox( height: 132, @@ -844,6 +929,7 @@ class _SleepDetailState extends State { /// What every signal read at the scrubbed instant. Each line abstains on its /// own — a night with no respiration series still shows heart rate. Widget _scrubCard(BuildContext c, P p, SleepData d) { + final l = AppLocalizations.of(c); final n = d.night; final t0 = (n['onset_ts'] as num?)?.toInt(); final t1 = (n['wake_ts'] as num?)?.toInt(); @@ -872,12 +958,15 @@ class _SleepDetailState extends State { ? null : stages[(_scrub! * (stages.length - 1)).round()]; final items = <(String, String, Color)>[ - if (at('hr') != null) ('Heart rate', '${at('hr')!.round()} bpm', C.red), - if (at('hrv') != null) ('HRV', '${at('hrv')!.round()} ms', C.green), + if (at('hr') != null) + (l?.sleepDetailHeartRate ?? 'Heart rate', '${at('hr')!.round()} bpm', C.red), + if (at('hrv') != null) + (l?.sleepDetailHrv ?? 'HRV', '${at('hrv')!.round()} ms', C.green), if (at('resp') != null) - ('Breathing', '${at('resp')!.toStringAsFixed(1)} br/min', C.teal), + (l?.sleepDetailBreathing ?? 'Breathing', + '${at('resp')!.toStringAsFixed(1)} br/min', C.teal), if (at('skin_temp') != null) - ('Temp', at('skin_temp')!.toStringAsFixed(2), C.orange), + (l?.sleepDetailTemp ?? 'Temp', at('skin_temp')!.toStringAsFixed(2), C.orange), ]; return Padding( @@ -891,15 +980,16 @@ class _SleepDetailState extends State { style: F.body.copyWith(color: p.ink, fontWeight: FontWeight.w600)), const Spacer(), if (stage != null) - Pill(_stageName(stage), Hypnogram.pigment[stage] ?? C.blue) + Pill(_stageName(c, stage), Hypnogram.pigment[stage] ?? C.blue) else if (stages.isNotEmpty) // Not a stage, so not a Pill: this instant has no colour because // the band was not recording it. - Text('Not measured', style: F.cap.copyWith(color: p.ink3)), + Text(l?.sleepDetailNotMeasuredCap ?? 'Not measured', + style: F.cap.copyWith(color: p.ink3)), ]), if (items.isEmpty) ...[ const SizedBox(height: S.x3), - Text('No signal recorded at this moment.', + Text(l?.sleepDetailNoSignalAtMoment ?? 'No signal recorded at this moment.', style: F.cap.copyWith(color: p.ink3)), ] else ...[ const SizedBox(height: S.x4), @@ -910,12 +1000,15 @@ class _SleepDetailState extends State { ); } - String _stageName(SleepStage s) => switch (s) { - SleepStage.awake => 'Awake', - SleepStage.rem => 'REM', - SleepStage.light => 'Light sleep', - SleepStage.deep => 'Deep sleep', - }; + String _stageName(BuildContext c, SleepStage s) { + final l = AppLocalizations.of(c); + return switch (s) { + SleepStage.awake => l?.sleepDetailStageAwake ?? 'Awake', + SleepStage.rem => l?.sleepDetailStageRem ?? 'REM', + SleepStage.light => l?.sleepDetailStageLight ?? 'Light sleep', + SleepStage.deep => l?.sleepDetailStageDeep ?? 'Deep sleep', + }; + } /// One stage of the night: a name and what it came to. The value is one /// string — the range for a staged figure, a plain duration for Awake — so @@ -967,18 +1060,21 @@ class _SleepDetailState extends State { /// no interval for it. Inventing one here would be exactly the fabricated /// precision this item removes. Widget _stages(BuildContext c, P p, Map n) { + final l = AppLocalizations.of(c); final r = _ranges(n); final awake = n['awake_min'] as num?; final rows = <(String, String, Color)>[ - if (r != null) ('Deep', _rangeText(r.deep), C.blue), - if (r != null) ('REM', _rangeText(r.rem), C.teal), - if (r != null) ('Light', _rangeText(r.light), C.sky), - if (awake != null) ('Awake', hm(awake), C.orange), + if (r != null) (l?.sleepDetailDeep ?? 'Deep', _rangeText(r.deep), C.blue), + if (r != null) (l?.sleepDetailStageRem ?? 'REM', _rangeText(r.rem), C.teal), + if (r != null) (l?.sleepDetailLight ?? 'Light', _rangeText(r.light), C.sky), + if (awake != null) + (l?.sleepDetailStageAwake ?? 'Awake', hm(awake), C.orange), ]; if (rows.isEmpty) { - return const StatusCard( - 'No stage split for this night', - 'No beat timing across the whole window.', + return StatusCard( + l?.sleepDetailNoStageSplitTitle ?? 'No stage split for this night', + l?.sleepDetailNoStageSplitBody ?? + 'No beat timing across the whole window.', icon: LucideIcons.chartNoAxesColumn, ); } @@ -1002,9 +1098,10 @@ class _SleepDetailState extends State { // The width is this night's own — better coverage, narrower range — // rather than one published figure applied to every night. Text( - 'Each stage is a range, not a count — the better we saw the night, ' - 'the narrower it is. Deep is the widest. Awake stays one figure. ' - 'Nerd stats has the exact counts.', + l?.sleepDetailStageRangeExplain ?? + 'Each stage is a range, not a count — the better we saw the night, ' + 'the narrower it is. Deep is the widest. Awake stays one figure. ' + 'Nerd stats has the exact counts.', style: F.over.copyWith(color: p.ink3, height: 1.5)), ], ]); @@ -1023,18 +1120,19 @@ class _SleepDetailState extends State { /// twice; the strip IS the delta, the sentence beside it IS the verdict. Widget? _versusUsual( BuildContext c, P p, SleepData d, Map n) { + final l = AppLocalizations.of(c); final rows = []; final tst = (n['duration_min'] as num?)?.toDouble(); if (tst != null) { rows.add(_Compare( - label: 'Time asleep', + label: l?.sleepDetailTimeAsleep ?? 'Time asleep', value: hm(tst), tonight: tst, history: d.tstHistory, color: C.indigo, - low: 'shorter than usual', - high: 'longer than usual', + low: l?.sleepDetailShorterThanUsual ?? 'shorter than usual', + high: l?.sleepDetailLongerThanUsual ?? 'longer than usual', fmt: (v) => hm(v), dfmt: (v) => hm(v), )); @@ -1050,14 +1148,14 @@ class _SleepDetailState extends State { if (deepRange != null) { final deep = deepRange.pointSec / 60; rows.add(_Compare( - label: 'Deep sleep', + label: l?.sleepDetailStageDeep ?? 'Deep sleep', value: _rangeText(deepRange), tonight: deep, blur: (deepRange.hiSec - deepRange.loSec) / 120, history: d.deepHistory, color: C.blue, - low: 'less than usual', - high: 'more than usual', + low: l?.sleepDetailLessThanUsual ?? 'less than usual', + high: l?.sleepDetailMoreThanUsual ?? 'more than usual', fmt: (v) => hm(v), dfmt: (v) => hm(v), )); @@ -1066,13 +1164,13 @@ class _SleepDetailState extends State { final eff = (n['efficiency'] as num?)?.toDouble(); if (eff != null) { rows.add(_Compare( - label: 'Asleep while in bed', + label: l?.sleepDetailAsleepWhileInBed ?? 'Asleep while in bed', value: _pct(eff * 100), tonight: eff * 100, history: d.effHistory, color: C.green, - low: 'lower than usual', - high: 'higher than usual', + low: l?.sleepDetailLowerThanUsual ?? 'lower than usual', + high: l?.sleepDetailHigherThanUsual ?? 'higher than usual', fmt: _pct, dfmt: _pts, )); @@ -1087,13 +1185,13 @@ class _SleepDetailState extends State { if (onset != null && d.onsetHistory.isNotEmpty) { final rel = [for (final o in d.onsetHistory) _relMinutes(o, onset)]; rows.add(_Compare( - label: 'Fell asleep', + label: l?.sleepDetailFellAsleep ?? 'Fell asleep', value: clockOfTs(onset), tonight: 0, history: rel, color: C.purple, - low: 'earlier than usual', - high: 'later than usual', + low: l?.sleepDetailEarlierThanUsual ?? 'earlier than usual', + high: l?.sleepDetailLaterThanUsual ?? 'later than usual', fmt: (v) => clockOfTs(onset + (v * 60).round()), dfmt: (v) => hm(v), )); @@ -1112,9 +1210,10 @@ class _SleepDetailState extends State { // card took the count with it. if (rows.isEmpty || have < _minNights) { return StatusCard( - 'Not enough nights to compare', + l?.sleepDetailNotEnoughNightsTitle ?? 'Not enough nights to compare', '', - fix: '$have of $_minNights nights so far', + fix: l?.sleepDetailNightsSoFar(have, _minNights) ?? + '$have of $_minNights nights so far', icon: LucideIcons.chartNoAxesColumn, ); } @@ -1129,7 +1228,9 @@ class _SleepDetailState extends State { ]), ), const SizedBox(height: S.x2), - Text('The bar is the middle half of your own nights.', + Text( + l?.sleepDetailBarExplain ?? + 'The bar is the middle half of your own nights.', style: F.over.copyWith(color: p.ink3, height: 1.5)), ]); } @@ -1153,6 +1254,7 @@ class _SleepDetailState extends State { /// extreme is already visible one section up, where it belongs. When nothing /// qualifies, that is the answer and it is shown. Widget? _unusual(BuildContext c, P p, SleepData d, Map n) { + final l = AppLocalizations.of(c); final items = []; void extreme( @@ -1172,22 +1274,29 @@ class _SleepDetailState extends State { if (v < lo) { items.add(InsightCard( lowLabel, - '$noun ${fmt(v)} — less than any of your last ${hist.length} ' - 'nights, the lowest of which was ${fmt(lo)}.', + l?.sleepDetailLessThanAny(noun, fmt(v), hist.length, fmt(lo)) ?? + '$noun ${fmt(v)} — less than any of your last ${hist.length} ' + 'nights, the lowest of which was ${fmt(lo)}.', icon: LucideIcons.trendingDown, color: C.orange)); } else if (v > hi) { items.add(InsightCard( highLabel, - '$noun ${fmt(v)} — more than any of your last ${hist.length} ' - 'nights, the highest of which was ${fmt(hi)}.', + l?.sleepDetailMoreThanAny(noun, fmt(v), hist.length, fmt(hi)) ?? + '$noun ${fmt(v)} — more than any of your last ${hist.length} ' + 'nights, the highest of which was ${fmt(hi)}.', icon: LucideIcons.trendingUp, color: C.green)); } } - extreme((n['duration_min'] as num?)?.toDouble(), d.tstHistory, 'You slept', - 'Your shortest night lately', 'Your longest night lately', hm); + extreme( + (n['duration_min'] as num?)?.toDouble(), + d.tstHistory, + l?.sleepDetailYouSlept ?? 'You slept', + l?.sleepDetailShortestNightLately ?? 'Your shortest night lately', + l?.sleepDetailLongestNightLately ?? 'Your longest night lately', + hm); // SLP-13a — NO deep-sleep extreme. `segment.dart` emits // `deep_low_confidence` and calls the Light/Deep split unvalidated; ranking // last night's deep minutes against 28 other nights of the same unvalidated @@ -1220,10 +1329,11 @@ class _SleepDetailState extends State { noc['elevated'] == true && vsBase != null) { items.add(InsightCard( - 'Sleeping heart rate ran high', - '${vsBase.toStringAsFixed(1)} bpm above your own baseline. Common ' - 'after alcohol, a late meal, a hard session or an infection ' - 'starting — this is a measurement, not a diagnosis.', + l?.sleepDetailSleepingHrHighTitle ?? 'Sleeping heart rate ran high', + l?.sleepDetailSleepingHrHighBody(vsBase.toStringAsFixed(1)) ?? + '${vsBase.toStringAsFixed(1)} bpm above your own baseline. Common ' + 'after alcohol, a late meal, a hard session or an infection ' + 'starting — this is a measurement, not a diagnosis.', icon: LucideIcons.heartPulse, color: C.red, )); @@ -1241,7 +1351,7 @@ class _SleepDetailState extends State { Icon(LucideIcons.check, size: 16, color: p.on(C.green)), const SizedBox(width: S.x3), Expanded( - child: Text('Nothing stood out.', + child: Text(l?.sleepDetailNothingStoodOut ?? 'Nothing stood out.', style: F.cap.copyWith(color: p.ink2, height: 1.5)), ), ]), @@ -1259,6 +1369,7 @@ class _SleepDetailState extends State { // ── OVERNIGHT SIGNALS ───────────────────────────────────────────────────── Widget _overnight(BuildContext c, P p, SleepData d) { + final loc = AppLocalizations.of(c); /// One lane as `(timestamp, value)`. The timestamp is the point — the /// signals arrive on different cadences. List<(int, double)> stamped(String key) { @@ -1290,16 +1401,19 @@ class _SleepDetailState extends State { // others rather than a reading of its own. final summary = <(String, String, Color)>[ if (noc is Map && noc['sleeping_hr_avg'] != null) - ('SLEEPING HR', '${noc['sleeping_hr_avg']} bpm', C.red), + (loc?.sleepDetailSleepingHr ?? 'SLEEPING HR', + '${noc['sleeping_hr_avg']} bpm', C.red), if (noc is Map && noc['sleeping_hr_min'] != null) - ('LOWEST', '${noc['sleeping_hr_min']} bpm', C.blue), + (loc?.sleepDetailLowest ?? 'LOWEST', '${noc['sleeping_hr_min']} bpm', + C.blue), if (respV != null) - ('BREATHING', '${respV.toStringAsFixed(1)} br/min', C.teal), + (loc?.sleepDetailBreathingCaps ?? 'BREATHING', + '${respV.toStringAsFixed(1)} br/min', C.teal), ]; if (all.isEmpty) { return summary.isEmpty - ? _noOvernightLines + ? _noOvernightLines(c) : Surface(child: InlineMetrics(summary)); } @@ -1397,16 +1511,17 @@ class _SleepDetailState extends State { // Solved against the card, like every other mark: raw pigment measures // 1.7-2.5:1 on white and a lane's colour is what tells you which signal // you are looking at. - lane(hr, 'Heart rate', 'bpm', p.on(C.red)); - lane(hrv, 'HRV', 'ms', p.on(C.green)); - lane(resp, 'Breathing', 'br/min', p.on(C.teal)); + lane(hr, loc?.sleepDetailHeartRate ?? 'Heart rate', 'bpm', p.on(C.red)); + lane(hrv, loc?.sleepDetailHrv ?? 'HRV', 'ms', p.on(C.green)); + lane(resp, loc?.sleepDetailBreathing ?? 'Breathing', 'br/min', p.on(C.teal)); // Skin temperature is ADC-relative — a deviation, never a °C. The unit // says so rather than implying a thermometer. - lane(temp, 'Skin temp', 'rel', p.on(C.orange), format: axisFixed); + lane(temp, loc?.sleepDetailSkinTemp ?? 'Skin temp', 'rel', p.on(C.orange), + format: axisFixed); if (series.isEmpty) { return summary.isEmpty - ? _noOvernightLines + ? _noOvernightLines(c) : Surface(child: InlineMetrics(summary)); } return Surface( @@ -1416,7 +1531,7 @@ class _SleepDetailState extends State { const SizedBox(height: S.x5), ], ChartFrame( - title: 'Through the night', + title: loc?.sleepDetailThroughTheNight ?? 'Through the night', unit: units.join(' · '), height: 44.0 * series.length + 20, xLabels: [ @@ -1441,18 +1556,23 @@ class _SleepDetailState extends State { /// target bed and target wake — six numbers, no instruction. A target bedtime /// is the only one of them anybody can act on before midnight. Widget _tonight(BuildContext c, P p, SleepData d) { + final l = AppLocalizations.of(c); final need = d.need.value; final bed = d.bedtime.value; final debt = d.debt.value; if (need == null && bed == null) { - return StatusCard.forMetric('Sleep need not established', d.need) ?? + return StatusCard.forMetric( + l?.sleepDetailSleepNeedNotEstablished ?? + 'Sleep need not established', + d.need) ?? const SizedBox.shrink(); } final reason = [ - if (need != null) 'Your need is ${hm(need)}', - if (debt != null && debt >= 1) 'you are ${hm(debt)} down', + if (need != null) l?.sleepDetailYourNeedIs(hm(need)) ?? 'Your need is ${hm(need)}', + if (debt != null && debt >= 1) + l?.sleepDetailYouAreDown(hm(debt)) ?? 'you are ${hm(debt)} down', ].join(', '); return Surface( @@ -1467,7 +1587,10 @@ class _SleepDetailState extends State { ), const SizedBox(width: S.x2), Flexible( - child: Text(bed != null ? 'lights out' : 'to aim for', + child: Text( + bed != null + ? (l?.sleepDetailLightsOut ?? 'lights out') + : (l?.sleepDetailToAimFor ?? 'to aim for'), style: F.cap.copyWith(color: p.ink3)), ), ]), @@ -1523,6 +1646,7 @@ class _Compare extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final band = _band(history); final head = Row(children: [ @@ -1542,7 +1666,8 @@ class _Compare extends StatelessWidget { head, const SizedBox(height: S.x1), Text( - 'No personal range yet — ${history.length} of $_minNights nights.', + l?.sleepDetailNoPersonalRangeYet(history.length, _minNights) ?? + 'No personal range yet — ${history.length} of $_minNights nights.', style: F.over.copyWith(color: p.ink3)), ]); } @@ -1557,8 +1682,9 @@ class _Compare extends StatelessWidget { // NOT "typical". The interval overlaps the band, which means // this night is not far enough from usual for us to tell — // a different statement, and the honest one. - ? 'Not far enough from usual to call' - : 'Typical for you'; + ? (l?.sleepDetailNotFarEnoughToCall ?? + 'Not far enough from usual to call') + : (l?.sleepDetailTypicalForYou ?? 'Typical for you'); final lo = math.min(tonight, history.reduce(math.min)); final hi = math.max(tonight, history.reduce(math.max)); @@ -1574,8 +1700,11 @@ class _Compare extends StatelessWidget { mark: tonight, color: color), const SizedBox(height: S.x2), - Text('$verdict · usual ${fmt(band.lo)}–${fmt(band.hi)} over ${band.n} ' - 'nights', + Text( + l?.sleepDetailVerdictSummary( + verdict, fmt(band.lo), fmt(band.hi), band.n) ?? + '$verdict · usual ${fmt(band.lo)}–${fmt(band.hi)} over ${band.n} ' + 'nights', style: F.over.copyWith(color: p.ink3, height: 1.5)), ]); } diff --git a/lib/ui2/screens/start_card.dart b/lib/ui2/screens/start_card.dart index ed42fa35..5b4771b1 100644 --- a/lib/ui2/screens/start_card.dart +++ b/lib/ui2/screens/start_card.dart @@ -34,6 +34,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; class StartCard extends StatelessWidget { @@ -45,7 +46,7 @@ class StartCard extends StatelessWidget { required this.asset, required this.accent, required this.deep, - this.sub = 'Pick one and go', + this.sub, this.mascotHeight = 126, this.onTap, }); @@ -71,7 +72,10 @@ class StartCard extends StatelessWidget { /// enough that the mascot is the brightest thing on the card. final Color accent, deep; - final String sub; + /// The subline under the count, e.g. "Pick one and go". Nullable so the + /// default copy can be localized in [build] rather than baked into a + /// const-context default parameter value. + final String? sub; /// The height of the ART, which is only true while the assets are cropped to /// their own alpha bounds. A mascot exported with transparent padding renders @@ -85,6 +89,8 @@ class StartCard extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); + final subText = sub ?? (l?.startCardDefaultSub ?? 'Pick one and go'); // No side radius when bleeding — a rounded corner against the screen edge // reads as a card that failed to fit. final card = Pressable( @@ -127,7 +133,7 @@ class StartCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis), const SizedBox(height: S.x1), - Text(sub, + Text(subText, style: F.cap.copyWith( color: C.white.withValues(alpha: .8)), maxLines: 1, diff --git a/lib/ui2/screens/wellness_screen.dart b/lib/ui2/screens/wellness_screen.dart index 33785447..830f0e7c 100644 --- a/lib/ui2/screens/wellness_screen.dart +++ b/lib/ui2/screens/wellness_screen.dart @@ -24,6 +24,7 @@ import '../../data/db.dart'; import '../../data/day_label.dart'; import '../../data/journal_fields.dart'; import '../../data/med_store.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart' show whyFromNote; import '../../state/app_state.dart'; import '../../stress/breath_phases.dart'; @@ -31,7 +32,7 @@ import '../ui2.dart'; import 'calm_breathing.dart'; import 'driver_breakdown.dart'; import 'cycle_screen.dart'; -import 'home_screen.dart' show envValue, metricOf; +import 'home_screen.dart' show envValue, metricOf, weekdayShortName; import 'journal_compose.dart'; import 'start_card.dart'; import 'metric_detail.dart' show detailScaffold; @@ -62,6 +63,9 @@ class WellnessScreen extends StatefulWidget { /// On the widget rather than the state so [medsTab] can be checked against /// it — a deep link that lands on the wrong tab because the list was /// reordered is not a failure anything else would catch. + /// + /// Fallback labels only — index bookkeeping uses `.length`, and the actual + /// display labels are localized in `build`. static const tabs = ['Mind', 'Recovery', 'Habits', 'Medication', 'Cycle']; static const int medsTab = 3; @@ -212,13 +216,21 @@ class _WellnessScreenState extends State with RevisionReload { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); final last = _breathing.isEmpty ? null : _breathing.first; // `select`, not `watch`: this screen lives in the shell's IndexedStack and // stays mounted, so a plain watch would rebuild it on every unrelated // AppState notification for the life of the app. final showCycle = c.select((a) => a.cycleTrackingEnabled); - final tabs = showCycle ? _tabs : _tabs.take(_tabs.length - 1).toList(); + final labels = [ + l?.wellnessTabMind ?? 'Mind', + l?.wellnessTabRecovery ?? 'Recovery', + l?.wellnessTabHabits ?? 'Habits', + l?.wellnessTabMedication ?? 'Medication', + l?.wellnessTabCycle ?? 'Cycle', + ]; + final tabs = showCycle ? labels : labels.take(labels.length - 1).toList(); // Clamped rather than reset: switching Cycle off while standing on it // lands on Medication, not back at Mind. final tab = _tab.clamp(0, tabs.length - 1); @@ -231,7 +243,7 @@ class _WellnessScreenState extends State with RevisionReload { padding: const EdgeInsets.fromLTRB(0, S.x4, 0, S.x16), children: [ for (final w in [ - const ScreenTitle('Wellness'), + ScreenTitle(l?.wellnessTitle ?? 'Wellness'), SubTabs(tabs, tab, (i) => setState(() => _tab = i), color: C.domMind), const SizedBox(height: S.x5), @@ -243,14 +255,16 @@ class _WellnessScreenState extends State with RevisionReload { // list would be an invitation to nothing. if (tab == 0) ...[ StartCard( - label: 'START A SITTING', + label: l?.wellnessStartASitting ?? 'START A SITTING', // What the picker actually offers. Three, not the number of // things on this tab. count: kBreathPatterns.length, - noun: 'exercises', + noun: l?.wellnessExercisesNoun ?? 'exercises', sub: last == null - ? 'Pick one and go' - : 'Last: ${(_reading(last['seconds']) ?? 0) ~/ 60} min', + ? (l?.wellnessPickOneAndGo ?? 'Pick one and go') + : (l?.wellnessLastMinutes( + (_reading(last['seconds']) ?? 0) ~/ 60) ?? + 'Last: ${(_reading(last['seconds']) ?? 0) ~/ 60} min'), asset: 'mascot_wellness.png', accent: C.domMind, deep: C.teal, @@ -297,13 +311,14 @@ class _WellnessScreenState extends State with RevisionReload { // ── MIND ───────────────────────────────────────────────────────────────── Widget _mind(BuildContext c) { + final l = AppLocalizations.of(c); // Same rule as `_recovery`'s coach block, and for the same reason: this // runs inside `build`, so a leaf of the wrong type here costs the whole // screen rather than this one card. See [_reading]. final stress = _stress['stress']; final score = _reading(stress is Map ? stress['score'] : null); final level = stress is Map && stress['level'] is String - ? stress['level'] as String + ? _stressLevelLabel(l, stress['level'] as String) : null; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -317,12 +332,12 @@ class _WellnessScreenState extends State with RevisionReload { ), const SizedBox(height: S.x4), ActionCard( - 'Write the day down', + l?.wellnessWriteTheDayDown ?? 'Write the day down', // Named from the field specs the journal actually holds. The old // literal listed four fields and went stale the moment a custom one // was added. - _journalSubtitle(), - 'Open', + _journalSubtitle(l), + l?.wellnessOpen ?? 'Open', LucideIcons.notebookPen, C.blue, onTap: () async { @@ -333,21 +348,22 @@ class _WellnessScreenState extends State with RevisionReload { }, ), Section( - 'Stress last night', + l?.wellnessStressLastNight ?? 'Stress last night', score == null // "Last night had none" was a claim about a gate this screen // never read — the stress payload carries no reason, so the card // states what stress IS and stops there. - ? const StatusCard( - 'No stress reading last night', - 'Stress is read from beat timing while you were resting ' - 'overnight, and last night produced no reading.', + ? StatusCard( + l?.wellnessNoStressTitle ?? 'No stress reading last night', + l?.wellnessNoStressBody ?? + 'Stress is read from beat timing while you were resting ' + 'overnight, and last night produced no reading.', icon: LucideIcons.activity, ) : SignalCard( LucideIcons.activity, C.purple, - 'Autonomic tension', + l?.wellnessAutonomicTension ?? 'Autonomic tension', score.round().toString(), unit: '/100', sub: (level ?? '').toUpperCase(), @@ -358,12 +374,23 @@ class _WellnessScreenState extends State with RevisionReload { } /// What the journal will actually ask you, read off its own field specs. - String _journalSubtitle() { - final names = [for (final f in _fields) f.label.toLowerCase()]; - if (names.isEmpty) return 'Anything you want to remember about today'; - if (names.length <= 4) return '${names.join(', ')} and a note'; - return '${names.take(4).join(', ')} and ' - '${names.length - 4} more, plus a note'; + String _journalSubtitle(AppLocalizations? l) { + // Not `.toLowerCase()`: these are user-entered/localized field labels + // (acronyms like HRV, or nouns a language capitalizes) — lowercasing + // them here would corrupt content the join has no business rewriting. + final names = [for (final f in _fields) f.label]; + if (names.isEmpty) { + return l?.wellnessJournalDefaultSubtitle ?? + 'Anything you want to remember about today'; + } + if (names.length <= 4) { + final joined = names.join(', '); + return l?.wellnessJournalSubtitleShort(joined) ?? '$joined and a note'; + } + final joined = names.take(4).join(', '); + final more = names.length - 4; + return l?.wellnessJournalSubtitleLong(joined, more) ?? + '$joined and $more more, plus a note'; } Future _setField(String key, double? v) async { @@ -395,6 +422,7 @@ class _WellnessScreenState extends State with RevisionReload { // ── RECOVERY ───────────────────────────────────────────────────────────── Widget _recovery(BuildContext c) { + final l = AppLocalizations.of(c); final coach = _insights['sleep_coach']; final coachMap = coach is Map ? coach.cast() : null; final needSec = _nested(coachMap, 'need', 'need_sec'); @@ -415,10 +443,12 @@ class _WellnessScreenState extends State with RevisionReload { Padding( padding: const EdgeInsets.only(bottom: S.x5), child: Recommendation( - 'Turn in by ${formatMinuteOfDay(bedMin.round())}', - 'You are ${_hm(debtH * 60)} down against your own need, and ' - 'tonight\'s is ${_hm(needSec / 60)}.', - 'See what last night cost you', + l?.wellnessTurnInBy(formatMinuteOfDay(bedMin.round())) ?? + 'Turn in by ${formatMinuteOfDay(bedMin.round())}', + l?.wellnessDebtBody(_hm(debtH * 60), _hm(needSec / 60)) ?? + 'You are ${_hm(debtH * 60)} down against your own need, and ' + 'tonight\'s is ${_hm(needSec / 60)}.', + l?.wellnessSeeWhatLastNightCost ?? 'See what last night cost you', color: C.indigo, onTap: () => Navigator.of(c).push( MaterialPageRoute(builder: (_) => const SleepDetail()), @@ -426,31 +456,34 @@ class _WellnessScreenState extends State with RevisionReload { ), ), Section( - 'What charged and drained you', + l?.wellnessWhatChargedAndDrained ?? 'What charged and drained you', // Two words and a full stop, before: "hrv", "rhr". No reading, no // usual, no direction, no size, and no way to tell a move that // mattered from one inside the noise — all of which were already // being written on every derive and read by nothing. _drivers.isEmpty ? StatusCard( - 'No readiness drivers yet', + l?.wellnessNoDriversTitle ?? 'No readiness drivers yet', whyFromNote(metricOf(_stress['readiness']).note) ?? - 'Needs enough nights to know what normal looks like ' - 'for you.', + (l?.wellnessNoDriversBody ?? + 'Needs enough nights to know what normal looks like ' + 'for you.'), icon: LucideIcons.sparkles, ) : DriverBreakdown(_drivers), ), Section( - 'Sleep need tonight', + l?.wellnessSleepNeedTonight ?? 'Sleep need tonight', needSec == null // The coach's own reason for the absent need — it names the // input that is actually missing. "Not enough of them yet" named // nothing, and was printed for every cause the estimator has. ? StatusCard( - 'No sleep need yet', + l?.wellnessNoSleepNeedTitle ?? 'No sleep need yet', whyFromNote(_noteOf(coachMap?['need'])) ?? - 'Nothing recorded says why there is no need for tonight.', + (l?.wellnessNoSleepNeedBody ?? + 'Nothing recorded says why there is no need for ' + 'tonight.'), icon: LucideIcons.bedDouble, ) : Surface( @@ -459,7 +492,7 @@ class _WellnessScreenState extends State with RevisionReload { MetricRow( LucideIcons.bedDouble, C.blue, - 'Tonight\'s need', + l?.wellnessTonightsNeed ?? 'Tonight\'s need', _hm(needSec / 60), ), // Null here means "we do not know", which is why it is a @@ -468,14 +501,14 @@ class _WellnessScreenState extends State with RevisionReload { MetricRow( LucideIcons.trendingDown, C.orange, - 'Sleep debt', + l?.wellnessSleepDebt ?? 'Sleep debt', _hm(debtH * 60), ), if (strainMin != null) MetricRow( LucideIcons.flame, C.purple, - 'Added for strain', + l?.wellnessAddedForStrain ?? 'Added for strain', '${strainMin.round()}', unit: 'min', ), @@ -483,7 +516,7 @@ class _WellnessScreenState extends State with RevisionReload { MetricRow( LucideIcons.sun, C.yellow, - 'Credited from naps', + l?.wellnessCreditedFromNaps ?? 'Credited from naps', '${napMin.round()}', unit: 'min', ), @@ -491,14 +524,14 @@ class _WellnessScreenState extends State with RevisionReload { MetricRow( LucideIcons.moon, C.indigo, - 'Target bedtime', + l?.wellnessTargetBedtime ?? 'Target bedtime', formatMinuteOfDay(bedMin.round()), ), if (wakeMin != null) MetricRow( LucideIcons.sunrise, C.orange, - 'Target wake', + l?.wellnessTargetWake ?? 'Target wake', formatMinuteOfDay(wakeMin.round()), ), ], @@ -519,6 +552,7 @@ class _WellnessScreenState extends State with RevisionReload { Widget _habitsTab(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -544,7 +578,9 @@ class _WellnessScreenState extends State with RevisionReload { // everything": the delete path existed end to end and // nothing reached it. Pressable( - semanticLabel: 'Remove ${h.label}', + semanticLabel: + l?.wellnessRemoveHabitSemantic(h.label) ?? + 'Remove ${h.label}', onTap: () => _confirmRemoveHabit(h), child: Padding( padding: const EdgeInsets.only(right: S.x3), @@ -575,7 +611,7 @@ class _WellnessScreenState extends State with RevisionReload { Consistency( _habitDaysDone(h.key), _habitDays, - 'Days you did it', + l?.wellnessDaysYouDidIt ?? 'Days you did it', C.domMind, ), ], @@ -584,7 +620,7 @@ class _WellnessScreenState extends State with RevisionReload { ), const SizedBox(height: S.x4), BigButton( - 'Add a habit', + l?.wellnessAddAHabit ?? 'Add a habit', icon: LucideIcons.plus, color: C.domMind, soft: true, @@ -596,9 +632,10 @@ class _WellnessScreenState extends State with RevisionReload { // this tab would bury the thing the tab is for, which is ticking. const SizedBox(height: S.x5), ActionCard( - 'What you log, against your numbers', - 'Dose, habit difference, and the day of the week', - 'Open', + l?.wellnessWhatYouLogTitle ?? 'What you log, against your numbers', + l?.wellnessWhatYouLogSubtitle ?? + 'Dose, habit difference, and the day of the week', + l?.wellnessOpen ?? 'Open', LucideIcons.scatterChart, C.domMind, onTap: () => Navigator.of(c).push( @@ -624,10 +661,13 @@ class _WellnessScreenState extends State with RevisionReload { /// that quietly keeps data is as much of a surprise as one that quietly loses /// it, so the confirm says which this is. Future _confirmRemoveHabit(JournalFieldSpec h) async { + final l = AppLocalizations.of(context); final ok = await confirmRemove( context, - title: 'Remove ${h.label}?', - body: 'It stops being asked. The days you already recorded stay.', + title: l?.wellnessRemoveHabitConfirmTitle(h.label) ?? + 'Remove ${h.label}?', + body: l?.wellnessRemoveHabitConfirmBody ?? + 'It stops being asked. The days you already recorded stay.', ); if (!ok || !mounted) return; final repo = context.read().repo; @@ -637,7 +677,12 @@ class _WellnessScreenState extends State with RevisionReload { } Future _addHabit(BuildContext c) async { - final name = await _askName(c, 'Add a habit', 'Walk after lunch'); + final l = AppLocalizations.of(c); + final name = await _askName( + c, + l?.wellnessAddAHabit ?? 'Add a habit', + l?.wellnessHabitHint ?? 'Walk after lunch', + ); if (name == null || name.isEmpty || !mounted) return; final repo = context.read().repo; if (repo == null) return; @@ -658,7 +703,12 @@ class _WellnessScreenState extends State with RevisionReload { // instead of silently rewriting the first one's definition. if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('You already track "$name".')), + SnackBar( + content: Text( + AppLocalizations.of(context)?.wellnessAlreadyTrack(name) ?? + 'You already track "$name".', + ), + ), ); return; } @@ -669,14 +719,15 @@ class _WellnessScreenState extends State with RevisionReload { // ── MEDICATION ─────────────────────────────────────────────────────────── Widget _medication(BuildContext c) { + final l = AppLocalizations.of(c); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (_meds.isEmpty) StatusCard( - 'Nothing scheduled', - 'Add what you take and when.', - fix: 'Add a medication', + l?.wellnessNothingScheduledTitle ?? 'Nothing scheduled', + l?.wellnessNothingScheduledBody ?? 'Add what you take and when.', + fix: l?.wellnessAddAMedication ?? 'Add a medication', icon: LucideIcons.pill, onFix: () => _addMed(c), ) @@ -689,9 +740,10 @@ class _WellnessScreenState extends State with RevisionReload { // tracker, nothing said. Absence states its reason, and the schedule // itself is the reason, so it is what gets printed. if (_slots.isEmpty) ...[ - const StatusCard( - 'Nothing due today', - 'What you take is scheduled for other days or times.', + StatusCard( + l?.wellnessNothingDueTodayTitle ?? 'Nothing due today', + l?.wellnessNothingDueTodayBody ?? + 'What you take is scheduled for other days or times.', icon: LucideIcons.pill, ), const SizedBox(height: S.x3), @@ -724,32 +776,34 @@ class _WellnessScreenState extends State with RevisionReload { ), ), Section( - 'Adherence', + l?.wellnessAdherence ?? 'Adherence', // An empty denominator is not an adherence of nothing. Consistency // would print "0 of 0 days" with an empty bar under it, which reads // as a failure; the reason is the honest answer until a dose has // actually come due. _adherence.of == 0 - ? const StatusCard( - 'Nothing to score yet', - 'No scheduled doses have come due yet.', + ? StatusCard( + l?.wellnessNothingToScoreTitle ?? 'Nothing to score yet', + l?.wellnessNothingToScoreBody ?? + 'No scheduled doses have come due yet.', icon: LucideIcons.pill, ) : Surface( child: Consistency( _adherence.taken, _adherence.of, - 'Taken, of those scheduled in the last seven days.', + l?.wellnessTakenOfScheduled ?? + 'Taken, of those scheduled in the last seven days.', C.blue, // Doses, not days — three a day over a week is 21 of // them inside a seven-day window. - unit: 'doses', + unit: l?.wellnessDosesUnit ?? 'doses', ), ), ), const SizedBox(height: S.x4), BigButton( - 'Add a medication', + l?.wellnessAddAMedication ?? 'Add a medication', icon: LucideIcons.plus, color: C.domMind, soft: true, @@ -778,7 +832,7 @@ class _WellnessScreenState extends State with RevisionReload { LucideIcons.pill, d.label, // `timeLabel`, so the two halves of this tab print a time the same way. - sub: '${_daysLabel(sch.days)} · ${slot.timeLabel}', + sub: '${_daysLabel(c, sch.days)} · ${slot.timeLabel}', onTap: () => _editSchedule(c, slot), ); } @@ -819,6 +873,7 @@ class _WellnessScreenState extends State with RevisionReload { Future _medActions(BuildContext c, MedSlot s) async { final p = P.of(c); + final l = AppLocalizations.of(c); final skipped = s.state == DoseState.skipped; await showModalBottomSheet( context: c, @@ -838,10 +893,13 @@ class _WellnessScreenState extends State with RevisionReload { ), _SheetAction( LucideIcons.circleSlash, - skipped ? 'Undo skipped' : 'Skipped on purpose', + skipped + ? (l?.wellnessUndoSkipped ?? 'Undo skipped') + : (l?.wellnessSkippedOnPurpose ?? 'Skipped on purpose'), sub: skipped - ? 'Back to not taken.' - : 'Recorded as a decision, not a miss.', + ? (l?.wellnessBackToNotTaken ?? 'Back to not taken.') + : (l?.wellnessRecordedAsDecision ?? + 'Recorded as a decision, not a miss.'), onTap: () { Navigator.of(sheet).pop(); _skipDose(s); @@ -849,8 +907,8 @@ class _WellnessScreenState extends State with RevisionReload { ), _SheetAction( LucideIcons.calendarDays, - 'Which days it is due', - sub: _daysLabel(_daysFor(s)), + l?.wellnessWhichDaysDue ?? 'Which days it is due', + sub: _daysLabel(c, _daysFor(s)), onTap: () { Navigator.of(sheet).pop(); _editSchedule(c, s); @@ -858,8 +916,10 @@ class _WellnessScreenState extends State with RevisionReload { ), _SheetAction( LucideIcons.trash2, - 'Remove ${s.def.label}', - sub: 'It stops being scheduled. Marked doses stay.', + l?.wellnessRemoveMedTitle(s.def.label) ?? + 'Remove ${s.def.label}', + sub: l?.wellnessRemoveMedBody ?? + 'It stops being scheduled. Marked doses stay.', onTap: () { Navigator.of(sheet).pop(); _confirmRemoveMed(s.def); @@ -920,12 +980,14 @@ class _WellnessScreenState extends State with RevisionReload { /// schedule — and with it the empty denominator that was dragging adherence /// down every day after the course ended. Future _confirmRemoveMed(MedDef d) async { + final l = AppLocalizations.of(context); final ok = await confirmRemove( context, - title: 'Remove ${d.label}?', - body: + title: l?.wellnessRemoveMedConfirmTitle(d.label) ?? + 'Remove ${d.label}?', + body: l?.wellnessRemoveMedConfirmBody ?? 'It stops being scheduled and stops counting towards adherence. ' - 'The doses you already marked stay.', + 'The doses you already marked stay.', ); if (!ok || !mounted) return; await MedDb.deleteDef(await LocalDb.instance, d.key); @@ -933,7 +995,12 @@ class _WellnessScreenState extends State with RevisionReload { } Future _addMed(BuildContext c) async { - final name = await _askName(c, 'Add a medication', 'Vitamin D'); + final l = AppLocalizations.of(c); + final name = await _askName( + c, + l?.wellnessAddAMedication ?? 'Add a medication', + l?.wellnessMedHint ?? 'Vitamin D', + ); if (name == null || name.isEmpty) return; if (!c.mounted) return; // The weekdays used to be hardcoded to all seven with no way back in, so a @@ -959,24 +1026,34 @@ class _WellnessScreenState extends State with RevisionReload { final ctrl = TextEditingController(); return showDialog( context: c, - builder: (d) => AlertDialog( - backgroundColor: P.of(d).card, - title: Text(title, style: F.head.copyWith(color: P.of(d).ink)), - content: OsTextField(controller: ctrl, label: 'Name', hint: hint), - actions: [ - TextButton( - onPressed: () => Navigator.of(d).pop(), - child: Text('Cancel', style: F.body.copyWith(color: P.of(d).ink2)), + builder: (d) { + final l = AppLocalizations.of(d); + return AlertDialog( + backgroundColor: P.of(d).card, + title: Text(title, style: F.head.copyWith(color: P.of(d).ink)), + content: OsTextField( + controller: ctrl, + label: l?.wellnessNameLabel ?? 'Name', + hint: hint, ), - TextButton( - onPressed: () => Navigator.of(d).pop(ctrl.text.trim()), - child: Text( - 'Add', - style: F.body.copyWith(color: P.of(d).on(C.domMind)), + actions: [ + TextButton( + onPressed: () => Navigator.of(d).pop(), + child: Text( + l?.actionCancel ?? 'Cancel', + style: F.body.copyWith(color: P.of(d).ink2), + ), ), - ), - ], - ), + TextButton( + onPressed: () => Navigator.of(d).pop(ctrl.text.trim()), + child: Text( + l?.wellnessAdd ?? 'Add', + style: F.body.copyWith(color: P.of(d).on(C.domMind)), + ), + ), + ], + ); + }, ).whenComplete(ctrl.dispose); } } @@ -1076,19 +1153,37 @@ class DriverRow extends StatelessWidget { } } -const _weekdayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +/// The pinned analytics package returns 'low'/'normal'/'elevated'/'high' — +/// English regardless of locale. Map to the localized word before display. +String? _stressLevelLabel(AppLocalizations? l, String raw) { + switch (raw) { + case 'low': + return l?.wellnessStressLevelLow ?? raw; + case 'normal': + return l?.wellnessStressLevelNormal ?? raw; + case 'elevated': + return l?.wellnessStressLevelElevated ?? raw; + case 'high': + return l?.wellnessStressLevelHigh ?? raw; + default: + return raw; + } +} /// The days a dose is due, in the words a person would use. `DateTime.weekday` /// values, 1 = Monday; empty means every day. -String _daysLabel(List days) { +String _daysLabel(BuildContext c, List days) { + final l = AppLocalizations.of(c); final set = days.toSet(); - if (set.isEmpty || set.length == 7) return 'Every day'; + if (set.isEmpty || set.length == 7) return l?.wellnessEveryDay ?? 'Every day'; if (set.length == 5 && !set.contains(6) && !set.contains(7)) { - return 'Weekdays'; + return l?.wellnessWeekdays ?? 'Weekdays'; + } + if (set.length == 2 && set.contains(6) && set.contains(7)) { + return l?.wellnessWeekends ?? 'Weekends'; } - if (set.length == 2 && set.contains(6) && set.contains(7)) return 'Weekends'; final sorted = set.toList()..sort(); - return [for (final d in sorted) _weekdayNames[(d - 1) % 7]].join(', '); + return [for (final d in sorted) weekdayShortName(d, l)].join(', '); } /// Pick a time and the weekdays it repeats on. Returns null if dismissed. @@ -1105,6 +1200,7 @@ Future pickMedSchedule( var minute = minuteOfDay; var picked = days.toSet(); final p = P.of(c); + final l = AppLocalizations.of(c); return showModalBottomSheet( context: c, backgroundColor: p.card, @@ -1118,10 +1214,13 @@ Future pickMedSchedule( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('When you take it', style: F.head.copyWith(color: p.ink)), + Text( + l?.wellnessWhenYouTakeIt ?? 'When you take it', + style: F.head.copyWith(color: p.ink), + ), const SizedBox(height: S.x4), Pressable( - semanticLabel: 'Change the time', + semanticLabel: l?.wellnessChangeTheTime ?? 'Change the time', onTap: () async { final at = await showTimePicker( context: sheet, @@ -1154,7 +1253,10 @@ Future pickMedSchedule( ), ), const SizedBox(height: S.x4), - Text('WHICH DAYS', style: F.over.copyWith(color: p.ink3)), + Text( + l?.wellnessWhichDays ?? 'WHICH DAYS', + style: F.over.copyWith(color: p.ink3), + ), const SizedBox(height: S.x2), Wrap( spacing: S.x2, @@ -1162,7 +1264,7 @@ Future pickMedSchedule( children: [ for (var d = 1; d <= 7; d++) Pressable( - semanticLabel: _weekdayNames[d - 1], + semanticLabel: weekdayShortName(d, l), onTap: () => setSheet(() { picked.contains(d) ? picked.remove(d) : picked.add(d); }), @@ -1184,7 +1286,7 @@ Future pickMedSchedule( ), ), child: Text( - _weekdayNames[d - 1], + weekdayShortName(d, l), style: F.cap.copyWith( color: picked.contains(d) ? p.on(C.domMind) @@ -1200,13 +1302,14 @@ Future pickMedSchedule( // saving one that can never come due. Text( picked.isEmpty - ? 'Pick at least one day.' - : 'Due ${_daysLabel(picked.toList()).toLowerCase()}.', + ? (l?.wellnessPickAtLeastOneDay ?? 'Pick at least one day.') + : (l?.wellnessDueDays(_daysLabel(c, picked.toList())) ?? + 'Due ${_daysLabel(c, picked.toList()).toLowerCase()}.'), style: F.cap.copyWith(color: p.ink3), ), const SizedBox(height: S.x4), BigButton( - 'Save', + l?.actionSave ?? 'Save', color: C.domMind, onTap: picked.isEmpty ? null @@ -1273,10 +1376,12 @@ class MedRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final l = AppLocalizations.of(c); final taken = slot.state == DoseState.taken; return Pressable( onTap: onTap, - semanticLabel: '${slot.def.label} at ${slot.timeLabel}', + semanticLabel: l?.wellnessMedAtTime(slot.def.label, slot.timeLabel) ?? + '${slot.def.label} at ${slot.timeLabel}', child: Padding( padding: const EdgeInsets.symmetric(vertical: S.x3), child: Row( @@ -1301,7 +1406,7 @@ class MedRow extends StatelessWidget { ), Text( '${slot.def.doseLabel} · ${slot.timeLabel} · ' - '${_stateLabel(slot.state)}', + '${_stateLabel(l, slot.state)}', style: F.over.copyWith(color: p.ink3), ), ], @@ -1309,7 +1414,9 @@ class MedRow extends StatelessWidget { ), if (onMore != null) Pressable( - semanticLabel: 'More for ${slot.def.label}', + semanticLabel: + l?.wellnessMoreForMed(slot.def.label) ?? + 'More for ${slot.def.label}', onTap: onMore, child: Padding( padding: const EdgeInsets.only(right: S.x3), @@ -1323,11 +1430,11 @@ class MedRow extends StatelessWidget { ); } - static String _stateLabel(DoseState s) => switch (s) { - DoseState.taken => 'taken', - DoseState.skipped => 'skipped', - DoseState.missed => 'not taken', - DoseState.upcoming => 'due later', + static String _stateLabel(AppLocalizations? l, DoseState s) => switch (s) { + DoseState.taken => l?.wellnessStateTaken ?? 'taken', + DoseState.skipped => l?.wellnessStateSkipped ?? 'skipped', + DoseState.missed => l?.wellnessStateNotTaken ?? 'not taken', + DoseState.upcoming => l?.wellnessStateDueLater ?? 'due later', }; } @@ -1353,8 +1460,11 @@ class _Check extends StatelessWidget { : const SizedBox.shrink(), ); if (onTap == null) return box; + final l = AppLocalizations.of(c); return Pressable( - semanticLabel: on ? 'Done' : 'Mark done', + semanticLabel: on + ? (l?.actionDone ?? 'Done') + : (l?.wellnessMarkDone ?? 'Mark done'), onTap: onTap, child: box, ); @@ -1432,8 +1542,10 @@ class _JournalFindingsState extends State { @override Widget build(BuildContext c) { + final l = AppLocalizations.of(c); + final title = l?.wellnessWhatYouLogScreenTitle ?? 'What you log'; if (_loading) { - return detailScaffold(c, 'What you log', const [ + return detailScaffold(c, title, const [ SizedBox(height: S.x8), Center(child: CircularProgressIndicator()), ]); @@ -1447,40 +1559,55 @@ class _JournalFindingsState extends State { for (final r in _rows) if (r['binary'] == true) r, ]; - return detailScaffold(c, 'What you log', [ + return detailScaffold(c, title, [ const SizedBox(height: S.x2), if (_rows.isEmpty) - const StatusCard( - 'Nothing separated itself yet', - 'Everything you log is tested against your recovery, HRV, resting ' - 'heart rate and sleep efficiency. Nothing has cleared the bar ' - 'yet.', + StatusCard( + l?.wellnessNothingSeparatedTitle ?? 'Nothing separated itself yet', + l?.wellnessNothingSeparatedBody ?? + 'Everything you log is tested against your recovery, HRV, ' + 'resting heart rate and sleep efficiency. Nothing has ' + 'cleared the bar yet.', icon: LucideIcons.scatterChart, ) else ...[ - if (habits.isNotEmpty) Section('The days you did it', _list(c, habits)), + if (habits.isNotEmpty) + Section( + l?.wellnessTheDaysYouDidIt ?? 'The days you did it', + _list(c, habits), + ), if (doses.isNotEmpty) - Section('How much, and what followed', _list(c, doses)), + Section( + l?.wellnessHowMuchAndWhatFollowed ?? 'How much, and what followed', + _list(c, doses), + ), const SizedBox(height: S.x2), Text( - 'A link on your own days — never a cause. The days you do a thing ' - 'are days you were already that kind of day.', + l?.wellnessLinkNeverCause ?? + 'A link on your own days — never a cause. The days you do a ' + 'thing are days you were already that kind of day.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], - Section('Which day of the week', _weekdayCard(c)), + Section( + l?.wellnessWhichDayOfWeek ?? 'Which day of the week', + _weekdayCard(c), + ), ]); } - Widget _list(BuildContext c, List> rows) => Surface( - pad: const EdgeInsets.symmetric(horizontal: S.x4), - child: Column( - children: [ - for (final r in rows) - DriverRow(label: _headline(r), detail: _detail(r)), - ], - ), - ); + Widget _list(BuildContext c, List> rows) { + final l = AppLocalizations.of(c); + return Surface( + pad: const EdgeInsets.symmetric(horizontal: S.x4), + child: Column( + children: [ + for (final r in rows) + DriverRow(label: _headline(l, r), detail: _detail(l, r)), + ], + ), + ); + } // ── copy ──────────────────────────────────────────────────────────────── @@ -1488,26 +1615,39 @@ class _JournalFindingsState extends State { /// not a rank correlation read out loud. "On the 11 nights you logged /// alcohol, your resting HR ran 6 bpm higher" is the same finding the rho /// carried and a sentence a person can check against their own memory. - String _headline(Map r) { + String _headline(AppLocalizations? l, Map r) { final field = (r['field_label'] ?? '').toString(); final outcome = (r['outcome_label'] ?? '').toString(); final unit = (r['unit'] ?? '').toString(); if (r['binary'] == true) { final delta = (r['delta'] as num?)?.toDouble() ?? 0; - return 'On the ${r['n_with']} days you logged $field, $outcome ran ' - '${_amount(delta.abs(), unit)} ${delta > 0 ? 'higher' : 'lower'}'; + final direction = delta > 0 + ? (l?.wellnessHigher ?? 'higher') + : (l?.wellnessLower ?? 'lower'); + final n = '${r['n_with']}'; + final amount = _amount(delta.abs(), unit); + return l?.wellnessHeadlineBinary(n, field, outcome, amount, direction) ?? + 'On the $n days you logged $field, $outcome ran $amount $direction'; } - final n = r['n']; + final n = '${r['n']}'; final slope = (r['slope_per_unit'] as num?)?.toDouble(); final rho = (r['rho'] as num?)?.toDouble() ?? 0; if (slope == null) { - return 'On the $n days you logged $field, more of it went with ' - '${rho > 0 ? 'higher' : 'lower'} $outcome'; + final direction = rho > 0 + ? (l?.wellnessHigher ?? 'higher') + : (l?.wellnessLower ?? 'lower'); + return l?.wellnessHeadlineNoSlope(n, field, direction, outcome) ?? + 'On the $n days you logged $field, more of it went with ' + '$direction $outcome'; } - final (per, step) = _perUnit(r); - return 'On the $n days you logged $field, $outcome ran ' - '${_amount((slope * per).abs(), unit)} ' - '${slope > 0 ? 'higher' : 'lower'} per $step'; + final (per, step) = _perUnit(l, r); + final direction = slope > 0 + ? (l?.wellnessHigher ?? 'higher') + : (l?.wellnessLower ?? 'lower'); + final amount = _amount((slope * per).abs(), unit); + return l?.wellnessHeadlineSlope(n, field, outcome, amount, direction, step) ?? + 'On the $n days you logged $field, $outcome ran ' + '$amount $direction per $step'; } /// MIND-02 — WHICH NIGHT this row is about. @@ -1524,7 +1664,7 @@ class _JournalFindingsState extends State { /// Said out loud on every row, because the alignment changed underneath /// findings people had already read, and a finding that quietly means a /// different night is a different finding. - String _alignment(Map r) { + String _alignment(AppLocalizations? l, Map r) { // Read from the same constant analytics paired on, so the sentence cannot // drift away from the arithmetic. final field = (r['field'] ?? '').toString(); @@ -1532,17 +1672,24 @@ class _JournalFindingsState extends State { final lag = journalFieldLagDays[field] ?? (field.startsWith('caffeine') ? journalFieldLagDays['caffeine'] : null); - if (lag == null) return 'Matched against the same day\'s numbers.'; + if (lag == null) { + return l?.wellnessMatchedSameDay ?? 'Matched against the same day\'s numbers.'; + } return lag > 0 - ? 'Matched against the night that followed.' - : 'Matched against the night that ended that morning.'; + ? (l?.wellnessMatchedNightFollowed ?? + 'Matched against the night that followed.') + : (l?.wellnessMatchedNightEnded ?? + 'Matched against the night that ended that morning.'); } - String _detail(Map r) { - final when = _alignment(r); + String _detail(AppLocalizations? l, Map r) { + final when = _alignment(l, r); if (r['binary'] == true) { final d = (r['cohens_d'] as num?)?.toDouble(); - return 'Against the ${r['n_without']} days you did not' + final n = '${r['n_without']}'; + final against = l?.wellnessAgainstDaysYouDidNot(n) ?? + 'Against the $n days you did not'; + return '$against' '${d == null ? '' : ' · d ${d.abs().toStringAsFixed(1)}'}. $when'; } final lo = (r['rho_low'] as num?)?.toDouble(); @@ -1550,18 +1697,19 @@ class _JournalFindingsState extends State { final rho = (r['rho'] as num?)?.toDouble(); final ci = (lo == null || hi == null) ? '' - : ' (${lo.toStringAsFixed(2)} to ${hi.toStringAsFixed(2)})'; + : ' (${l?.wellnessRangeTo(lo.toStringAsFixed(2), hi.toStringAsFixed(2)) ?? '${lo.toStringAsFixed(2)} to ${hi.toStringAsFixed(2)}'})'; final base = rho == null ? '' - : 'Rank correlation ${rho.toStringAsFixed(2)}$ci. '; + : (l?.wellnessRankCorrelation(rho.toStringAsFixed(2), ci) ?? + 'Rank correlation ${rho.toStringAsFixed(2)}$ci. '); // MT-06's own ceiling, said where the finding is: `at_min` is the LAST // occurrence, so timing cannot tell two coffees from five, and a late // stressful day produces both the late coffee and the bad night. if (r['field'] == 'caffeine_last_min') { - return '$base$when This is your last caffeine of the day only — two cups ' - 'and five look identical here, so "later" can quietly mean "more". A ' - 'long, stressful day produces both the late coffee and the poor ' - 'night.'; + return '$base$when ${l?.wellnessCaffeineCaveat ?? 'This is your last ' + 'caffeine of the day only — two cups and five look identical ' + 'here, so "later" can quietly mean "more". A long, stressful day ' + 'produces both the late coffee and the poor night.'}'; } return '$base$when'.trim(); } @@ -1569,12 +1717,14 @@ class _JournalFindingsState extends State { /// How to say one step of this field. Minutes-past-midnight is unreadable per /// minute, so caffeine timing is stated per HOUR later — a slope, never a /// cutoff time, which is a threshold read off a dozen self-reported points. - (double, String) _perUnit(Map r) { - if (r['field'] == 'caffeine_last_min') return (60.0, 'hour later'); + (double, String) _perUnit(AppLocalizations? l, Map r) { + if (r['field'] == 'caffeine_last_min') { + return (60.0, l?.wellnessHourLater ?? 'hour later'); + } final u = (r['field_unit'] ?? '').toString(); // Singular: the phrase is "per unit", "per mg", "per point". final one = u.isEmpty - ? 'point' + ? (l?.wellnessPointUnit ?? 'point') : (u.endsWith('s') ? u.substring(0, u.length - 1) : u); return (1.0, one); } @@ -1592,40 +1742,64 @@ class _JournalFindingsState extends State { /// worst. Without it this is a machine for manufacturing weekday /// superstitions. Widget _weekdayCard(BuildContext c) { + final l = AppLocalizations.of(c); if (_weekday['present'] != true) { - return const StatusCard( - 'Not enough weeks yet', - 'Comparing seven weekdays needs at least eight weeks of days, with ' - 'five of every weekday in them.', + return StatusCard( + l?.wellnessNotEnoughWeeksTitle ?? 'Not enough weeks yet', + l?.wellnessNotEnoughWeeksBody ?? + 'Comparing seven weekdays needs at least eight weeks of days, ' + 'with five of every weekday in them.', icon: LucideIcons.calendarDays, ); } if (_weekday['meaningful'] != true) { - return const StatusCard( - 'No day of the week stands out', - 'No day stands apart from the other six once we account for having ' - 'checked all seven.', + return StatusCard( + l?.wellnessNoDayStandsOutTitle ?? 'No day of the week stands out', + l?.wellnessNoDayStandsOutBody ?? + 'No day stands apart from the other six once we account for ' + 'having checked all seven.', icon: LucideIcons.calendarDays, ); } final day = (_weekday['peak_weekday'] as num?)?.toInt() ?? 1; final delta = (_weekday['peak_delta'] as num?)?.toDouble() ?? 0; final n = (_weekday['n_by_weekday'] as Map?)?['$day']; + final direction = delta > 0 + ? (l?.wellnessHigher ?? 'higher') + : (l?.wellnessLower ?? 'lower'); + final weekdayPlural = _weekdayPlural(l, day); return Surface( pad: const EdgeInsets.symmetric(horizontal: S.x4), child: DriverRow( - label: + label: l?.wellnessWeekdayHeadline( + weekdayPlural, + '${delta.abs().round()}', + direction, + ) ?? '${_weekdayName(day)}s: readiness runs ' - '${delta.abs().round()} ${delta > 0 ? 'higher' : 'lower'} than ' - 'your overall median', - detail: + '${delta.abs().round()} $direction than your overall median', + detail: l?.wellnessWeekdayDetail('$n') ?? 'From $n of them. A weekday is not a cause — it is a container ' - 'for what you do on it. Nothing here is advice.', + 'for what you do on it. Nothing here is advice.', ), ); } } +/// The localized plural weekday name ("Mondays"), for [weekday] 1 = Monday. +String _weekdayPlural(AppLocalizations? l, int weekday) { + final fallback = '${_weekdayName(weekday)}s'; + return switch (weekday) { + 1 => l?.wellnessPluralMonday ?? fallback, + 2 => l?.wellnessPluralTuesday ?? fallback, + 3 => l?.wellnessPluralWednesday ?? fallback, + 4 => l?.wellnessPluralThursday ?? fallback, + 5 => l?.wellnessPluralFriday ?? fallback, + 6 => l?.wellnessPluralSaturday ?? fallback, + _ => l?.wellnessPluralSunday ?? fallback, + }; +} + const _kWeekdayNames = [ 'Monday', 'Tuesday', diff --git a/lib/ui2/screens/what_changed.dart b/lib/ui2/screens/what_changed.dart index f0886309..29fecc3c 100644 --- a/lib/ui2/screens/what_changed.dart +++ b/lib/ui2/screens/what_changed.dart @@ -28,6 +28,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../ai/briefing_engine.dart' show collectSweepSeries; import '../../ai/nightly_sweep.dart'; import '../../data/local_repository.dart'; +import '../../l10n/app_localizations.dart'; import '../ui2.dart'; import 'day_timeline.dart' show DayTimelineScreen; import 'home_screen.dart' show go, repoOf; @@ -142,7 +143,10 @@ class _WhatChangedScreenState extends State { @override Widget build(BuildContext c) { final d = _d ?? const WhatChangedData(); - return detailScaffold(c, 'What changed', sub: 'AGAINST YOUR OWN HISTORY', [ + final l = AppLocalizations.of(c); + return detailScaffold( + c, l?.whatChangedTitle ?? 'What changed', + sub: l?.whatChangedSub ?? 'AGAINST YOUR OWN HISTORY', [ ...dayNavRow(_day ?? d.day, d.days, _goDay), if (_loading) ...[ const SizedBox(height: S.x8), @@ -157,23 +161,26 @@ class _WhatChangedScreenState extends State { /// without a repository. List whatChangedBody(BuildContext c, WhatChangedData d) { final p = P.of(c); + final l = AppLocalizations.of(c); final pairing = sweepPairing(d.findings); return [ if (!d.hadToday) - const StatusCard( - 'Nothing has landed for this day yet', - 'The sweep compares a day against the ones before it, and this day has ' - 'no value to compare. Nothing about it is unusual because nothing ' - 'about it is known.', + StatusCard( + l?.whatChangedNoDataTitle ?? 'Nothing has landed for this day yet', + l?.whatChangedNoDataBody ?? + 'The sweep compares a day against the ones before it, and this day has ' + 'no value to compare. Nothing about it is unusual because nothing ' + 'about it is known.', icon: LucideIcons.circleSlash, ) else if (d.findings.isEmpty && d.longestHistory < kSweepMinHistory) ...[ StatusCard( - 'Still learning your usual', - 'Unusual only means anything against a range, and there ' - '${d.longestHistory == 1 ? 'is' : 'are'} ${d.longestHistory} ' - 'day${d.longestHistory == 1 ? '' : 's'} of history behind this ' - 'one. The sweep starts at $kSweepMinHistory.', + l?.whatChangedLearningTitle ?? 'Still learning your usual', + l?.whatChangedLearningBody(d.longestHistory, kSweepMinHistory) ?? + 'Unusual only means anything against a range, and there ' + '${d.longestHistory == 1 ? 'is' : 'are'} ${d.longestHistory} ' + 'day${d.longestHistory == 1 ? '' : 's'} of history behind this ' + 'one. The sweep starts at $kSweepMinHistory.', icon: LucideIcons.hourglass, ), ] else if (d.findings.isEmpty) @@ -181,10 +188,11 @@ List whatChangedBody(BuildContext c, WhatChangedData d) { // present tense rather than that constant's "tonight": this screen can be // steered onto a day in March, and a sentence about tonight is wrong on // every one of them. - const StatusCard( - 'Nothing stood out', - 'Every metric with enough history sat inside the range your own days ' - 'have set. That is the normal answer, and it is a complete one.', + StatusCard( + l?.whatChangedNothingTitle ?? 'Nothing stood out', + l?.whatChangedNothingBody ?? + 'Every metric with enough history sat inside the range your own days ' + 'have set. That is the normal answer, and it is a complete one.', icon: LucideIcons.check, ) else ...[ @@ -204,9 +212,10 @@ List whatChangedBody(BuildContext c, WhatChangedData d) { ], const SizedBox(height: S.x3), Text( - 'Measured against your own trailing days, in your own units, with the ' - 'window attached — so you can disbelieve it. Nothing here is a cause ' - 'and nothing here is a diagnosis.', + l?.whatChangedMethodologyNote ?? + 'Measured against your own trailing days, in your own units, with the ' + 'window attached — so you can disbelieve it. Nothing here is a cause ' + 'and nothing here is a diagnosis.', style: F.over.copyWith(color: p.ink3, height: 1.5), ), ], @@ -215,13 +224,13 @@ List whatChangedBody(BuildContext c, WhatChangedData d) { detailLinkRow( c, LucideIcons.listOrdered, - 'What happened that day', - 'Sleep, sessions, meals and logs in time order', + l?.whatChangedDayLinkTitle ?? 'What happened that day', + l?.whatChangedDayLinkSub ?? 'Sleep, sessions, meals and logs in time order', () => go(c, DayTimelineScreen(day: d.day)), ), ], if (d.grid.isNotEmpty) - Section('The month behind it', MonthGrid(d.grid)), + Section(l?.whatChangedMonthSection ?? 'The month behind it', MonthGrid(d.grid)), ]; } diff --git a/lib/ui2/screens/workout_screen.dart b/lib/ui2/screens/workout_screen.dart index 28326ad5..fed5fb6f 100644 --- a/lib/ui2/screens/workout_screen.dart +++ b/lib/ui2/screens/workout_screen.dart @@ -24,6 +24,7 @@ import '../../gps/route_models.dart'; import '../../health/health_import_state.dart'; import '../../health/auto_workout_import.dart'; import '../../health/health_workout_import.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/metric.dart'; import '../../state/app_state.dart'; import '../activity/catalogue.dart'; @@ -38,6 +39,7 @@ import '../profile/profile.dart' show openProfile; import '../grammar.dart'; import '../revision.dart'; import '../theme.dart'; +import 'home_screen.dart' show calendarDaysBetween; import 'log_workout.dart'; import 'start_card.dart'; @@ -50,7 +52,11 @@ class WorkoutScreen extends StatefulWidget { class _WorkoutScreenState extends State with RevisionReload { int tab = 0; - static const _tabs = ['For you', 'Activities', 'History']; + List _tabs(AppLocalizations? loc) => [ + loc?.workoutTabForYou ?? 'For you', + loc?.workoutTabActivities ?? 'Activities', + loc?.workoutTabHistory ?? 'History', + ]; Future<_WorkoutData>? _load; @@ -74,6 +80,7 @@ class _WorkoutScreenState extends State with RevisionReload { return FutureBuilder<_WorkoutData>( future: _load, builder: (c, snap) { + final loc = AppLocalizations.of(c); final d = snap.data ?? const _WorkoutData.empty(); // THE LIST DROPS ITS SIDE PADDING and hands it to each child instead, // so the hero card can be the one child that does not get it and runs @@ -86,8 +93,8 @@ class _WorkoutScreenState extends State with RevisionReload { padding: const EdgeInsets.fromLTRB(0, S.x2, 0, S.x16), children: [ for (final w in [ - const ScreenTitle('Workout'), - SubTabs(_tabs, tab, (i) => setState(() => tab = i), + ScreenTitle(loc?.workoutScreenTitle ?? 'Workout'), + SubTabs(_tabs(loc), tab, (i) => setState(() => tab = i), color: C.domMove), const SizedBox(height: S.x5), ...switch (tab) { @@ -119,11 +126,12 @@ class _WorkoutScreenState extends State with RevisionReload { // ─────────────── FOR YOU ─────────────── List _forYou(BuildContext c, _WorkoutData d) { final p = P.of(c); + final loc = AppLocalizations.of(c); return [ StartCard( - label: 'START A SESSION', + label: loc?.workoutStartSessionLabel ?? 'START A SESSION', count: allActivities.length, - noun: 'activities', + noun: loc?.workoutActivitiesNoun ?? 'activities', asset: 'mascot_workout.png', accent: C.purple, deep: C.indigo, @@ -144,11 +152,19 @@ class _WorkoutScreenState extends State with RevisionReload { ], ]), Section( - 'This week', + loc?.workoutThisWeek ?? 'This week', Surface( child: Row( children: List.generate(7, (i) { - const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + final days = [ + loc?.workoutWeekdayLetterMon ?? 'M', + loc?.workoutWeekdayLetterTue ?? 'T', + loc?.workoutWeekdayLetterWed ?? 'W', + loc?.workoutWeekdayLetterThu ?? 'T', + loc?.workoutWeekdayLetterFri ?? 'F', + loc?.workoutWeekdayLetterSat ?? 'S', + loc?.workoutWeekdayLetterSun ?? 'S', + ]; final today = DateTime.now().weekday - 1; final done = d.weekDays.contains(i); return Expanded( @@ -177,7 +193,7 @@ class _WorkoutScreenState extends State with RevisionReload { ), ), Section( - 'Training load', + loc?.workoutTrainingLoad ?? 'Training load', Column(children: [ _loadCard(c, p, d), // TS-12 — two facts and no verb, directly under the card that holds @@ -186,7 +202,7 @@ class _WorkoutScreenState extends State with RevisionReload { // and not a reassurance we can make. if (d.overreach != null) ...[ const SizedBox(height: S.x3), - _overreachCard(d.overreach!), + _overreachCard(c, d.overreach!), ], // TS-08 — a SECOND axis, beside the cardiovascular one and never // inside it. Its own card because that is what "not fused" means: @@ -195,14 +211,14 @@ class _WorkoutScreenState extends State with RevisionReload { // any, so a runner never sees an empty kilo chart. if (d.tonnage7.any((v) => v != null)) ...[ const SizedBox(height: S.x3), - _tonnageCard(p, d), + _tonnageCard(c, p, d), ], ]), // TS-02 — the door onto the day's own strain trace. `getDayStrain` and // `series.strain_curve` were both fully implemented and read by no // screen. A link, not a card: this tab is about the fortnight, and the // shape of one day belongs behind a tap. - action: "Today's strain", + action: loc?.workoutTodaysStrainAction ?? "Today's strain", onAction: () => Navigator.of(c) .push(MaterialPageRoute(builder: (_) => const DayStrainDetail())), ), @@ -215,24 +231,30 @@ class _WorkoutScreenState extends State with RevisionReload { /// nullable so a bodyweight set is not a zero-kilo set, which means a week of /// pull-ups contributes nothing here and must not be read as an easy week. /// No records, no bests, no comparison with last week. - Widget _tonnageCard(P p, _WorkoutData d) { + Widget _tonnageCard(BuildContext c, P p, _WorkoutData d) { + final loc = AppLocalizations.of(c); final end = d.trimpEnd ?? DateTime.now(); final axis = AxisSpec.of([for (final v in d.tonnage7) ?v], floor: 0); return Surface( child: ChartFrame( - title: 'MECHANICAL LOAD', - unit: 'kg lifted', + title: loc?.workoutMechanicalLoadTitle ?? 'MECHANICAL LOAD', + unit: loc?.workoutKgLiftedUnit ?? 'kg lifted', height: 88, yAxis: axis, xLabels: [ for (var i = 6; i >= 0; i--) - _weekdayLetter(end.subtract(Motion.tick * 86400 * i)), + _weekdayLetter(c, end.subtract(Motion.tick * 86400 * i)), ], - footnote: 'Reps × load over the sets you logged with a weight. ' - '${d.tonnagePartial ? 'Sets logged without one are not in it, so ' - 'this is a floor rather than a total. ' : ''}' - 'Exact for what you typed and worthless across exercises — kept ' - 'out of strain and recovery for that reason.', + footnote: (loc?.workoutTonnageFootnoteIntro ?? + 'Reps × load over the sets you logged with a weight. ') + + (d.tonnagePartial + ? (loc?.workoutTonnageFootnotePartial ?? + 'Sets logged without one are not in it, so ' + 'this is a floor rather than a total. ') + : '') + + (loc?.workoutTonnageFootnoteOutro ?? + 'Exact for what you typed and worthless across exercises — ' + 'kept out of strain and recovery for that reason.'), series: d.tonnage7, child: CustomPaint( size: Size.infinite, @@ -255,42 +277,53 @@ class _WorkoutScreenState extends State with RevisionReload { /// In-app only, by construction: nothing here schedules a notification, and /// the pipeline deliberately keeps `overreaching` out of the keys /// `_runNotifications` reads. - Widget _overreachCard(Overreach o) => InsightCard( - 'Your last 7 days of load are ' - '${o.ratio.toStringAsFixed(1)}× your usual six weeks, and your resting ' - 'heart rate was above your usual on ${o.nightsElevated} of ' - '${o.nightsConsidered} nights.', - 'Two measurements that happen to point the same way. Illness, travel, ' - 'altitude, alcohol and a run of poor sleep all produce this same ' - 'pair, and nothing here can tell them apart.', - icon: LucideIcons.activity, - color: C.orange, - ); + Widget _overreachCard(BuildContext c, Overreach o) { + final loc = AppLocalizations.of(c); + final ratio = o.ratio.toStringAsFixed(1); + return InsightCard( + loc?.workoutOverreachHeadline( + ratio, o.nightsElevated, o.nightsConsidered) ?? + 'Your last 7 days of load are ' + '$ratio× your usual six weeks, and your resting ' + 'heart rate was above your usual on ${o.nightsElevated} of ' + '${o.nightsConsidered} nights.', + loc?.workoutOverreachBody ?? + 'Two measurements that happen to point the same way. Illness, travel, ' + 'altitude, alcohol and a run of poor sleep all produce this same ' + 'pair, and nothing here can tell them apart.', + icon: LucideIcons.activity, + color: C.orange, + ); + } Widget _loadCard(BuildContext c, P p, _WorkoutData d) { + final loc = AppLocalizations.of(c); if (d.load == null) { return StatusCard( - 'No training load yet', + loc?.workoutNoLoadTitle ?? 'No training load yet', d.loadNote ?? - 'Fitness and fatigue are 42-day and 7-day averages. They need ' - 'about two weeks of sessions.', + (loc?.workoutNoLoadBody ?? + 'Fitness and fatigue are 42-day and 7-day averages. They need ' + 'about two weeks of sessions.'), icon: LucideIcons.trendingUp, ); } - final l = d.load!; + final ld = d.load!; + final notYet = loc?.workoutNotYet ?? 'Not yet'; return Surface( child: Column(children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text(l.ctl.round().toString(), + Text(ld.ctl.round().toString(), style: F.n34.copyWith(color: p.ink)), const SizedBox(width: S.x2), - Text('fitness', style: F.cap.copyWith(color: p.ink3)), + Text(loc?.workoutFitnessLabel ?? 'fitness', + style: F.cap.copyWith(color: p.ink3)), const Spacer(), - if (l.tsb != null) - Pill(_form(l.tsb!), l.tsb! >= 0 ? C.green : C.orange), + if (ld.tsb != null) + Pill(_form(c, ld.tsb!), ld.tsb! >= 0 ? C.green : C.orange), ]), if (d.trimp7.any((v) => v != null)) ...[ const SizedBox(height: S.x4), @@ -299,20 +332,24 @@ class _WorkoutScreenState extends State with RevisionReload { final axis = AxisSpec.of([for (final v in d.trimp7) ?v], floor: 0); final days = d.trimp7.where((v) => v != null).length; return ChartFrame( - title: 'DAILY LOAD', - unit: 'TRIMP', + title: loc?.workoutDailyLoadTitle ?? 'DAILY LOAD', + unit: loc?.workoutTrimpUnit ?? 'TRIMP', height: 88, yAxis: axis, // Seven slots, seven real dates. A day with nothing keeps its // place and its letter, and draws as the gap it is. xLabels: [ for (var i = 6; i >= 0; i--) - _weekdayLetter(end.subtract(Motion.tick * 86400 * i)), + _weekdayLetter(c, end.subtract(Motion.tick * 86400 * i)), ], - footnote: 'Banister training impulse — minutes weighted by ' - 'heart-rate reserve. ' - '${days == 7 ? 'Last seven days.' : '$days of the last ' - 'seven days produced a figure.'}', + footnote: (loc?.workoutDailyLoadFootnoteIntro ?? + 'Banister training impulse — minutes weighted by ' + 'heart-rate reserve. ') + + (days == 7 + ? (loc?.workoutDailyLoadAllDays ?? 'Last seven days.') + : (loc?.workoutDailyLoadPartialDays(days) ?? + '$days of the last ' + 'seven days produced a figure.')), series: d.trimp7, child: CustomPaint( size: Size.infinite, @@ -328,15 +365,16 @@ class _WorkoutScreenState extends State with RevisionReload { // Absent is absent. `?? 0` used to render "Fatigue 0" — a rest week — // for a pipeline that had simply not produced the number. // - // No 'Fitness' entry: it is `l.ctl`, which the headline two rows up is + // No 'Fitness' entry: it is `ld.ctl`, which the headline two rows up is // already printing at 34 pt under the word "fitness". One number, once. InlineMetrics([ - ('Fatigue', l.atl?.round().toString() ?? 'Not yet', C.orange), + (loc?.workoutFatigueLabel ?? 'Fatigue', + ld.atl?.round().toString() ?? notYet, C.orange), ( - 'Form', - l.tsb == null - ? 'Not yet' - : '${l.tsb! >= 0 ? '+' : '−'}${l.tsb!.abs().round()}', + loc?.workoutFormLabel ?? 'Form', + ld.tsb == null + ? notYet + : '${ld.tsb! >= 0 ? '+' : '−'}${ld.tsb!.abs().round()}', C.purple ), ]), @@ -345,21 +383,25 @@ class _WorkoutScreenState extends State with RevisionReload { } /// Coggan's form bands, named rather than numbered. - String _form(double tsb) => tsb > 5 - ? 'Fresh' - : tsb >= -10 - ? 'Steady' - : tsb >= -30 - ? 'Building' - : 'Overreaching'; + String _form(BuildContext c, double tsb) { + final loc = AppLocalizations.of(c); + return tsb > 5 + ? (loc?.workoutFormFresh ?? 'Fresh') + : tsb >= -10 + ? (loc?.workoutFormSteady ?? 'Steady') + : tsb >= -30 + ? (loc?.workoutFormBuilding ?? 'Building') + : (loc?.workoutFormOverreaching ?? 'Overreaching'); + } // ─────────────── ACTIVITIES ─────────────── List _activities(BuildContext c, _WorkoutData d) { final p = P.of(c); + final loc = AppLocalizations.of(c); return [ Pressable( onTap: () => _openPicker(c, d), - semanticLabel: 'Search activities', + semanticLabel: loc?.workoutSearchActivitiesLabel ?? 'Search activities', child: Container( constraints: const BoxConstraints(minHeight: S.tap), padding: const EdgeInsets.symmetric(horizontal: S.x4), @@ -367,13 +409,16 @@ class _WorkoutScreenState extends State with RevisionReload { child: Row(children: [ Icon(LucideIcons.search, size: 17, color: p.ink3), const SizedBox(width: S.x2), - Text('Search ${allActivities.length} activities', + Text( + loc?.workoutSearchActivitiesCount(allActivities.length) ?? + 'Search ${allActivities.length} activities', style: F.body.copyWith(color: p.ink3)), ]), ), ), const SizedBox(height: S.x5), - Text('QUICK START', style: F.over.copyWith(color: p.ink3)), + Text(loc?.workoutQuickStartHeader ?? 'QUICK START', + style: F.over.copyWith(color: p.ink3)), const SizedBox(height: S.x3), for (var row = 0; row < 2; row++) ...[ if (row > 0) const SizedBox(height: S.x3), @@ -413,16 +458,17 @@ class _WorkoutScreenState extends State with RevisionReload { // at all, and this is the only place that says so and offers the fix. if (d.weightKg == null) StatusCard( - 'Calorie estimates need your weight', + loc?.workoutCalorieNeedWeightTitle ?? + 'Calorie estimates need your weight', '', - fix: 'Add weight in profile', + fix: loc?.workoutAddWeightFix ?? 'Add weight in profile', onFix: () => openProfile(c), icon: LucideIcons.flame, ), if (d.weightKg != null) ...[ const SizedBox(height: S.x5), - const StatusCard( - 'Calorie figures are estimates', + StatusCard( + loc?.workoutCalorieEstimatesTitle ?? 'Calorie figures are estimates', kCalorieWhy, icon: LucideIcons.flame, ), @@ -451,15 +497,18 @@ class _WorkoutScreenState extends State with RevisionReload { /// everything drained later still has to be reviewable here. List _suggestionCards(BuildContext c, _WorkoutData d) { if (d.suggestions.isEmpty) return const []; + final loc = AppLocalizations.of(c); final n = d.suggestions.length; return [ StatusCard( - n == 1 - ? 'One effort we spotted but did not log' - : '$n efforts we spotted but did not log', - 'The band saw sustained work and nothing was started for it. Nothing ' - 'is logged until you say so.', - fix: 'Review ${n == 1 ? 'it' : 'them'}', + loc?.workoutSuggestionsTitle(n) ?? + (n == 1 + ? '$n effort we spotted but did not log' + : '$n efforts we spotted but did not log'), + loc?.workoutSuggestionsBody ?? + 'The band saw sustained work and nothing was started for it. Nothing ' + 'is logged until you say so.', + fix: loc?.workoutReviewFix(n) ?? 'Review ${n == 1 ? 'it' : 'them'}', icon: LucideIcons.radar, onFix: () => _push(c, WorkoutSuggestionScreen(preloaded: d.suggestions)), @@ -469,24 +518,29 @@ class _WorkoutScreenState extends State with RevisionReload { } /// Back-log a session the band never saw, or never saw the whole of. - Widget _logPastCard(BuildContext c) => StatusCard( - 'Did something the band missed?', - 'Enter the times yourself and it is scored from the heart rate ' - 'recorded across them, like any other session.', - fix: 'Log a past workout', - icon: LucideIcons.calendarPlus, - onFix: () => _push(c, const LogWorkout()), - ); + Widget _logPastCard(BuildContext c) { + final loc = AppLocalizations.of(c); + return StatusCard( + loc?.workoutLogPastTitle ?? 'Did something the band missed?', + loc?.workoutLogPastBody ?? + 'Enter the times yourself and it is scored from the heart rate ' + 'recorded across them, like any other session.', + fix: loc?.workoutLogPastFix ?? 'Log a past workout', + icon: LucideIcons.calendarPlus, + onFix: () => _push(c, const LogWorkout()), + ); + } List _history(BuildContext c, _WorkoutData d) { final p = P.of(c); + final loc = AppLocalizations.of(c); if (d.workouts.isEmpty) { return [ ..._suggestionCards(c, d), StatusCard( - 'No sessions recorded yet', - 'Sessions appear here once you start one.', - fix: 'Start a workout', + loc?.workoutNoSessionsTitle ?? 'No sessions recorded yet', + loc?.workoutNoSessionsBody ?? 'Sessions appear here once you start one.', + fix: loc?.workoutStartWorkoutFix ?? 'Start a workout', onFix: () => _openPicker(c, d), icon: LucideIcons.dumbbell, ), @@ -499,15 +553,18 @@ class _WorkoutScreenState extends State with RevisionReload { ..._suggestionCards(c, d), Row(children: [ Expanded(child: _sum(p, '${d.workoutsTracked ?? d.workouts.length}', - 'Tracked')), + loc?.workoutTrackedLabel ?? 'Tracked')), const SizedBox(width: S.x3), - Expanded(child: _sum(p, '${d.weekCount}', 'This week')), + Expanded(child: _sum(p, '${d.weekCount}', + loc?.workoutThisWeek ?? 'This week')), const SizedBox(width: S.x3), Expanded( child: _sum( p, - d.weekLoad == null ? 'None' : d.weekLoad!.round().toString(), - 'Weekly load')), + d.weekLoad == null + ? (loc?.workoutNoneLabel ?? 'None') + : d.weekLoad!.round().toString(), + loc?.workoutWeeklyLoadLabel ?? 'Weekly load')), ]), // The seam, said where the two numbers sit next to each other. "This // week" counts every session you did; "Weekly load" counts only the ones @@ -516,10 +573,11 @@ class _WorkoutScreenState extends State with RevisionReload { if (importedThisWeek > 0) ...[ const SizedBox(height: S.x3), Text( - '$importedThisWeek of this week’s sessions came from $storeName. ' - 'They count here, and they are left out of weekly load — an ' - 'imported workout arrives with no heart-rate trace, and a load ' - 'number without one would be invented.', + loc?.workoutImportedThisWeekNote(importedThisWeek, storeName) ?? + '$importedThisWeek of this week’s sessions came from $storeName. ' + 'They count here, and they are left out of weekly load — an ' + 'imported workout arrives with no heart-rate trace, and a load ' + 'number without one would be invented.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ], @@ -528,7 +586,7 @@ class _WorkoutScreenState extends State with RevisionReload { // session it exists to fill. const SizedBox(height: S.x3), ..._importCard(c, d), - ..._morningAfter(p, d), + ..._morningAfter(c, p, d), const SizedBox(height: S.x5), for (final w in d.workouts) ...[ _HistoryRow(w, @@ -548,7 +606,7 @@ class _WorkoutScreenState extends State with RevisionReload { start: w.start, end: w.start.add(w.duration), activity: w.activity, - title: 'Fix the times', + title: loc?.workoutFixTimes ?? 'Fix the times', ), ) : null), @@ -565,14 +623,18 @@ class _WorkoutScreenState extends State with RevisionReload { /// removed straight back. A copy in Apple Health / Health Connect itself is /// out of our reach by design and the confirm says so. Future _confirmDeleteWorkout(BuildContext c, _PastWorkout w) async { + final loc = AppLocalizations.of(c); final ok = await confirmRemove( c, - title: 'Delete this ${w.activity.name.toLowerCase()}?', + title: loc?.workoutConfirmDeleteTitle(w.activity.name.toLowerCase()) ?? + 'Delete this ${w.activity.name.toLowerCase()}?', body: w.importedFrom == null - ? 'It disappears from OpenStrap. A copy in $storeName, if there is ' - 'one, stays where it is.' - : 'It disappears from OpenStrap and will not be re-imported. ' - 'The original in $storeName stays.', + ? (loc?.workoutDeleteBodyOwn(storeName) ?? + 'It disappears from OpenStrap. A copy in $storeName, if there is ' + 'one, stays where it is.') + : (loc?.workoutDeleteBodyImported(storeName) ?? + 'It disappears from OpenStrap and will not be re-imported. ' + 'The original in $storeName stays.'), ); if (!ok || !mounted) return; if (w.importedFrom != null && w.id.isNotEmpty) { @@ -590,6 +652,7 @@ class _WorkoutScreenState extends State with RevisionReload { /// database export, which is not where anybody looks for their Sunday run. List _importCard(BuildContext c, _WorkoutData d) { final p = P.of(c); + final loc = AppLocalizations.of(c); return [ Surface( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -600,8 +663,10 @@ class _WorkoutScreenState extends State with RevisionReload { children: [ Pressable( semanticLabel: _autoImport - ? 'Auto-import on. Tap to turn off.' - : 'Auto-import off. Tap to turn on.', + ? (loc?.workoutAutoImportOnLabel ?? + 'Auto-import on. Tap to turn off.') + : (loc?.workoutAutoImportOffLabel ?? + 'Auto-import off. Tap to turn on.'), onTap: () => _setAutoImport(!_autoImport), child: Icon( _autoImport ? LucideIcons.circleCheckBig : LucideIcons.circle, @@ -611,12 +676,14 @@ class _WorkoutScreenState extends State with RevisionReload { ), const SizedBox(width: S.x3), Expanded( - child: Text('Import from $storeName', + child: Text( + loc?.workoutImportFromStore(storeName) ?? + 'Import from $storeName', style: F.body.copyWith( color: p.ink, fontWeight: FontWeight.w600)), ), Pressable( - semanticLabel: 'Fetch workouts now', + semanticLabel: loc?.workoutFetchNowLabel ?? 'Fetch workouts now', onTap: (!_autoImport || _importing) ? null : () => unawaited(_importWorkouts()), @@ -690,8 +757,10 @@ class _WorkoutScreenState extends State with RevisionReload { // us to read is how the whole set gets denied in one go. if (!await importer.requestPermission()) { if (!mounted) return; + final loc = AppLocalizations.of(context); setState(() { - _importNote = '$storeName did not grant workouts. Nothing was read.'; + _importNote = loc?.workoutImportDenied(storeName) ?? + '$storeName did not grant workouts. Nothing was read.'; _importFailed = true; }); return; @@ -699,9 +768,11 @@ class _WorkoutScreenState extends State with RevisionReload { final res = await importer.sync(); if (res.workouts == 0) { if (!mounted) return; + final loc = AppLocalizations.of(context); setState(() { - _importNote = 'Nothing came back. $storeName holds no workouts ' - 'inside the window it will share.'; + _importNote = loc?.workoutImportEmpty(storeName) ?? + 'Nothing came back. $storeName holds no workouts ' + 'inside the window it will share.'; _importFailed = false; }); return; @@ -710,23 +781,31 @@ class _WorkoutScreenState extends State with RevisionReload { // as an error, so marking a zero-row read as done would put "Refresh" on // the button for someone who said no. await markImported(HealthImport.workouts); + if (!mounted) return; + final loc = AppLocalizations.of(context); final route = !res.routesSupported // Said with the result rather than near it: this is the moment the // user is looking for their map. - ? ' $storeName will not share routes, so none have coordinates.' + ? (loc?.workoutImportNoRoutes(storeName) ?? + ' $storeName will not share routes, so none have coordinates.') : res.withRoutes == 0 - ? ' None of them had a route recorded.' - : ' ${res.withRoutes} came with a route.'; + ? (loc?.workoutImportNoneWithRoute ?? + ' None of them had a route recorded.') + : (loc?.workoutImportSomeWithRoute(res.withRoutes) ?? + ' ${res.withRoutes} came with a route.'); if (!mounted) return; setState(() { - _importNote = '${res.workouts} workout' - '${res.workouts == 1 ? '' : 's'} brought in.$route'; + _importNote = (loc?.workoutImportBroughtIn(res.workouts) ?? + '${res.workouts} workout' + '${res.workouts == 1 ? '' : 's'} brought in.') + + route; _importFailed = false; }); } catch (e) { if (!mounted) return; + final loc = AppLocalizations.of(context); setState(() { - _importNote = 'Failed: $e'; + _importNote = loc?.workoutImportFailed(e.toString()) ?? 'Failed: $e'; _importFailed = true; }); } finally { @@ -752,19 +831,21 @@ class _WorkoutScreenState extends State with RevisionReload { /// dropped, and a type under ten mornings is refused outright rather than /// shown with a small n. So this section simply does not exist for months, /// which is the honest state and not an empty card. - List _morningAfter(P p, _WorkoutData d) { + List _morningAfter(BuildContext c, P p, _WorkoutData d) { if (d.morningAfter.isEmpty) return const []; + final loc = AppLocalizations.of(c); return [ Section( - 'The morning after', + loc?.workoutMorningAfterTitle ?? 'The morning after', Surface( child: Column(children: [ - for (final e in d.morningAfter) _morningRow(e), + for (final e in d.morningAfter) _morningRow(c, e), const SizedBox(height: S.x3), Text( - 'Your own history, not a rule about the activity — these ' - 'mornings also had whatever evening came with them. Nothing ' - 'here is a reason to skip a session.', + loc?.workoutMorningAfterBody ?? + 'Your own history, not a rule about the activity — these ' + 'mornings also had whatever evening came with them. Nothing ' + 'here is a reason to skip a session.', style: F.cap.copyWith(color: p.ink3, height: 1.5), ), ]), @@ -773,7 +854,8 @@ class _WorkoutScreenState extends State with RevisionReload { ]; } - Widget _morningRow(MorningEffect e) { + Widget _morningRow(BuildContext c, MorningEffect e) { + final loc = AppLocalizations.of(c); final a = activityByName(e.type); final rhr = e.metric == 'rhr'; // Inside the metric's own minimal detectable change is not a finding, and @@ -782,12 +864,24 @@ class _WorkoutScreenState extends State with RevisionReload { return MetricRow( a?.icon ?? LucideIcons.activity, a?.color ?? C.purple, - 'After ${a?.name ?? e.type}', - e.exceedsMdc ? '$sign${e.delta.abs().toStringAsFixed(1)}' : 'Unchanged', + loc?.workoutAfterActivity(a?.name ?? e.type) ?? + 'After ${a?.name ?? e.type}', + e.exceedsMdc + ? '$sign${e.delta.abs().toStringAsFixed(1)}' + : (loc?.workoutUnchangedLabel ?? 'Unchanged'), unit: e.exceedsMdc ? (rhr ? 'bpm' : 'ms') : '', - sub: '${rhr ? 'Resting heart rate' : 'HRV'} · ' - '${e.n} morning${e.n == 1 ? '' : 's'}' - '${e.exceedsMdc ? '' : ' · inside your night-to-night range'}', + sub: () { + final metricLabel = rhr + ? (loc?.workoutRestingHeartRateLabel ?? 'Resting heart rate') + : (loc?.workoutHrvLabel ?? 'HRV'); + final morningCount = loc?.workoutMorningCount(e.n) ?? + '${e.n} morning${e.n == 1 ? '' : 's'}'; + final insideRangeSuffix = e.exceedsMdc + ? '' + : (loc?.workoutInsideRangeSuffix ?? + ' · inside your night-to-night range'); + return '$metricLabel · $morningCount$insideRangeSuffix'; + }(), ); } @@ -860,8 +954,9 @@ class _HistoryRow extends StatelessWidget { @override Widget build(BuildContext c) { final p = P.of(c); + final loc = AppLocalizations.of(c); final a = w.activity; - final stats = _stats; + final stats = _stats(c); return Surface( // An imported row does not open. The summary screen behind this tap is // built to show a session THIS band measured — its rating control, its @@ -908,8 +1003,8 @@ class _HistoryRow extends StatelessWidget { // the name of the thing that took it is the difference. Text( w.importedFrom == null - ? w.when - : '${w.importedFrom} · ${w.when}', + ? w.when(loc) + : '${w.importedFrom} · ${w.when(loc)}', style: F.over.copyWith(color: p.ink3)), ]), ), @@ -922,13 +1017,14 @@ class _HistoryRow extends StatelessWidget { // "strain", not "load". Training load is CTL/ATL over weeks; // this is one session's 0–21 strain, and the two were being // shown under the same word on the same screen. - child: Text('strain', style: F.over.copyWith(color: p.ink3)), + child: Text(loc?.workoutStrainLabel ?? 'strain', + style: F.over.copyWith(color: p.ink3)), ), ], if (onDelete != null) ...[ const SizedBox(width: S.x2), Pressable( - semanticLabel: 'Delete this session', + semanticLabel: loc?.workoutDeleteSessionLabel ?? 'Delete this session', onTap: onDelete, child: Padding( padding: const EdgeInsets.all(S.x1), @@ -941,8 +1037,8 @@ class _HistoryRow extends StatelessWidget { if (w.zoneMinutes.length == 5) ...[ const SizedBox(height: S.x4), ChartFrame( - title: 'TIME IN ZONES', - unit: 'minutes', + title: loc?.workoutTimeInZonesTitle ?? 'TIME IN ZONES', + unit: loc?.workoutMinutesUnit ?? 'minutes', height: 8, legend: [ for (var i = 0; i < 5; i++) @@ -970,11 +1066,12 @@ class _HistoryRow extends StatelessWidget { Divider(color: p.line, height: S.x5), Pressable( onTap: onRetime, - semanticLabel: 'Fix the times on this session', + semanticLabel: loc?.workoutFixTimesOnSessionLabel ?? + 'Fix the times on this session', child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.clock, size: 14, color: p.on(C.blue)), const SizedBox(width: S.x2), - Text('Fix the times', + Text(loc?.workoutFixTimes ?? 'Fix the times', style: F.cap.copyWith( color: p.on(C.blue), fontWeight: FontWeight.w600)), ]), @@ -990,7 +1087,11 @@ class _HistoryRow extends StatelessWidget { /// quietly loses its calorie line reads as a lighter session rather than an /// uncosted one. The unit is null in that case — 'Not costed kcal' is not a /// sentence. - List<(String, String, String?)> get _stats => w.importedFrom != null + List<(String, String, String?)> _stats(BuildContext c) { + final loc = AppLocalizations.of(c); + final timeLabel = loc?.workoutTimeStatLabel ?? 'Time'; + final caloriesLabel = loc?.workoutCaloriesStatLabel ?? 'Calories'; + return w.importedFrom != null ? [ // An imported row prints only what the recording app actually // recorded, and drops the rest rather than saying "No reading": this @@ -999,29 +1100,44 @@ class _HistoryRow extends StatelessWidget { // shown under the source's name a line above — never added to ours, // because two devices' calorie models summed is a number neither of // them would stand behind. - ('Time', hms(w.duration), null), + (timeLabel, hms(w.duration), null), if (w.distanceM != null && w.distanceM! > 0) - ('Distance', (w.distanceM! / 1000).toStringAsFixed(2), 'km'), - if (w.calories != null) ('Calories', grouped(w.calories!), 'kcal'), + (loc?.workoutDistanceStatLabel ?? 'Distance', + (w.distanceM! / 1000).toStringAsFixed(2), 'km'), + if (w.calories != null) + (caloriesLabel, grouped(w.calories!), 'kcal'), ] : [ - ('Time', hms(w.duration), null), + (timeLabel, hms(w.duration), null), if (w.calories == null) - ('Calories', 'Not costed', null) + (caloriesLabel, loc?.workoutNotCostedValue ?? 'Not costed', null) else - ('Calories', grouped(w.calories!), 'kcal'), + (caloriesLabel, grouped(w.calories!), 'kcal'), if (w.maxHr == null) - ('Max HR', 'No reading', null) + (loc?.workoutMaxHrStatLabel ?? 'Max HR', + loc?.workoutNoReadingValue ?? 'No reading', null) else - ('Max HR', '${w.maxHr}', 'bpm'), + (loc?.workoutMaxHrStatLabel ?? 'Max HR', '${w.maxHr}', 'bpm'), ]; + } } /// One day's initial. Taken from the date the point carries — deriving it from /// the point's position in the list is how a chart comes to name days its data /// did not come from. -String _weekdayLetter(DateTime d) => - const ['M', 'T', 'W', 'T', 'F', 'S', 'S'][d.weekday - 1]; +String _weekdayLetter(BuildContext c, DateTime d) { + final loc = AppLocalizations.of(c); + final letters = [ + loc?.workoutWeekdayLetterMon ?? 'M', + loc?.workoutWeekdayLetterTue ?? 'T', + loc?.workoutWeekdayLetterWed ?? 'W', + loc?.workoutWeekdayLetterThu ?? 'T', + loc?.workoutWeekdayLetterFri ?? 'F', + loc?.workoutWeekdayLetterSat ?? 'S', + loc?.workoutWeekdayLetterSun ?? 'S', + ]; + return letters[d.weekday - 1]; +} /// Which of the seven slots [at] falls in — 6 being [end]'s own day — or null /// when it is outside the window. @@ -1388,6 +1504,14 @@ Map? _topBand(Object? bands) { return top is Map ? top.cast() : null; } +/// `zone_min` comes back from the repo as raw decoded JSON — guard the type +/// once here so a non-`List` value (or a non-numeric entry) never throws in +/// either read path. +List _decodeZoneMinutes(Object? raw) => [ + for (final z in (raw is List ? raw : const [])) + if (z is num) z.toDouble(), + ]; + /// One past session, opened from history — built from what the stores hold /// rather than from the six columns the list row carries. Future _detailOf(AppState app, _PastWorkout w) async { @@ -1402,10 +1526,7 @@ Future _detailOf(AppState app, _PastWorkout w) async { // rather than blanking the bars, but that is a different read from a // different moment, so it is one more case where the bands beside it // describe a different set. - final decoded = [ - for (final z in (b['zone_min'] as List? ?? const [])) - if (z is num) z.toDouble(), - ]; + final decoded = _decodeZoneMinutes(b['zone_min']); final usedBundleSplit = decoded.length == 5; // …and whether that split was binned by the pass that produced the bands. final rebinned = usedBundleSplit && b['zone_min_rebinned'] != false; @@ -1648,16 +1769,21 @@ class _PastWorkout { return [for (final z in zoneMinutes) z / total]; } - String get when { - final now = DateTime.now(); - final days = DateTime(now.year, now.month, now.day) - .difference(DateTime(start.year, start.month, start.day)) - .inDays; - const names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + String when(AppLocalizations? loc) { + final days = calendarDaysBetween(start, DateTime.now()); + final names = [ + loc?.workoutWeekdayAbbrMon ?? 'Mon', + loc?.workoutWeekdayAbbrTue ?? 'Tue', + loc?.workoutWeekdayAbbrWed ?? 'Wed', + loc?.workoutWeekdayAbbrThu ?? 'Thu', + loc?.workoutWeekdayAbbrFri ?? 'Fri', + loc?.workoutWeekdayAbbrSat ?? 'Sat', + loc?.workoutWeekdayAbbrSun ?? 'Sun', + ]; final t = '${start.hour.toString().padLeft(2, '0')}:' '${start.minute.toString().padLeft(2, '0')}'; - if (days == 0) return 'Today, $t'; - if (days == 1) return 'Yesterday, $t'; + if (days == 0) return loc?.workoutWhenToday(t) ?? 'Today, $t'; + if (days == 1) return loc?.workoutWhenYesterday(t) ?? 'Yesterday, $t'; if (days < 7) return '${names[start.weekday - 1]}, $t'; return '${start.day}/${start.month}, $t'; } @@ -1851,10 +1977,7 @@ Future<_WorkoutData> _loadWorkoutData(AppState app) async { maxHr: (r['max_hr'] as num?)?.toInt(), hrr60: (r['hrr60'] as num?)?.round(), steps: (r['steps'] as num?)?.toInt(), - zoneMinutes: [ - for (final z in (r['zone_min'] as List? ?? const [])) - if (z is num) z.toDouble(), - ], + zoneMinutes: _decodeZoneMinutes(r['zone_min']), private: r['private'] == true, )); } diff --git a/test/rough_night_test.dart b/test/rough_night_test.dart index 8223a492..a4b61347 100644 --- a/test/rough_night_test.dart +++ b/test/rough_night_test.dart @@ -238,6 +238,7 @@ void main() { descriptor: 'a rougher night than usual for you — x', moved: ['your HRV ran lower'], knows: ['The illness watch flagged this night too — a rise.'], + illnessFlagged: true, ); expect(flagged.ask, isNot(contains('sick'))); // And the one it never offers on any night: the card IS the sleep diff --git a/test/ui2_activity_test.dart b/test/ui2_activity_test.dart index 46d637b1..09894707 100644 --- a/test/ui2_activity_test.dart +++ b/test/ui2_activity_test.dart @@ -1061,7 +1061,7 @@ void main() { // them says so in words. This was only ever pinned by the goldens, // which is why it went four sweeps without anyone being able to say // what the 20 red PNGs were red about. - expect(find.text(kZonesWhy), findsOneWidget, + expect(find.text(zonesWhyFootnote()), findsOneWidget, reason: '${arch.name} zones must admit the ceiling is estimated'); } expect(tester.takeException(), isNull); diff --git a/test/ui2_revision_reload_test.dart b/test/ui2_revision_reload_test.dart index 7f02e2a3..05db5935 100644 --- a/test/ui2_revision_reload_test.dart +++ b/test/ui2_revision_reload_test.dart @@ -24,6 +24,7 @@ import 'package:openstrap_edge/data/db.dart'; import 'package:openstrap_edge/data/journal_fields.dart'; import 'package:openstrap_edge/data/local_repository.dart'; import 'package:openstrap_edge/state/app_state.dart'; +import 'package:openstrap_edge/state/locale_controller.dart'; import 'package:openstrap_edge/ui2/screens/screens.dart'; import 'package:openstrap_edge/ui2/ui2.dart'; @@ -59,10 +60,15 @@ Future _until(WidgetTester t, Finder f, {int n = 60}) async { } } -Widget _app(AppState app) => MaterialApp( +Widget _app(AppState app, {LocaleController? locale}) => MaterialApp( theme: buildTheme(Brightness.light), - home: ChangeNotifierProvider.value( - value: app, + home: MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: app), + ChangeNotifierProvider.value( + value: locale ?? LocaleController.seed(null), + ), + ], child: const Scaffold(body: NutritionScreen()), ), ); @@ -144,6 +150,43 @@ void main() { reason: 'the screen re-read with nothing having landed'); }); + testWidgets('a language switch reaches the live tab', (t) async { + t.view.physicalSize = const Size(390 * 3, 2400 * 3); + t.view.devicePixelRatio = 3; + addTearDown(t.view.reset); + + final app = AppState.forTesting(); + addTearDown(app.dispose); + final repo = _Repo(); + app.repo = repo; + final locale = LocaleController.seed(null); + addTearDown(locale.dispose); + + await t.pumpWidget(_app(app, locale: locale)); + await _until(t, find.text('None yet')); + final before = t.state(find.byType(NutritionScreen)); + final reads = repo.reads; + expect(reads, greaterThan(0)); + + // Nothing wrote underneath the screen — only the language changed. + // `NutritionData` bakes `AppLocalizations` strings into what it reads, so + // this has to reach the screen exactly like a durable write does, or a + // switch to Spanish leaves English on screen until something else forces + // a reload. + await locale.setCode('es'); + for (var i = 0; i < 60 && repo.reads <= reads; i++) { + await t.runAsync( + () => Future.delayed(const Duration(milliseconds: 20))); + await t.pump(); + } + + expect(repo.reads, greaterThan(reads), + reason: 'the screen did not notice the language changed'); + expect(identical(t.state(find.byType(NutritionScreen)), before), isTrue, + reason: + 'the screen was remounted — that is the workaround, not the fix'); + }); + // ── the signal has to be raised where the writes are ────────────────────── // // A behavioural test of the importers needs a vendor export, a database and