From fd4f83539c6c3aa8d1661bb332324931ce14906c Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:05:30 +0200 Subject: [PATCH 01/28] Point update mechanism and install docs at this fork The in-app update checkers (macOS + Android), AltStore/SideStore source metadata, bug-report links, and install/README docs all still pointed at ParthJadhav/noop. Retarget them at MatB57/noop so future releases published here are the ones this fork's users (and their sideloaded devices) actually discover and auto-update to. Co-Authored-By: Claude Sonnet 5 --- README.md | 4 ++-- Strand/Screens/SettingsView.swift | 2 +- Strand/System/TestReportLink.swift | 4 ++-- Strand/System/UpdateChecker.swift | 2 +- Tools/update-altstore-source.sh | 4 ++-- altstore-source.json | 10 +++++----- .../main/java/com/noop/testcentre/TestReportLink.kt | 2 +- .../app/src/main/java/com/noop/ui/SettingsScreen.kt | 4 ++-- .../app/src/main/java/com/noop/update/UpdateCheck.kt | 2 +- docs/IOS.md | 6 +++--- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c1f094abf..1385c15f7 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ Pre-built apps you can run right now: | Platform | Build | Notes | |---|---|---| -| **macOS** | `NOOP-vX-macos.zip` (see [Releases](https://github.com/ParthJadhav/noop/releases)) | Apple Silicon + Intel. Unzip and drag `NOOP.app` to Applications. Not notarized — see **First launch on macOS** below. | -| **Android** | `NOOP-vX.apk` (see [Releases](https://github.com/ParthJadhav/noop/releases)) | The full app. `minSdk 26` (Android 8+). Sideload — enable "install unknown apps". Blocked by Play Protect? See **Installing on Android** below. | +| **macOS** | `NOOP-vX-macos.zip` (see [Releases](https://github.com/MatB57/noop/releases)) | Apple Silicon + Intel. Unzip and drag `NOOP.app` to Applications. Not notarized — see **First launch on macOS** below. | +| **Android** | `NOOP-vX.apk` (see [Releases](https://github.com/MatB57/noop/releases)) | The full app. `minSdk 26` (Android 8+). Sideload — enable "install unknown apps". Blocked by Play Protect? See **Installing on Android** below. | | **iOS** | `NOOP-vX-ios.ipa` (see [Releases](https://github.com/ParthJadhav/noop/releases)) — sideload with AltStore/SideStore | Now a **direct download**. The `.ipa` is unsigned; **you** sign it on your iPhone with your own free Apple ID (no App Store, no developer account — NOOP stays anonymous). Re-signs every 7 days (AltStore automates it); Apple Health + Live Activity widgets may be limited under a free signing identity. See [docs/IOS.md](docs/IOS.md). Or build from source in Xcode. | > **First launch on macOS.** NOOP is **not notarized** by Apple — notarization needs a paid Apple diff --git a/Strand/Screens/SettingsView.swift b/Strand/Screens/SettingsView.swift index 8ab6f6956..ce3b8f830 100644 --- a/Strand/Screens/SettingsView.swift +++ b/Strand/Screens/SettingsView.swift @@ -1907,7 +1907,7 @@ struct SettingsView: View { } // Project home — NOOP's code, releases, issues and wiki live on GitHub. - Link(destination: URL(string: "https://github.com/ParthJadhav/noop")!) { + Link(destination: URL(string: "https://github.com/MatB57/noop")!) { HStack(spacing: 10) { Image(systemName: "chevron.left.forwardslash.chevron.right") .foregroundStyle(StrandPalette.accent) diff --git a/Strand/System/TestReportLink.swift b/Strand/System/TestReportLink.swift index 47dda1659..f225194e1 100644 --- a/Strand/System/TestReportLink.swift +++ b/Strand/System/TestReportLink.swift @@ -5,7 +5,7 @@ import StrandAnalytics /// the bug form's existing id fields (version, platform, os_version, test_profile, title) and /// self-applies the "bug,test:" labels so a submission lands pre-labelled on the right cluster. /// No network, no cloud: this only composes a URL the caller opens in the browser. Repo is -/// ParthJadhav/noop (confirmed in bug_report.yml). +/// MatB57/noop (this fork's own issue tracker). /// /// CAPTURE-A (#812): a report submitted WITHOUT the .zip attached used to land empty, because the form's /// `log` / `what_happens` textareas were blank and the user often forgot the paperclip. We now PREFILL @@ -93,7 +93,7 @@ enum TestReportLink { if let seed = whatHappensSeed, !seed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { query.append("what_happens=" + enc(seed)) } - let base = "https://github.com/ParthJadhav/noop/issues/new?" + let base = "https://github.com/MatB57/noop/issues/new?" // Add the log block ONLY if the whole URL stays under the GitHub prefill ceiling. If it would // breach maxURLLength, drop `log` entirely (never truncate it into a broken
); the full // trace is in the attached .zip. The seed + id fields alone keep the body non-empty (#812). diff --git a/Strand/System/UpdateChecker.swift b/Strand/System/UpdateChecker.swift index 4982bafae..0a3bbd796 100644 --- a/Strand/System/UpdateChecker.swift +++ b/Strand/System/UpdateChecker.swift @@ -18,7 +18,7 @@ final class UpdateChecker: ObservableObject { @Published var state: State = .idle - private static let endpoint = URL(string: "https://api.github.com/repos/ParthJadhav/noop/releases/latest")! + private static let endpoint = URL(string: "https://api.github.com/repos/MatB57/noop/releases/latest")! func check(currentVersion: String) { guard state != .checking else { return } diff --git a/Tools/update-altstore-source.sh b/Tools/update-altstore-source.sh index 445108d59..154d2dde9 100755 --- a/Tools/update-altstore-source.sh +++ b/Tools/update-altstore-source.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # update-altstore-source.sh [desc] — refresh altstore-source.json with a new iOS release. -# The downloadURL points at the canonical GitHub release asset (github.com/ParthJadhav/noop/releases); +# The downloadURL points at the canonical GitHub release asset (github.com/MatB57/noop/releases); # noop.fans stays a mirror. Everything else reads CFBundleVersion + size from the IPA, # prepends/replaces apps[0].versions[0], and mirrors legacy top-level fields. # @@ -12,7 +12,7 @@ set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" [ -f "$HERE/../deploy.env" ] && source "$HERE/../deploy.env" DOMAIN="${FORGE_DOMAIN:-${NOOP_DOMAIN:-noop.fans}}" -ORG="${FORGE_ORG:-NoopApp}"; REPO="${FORGE_REPO:-noop}" +ORG="${FORGE_ORG:-MatB57}"; REPO="${FORGE_REPO:-noop}" VERSION="${1:?usage: $0 [desc]}" IPA="${2:?usage: $0 [desc]}" diff --git a/altstore-source.json b/altstore-source.json index 5fa019759..10f09caf8 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -2,8 +2,8 @@ "name": "NOOP", "subtitle": "A standalone WHOOP app — your strap, your device, fully offline.", "description": "An AltStore / SideStore source for NOOP, the independent open-source WHOOP companion. Add this source to get one-tap updates for the sideloaded iOS build.\n\nNOOP is not affiliated with WHOOP. It reads a strap you own, directly over Bluetooth, on your own device — no WHOOP app, no subscription, no cloud.", - "iconURL": "https://github.com/ParthJadhav/noop/raw/main/Strand/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", - "website": "https://github.com/ParthJadhav/noop", + "iconURL": "https://github.com/MatB57/noop/raw/main/Strand/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", + "website": "https://github.com/MatB57/noop", "tintColor": "44E2B0", "apps": [ { @@ -12,7 +12,7 @@ "developerName": "NoopApp", "subtitle": "Read your own WHOOP strap, fully offline.", "localizedDescription": "NOOP talks to a WHOOP 4.0 / 5.0 / MG strap you own directly over Bluetooth — no WHOOP app, no subscription, no cloud. Live heart rate, history sync, and NOOP's own Charge / Effort / Rest scores, all computed on-device and stored only on your iPhone.\n\nIndependent and experimental; not affiliated with WHOOP. Source: https://github.com/ParthJadhav/noop", - "iconURL": "https://github.com/ParthJadhav/noop/raw/main/Strand/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", + "iconURL": "https://github.com/MatB57/noop/raw/main/Strand/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", "tintColor": "44E2B0", "category": "healthcare-fitness", "screenshotURLs": [], @@ -22,7 +22,7 @@ "buildVersion": "170", "date": "2026-07-05", "localizedDescription": "NOOP 8.2.0. See the release notes for what changed.", - "downloadURL": "https://github.com/ParthJadhav/noop/releases/download/v8.2.0/NOOP-v8.2.0-ios.ipa", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.0/NOOP-v8.2.0-ios.ipa", "size": 14027253, "minOSVersion": "17.0" } @@ -41,7 +41,7 @@ "buildVersion": "170", "versionDate": "2026-07-05", "versionDescription": "NOOP 8.2.0. See the release notes for what changed.", - "downloadURL": "https://github.com/ParthJadhav/noop/releases/download/v8.2.0/NOOP-v8.2.0-ios.ipa", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.0/NOOP-v8.2.0-ios.ipa", "size": 14027253, "minOSVersion": "17.0" } diff --git a/android/app/src/main/java/com/noop/testcentre/TestReportLink.kt b/android/app/src/main/java/com/noop/testcentre/TestReportLink.kt index 3bf2c687e..3af479ac5 100644 --- a/android/app/src/main/java/com/noop/testcentre/TestReportLink.kt +++ b/android/app/src/main/java/com/noop/testcentre/TestReportLink.kt @@ -89,7 +89,7 @@ object TestReportLink { // CAPTURE-A: seed what_happens so the body is never empty (#812). if (!whatHappensSeed.isNullOrBlank()) add("what_happens" to whatHappensSeed) } - val base = "https://github.com/ParthJadhav/noop/issues/new?" + val base = "https://github.com/MatB57/noop/issues/new?" fun render(list: List>) = base + list.joinToString("&") { (k, v) -> "$k=${enc(v)}" } // Add the log block ONLY if the whole URL stays under the GitHub prefill ceiling; otherwise drop it // entirely (never truncate into a broken
) and rely on the attached/shared .zip (#812). diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 9768481c4..f9d2a018b 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -1995,11 +1995,11 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { interactionSource = projectHomeInteraction, indication = null, ) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/ParthJadhav/noop")) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/MatB57/noop")) try { context.startActivity(intent) } catch (_: ActivityNotFoundException) { - Toast.makeText(context, "github.com/ParthJadhav/noop", Toast.LENGTH_LONG).show() + Toast.makeText(context, "github.com/MatB57/noop", Toast.LENGTH_LONG).show() } } .padding(horizontal = 14.dp, vertical = 12.dp) diff --git a/android/app/src/main/java/com/noop/update/UpdateCheck.kt b/android/app/src/main/java/com/noop/update/UpdateCheck.kt index e894934fe..f2e56af5b 100644 --- a/android/app/src/main/java/com/noop/update/UpdateCheck.kt +++ b/android/app/src/main/java/com/noop/update/UpdateCheck.kt @@ -15,7 +15,7 @@ import java.net.URL */ object UpdateCheck { - private const val ENDPOINT = "https://api.github.com/repos/ParthJadhav/noop/releases/latest" + private const val ENDPOINT = "https://api.github.com/repos/MatB57/noop/releases/latest" sealed interface Result { data class UpToDate(val version: String) : Result diff --git a/docs/IOS.md b/docs/IOS.md index 2986e5845..34d25ec9b 100644 --- a/docs/IOS.md +++ b/docs/IOS.md @@ -1,7 +1,7 @@ # iOS — Install & Build > **iOS is now a direct download (v1.96).** Grab **`NOOP-v-ios.ipa`** from the -> [Releases](https://github.com/ParthJadhav/noop/releases) page and install it with **AltStore** or **SideStore** — see +> [Releases](https://github.com/MatB57/noop/releases) page and install it with **AltStore** or **SideStore** — see > **[Install (sideload)](#install-sideload)** below. No Mac, no Xcode, no App Store, and no Apple > Developer account needed — **and NOOP stays anonymous**, because the `.ipa` we ship is *unsigned* > and **you** sign it on your own iPhone with your own free Apple ID. The app target (`NOOPiOS` + @@ -18,7 +18,7 @@ Nothing about this touches NOOP's identity or Apple's servers on our side. 1. **Install a sideloader on your computer** — [AltStore](https://altstore.io) or [SideStore](https://sidestore.io) (both free). Follow their one-time setup (it installs a helper + AltStore/SideStore onto your iPhone using your own Apple ID). -2. **Download `NOOP-v-ios.ipa`** from [Releases](https://github.com/ParthJadhav/noop/releases) to your iPhone (or your +2. **Download `NOOP-v-ios.ipa`** from [Releases](https://github.com/MatB57/noop/releases) to your iPhone (or your computer, then AirDrop/transfer it). 3. **Open the `.ipa` with AltStore/SideStore** (Share → AltStore, or the app's "+" button). It signs and installs NOOP. First launch may need **Settings → General → VPN & Device Management → trust @@ -29,7 +29,7 @@ Nothing about this touches NOOP's identity or Apple's servers on our side. So you never have to manually re-download, add NOOP's **source** to AltStore/SideStore once — new releases then show up (and re-sign) automatically: -**Source URL:** `https://raw.githubusercontent.com/ParthJadhav/noop/main/altstore-source.json` +**Source URL:** `https://raw.githubusercontent.com/MatB57/noop/main/altstore-source.json` > Make sure you copy the **raw** URL above exactly. If a sideloader says **"given data not valid > JSON"** when you add the source, you've pasted a normal web page URL (which returns HTML) instead of From e09d8ee9dc0f128274ae2f20b361db5da5d6321f Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:15:46 +0200 Subject: [PATCH 02/28] Complete French localization across app, watch and Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit French was ~15% complete on the main catalog (458/2988 keys) and partial elsewhere. Fills in all remaining keys across the four Xcode string catalogs (Strand, NOOPWatch, NOOPWatchComplications, StrandDesign) and adds the missing android/values-fr/strings.xml, matching the established "vous"-register terminology already used in the existing French strings (Récupération, Effort, VFC, Bracelet, Sources de données, etc.). Co-Authored-By: Claude Sonnet 5 --- NOOPWatch/Localizable.xcstrings | 228 + NOOPWatchComplications/Localizable.xcstrings | 162 + .../Resources/Localizable.xcstrings | 216 + Strand/Resources/Localizable.xcstrings | 15174 ++++++++++++++++ .../app/src/main/res/values-fr/strings.xml | 76 + 5 files changed, 15856 insertions(+) create mode 100644 android/app/src/main/res/values-fr/strings.xml diff --git a/NOOPWatch/Localizable.xcstrings b/NOOPWatch/Localizable.xcstrings index 2973ee88a..ad1d612d4 100644 --- a/NOOPWatch/Localizable.xcstrings +++ b/NOOPWatch/Localizable.xcstrings @@ -32,6 +32,12 @@ "state": "translated", "value": "%@ 次/分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ resp/min" + } } } }, @@ -66,6 +72,12 @@ "state": "translated", "value": "配速 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Allure %@" + } } } }, @@ -100,6 +112,12 @@ "state": "translated", "value": "%@ · 吸 %@ 秒 / 呼 %@ 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · %@s inspir. / %@s expir." + } } } }, @@ -134,6 +152,12 @@ "state": "translated", "value": "%@ · %lld kcal" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · %lld kcal" + } } } }, @@ -168,6 +192,12 @@ "state": "translated", "value": "%lld 次呼吸 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld respirations · %@" + } } } }, @@ -202,6 +232,12 @@ "state": "translated", "value": "%2$@ 還剩 %1$lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld secondes restantes en %@" + } } } }, @@ -236,6 +272,12 @@ "state": "translated", "value": "允許存取" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l'accès" + } } } }, @@ -270,6 +312,12 @@ "state": "translated", "value": "箱式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Carré" + } } } }, @@ -356,6 +404,12 @@ "state": "translated", "value": "吸氣" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspirez" + } } } }, @@ -390,6 +444,12 @@ "state": "translated", "value": "吸氣 %lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspirez pendant %lld secondes" + } } } }, @@ -424,6 +484,12 @@ "state": "translated", "value": "吐氣" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expirez" + } } } }, @@ -458,6 +524,12 @@ "state": "translated", "value": "吐氣 %lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expirez pendant %lld secondes" + } } } }, @@ -544,6 +616,12 @@ "state": "translated", "value": "諧振" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cohérence" + } } } }, @@ -578,6 +656,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TERMINÉ" + } } } }, @@ -612,6 +696,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminé" + } } } }, @@ -698,6 +788,12 @@ "state": "translated", "value": "結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fin" + } } } }, @@ -732,6 +828,12 @@ "state": "translated", "value": "結束鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer l'entraînement" + } } } }, @@ -766,6 +868,12 @@ "state": "translated", "value": "功能性肌力" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Force fonctionnelle" + } } } }, @@ -800,6 +908,12 @@ "state": "translated", "value": "授予「健康」存取權" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l'accès à Santé" + } } } }, @@ -886,6 +1000,12 @@ "state": "translated", "value": "無法取得心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC indisponible" + } } } }, @@ -920,6 +1040,12 @@ "state": "translated", "value": "即時心率和能量消耗,記錄在你的手腕上。留在裝置上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque et énergie en direct, enregistrées au poignet. Reste sur l'appareil." + } } } }, @@ -954,6 +1080,12 @@ "state": "translated", "value": "在你的 iPhone 上打開 NOOP 以同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez NOOP sur votre iPhone pour synchroniser" + } } } }, @@ -988,6 +1120,12 @@ "state": "translated", "value": "已暫停" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EN PAUSE" + } } } }, @@ -1074,6 +1212,12 @@ "state": "translated", "value": "記錄中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ENREGISTREMENT" + } } } }, @@ -1108,6 +1252,12 @@ "state": "translated", "value": "休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "REPOS" + } } } }, @@ -1142,6 +1292,12 @@ "state": "translated", "value": "準備好呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à respirer" + } } } }, @@ -1176,6 +1332,12 @@ "state": "translated", "value": "放鬆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détendez-vous" + } } } }, @@ -1366,6 +1528,12 @@ "state": "translated", "value": "繼續" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reprendre" + } } } }, @@ -1400,6 +1568,12 @@ "state": "translated", "value": "第 %lld 輪,共 %lld 輪" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Round %lld sur %lld" + } } } }, @@ -1434,6 +1608,12 @@ "state": "translated", "value": "秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SEC" + } } } }, @@ -1468,6 +1648,12 @@ "state": "translated", "value": "工作階段結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séance terminée" + } } } }, @@ -1606,6 +1792,12 @@ "state": "translated", "value": "停止" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrêter" + } } } }, @@ -1692,6 +1884,12 @@ "state": "translated", "value": "運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TRAVAIL" + } } } }, @@ -1726,6 +1924,12 @@ "state": "translated", "value": "鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînement" + } } } }, @@ -1760,6 +1964,12 @@ "state": "translated", "value": "運動已保存" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînement enregistré" + } } } }, @@ -1794,6 +2004,12 @@ "state": "translated", "value": "截至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au %@" + } } } }, @@ -1856,6 +2072,12 @@ "state": "translated", "value": "cal" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cal" + } } } }, @@ -1890,6 +2112,12 @@ "state": "translated", "value": "過時 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "obsolète · %@" + } } } } diff --git a/NOOPWatchComplications/Localizable.xcstrings b/NOOPWatchComplications/Localizable.xcstrings index 5e4fa4293..7a885af1a 100644 --- a/NOOPWatchComplications/Localizable.xcstrings +++ b/NOOPWatchComplications/Localizable.xcstrings @@ -8,6 +8,12 @@ "state": "translated", "value": "%@ %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ %lld" + } } } }, @@ -18,6 +24,12 @@ "state": "translated", "value": "%@ calibrating" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ en calibration" + } } } }, @@ -28,6 +40,12 @@ "state": "translated", "value": "%@ unavailable" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ indisponible" + } } } }, @@ -90,6 +108,12 @@ "state": "translated", "value": "Charge %lld out of 100" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge %lld sur 100" + } } } }, @@ -100,6 +124,12 @@ "state": "translated", "value": "Charge %lld · %lld bpm%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge %lld · %lld bpm%@" + } } } }, @@ -110,6 +140,12 @@ "state": "translated", "value": "Charge %lld%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge %lld%@" + } } } }, @@ -120,6 +156,12 @@ "state": "translated", "value": "Charge calibrating" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge en calibration" + } } } }, @@ -130,6 +172,12 @@ "state": "translated", "value": "Charge calibrating, needs more data" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge en calibration, besoin de plus de données" + } } } }, @@ -140,6 +188,12 @@ "state": "translated", "value": "Charge out of date, last synced %@. Open NOOP on iPhone." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge non à jour, dernière synchro %@. Ouvrez NOOP sur iPhone." + } } } }, @@ -150,6 +204,12 @@ "state": "translated", "value": "Charge stale · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge obsolète · %@" + } } } }, @@ -160,6 +220,12 @@ "state": "translated", "value": "Charge unavailable" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge indisponible" + } } } }, @@ -170,6 +236,12 @@ "state": "translated", "value": "Charge · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge · %@" + } } } }, @@ -180,6 +252,12 @@ "state": "translated", "value": "Charge · cal" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge · cal" + } } } }, @@ -190,6 +268,12 @@ "state": "translated", "value": "Charge –" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge –" + } } } }, @@ -276,6 +360,12 @@ "state": "translated", "value": "NOOP 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge NOOP" + } } } }, @@ -286,6 +376,12 @@ "state": "translated", "value": "NOOP · open on iPhone" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP · ouvrir sur iPhone" + } } } }, @@ -296,6 +392,12 @@ "state": "translated", "value": "NOOP. %@, %@, %@." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP. %@, %@, %@." + } } } }, @@ -306,6 +408,12 @@ "state": "translated", "value": "NOOP. No data yet, open NOOP on your iPhone to sync." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP. Pas encore de données, ouvrez NOOP sur votre iPhone pour synchroniser." + } } } }, @@ -316,6 +424,12 @@ "state": "translated", "value": "NOOP. Scores out of date, last synced %@. Open NOOP on iPhone to refresh." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP. Scores non à jour, dernière synchro %@. Ouvrez NOOP sur iPhone pour actualiser." + } } } }, @@ -326,6 +440,12 @@ "state": "translated", "value": "No data, open NOOP on iPhone" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas de données, ouvrez NOOP sur iPhone" + } } } }, @@ -336,6 +456,12 @@ "state": "translated", "value": "Open NOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir NOOP" + } } } }, @@ -422,6 +548,12 @@ "state": "translated", "value": "錶面上顯示你的能量(恢復),矩形卡片中則是消耗與休息。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre Charge (récupération) sur le cadran, avec Effort et Repos dans la carte rectangulaire." + } } } }, @@ -432,6 +564,12 @@ "state": "translated", "value": "a while ago" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a un moment" + } } } }, @@ -442,6 +580,12 @@ "state": "translated", "value": "cal" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cal" + } } } }, @@ -452,6 +596,12 @@ "state": "translated", "value": "old" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ancien" + } } } }, @@ -462,6 +612,12 @@ "state": "translated", "value": "open iPhone" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ouvrir iPhone" + } } } }, @@ -472,6 +628,12 @@ "state": "translated", "value": "stale" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "obsolète" + } } } } diff --git a/Packages/StrandDesign/Sources/StrandDesign/Resources/Localizable.xcstrings b/Packages/StrandDesign/Sources/StrandDesign/Resources/Localizable.xcstrings index 88227f111..b6cd14e52 100644 --- a/Packages/StrandDesign/Sources/StrandDesign/Resources/Localizable.xcstrings +++ b/Packages/StrandDesign/Sources/StrandDesign/Resources/Localizable.xcstrings @@ -8,6 +8,12 @@ "state": "translated", "value": "%lld days ago" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld jours" + } } } }, @@ -42,6 +48,12 @@ "state": "translated", "value": "%lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld heures" + } } } }, @@ -52,6 +64,12 @@ "state": "translated", "value": "%lld minutes" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld minutes" + } } } }, @@ -62,6 +80,12 @@ "state": "translated", "value": "%lld points, mean %@, range %@ to %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld points, moyenne %@, plage %@ à %@" + } } } }, @@ -72,6 +96,12 @@ "state": "translated", "value": "%lld readings" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld mesures" + } } } }, @@ -82,6 +112,12 @@ "state": "translated", "value": "%lld workouts" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld entraînements" + } } } }, @@ -92,6 +128,12 @@ "state": "translated", "value": "%lldd ago" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lldj" + } } } }, @@ -126,6 +168,12 @@ "state": "translated", "value": "%lld 小時前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lldh" + } } } }, @@ -136,6 +184,12 @@ "state": "translated", "value": "%lldm ago" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lldmin" + } } } }, @@ -170,6 +224,12 @@ "state": "translated", "value": "1 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 heure" + } } } }, @@ -180,6 +240,12 @@ "state": "translated", "value": "1 minute" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 minute" + } } } }, @@ -190,6 +256,12 @@ "state": "translated", "value": "1 workout" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 entraînement" + } } } }, @@ -200,6 +272,12 @@ "state": "translated", "value": "ALL-OUT" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MAXIMAL" + } } } }, @@ -262,6 +340,12 @@ "state": "translated", "value": "Classic" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Classique" + } } } }, @@ -272,6 +356,12 @@ "state": "translated", "value": "DEPLETED" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ÉPUISÉ" + } } } }, @@ -282,6 +372,12 @@ "state": "translated", "value": "Dark" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sombre" + } } } }, @@ -368,6 +464,12 @@ "state": "translated", "value": "預設" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut" + } } } }, @@ -402,6 +504,12 @@ "state": "translated", "value": "高" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ÉLEVÉ" + } } } }, @@ -412,6 +520,12 @@ "state": "translated", "value": "LIGHT" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LÉGER" + } } } }, @@ -446,6 +560,12 @@ "state": "translated", "value": "低" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FAIBLE" + } } } }, @@ -508,6 +628,12 @@ "state": "translated", "value": "MODERATE" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MODÉRÉ" + } } } }, @@ -570,6 +696,12 @@ "state": "translated", "value": "No heart-rate data" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée de fréquence cardiaque" + } } } }, @@ -604,6 +736,12 @@ "state": "translated", "value": "巔峰" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PIC" + } } } }, @@ -614,6 +752,12 @@ "state": "translated", "value": "PRIMED" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PRÊT" + } } } }, @@ -624,6 +768,12 @@ "state": "translated", "value": "Recovery calendar, %lld days, average %lld, low %lld, high %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calendrier de récupération, %lld jours, moyenne %lld, minimum %lld, maximum %lld" + } } } }, @@ -634,6 +784,12 @@ "state": "translated", "value": "Recovery calendar, no data" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calendrier de récupération, aucune donnée" + } } } }, @@ -644,6 +800,12 @@ "state": "translated", "value": "STRENUOUS" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "INTENSE" + } } } }, @@ -654,6 +816,12 @@ "state": "translated", "value": "Sleep stages, %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stades de sommeil, %@" + } } } }, @@ -664,6 +832,12 @@ "state": "translated", "value": "Sleep stages, no data" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stades de sommeil, aucune donnée" + } } } }, @@ -674,6 +848,12 @@ "state": "translated", "value": "System" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Système" + } } } }, @@ -736,6 +916,12 @@ "state": "translated", "value": "Trend, %lld points, latest %@, low %@, high %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance, %lld points, dernier %@, minimum %@, maximum %@" + } } } }, @@ -770,6 +956,12 @@ "state": "translated", "value": "昨天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hier" + } } } }, @@ -780,6 +972,12 @@ "state": "translated", "value": "asleep %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "endormi %@" + } } } }, @@ -790,6 +988,12 @@ "state": "translated", "value": "average %@ bpm" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moyenne %@ bpm" + } } } }, @@ -824,6 +1028,12 @@ "state": "translated", "value": "剛剛" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à l'instant" + } } } }, @@ -834,6 +1044,12 @@ "state": "translated", "value": "range %@ to %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "plage %@ à %@" + } } } } diff --git a/Strand/Resources/Localizable.xcstrings b/Strand/Resources/Localizable.xcstrings index 8f20fec27..800f38f29 100644 --- a/Strand/Resources/Localizable.xcstrings +++ b/Strand/Resources/Localizable.xcstrings @@ -42,6 +42,12 @@ "state": "translated", "value": "%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%" + } } } }, @@ -128,6 +134,12 @@ "state": "translated", "value": "%.0f%% 效率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%.0f%% eff" + } } } }, @@ -272,6 +284,12 @@ "state": "translated", "value": "%@ %@ 的關聯(r = %@,n = %lld)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Relation %1$@ %2$@ (r = %3$@, n = %4$lld)." + } } } }, @@ -306,6 +324,12 @@ "state": "translated", "value": "%@(Beta)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ (bêta)" + } } } }, @@ -340,6 +364,12 @@ "state": "translated", "value": "%@(實驗性)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ (expérimental)" + } } } }, @@ -432,6 +462,12 @@ "state": "translated", "value": "%@ Cohen's d %@。%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ d de Cohen %2$@. %3$@" + } } } }, @@ -472,6 +508,12 @@ "state": "translated", "value": "%@ Cohen's d %@。屬探索性質,尚未達顯著。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ d de Cohen %2$@. Exploratoire, pas encore significatif." + } } } }, @@ -512,6 +554,12 @@ "state": "translated", "value": "%@ Cohen's d %@。具統計顯著性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ d de Cohen %2$@. Statistiquement significatif." + } } } }, @@ -546,6 +594,12 @@ "state": "translated", "value": "%@ 消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effort %@" + } } } }, @@ -580,6 +634,12 @@ "state": "translated", "value": "%@ 沒有明顯關聯。兩者大致各自獨立變化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ Aucune relation claire. Ils évoluent en grande partie de façon indépendante." + } } } }, @@ -620,6 +680,12 @@ "state": "translated", "value": "%@ 當 %@ 上升時,%@ 傾向下降,屬 %@ 的 %@ 關聯。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ Quand %2$@ augmente, %3$@ a tendance à baisser, un lien %4$@ %5$@." + } } } }, @@ -660,6 +726,12 @@ "state": "translated", "value": "%@ 當 %@ 上升時,%@ 傾向上升,屬 %@ 的 %@ 關聯。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ Quand %2$@ augmente, %3$@ a tendance à augmenter, un lien %4$@ %5$@." + } } } }, @@ -700,6 +772,12 @@ "state": "translated", "value": "%1$@ 表示認同:%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ est d'accord : %2$@" + } } } }, @@ -734,6 +812,12 @@ "state": "translated", "value": "%@ bpm" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ bpm" + } } } }, @@ -768,6 +852,12 @@ "state": "translated", "value": "%@ 次/分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ resp/min" + } } } }, @@ -808,6 +898,12 @@ "state": "translated", "value": "每分鐘 %@ 次呼吸:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ respirations par minute : %2$@" + } } } }, @@ -842,6 +938,12 @@ "state": "translated", "value": "平均 %@ 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ h moy." + } } } }, @@ -876,6 +978,12 @@ "state": "translated", "value": "臥床 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ au lit" + } } } }, @@ -916,6 +1024,12 @@ "state": "translated", "value": "臥床 %@ · 效率 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ au lit · %2$@ d'efficacité" + } } } }, @@ -956,6 +1070,12 @@ "state": "translated", "value": "臥床 %@ · 效率 %@ · 睡眠階段為近似值(裝置端估算)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ au lit · %2$@ d'efficacité · phases approximatives (sur l'appareil)" + } } } }, @@ -996,6 +1116,12 @@ "state": "translated", "value": "臥床 %@ · 效率 %lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ au lit · %2$lld%% d'efficacité" + } } } }, @@ -1036,6 +1162,12 @@ "state": "translated", "value": "在你記錄這項行為的日子,%@ 比基線好 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ est %2$@ meilleur que la référence les jours où vous avez consigné ce comportement." + } } } }, @@ -1076,6 +1208,12 @@ "state": "translated", "value": "在你記錄這項行為的日子,%@ 比基線差 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ est %2$@ moins bon que la référence les jours où vous avez consigné ce comportement." + } } } }, @@ -1110,6 +1248,12 @@ "state": "translated", "value": "在有記錄介入的日子,%@ 與基線持平。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est stable par rapport à la référence les jours d'intervention consignés." + } } } }, @@ -1144,6 +1288,12 @@ "state": "translated", "value": "%@ 是你目前使用中的手環。可在「裝置」中配對或切換手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est votre bracelet actif. Associez ou changez de bracelet dans Appareils." + } } } }, @@ -1178,6 +1328,12 @@ "state": "translated", "value": "總量 %@ 公斤" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ kg de volume total" + } } } }, @@ -1218,6 +1374,12 @@ "state": "translated", "value": "%@ 關聯 · r = %@ · n = %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lien %1$@ · r = %2$@ · n = %3$lld jours" + } } } }, @@ -1258,6 +1420,12 @@ "state": "translated", "value": "%1$@ / %2$@ 公升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ sur %2$@ litres" + } } } }, @@ -1292,6 +1460,12 @@ "state": "translated", "value": "配速 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Allure %@" + } } } }, @@ -1332,6 +1506,12 @@ "state": "translated", "value": "%@ 顯示 %@。點擊以比較" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ dit %2$@. Touchez pour comparer" + } } } }, @@ -1366,6 +1546,12 @@ "state": "translated", "value": "每個運動單位 %@ 步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ pas par unité de mouvement" + } } } }, @@ -1400,6 +1586,12 @@ "state": "translated", "value": "%@ 測試模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode test %@" + } } } }, @@ -1434,6 +1626,12 @@ "state": "translated", "value": "%@ 趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance %@" + } } } }, @@ -1526,6 +1724,12 @@ "state": "translated", "value": "%@ 對比起始 · 峰值 %@ 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ vs départ · pic %2$@ ms" + } } } }, @@ -1560,6 +1764,12 @@ "state": "translated", "value": "%@ 起床時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure de réveil %@" + } } } }, @@ -1594,6 +1804,12 @@ "state": "translated", "value": "%@ 稍後會要求 Bluetooth 權限。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ va demander l'accès au Bluetooth dans un instant." + } } } }, @@ -1732,6 +1948,12 @@ "state": "translated", "value": "%@ · 吸 %@ 秒 / 呼 %@ 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · %2$@s en / %3$@s hors" + } } } }, @@ -1766,6 +1988,12 @@ "state": "translated", "value": "%@ · %lld kcal" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · %lld kcal" + } } } }, @@ -1806,6 +2034,12 @@ "state": "translated", "value": "%@ · %lld mg" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · %2$lld mg" + } } } }, @@ -1840,6 +2074,12 @@ "state": "translated", "value": "%@ · Beta" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · Bêta" + } } } }, @@ -1874,6 +2114,12 @@ "state": "translated", "value": "%@ · 未記錄份量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · quantité non consignée" + } } } }, @@ -1914,6 +2160,12 @@ "state": "translated", "value": "%1$@ · 每項指標的最佳訊號,來自 %2$lld 個來源。一切都留在 %3$@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · meilleur signal par métrique, à partir de %2$lld sources. Tout reste sur %3$@." + } } } }, @@ -2000,6 +2252,12 @@ "state": "translated", "value": "%@ · 部分稀疏的資料序列已放寬區間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · certaines séries clairsemées élargies" + } } } }, @@ -2040,6 +2298,12 @@ "state": "translated", "value": "%1$@ · 你的記錄,位於 %2$@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · votre record, le %2$@." + } } } }, @@ -2132,6 +2396,12 @@ "state": "translated", "value": "%@ – %@ · %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ – %2$@ · %3$lld jours" + } } } }, @@ -2172,6 +2442,12 @@ "state": "translated", "value": "%@ – %@ · 1 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ – %2$@ · 1 jour" + } } } }, @@ -2264,6 +2540,12 @@ "state": "translated", "value": "%@ ↔ %@:r = %@(%@ %@),涵蓋 %lld 個共同天數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ ↔ %2$@ : r = %3$@ (%4$@ %5$@) sur %6$lld jours communs." + } } } }, @@ -2356,6 +2638,12 @@ "state": "translated", "value": "%@%@ C,相對你的正常值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@%2$@ C vs votre normale" + } } } }, @@ -2396,6 +2684,12 @@ "state": "translated", "value": "%@%@ 對比一般" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@%2$@ vs typ." + } } } }, @@ -2436,6 +2730,12 @@ "state": "translated", "value": "%@%@%@ 對比一般水準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@%2$@%3$@ vs typique" + } } } }, @@ -2476,6 +2776,12 @@ "state": "translated", "value": "%@%lld 對比基準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@%2$lld vs base" + } } } }, @@ -2516,6 +2822,12 @@ "state": "translated", "value": "%1$@,%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, %2$@" + } } } }, @@ -2609,6 +2921,12 @@ "state": "translated", "value": "%@,%@,使用中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, %2$@, en cours d'utilisation" + } } } }, @@ -2649,6 +2967,12 @@ "state": "translated", "value": "%@,%@,沒有資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, %2$@, aucune donnée" + } } } }, @@ -2689,6 +3013,12 @@ "state": "translated", "value": "%1$@,%2$@。%3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, %2$@. %3$@" + } } } }, @@ -2729,6 +3059,12 @@ "state": "translated", "value": "%1$@,%2$@:%3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, %2$@ : %3$@" + } } } }, @@ -2763,6 +3099,12 @@ "state": "translated", "value": "%@,是個好跡象。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, bon signe." + } } } }, @@ -2855,6 +3197,12 @@ "state": "translated", "value": "%1$@,最新 %2$@,%3$lld 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, dernière %2$@, %3$lld mesures" + } } } }, @@ -2895,6 +3243,12 @@ "state": "translated", "value": "%1$@,心情 %2$lld / 5" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, humeur %2$lld sur 5" + } } } }, @@ -2987,6 +3341,12 @@ "state": "translated", "value": "%1$@,訊號 %2$lld / 4" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@, signal %2$lld sur 4" + } } } }, @@ -3021,6 +3381,12 @@ "state": "translated", "value": "%@,值得留意。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, à surveiller." + } } } }, @@ -3055,6 +3421,12 @@ "state": "translated", "value": "%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@." + } } } }, @@ -3095,6 +3467,12 @@ "state": "translated", "value": "%1$@。%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@. %2$@" + } } } }, @@ -3135,6 +3513,12 @@ "state": "translated", "value": "%1$@。%2$@。%3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@. %2$@. %3$@" + } } } }, @@ -3169,6 +3553,12 @@ "state": "translated", "value": "%@。滑動或點擊以切換日期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@. Balayez ou touchez pour changer de jour" + } } } }, @@ -3209,6 +3599,12 @@ "state": "translated", "value": "%1$@:%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@" + } } } }, @@ -3249,6 +3645,12 @@ "state": "translated", "value": "%1$@:%2$@ %3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ %3$@" + } } } }, @@ -3342,6 +3744,12 @@ "state": "translated", "value": "%@:昨晚 %@,佔整晚的百分之 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit" + } } } }, @@ -3383,6 +3791,12 @@ "state": "translated", "value": "%1$@:昨晚 %2$@,佔整晚的百分之 %3$lld%4$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit%4$@" + } } } }, @@ -3423,6 +3837,12 @@ "state": "translated", "value": "%@:昨晚 %@,佔整晚的百分之 %lld,一般為 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit, typique %4$@" + } } } }, @@ -3463,6 +3883,12 @@ "state": "translated", "value": "%1$@:%2$@ 公升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ litres" + } } } }, @@ -3503,6 +3929,12 @@ "state": "translated", "value": "%1$@:%2$@ 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ de Charge" + } } } }, @@ -3543,6 +3975,12 @@ "state": "translated", "value": "%@:本週 %@,較上週下降 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette semaine, en baisse de %3$@ par rapport à la semaine dernière" + } } } }, @@ -3583,6 +4021,12 @@ "state": "translated", "value": "%@:本週 %@,沒有可比較的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette semaine, aucune comparaison." + } } } }, @@ -3623,6 +4067,12 @@ "state": "translated", "value": "%@:本週 %@,與上週持平 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette semaine, stable à %3$@ par rapport à la semaine dernière" + } } } }, @@ -3663,6 +4113,12 @@ "state": "translated", "value": "%@:本週 %@,較上週上升 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@ cette semaine, en hausse de %3$@ par rapport à la semaine dernière" + } } } }, @@ -3703,6 +4159,12 @@ "state": "translated", "value": "%@:%@,%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@, %3$@" + } } } }, @@ -3743,6 +4205,12 @@ "state": "translated", "value": "%1$@:%2$@,佔整晚的百分之 %3$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@, %3$lld pour cent de la nuit" + } } } }, @@ -3783,6 +4251,12 @@ "state": "translated", "value": "%1$@:%2$@。%3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@. %3$@" + } } } }, @@ -3823,6 +4297,12 @@ "state": "translated", "value": "%@:%@。%@,%@。%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@. %3$@, %4$@. %5$@." + } } } }, @@ -3863,6 +4343,12 @@ "state": "translated", "value": "%@:%@。%@。%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@. %3$@. %4$@." + } } } }, @@ -3903,6 +4389,12 @@ "state": "translated", "value": "%1$@:%2$@。請在設定中修正。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$@. À corriger dans Paramètres." + } } } }, @@ -3943,6 +4435,12 @@ "state": "translated", "value": "%1$@:估算 %2$lld 步,手機 %3$lld 步,相差百分之 %4$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ : %2$lld pas estimés, %3$lld pas sur le téléphone, %4$lld pour cent de différence" + } } } }, @@ -3977,6 +4475,12 @@ "state": "translated", "value": "%@:沒有資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ : aucune donnée" + } } } }, @@ -4011,6 +4515,12 @@ "state": "translated", "value": "%@ 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@h" + } } } }, @@ -4103,6 +4613,12 @@ "state": "translated", "value": "%1$lld %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld %2$@" + } } } }, @@ -4144,6 +4660,12 @@ "state": "translated", "value": "%1$lld %2$@ 正在讀取你的心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld %2$@ lisent votre fréquence cardiaque" + } } } }, @@ -4283,6 +4805,12 @@ "state": "translated", "value": "已開啟 %lld 個應用程式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld applications actives" + } } } }, @@ -4317,6 +4845,12 @@ "state": "translated", "value": "平均 %lld bpm" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld bpm en moyenne" + } } } }, @@ -4357,6 +4891,12 @@ "state": "translated", "value": "每分鐘 %1$lld 次,區間 %2$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld battements par minute, zone %2$lld" + } } } }, @@ -4391,6 +4931,12 @@ "state": "translated", "value": "%lld bpm" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld bpm" + } } } }, @@ -4477,6 +5023,12 @@ "state": "translated", "value": "%lld 次呼吸 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld respirations · %2$@" + } } } }, @@ -4511,6 +5063,12 @@ "state": "translated", "value": "%lld 個資料塊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld blocs" + } } } }, @@ -4545,6 +5103,12 @@ "state": "translated", "value": "已拉取 %lld 個資料塊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld blocs récupérés" + } } } }, @@ -4579,6 +5143,12 @@ "state": "translated", "value": "%lld 天前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld j" + } } } }, @@ -4613,6 +5183,12 @@ "state": "translated", "value": "%lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld jours" + } } } }, @@ -4647,6 +5223,12 @@ "state": "translated", "value": "%lld 天沒有它" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld jours sans" + } } } }, @@ -4681,6 +5263,12 @@ "state": "translated", "value": "有 %lld 天你的手機也有計步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld jours également comptés par votre téléphone" + } } } }, @@ -4721,6 +5309,12 @@ "state": "translated", "value": "%lld 天 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld jours · %2$@" + } } } }, @@ -4761,6 +5355,12 @@ "state": "translated", "value": "%lld 天 · %@ · 部分稀疏的資料序列已放寬區間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld jours · %2$@ · certaines séries clairsemées élargies" + } } } }, @@ -4801,6 +5401,12 @@ "state": "translated", "value": "%lld 天 · %lld 次睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld jours · %2$lld sommeils" + } } } }, @@ -4893,6 +5499,12 @@ "state": "translated", "value": "%lld 天 · %lld 次運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld jours · %2$lld entraînements" + } } } }, @@ -4933,6 +5545,12 @@ "state": "translated", "value": "已解碼 %lld,主控台 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld décodés, %2$lld console" + } } } }, @@ -4967,6 +5585,12 @@ "state": "translated", "value": "%lld 部裝置正在讀取你的心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld appareils lisent votre fréquence cardiaque" + } } } }, @@ -5001,6 +5625,12 @@ "state": "translated", "value": "%lld 杯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld boissons" + } } } }, @@ -5088,6 +5718,12 @@ "state": "translated", "value": "本次工作階段已擷取 %lld 幀。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld images capturées lors de cette séance." + } } } }, @@ -5122,6 +5758,12 @@ "state": "translated", "value": "%lld 小時前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld h" + } } } }, @@ -5156,6 +5798,12 @@ "state": "translated", "value": "%lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld heures" + } } } }, @@ -5190,6 +5838,12 @@ "state": "translated", "value": "估算中仍包含 %lld 筆攝取。僅供粗略參考,依據你記錄的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez consigné." + } } } }, @@ -5224,6 +5878,12 @@ "state": "translated", "value": "有 %lld 筆咖啡因記錄超過了你的就寢截止時間。它們可能還在體內作用,讓你睡不著。只是提醒你留意時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld prises de caféine consignées dépassent votre limite avant le coucher. Elles peuvent encore agir et vous empêcher de dormir. Juste un repère de timing." + } } } }, @@ -5258,6 +5918,12 @@ "state": "translated", "value": "已記錄 %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld jours consignés" + } } } }, @@ -5292,6 +5958,12 @@ "state": "translated", "value": "%lld 個標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld marqueurs" + } } } }, @@ -5332,6 +6004,12 @@ "state": "translated", "value": "追蹤 %lld 個標記 · %lld 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld marqueurs suivis · %2$lld mesures" + } } } }, @@ -5366,6 +6044,12 @@ "state": "translated", "value": "追蹤 %lld 個標記 · 1 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld marqueurs suivis · 1 mesure" + } } } }, @@ -5400,6 +6084,12 @@ "state": "translated", "value": "%lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld millilitres" + } } } }, @@ -5486,6 +6176,12 @@ "state": "translated", "value": "%lld 分鐘前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld min" + } } } }, @@ -5520,6 +6216,12 @@ "state": "translated", "value": "%lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ml" + } } } }, @@ -5554,6 +6256,12 @@ "state": "translated", "value": "%lld 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ms" + } } } }, @@ -5588,6 +6296,12 @@ "state": "translated", "value": "新增了 %lld 天的歷史資料。打開「趨勢」查看。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld nouveaux jours d'historique sont arrivés. Ouvrez Tendances pour les voir." + } } } }, @@ -5622,6 +6336,12 @@ "state": "translated", "value": "%lld 晚前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld nuits" + } } } }, @@ -5656,6 +6376,12 @@ "state": "translated", "value": "還剩 %lld 晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "encore %lld nuits" + } } } }, @@ -5696,6 +6422,12 @@ "state": "translated", "value": "%1$lld / %2$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld sur %2$lld" + } } } }, @@ -5736,6 +6468,12 @@ "state": "translated", "value": "%1$lld / %2$lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld sur %2$lld jours" + } } } }, @@ -5829,6 +6567,12 @@ "state": "translated", "value": "%lld / %lld 次次訓練" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld sur %2$lld séances" + } } } }, @@ -5863,6 +6607,12 @@ "state": "translated", "value": "%lld / 1 次工作階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld sur 1 séance" + } } } }, @@ -5897,6 +6647,12 @@ "state": "translated", "value": "本週有 %lld / 7 天有資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld jours sur 7 avaient des données cette semaine" + } } } }, @@ -5983,6 +6739,12 @@ "state": "translated", "value": "%lld 組" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld paires" + } } } }, @@ -6017,6 +6779,12 @@ "state": "translated", "value": "百分之 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld pour cent" + } } } }, @@ -6051,6 +6819,12 @@ "state": "translated", "value": "%lld 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld pt" + } } } }, @@ -6085,6 +6859,12 @@ "state": "translated", "value": "%lld 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld pts" + } } } }, @@ -6126,6 +6906,12 @@ "state": "translated", "value": "目前已有 %1$lld 筆讀數%2$@對齊,還不足以讀出趨勢(NOOP 需等待 %3$lld 筆)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesure%2$@ concordent jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %3$lld)." + } } } }, @@ -6167,6 +6953,12 @@ "state": "translated", "value": "%1$lld 筆讀數%2$@ · 你自己的記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesure%2$@ · vos propres entrées" + } } } }, @@ -6207,6 +6999,12 @@ "state": "translated", "value": "%lld 筆讀數,跨 %lld · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures sur %2$lld · %3$@" + } } } }, @@ -6247,6 +7045,12 @@ "state": "translated", "value": "%lld 筆讀數,跨 %lld · %@ · 稀疏處已放寬" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures sur %2$lld · %3$@ · clairsemé élargi" + } } } }, @@ -6287,6 +7091,12 @@ "state": "translated", "value": "目前有 %lld 筆讀數對齊,還不足以看出趨勢(NOOP 會等到 %lld 筆)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures concordent jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %2$lld)." + } } } }, @@ -6321,6 +7131,12 @@ "state": "translated", "value": "有 %lld 筆讀數對齊,但變化不足以計算出關係。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld mesures concordent, mais il n'y a pas assez de variation pour calculer une relation." + } } } }, @@ -6361,6 +7177,12 @@ "state": "translated", "value": "使用了 %1$lld 筆讀數 · %2$@ %3$@ 關聯。這是你自己的資料並排呈現,它不是醫學結論,只顯示關聯,而非因果。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures utilisées · association %2$@ %3$@. Ce sont vos propres données mises côte à côte. Ce n'est pas un constat médical, et cela montre une association, pas une cause." + } } } }, @@ -6401,6 +7223,12 @@ "state": "translated", "value": "%lld 筆讀數 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures · %2$@" + } } } }, @@ -6441,6 +7269,12 @@ "state": "translated", "value": "%lld 筆讀數 · 稀疏,已放寬至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld mesures · clairsemé, élargi à %2$@" + } } } }, @@ -6475,6 +7309,12 @@ "state": "translated", "value": "%lld 筆讀數 · 你自己輸入的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld mesures · vos propres entrées" + } } } }, @@ -6509,6 +7349,12 @@ "state": "translated", "value": "最近 %lld 筆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld récentes" + } } } }, @@ -6543,6 +7389,12 @@ "state": "translated", "value": "已略過 %lld 列" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld lignes ignorées" + } } } }, @@ -6583,6 +7435,12 @@ "state": "translated", "value": "%2$@ 還剩 %1$lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld secondes restantes dans %2$@" + } } } }, @@ -6617,6 +7475,12 @@ "state": "translated", "value": "%lld 個序列" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld séries" + } } } }, @@ -6651,6 +7515,12 @@ "state": "translated", "value": "%lld 次次訓練" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld séances" + } } } }, @@ -6691,6 +7561,12 @@ "state": "translated", "value": "%lld 次次訓練 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld séances · %2$@" + } } } }, @@ -6731,6 +7607,12 @@ "state": "translated", "value": "%lld 次次訓練 · 稀疏,已放寬至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld séances · clairsemé, élargi à %2$@" + } } } }, @@ -6765,6 +7647,12 @@ "state": "translated", "value": "已略過 %lld 筆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ignorés" + } } } }, @@ -6799,6 +7687,12 @@ "state": "translated", "value": "%lld 種運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld sports" + } } } }, @@ -6833,6 +7727,12 @@ "state": "translated", "value": "共 %lld 筆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld au total" + } } } }, @@ -6874,6 +7774,12 @@ "state": "translated", "value": "在我們的同步之外偵測到 %1$lld 個 type-0x2F 歷史卸載幀%2$@,這些是歷史資料(例如另一個應用程式在拉取手環的積壓資料),而非即時 R22 流(#494)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld trame%2$@ de déchargement historique type-0x2F vue(s) en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494)." + } } } }, @@ -6908,6 +7814,12 @@ "state": "translated", "value": "在我們的同步之外偵測到 %lld 個 type-0x2F 歷史卸載幀。這些是歷史資料(例如另一個應用程式在拉取手環的積存資料),不是即時 R22 串流(#494)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld trames de déchargement historique type-0x2F vues en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494)." + } } } }, @@ -6942,6 +7854,12 @@ "state": "translated", "value": "%lld 則未讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld non lus" + } } } }, @@ -6976,6 +7894,12 @@ "state": "translated", "value": "%lld 週前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lld semaines" + } } } }, @@ -7017,6 +7941,12 @@ "state": "translated", "value": "比你的年齡%3$@ %1$lld 歲%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld an%2$@ %3$@ que votre âge" + } } } }, @@ -7051,6 +7981,12 @@ "state": "translated", "value": "比你的實際年齡老 %lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ans de plus que votre âge" + } } } }, @@ -7085,6 +8021,12 @@ "state": "translated", "value": "比你的實際年齡年輕 %lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ans de moins que votre âge" + } } } }, @@ -7126,6 +8068,12 @@ "state": "translated", "value": "%1$lld 歲%2$@ %3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld an%2$@ %3$@" + } } } }, @@ -7160,6 +8108,12 @@ "state": "translated", "value": "%lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ans" + } } } }, @@ -7194,6 +8148,12 @@ "state": "translated", "value": "老 %lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ans de plus" + } } } }, @@ -7228,6 +8188,12 @@ "state": "translated", "value": "年輕 %lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld ans de moins" + } } } }, @@ -7262,6 +8228,12 @@ "state": "translated", "value": "%lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld%%" + } } } }, @@ -7296,6 +8268,12 @@ "state": "translated", "value": "%lld%% 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld%% de Charge" + } } } }, @@ -7330,6 +8308,12 @@ "state": "translated", "value": "今日目標的 %lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld%% de l'objectif du jour" + } } } }, @@ -7364,6 +8348,12 @@ "state": "translated", "value": "%lld+ 杯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld+ boissons" + } } } }, @@ -7398,6 +8388,12 @@ "state": "translated", "value": "%lld 分鐘平均" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyenne sur %lld minutes" + } } } }, @@ -7432,6 +8428,12 @@ "state": "translated", "value": "%lld 秒平均" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyenne sur %lld secondes" + } } } }, @@ -7466,6 +8468,12 @@ "state": "translated", "value": "%lld/7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld/7 jours" + } } } }, @@ -7500,6 +8508,12 @@ "state": "translated", "value": "%lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lldh" + } } } }, @@ -7540,6 +8554,12 @@ "state": "translated", "value": "%lld 小時 %lld 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lldh %2$lldm" + } } } }, @@ -7574,6 +8594,12 @@ "state": "translated", "value": "%lld 小時前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %lldh" + } } } }, @@ -7608,6 +8634,12 @@ "state": "translated", "value": "升高 %lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lldh élevée" + } } } }, @@ -7642,6 +8674,12 @@ "state": "translated", "value": "%lld 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lldm" + } } } }, @@ -7676,6 +8714,12 @@ "state": "translated", "value": "區間內 %lld 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lldm en zone" + } } } }, @@ -7716,6 +8760,12 @@ "state": "translated", "value": "還剩 %1$lld 秒 · %2$lld 次心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$llds restantes · %2$lld battements" + } } } }, @@ -7808,6 +8858,12 @@ "state": "translated", "value": "%1$lld′ %2$lld″" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld′ %2$lld″" + } } } }, @@ -7842,6 +8898,12 @@ "state": "translated", "value": "(還沒有可分享的內容)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "(rien à partager pour l'instant)" + } } } }, @@ -7876,6 +8938,12 @@ "state": "translated", "value": "* 裝置端估算:皮膚溫度為每晚 ±°C 的偏差值(視韌體而定);4.0 無法透過 BLE 讀取步數。手環無法提供 SpO₂ %;匯入 WHOOP CSV 才能取得真實百分比。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (selon le firmware) ; pas de pas via BLE sur un 4.0. Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %." + } } } }, @@ -7910,6 +8978,12 @@ "state": "translated", "value": "* 裝置端估算:皮膚溫度為每晚 ±°C 的偏差值,步數是原始動作計數(#78)。手環無法提供 SpO₂ %;匯入 WHOOP CSV 才能取得真實百分比。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut de mouvement (#78). Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %." + } } } }, @@ -7944,6 +9018,12 @@ "state": "translated", "value": "+%lld 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "+%lld pt" + } } } }, @@ -7978,6 +9058,12 @@ "state": "translated", "value": "+%lld 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "+%lld pts" + } } } }, @@ -8064,6 +9150,12 @@ "state": "translated", "value": "0 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "0 pt" + } } } }, @@ -8098,6 +9190,12 @@ "state": "translated", "value": "0123456789abcdef0123456789abcdef" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "0123456789abcdef0123456789abcdef" + } } } }, @@ -8132,6 +9230,12 @@ "state": "translated", "value": "0分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "0m" + } } } }, @@ -8166,6 +9270,12 @@ "state": "translated", "value": "0–%@ 量表" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échelle 0–%@" + } } } }, @@ -8200,6 +9310,12 @@ "state": "translated", "value": "0–100" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "0–100" + } } } }, @@ -8234,6 +9350,12 @@ "state": "translated", "value": "0–21" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "0–21" + } } } }, @@ -8268,6 +9390,12 @@ "state": "translated", "value": "已開啟 1 個應用程式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 application active" + } } } }, @@ -8302,6 +9430,12 @@ "state": "translated", "value": "1 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 jour" + } } } }, @@ -8336,6 +9470,12 @@ "state": "translated", "value": "你的手機也計入的 1 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 jour également compté par votre téléphone" + } } } }, @@ -8370,6 +9510,12 @@ "state": "translated", "value": "1 天 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 jour · %@" + } } } }, @@ -8404,6 +9550,12 @@ "state": "translated", "value": "1 天 · %@ · 部分稀疏序列已加寬" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 jour · %@ · certaines séries clairsemées élargies" + } } } }, @@ -8438,6 +9590,12 @@ "state": "translated", "value": "1 部裝置正在讀取你的心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 appareil lit votre fréquence cardiaque" + } } } }, @@ -8472,6 +9630,12 @@ "state": "translated", "value": "本次工作階段已擷取 1 幀。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 image capturée lors de cette séance." + } } } }, @@ -8506,6 +9670,12 @@ "state": "translated", "value": "1 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 heure" + } } } }, @@ -8540,6 +9710,12 @@ "state": "translated", "value": "1 個標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 marqueur" + } } } }, @@ -8574,6 +9750,12 @@ "state": "translated", "value": "追蹤 1 個標記 · %lld 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 marqueur suivi · %lld mesures" + } } } }, @@ -8608,6 +9790,12 @@ "state": "translated", "value": "追蹤 1 個標記 · 1 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 marqueur suivi · 1 mesure" + } } } }, @@ -8642,6 +9830,12 @@ "state": "translated", "value": "1 天的新歷史資料已到位。打開趨勢即可查看。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 nouveau jour d'historique est arrivé. Ouvrez Tendances pour le voir." + } } } }, @@ -8676,6 +9870,12 @@ "state": "translated", "value": "1 晚前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a 1 nuit" + } } } }, @@ -8710,6 +9910,12 @@ "state": "translated", "value": "還差 1 晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "encore 1 nuit" + } } } }, @@ -8744,6 +9950,12 @@ "state": "translated", "value": "1 雙" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 paire" + } } } }, @@ -8784,6 +9996,12 @@ "state": "translated", "value": "%lld 項中有 1 筆讀數 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure sur %1$lld · %2$@" + } } } }, @@ -8824,6 +10042,12 @@ "state": "translated", "value": "%lld 項中有 1 筆讀數 · %@ · 稀疏已加寬" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure sur %1$lld · %2$@ · clairsemé élargi" + } } } }, @@ -8858,6 +10082,12 @@ "state": "translated", "value": "目前已有 1 筆讀數對齊,還不足以讀出趨勢(NOOP 需等待 %lld 筆)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure concorde jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %lld)." + } } } }, @@ -8892,6 +10122,12 @@ "state": "translated", "value": "1 筆讀數 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure · %@" + } } } }, @@ -8926,6 +10162,12 @@ "state": "translated", "value": "1 筆讀數 · 稀疏,已加寬至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure · clairsemé, élargi à %@" + } } } }, @@ -8960,6 +10202,12 @@ "state": "translated", "value": "1 筆讀數 · 你自己的記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mesure · votre propre entrée" + } } } }, @@ -8994,6 +10242,12 @@ "state": "translated", "value": "已略過 1 列" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 ligne ignorée" + } } } }, @@ -9028,6 +10282,12 @@ "state": "translated", "value": "1 次訓練" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 séance" + } } } }, @@ -9062,6 +10322,12 @@ "state": "translated", "value": "1 次訓練 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 séance · %@" + } } } }, @@ -9096,6 +10362,12 @@ "state": "translated", "value": "1 次訓練 · 稀疏,已加寬至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 séance · clairsemé, élargi à %@" + } } } }, @@ -9130,6 +10402,12 @@ "state": "translated", "value": "1 項運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 sport" + } } } }, @@ -9164,6 +10442,12 @@ "state": "translated", "value": "在我們的同步之外偵測到 1 個 type-0x2F 歷史卸載幀。這些是歷史資料(例如另一個應用程式正在拉取手環的積存資料),而非即時 R22 串流(#494)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 trame de déchargement historique type-0x2F vue en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494)." + } } } }, @@ -9198,6 +10482,12 @@ "state": "translated", "value": "1 年" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 an" + } } } }, @@ -9232,6 +10522,12 @@ "state": "translated", "value": "比你的實際年齡大 1 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 an de plus que votre âge" + } } } }, @@ -9266,6 +10562,12 @@ "state": "translated", "value": "比你的實際年齡小 1 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 an de moins que votre âge" + } } } }, @@ -9300,6 +10602,12 @@ "state": "translated", "value": "大 1 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 an de plus" + } } } }, @@ -9334,6 +10642,12 @@ "state": "translated", "value": "小 1 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 an de moins" + } } } }, @@ -9368,6 +10682,12 @@ "state": "translated", "value": "14 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "14 jours" + } } } }, @@ -9402,6 +10722,12 @@ "state": "translated", "value": "14 天趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance sur 14 jours" + } } } }, @@ -9436,6 +10762,12 @@ "state": "translated", "value": "14天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "14j" + } } } }, @@ -9470,6 +10802,12 @@ "state": "translated", "value": "180 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "180 JOURS" + } } } }, @@ -9504,6 +10842,12 @@ "state": "translated", "value": "1年" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1A" + } } } }, @@ -9538,6 +10882,12 @@ "state": "translated", "value": "28天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "28j" + } } } }, @@ -9572,6 +10922,12 @@ "state": "translated", "value": "下午 2 點後" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à partir de 14h" + } } } }, @@ -9606,6 +10962,12 @@ "state": "translated", "value": "3 個月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "3 mois" + } } } }, @@ -9640,6 +11002,12 @@ "state": "translated", "value": "30 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30 JOURS" + } } } }, @@ -9674,6 +11042,12 @@ "state": "translated", "value": "30 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30 jours" + } } } }, @@ -9708,6 +11082,12 @@ "state": "translated", "value": "30天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30J" + } } } }, @@ -9742,6 +11122,12 @@ "state": "translated", "value": "30天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30j" + } } } }, @@ -9776,6 +11162,12 @@ "state": "translated", "value": "365 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "365 JOURS" + } } } }, @@ -9810,6 +11202,12 @@ "state": "translated", "value": "3個月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "3M" + } } } }, @@ -9844,6 +11242,12 @@ "state": "translated", "value": "5 分鐘平均 · 所選日期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyenne sur 5 minutes · jour sélectionné" + } } } }, @@ -9878,6 +11282,12 @@ "state": "translated", "value": "5 分鐘平均 · 自午夜起" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyenne sur 5 minutes · depuis minuit" + } } } }, @@ -9912,6 +11322,12 @@ "state": "translated", "value": "6 個月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "6 mois" + } } } }, @@ -9946,6 +11362,12 @@ "state": "translated", "value": "6個月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "6M" + } } } }, @@ -9980,6 +11402,12 @@ "state": "translated", "value": "7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7 JOURS" + } } } }, @@ -10014,6 +11442,12 @@ "state": "translated", "value": "7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7 jours" + } } } }, @@ -10048,6 +11482,12 @@ "state": "translated", "value": "7天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7J" + } } } }, @@ -10082,6 +11522,12 @@ "state": "translated", "value": "7天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7j" + } } } }, @@ -10116,6 +11562,12 @@ "state": "translated", "value": "90 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "90 JOURS" + } } } }, @@ -10150,6 +11602,12 @@ "state": "translated", "value": "90天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "90J" + } } } }, @@ -10184,6 +11642,12 @@ "state": "translated", "value": "90天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "90j" + } } } }, @@ -10224,6 +11688,12 @@ "state": "translated", "value": "今晚的%@,往往對應著高出約 %@ 的明日%@,依據是你 %lld / %@ 天的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de plus sur votre %3$@ de demain, sur la base de %4$lld de vos %5$@ jours." + } } } }, @@ -10264,6 +11734,12 @@ "state": "translated", "value": "今晚的%@,往往對應著高出約 %@ 的明日%@,這是依據一般模式得出的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de plus sur votre %3$@ de demain, sur la base de schémas typiques." + } } } }, @@ -10304,6 +11780,12 @@ "state": "translated", "value": "今晚的%@,往往對應著低出約 %@ 的明日%@,依據是你 %lld / %@ 天的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de %4$lld de vos %5$@ jours." + } } } }, @@ -10344,6 +11826,12 @@ "state": "translated", "value": "今晚的%@,往往對應著低出約 %@ 的明日%@,這是依據一般模式得出的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de schémas typiques." + } } } }, @@ -10378,6 +11866,12 @@ "state": "translated", "value": "來自手環的 60 秒逐拍(R-R)間期快照,在計算 RMSSD 之前經過清洗(範圍和異位搏動過濾),計算方式與你的夜間 HRV 相同。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un instantané de 60 secondes de vos intervalles battement à battement (R-R) provenant du bracelet, nettoyés (filtrage de plage et des battements ectopiques) avant de calculer le RMSSD de la même façon que votre VFC nocturne." + } } } }, @@ -10412,6 +11906,12 @@ "state": "translated", "value": "WHOOP 匯出可回填恢復、強度、睡眠與訓練。Apple Health 則可補充心率、HRV、睡眠、SpO₂、步數、訓練與體重。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un export WHOOP complète rétroactivement récupération, effort, sommeil et entraînements. Apple Santé peut ajouter FC, VFC, sommeil, SpO₂, pas, entraînements et poids." + } } } }, @@ -10446,6 +11946,12 @@ "state": "translated", "value": "WHOOP 會員資格每年 300–480 美元,終身如此。NOOP 免費做到這一點,一個人,沒有伺服器,沒有訂閱。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un abonnement WHOOP coûte 300 à 480 $ par an, à vie. NOOP fait cela gratuitement : une personne, aucun serveur, aucun abonnement." + } } } }, @@ -10480,6 +11986,12 @@ "state": "translated", "value": "徽章會標明某個數字是由 NOOP 在裝置上計算,還是從 Whoop 或 Apple Health 匯入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un badge indique si un chiffre a été calculé sur l'appareil par NOOP, ou importé depuis Whoop ou Apple Santé." + } } } }, @@ -10514,6 +12026,12 @@ "state": "translated", "value": "一個以基線正規化的恢復分數:你的靜息心率、睡眠品質與每晚的一致性,皆對照你自己的基線加權,只要手環給出乾淨的讀數,就以心率變異性(rMSSD)為主導。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un score de récupération normalisé par rapport à une référence : votre fréquence cardiaque au repos, la qualité de votre sommeil et la régularité nuit après nuit, pondérées par rapport à votre propre référence, la variabilité de la fréquence cardiaque (rMSSD) prenant le dessus partout où le bracelet fournit une mesure propre." + } } } }, @@ -10548,6 +12066,12 @@ "state": "translated", "value": "綜合考量你的睡眠時長相對於個人需求(最大的因素)、睡眠效率(實際入睡與躺床時間之比)、恢復性睡眠的比例(深睡 + REM 睡眠),以及你入睡與起床時間的一致性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un mélange de la durée de votre sommeil par rapport à votre besoin personnel (le facteur le plus important), de son efficacité (endormi par rapport au temps au lit), de sa part réparatrice (sommeil profond + REM), et de la régularité de vos horaires de sommeil et de réveil." + } } } }, @@ -10582,6 +12106,12 @@ "state": "translated", "value": "批量標記 CSV 匯入(日期、標記、數值、單位)將隨「資料來源」中的檔案匯入器一起推出,與營養和力量訓練相同。目前請在上方逐筆新增讀數。你匯入的一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un import CSV groupé de marqueurs (date, marqueur, valeur, unité) arrive avec les importateurs de fichiers dans Sources de données, comme pour la nutrition et la musculation. Pour l'instant, ajoutez les mesures une par une ci-dessus. Tout ce que vous importez reste sur %@." + } } } }, @@ -10616,6 +12146,12 @@ "state": "translated", "value": "一次震動,而非橫幅通知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une vibration, pas une bannière" + } } } }, @@ -10650,6 +12186,12 @@ "state": "translated", "value": "一個平靜的傍晚提醒,根據你的起床時間和通常的睡眠需求來定時。它是一個建議,而非鬧鐘。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un rappel calme le soir, calé sur votre heure de réveil et votre besoin de sommeil habituel. C'est une suggestion, pas une alarme." + } } } }, @@ -10684,6 +12226,12 @@ "state": "translated", "value": "現在安靜地度過一小時,有助於你在起床時間醒來時精神飽滿。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une heure calme maintenant vous aide à atteindre votre heure de réveil bien reposé." + } } } }, @@ -10724,6 +12272,12 @@ "state": "translated", "value": "在你起床時間前 %1$lld 小時 %2$lld 分鐘的一次平靜提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un rappel discret %1$lldh %2$lldm avant votre heure de réveil." + } } } }, @@ -10758,6 +12312,12 @@ "state": "translated", "value": "屬於 Banister TRIMP 家族的心血管負荷:在各心率區間停留的時間,較高強度區間權重更大,加總成每日一個數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une charge cardiovasculaire de la famille Banister TRIMP : le temps passé dans chaque zone de fréquence cardiaque, pondéré pour que les zones plus intenses comptent davantage, additionné en un seul chiffre quotidien." + } } } }, @@ -10792,6 +12352,12 @@ "state": "translated", "value": "一份簡潔、可分享的單頁 PDF,涵蓋某個日期範圍內你的恢復、睡眠、HRV、靜息心率和強度。保存在你的 %@ 上,沒有任何內容離開裝置。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un PDF d'une page, clair et partageable, de votre récupération, sommeil, VFC, fréquence cardiaque au repos et effort sur une période donnée. Enregistré sur votre %@. Rien ne quitte l'appareil." + } } } }, @@ -10826,6 +12392,12 @@ "state": "translated", "value": "有幾項指標取決於你佩戴的 Apple Watch 型號。為皮膚溫度提供數據的手腕溫度自 Series 8 起才出現,因此更舊的手錶不會報告它。血氧影響更大:由於一項專利糾紛,Apple 從最新的美國版機型中移除了 SpO₂ 感測器,所以那些機型根本無法測量它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelques métriques dépendent de l'Apple Watch que vous portez. La température du poignet, qui alimente la température cutanée, est arrivée avec la Series 8 ; les montres plus anciennes ne la fournissent donc pas. L'oxygène sanguin est le cas le plus notable : Apple a retiré le capteur SpO₂ des derniers modèles américains à la suite d'un litige de brevet, ces derniers ne le mesurent donc tout simplement pas." + } } } }, @@ -10860,6 +12432,12 @@ "state": "translated", "value": "有幾項指標取決於你的型號" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelques métriques dépendent de votre modèle" + } } } }, @@ -10894,6 +12472,12 @@ "state": "translated", "value": "先坦白說幾句,免得之後有意外。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelques mots honnêtes, pour qu'il n'y ait pas de surprise." + } } } }, @@ -10928,6 +12512,12 @@ "state": "translated", "value": "再過幾天,我們就能顯示你的體能年齡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Encore quelques jours et nous pourrons afficher votre Âge de forme physique." + } } } }, @@ -10962,6 +12552,12 @@ "state": "translated", "value": "再過幾天,我們就能顯示你的活力。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Encore quelques jours et nous pourrons afficher votre Vitalité." + } } } }, @@ -10996,6 +12592,12 @@ "state": "translated", "value": "再佩戴幾天,加上下面的基礎資訊,我們就能顯示你的體能年齡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Encore quelques jours de port, plus les informations de base ci-dessous, et nous pourrons afficher votre Âge de forme physique." + } } } }, @@ -11030,6 +12632,12 @@ "state": "translated", "value": "有幾項訊號偏高" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelques signaux sont en hausse" + } } } }, @@ -11064,6 +12672,12 @@ "state": "translated", "value": "當你的 HRV 下降而心率平穩時,給你一次輕柔的震動,提示你做一次節奏呼吸。每 15 分鐘最多一次;預設關閉。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une vibration douce quand votre VFC chute alors que votre fréquence cardiaque est calme, une invitation à respirer à un rythme régulier. Limité à une fois toutes les 15 minutes ; désactivé par défaut." + } } } }, @@ -11098,6 +12712,12 @@ "state": "translated", "value": "久坐太久時手腕會輕輕震動一下,提醒你起身活動。這是從每次歷史同步時手環的動作推斷的,所以會比即時慢一到兩次同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une légère vibration au poignet quand vous êtes assis depuis trop longtemps, une invitation à vous lever et à bouger. Déduite du mouvement du bracelet à chaque synchronisation d'historique, elle accuse donc un retard d'une ou deux synchronisations par rapport au temps réel." + } } } }, @@ -11132,6 +12752,12 @@ "state": "translated", "value": "一筆記錄的咖啡因已超過你的就寢截止時間。它可能還在體內作用,讓你睡不著。只是提醒一下時間點。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une prise de caféine consignée dépasse votre limite avant le coucher. Elle peut encore agir et vous empêcher de dormir. Juste un repère de timing." + } } } }, @@ -11166,6 +12792,12 @@ "state": "translated", "value": "靜息心率越低,通常能量越高。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une fréquence cardiaque au repos plus basse signifie généralement une charge plus élevée." + } } } }, @@ -11200,6 +12832,12 @@ "state": "translated", "value": "關於血氧和你的機型的說明" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une note sur l'oxygène sanguin et votre modèle" + } } } }, @@ -11240,6 +12878,12 @@ "state": "translated", "value": "經期很可能在 %1$@ 到 %2$@ 之間,這是一個時間窗口,而非固定日期。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un cycle est probable entre le %1$@ et le %2$@ (une fenêtre, pas une date fixe)." + } } } }, @@ -11274,6 +12918,12 @@ "state": "translated", "value": "一個私密的筆記本,而非醫療服務。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un carnet privé, pas un service médical." + } } } }, @@ -11361,6 +13011,12 @@ "state": "translated", "value": "一扇通往你恢復、睡眠與強度的私密窗口,直接從手環讀取,只保存在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une fenêtre privée sur votre récupération, votre sommeil et votre effort. Lue directement depuis votre bracelet, conservée uniquement sur %@." + } } } }, @@ -11395,6 +13051,12 @@ "state": "translated", "value": "連線前先說幾句" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un mot rapide avant de nous connecter" + } } } }, @@ -11429,6 +13091,12 @@ "state": "translated", "value": "對 NOOP 已儲存的已解碼感測器資料流進行唯讀匯出。適用於任何手環,不會向你的裝置寫入任何內容,也不會上傳任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un export en lecture seule des flux de capteurs décodés que NOOP stocke déjà. Fonctionne avec n'importe quel bracelet. Rien n'est écrit sur votre appareil, rien n'est envoyé en ligne." + } } } }, @@ -11463,6 +13131,12 @@ "state": "translated", "value": "一種放鬆的節律,而非對心臟的控制。它絕不會將節奏降到安全心率以下,你可以隨時停止。如果你的心率沒有平復,我們會如實告知。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un rythme de relaxation, pas un contrôle cardiaque. Il ne descend jamais sous une fréquence sûre et vous pouvez arrêter à tout moment. Si votre fréquence cardiaque ne se stabilise pas, nous vous le dirons clairement." + } } } }, @@ -11497,6 +13171,12 @@ "state": "translated", "value": "手環電量降到 15% 時提醒你睡前充電,充到 100% 時通知你,每個充電週期各最多一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un rappel de recharger avant le coucher quand le bracelet descend à 15 %, et un avis quand il atteint 100 %, chacun au plus une fois par cycle de charge." + } } } }, @@ -11531,6 +13211,12 @@ "state": "translated", "value": "戒指一次只能與一個持有者通信。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une bague ne communique qu'avec un seul propriétaire à la fois." + } } } }, @@ -11565,6 +13251,12 @@ "state": "translated", "value": "一份可分享的單頁 PDF,涵蓋某個範圍內的恢復、睡眠、HRV、靜息心率和強度,保存在你的 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un PDF d'une page partageable de récupération, sommeil, VFC, FC au repos et effort sur une période, enregistré sur votre %@." + } } } }, @@ -11599,6 +13291,12 @@ "state": "translated", "value": "一個標誌性的圓環,將 HRV、靜息心率與睡眠濃縮成一個平靜的判讀:該衝還是該休息。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une bague signature condense VFC, fréquence cardiaque au repos et sommeil en une lecture claire pour savoir s'il faut pousser ou se reposer." + } } } }, @@ -11633,6 +13331,12 @@ "state": "translated", "value": "一個會根據你的消耗進行調整的簡單目標。一般性的健康指引,而非醫療建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un objectif simple qui s'adapte à votre effort. Orientation générale de bien-être, pas un avis médical." + } } } }, @@ -11720,6 +13424,12 @@ "state": "translated", "value": "為你的 WHOOP 打造的獨立伴侶應用程式。一切都留在這台裝置上,你的歷史、你的即時資料流、你的各項數值。不上傳任何內容。NOOP 是一個獨立的實驗性專案,並非 WHOOP 官方應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil : votre historique, votre flux en direct, vos chiffres. Rien n'est envoyé en ligne. NOOP est un projet indépendant et expérimental, pas l'application WHOOP." + } } } }, @@ -11754,6 +13464,12 @@ "state": "translated", "value": "在靜坐狀態下對你心率變異性的一次快照" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un instantané, assis et immobile, de votre variabilité de fréquence cardiaque" + } } } }, @@ -11788,6 +13504,12 @@ "state": "translated", "value": "同步已在進行中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une synchronisation est déjà en cours." + } } } }, @@ -11822,6 +13544,12 @@ "state": "translated", "value": "一套透明的心肺演算法,比預設的分期能更好地還原深睡和 REM。需主動開啟且為實驗性,它只改變已偵測到的夜晚如何被劃分為各階段(偵測和評分不變),如果你保持關閉,則沿用預設分期。將在下次分期的夜晚生效。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une recette cardiorespiratoire transparente qui restitue mieux le sommeil profond et REM que la répartition par défaut. Facultatif et expérimental : cela change seulement la manière dont les nuits déjà détectées sont réparties en phases (la détection et les scores restent inchangés), et la répartition par défaut reste en place si vous laissez cette option désactivée. Prend effet sur les prochaines nuits traitées." + } } } }, @@ -11856,6 +13584,12 @@ "state": "translated", "value": "近期的一個典型日子" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une journée récente typique" + } } } }, @@ -11890,6 +13624,12 @@ "state": "translated", "value": "手錶是對你的心率變異性抽樣,而非整夜串流,所以你的能量分數大約需要七個晚上來校準。在那之前 NOOP 會顯示「需要更多資料」,絕不顯示猜測的數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une montre échantillonne votre variabilité de fréquence cardiaque au lieu de la diffuser toute la nuit ; votre score de Charge a donc besoin d'environ sept nuits pour se calibrer. En attendant, NOOP affiche « données insuffisantes », jamais un chiffre deviné." + } } } }, @@ -11924,6 +13664,12 @@ "state": "translated", "value": "每週摘要需要幾天的歷史資料。請佩戴你的手環,或在「資料來源」中匯入你的 WHOOP 匯出檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un résumé hebdomadaire nécessite quelques jours d'historique. Portez votre bracelet ou importez votre export WHOOP dans Sources de données." + } } } }, @@ -11958,6 +13704,12 @@ "state": "translated", "value": "根據你的習慣得出的健康估算,而非臨床生物學年齡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une estimation de bien-être basée sur vos habitudes, pas un âge biologique clinique." + } } } }, @@ -11992,6 +13744,12 @@ "state": "translated", "value": "進階" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AVANCÉ" + } } } }, @@ -12026,6 +13784,12 @@ "state": "translated", "value": "全部" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TOUT" + } } } }, @@ -12060,6 +13824,12 @@ "state": "translated", "value": "全部時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DEPUIS TOUJOURS" + } } } }, @@ -12146,6 +13916,12 @@ "state": "translated", "value": "上午" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AM" + } } } }, @@ -12232,6 +14008,12 @@ "state": "translated", "value": "API 金鑰(可選)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé API (facultatif)" + } } } }, @@ -12266,6 +14048,12 @@ "state": "translated", "value": "APPLE WATCH" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "APPLE WATCH" + } } } }, @@ -12300,6 +14088,12 @@ "state": "translated", "value": "平均" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MOY" + } } } }, @@ -12334,6 +14128,12 @@ "state": "translated", "value": "平均心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC MOY" + } } } }, @@ -12368,6 +14168,12 @@ "state": "translated", "value": "平均/次" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MOY/SÉANCE" + } } } }, @@ -12454,6 +14260,12 @@ "state": "translated", "value": "約 %lld 毫克可能仍在作用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environ %lld mg pourraient encore être actifs" + } } } }, @@ -12488,6 +14300,12 @@ "state": "translated", "value": "比你的作息早約 %lld 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environ %lld min plus tôt que prévu" + } } } }, @@ -12522,6 +14340,12 @@ "state": "translated", "value": "比你的作息晚約 %lld 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environ %lld min plus tard que prévu" + } } } }, @@ -12556,6 +14380,12 @@ "state": "translated", "value": "關於 Apple Watch 資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos des données Apple Watch" + } } } }, @@ -12590,6 +14420,12 @@ "state": "translated", "value": "關於實驗手記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos de Lab Book" + } } } }, @@ -12624,6 +14460,12 @@ "state": "translated", "value": "與你的作息大致同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À peu près en phase avec votre programme" + } } } }, @@ -12658,6 +14500,12 @@ "state": "translated", "value": "與你的年齡大致相同" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À peu près comme votre âge" + } } } }, @@ -12692,6 +14540,12 @@ "state": "translated", "value": "關於這次小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos de cette sieste" + } } } }, @@ -12726,6 +14580,12 @@ "state": "translated", "value": "關於你" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos de vous" + } } } }, @@ -12760,6 +14620,12 @@ "state": "translated", "value": "關於你的主睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos de votre sommeil principal" + } } } }, @@ -12794,6 +14660,12 @@ "state": "translated", "value": "接受並繼續" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Accepter et continuer" + } } } }, @@ -12828,6 +14700,12 @@ "state": "translated", "value": "在我們能夠讀取的安靜時段中,你的逐拍間隔保持著緊密而均勻的形態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur les fenêtres calmes que nous avons pu lire, le rythme battement à battement est resté serré et régulier." + } } } }, @@ -12862,6 +14740,12 @@ "state": "translated", "value": "活躍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actif" + } } } }, @@ -12896,6 +14780,12 @@ "state": "translated", "value": "活動能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Énergie active" + } } } }, @@ -12982,6 +14872,12 @@ "state": "translated", "value": "活躍時段結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fin des heures actives" + } } } }, @@ -13016,6 +14912,12 @@ "state": "translated", "value": "活躍時段開始" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Début des heures actives" + } } } }, @@ -13050,6 +14952,12 @@ "state": "translated", "value": "活躍 · 即時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actif · En direct" + } } } }, @@ -13240,6 +15148,12 @@ "state": "translated", "value": "活動消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coût de l'activité" + } } } }, @@ -13274,6 +15188,12 @@ "state": "translated", "value": "作為標準的藍牙心率手環使用。從你的跑步機、單車或應用程式中配對 NOOP,即可在那裡看到手環的心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fonctionne comme un ceinture cardio Bluetooth standard. Appairez NOOP depuis votre tapis, vélo ou application pour y voir la fréquence cardiaque de votre bracelet." + } } } }, @@ -13360,6 +15280,12 @@ "state": "translated", "value": "新增" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter" + } } } }, @@ -13394,6 +15320,12 @@ "state": "translated", "value": "新增鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un entraînement" + } } } }, @@ -13428,6 +15360,12 @@ "state": "translated", "value": "新增自訂標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un marqueur personnalisé" + } } } }, @@ -13462,6 +15400,12 @@ "state": "translated", "value": "新增自訂問題…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une question personnalisée…" + } } } }, @@ -13496,6 +15440,12 @@ "state": "translated", "value": "新增裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un appareil" + } } } }, @@ -13530,6 +15480,12 @@ "state": "translated", "value": "新增標記讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une mesure de marqueur" + } } } }, @@ -13616,6 +15572,12 @@ "state": "translated", "value": "新增小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une sieste" + } } } }, @@ -13650,6 +15612,12 @@ "state": "translated", "value": "新增讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une mesure" + } } } }, @@ -13684,6 +15652,12 @@ "state": "translated", "value": "新增鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un entraînement" + } } } }, @@ -13770,6 +15744,12 @@ "state": "translated", "value": "新增小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une sieste" + } } } }, @@ -13804,6 +15784,12 @@ "state": "translated", "value": "新增鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un entraînement" + } } } }, @@ -13838,6 +15824,12 @@ "state": "translated", "value": "新增一個簡單的水分記錄,其每日目標會根據你的消耗進行調整。點擊以新增一口、一杯或一瓶,並看著進度環填滿。僅在 %@ 上,不會同步任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoute un journal d'hydratation simple avec un objectif quotidien qui s'ajuste à votre effort. Touchez pour ajouter une gorgée, une tasse ou une bouteille et regardez l'anneau de progression se remplir. Sur %@ uniquement. Rien n'est synchronisé." + } } } }, @@ -13872,6 +15864,12 @@ "state": "translated", "value": "在你的儀表板上新增一張水分記錄卡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoute une carte de suivi de l'eau à votre tableau de bord" + } } } }, @@ -13906,6 +15904,12 @@ "state": "translated", "value": "調整量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajuster la quantité" + } } } }, @@ -13940,6 +15944,12 @@ "state": "translated", "value": "手動調整" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajuster manuellement" + } } } }, @@ -13974,6 +15984,12 @@ "state": "translated", "value": "調整本次工作階段的詳情。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajustez les détails de cette séance." + } } } }, @@ -14008,6 +16024,12 @@ "state": "translated", "value": "進階" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avancé" + } } } }, @@ -14042,6 +16064,12 @@ "state": "translated", "value": "進階 HRV" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VFC avancée" + } } } }, @@ -14076,6 +16104,12 @@ "state": "translated", "value": "進階。我已有戒指的金鑰。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avancé. J'ai déjà la clé de ma bague." + } } } }, @@ -14110,6 +16144,12 @@ "state": "translated", "value": "進階:我已有戒指的金鑰" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avancé : j'ai déjà la clé de ma bague" + } } } }, @@ -14144,6 +16184,12 @@ "state": "translated", "value": "進階:使用你自己的金鑰" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avancé : utiliser votre propre clé" + } } } }, @@ -14178,6 +16224,12 @@ "state": "translated", "value": "有氧" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aérobie" + } } } }, @@ -14212,6 +16264,12 @@ "state": "translated", "value": "同步後,NOOP 會檢查你近期的心率,尋找一段看起來像運動的、持續升高的區段,並建議保存它。它只會建議,在你點擊「儲存」之前不會保存任何內容,你也可以忽略任何建議。它刻意保守,因此偶爾可能會漏掉某次鍛鍊。僅在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après une synchronisation, NOOP examine votre fréquence cardiaque récente à la recherche d'une période soutenue et élevée qui ressemble à de l'exercice, et propose de l'enregistrer. Il ne fait que suggérer. Rien n'est enregistré tant que vous n'appuyez pas sur Enregistrer, et vous pouvez ignorer toute suggestion. Volontairement prudent, il se peut donc qu'un entraînement occasionnel passe inaperçu. Sur %@ uniquement." + } } } }, @@ -14298,6 +16356,12 @@ "state": "translated", "value": "iPhone 重新開機後,請先解鎖一次。在你解鎖之前,iOS 會鎖定 NOOP 的檔案(資料保護),新的歷史資料無法寫入或同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après le redémarrage de votre iPhone, déverrouillez-le une fois. Jusque-là, iOS garde les fichiers de NOOP verrouillés (protection des données), donc aucun nouvel historique ne peut être écrit ni synchronisé." + } } } }, @@ -14436,6 +16500,12 @@ "state": "translated", "value": "鬧鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alarmes" + } } } }, @@ -14470,6 +16540,12 @@ "state": "translated", "value": "酒精" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alcool" + } } } }, @@ -14504,6 +16580,12 @@ "state": "translated", "value": "提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alerte" + } } } }, @@ -14538,6 +16620,12 @@ "state": "translated", "value": "提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alertes" + } } } }, @@ -14572,6 +16660,12 @@ "state": "translated", "value": "全部" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout" + } } } }, @@ -14658,6 +16752,12 @@ "state": "translated", "value": "已全部同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout est à jour" + } } } }, @@ -14692,6 +16792,12 @@ "state": "translated", "value": "全部歷史" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout l'historique" + } } } }, @@ -14726,6 +16832,12 @@ "state": "translated", "value": "這一切都是在你自己的裝置上,用你已有的資料計算的。沒有任何心跳資料離開裝置。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ceci est calculé sur votre propre appareil à partir de données que vous avez déjà. Aucune donnée de battement cardiaque n'en sort." + } } } }, @@ -14760,6 +16872,12 @@ "state": "translated", "value": "一切都留在 %@ 上。不會發送到任何地方。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout reste sur %@. Rien n'est envoyé nulle part." + } } } }, @@ -14794,6 +16912,12 @@ "state": "translated", "value": "允許存取 Apple 健康" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l'accès à Apple Santé" + } } } }, @@ -14828,6 +16952,12 @@ "state": "translated", "value": "允許存取" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l'accès" + } } } }, @@ -14862,6 +16992,12 @@ "state": "translated", "value": "已經是乾淨的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déjà propre." + } } } }, @@ -14896,6 +17032,12 @@ "state": "translated", "value": "同時分享我的規律與實驗手記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partager aussi mes tendances et mon carnet de laboratoire" + } } } }, @@ -14930,6 +17072,12 @@ "state": "translated", "value": "同時與教練分享我的規律和實驗手記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partager aussi mes tendances et mon carnet de laboratoire avec le Coach" + } } } }, @@ -14964,6 +17112,12 @@ "state": "translated", "value": "解讀結果和做決定時,請永遠仰賴你的醫師、藥師或合格的專業人員。如果某個數字讓你擔心,去找他們談,而不是找一個應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fiez-vous toujours à votre médecin, pharmacien ou un professionnel qualifié pour interpréter les résultats et prendre des décisions. Si un chiffre vous inquiète, parlez-en à eux, pas à une application." + } } } }, @@ -14998,6 +17152,12 @@ "state": "translated", "value": "量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quantité" + } } } }, @@ -15032,6 +17192,12 @@ "state": "translated", "value": "用量(毫克,可選)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quantité en mg (facultatif)" + } } } }, @@ -15066,6 +17232,12 @@ "state": "translated", "value": "用量(毫升)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quantité en millilitres" + } } } }, @@ -15100,6 +17272,12 @@ "state": "translated", "value": "HRV 讀數需要即時 R-R 資料流。請打開「即時」畫面並連接你的手環,然後再返回。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une mesure de VFC nécessite le flux R-R en direct. Ouvrez l'écran En direct et connectez votre bracelet, puis revenez." + } } } }, @@ -15134,6 +17312,12 @@ "state": "translated", "value": "依 Nes / HUNT Fitness Study 模型估算的 VO2max(靜息心率、年齡與活動量),對照人口常模換算成體能年齡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un VO2max estimé selon le modèle de l'étude Nes / HUNT Fitness (fréquence cardiaque au repos, âge et activité), comparé aux normes de population pour l'exprimer sous forme d'âge physique." + } } } }, @@ -15168,6 +17352,12 @@ "state": "translated", "value": "一幅你逐拍心跳間隔的實驗性圖像。不是 ECG,也不是診斷。在它顯示任何內容之前,你需要先閱讀並接受一份實驗性說明。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un aperçu expérimental du rythme battement par battement de votre cœur. Ce n'est ni un ECG ni un diagnostic. Vous lirez et accepterez une note expérimentale avant qu'il n'affiche quoi que ce soit." + } } } }, @@ -15202,6 +17392,12 @@ "state": "translated", "value": "對你逐拍節律的一次實驗性呈現" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un aperçu expérimental de votre rythme battement par battement" + } } } }, @@ -15236,6 +17432,12 @@ "state": "translated", "value": "對你逐拍節律的一次實驗性呈現。請先閱讀以下內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un aperçu expérimental de votre rythme battement par battement. Veuillez d'abord lire ceci." + } } } }, @@ -15270,6 +17472,12 @@ "state": "translated", "value": "分析我的睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyser mon sommeil" + } } } }, @@ -15304,6 +17512,12 @@ "state": "translated", "value": "還要確保它有電。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Et assurez-vous qu'il est chargé." + } } } }, @@ -15338,6 +17552,12 @@ "state": "translated", "value": "回答涉及的是通向今天早晨的這個夜晚和白天,與 WHOOP 匯出所用的歸屬方式相同,因此記錄的日期和匯入的日期能夠對齊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les réponses portent sur la nuit et la journée précédant ce matin, la même attribution qu'utilise un export WHOOP, afin que les jours enregistrés et importés correspondent." + } } } }, @@ -15372,6 +17592,12 @@ "state": "translated", "value": "任何相容 OpenAI 的伺服器,Ollama、LM Studio、llama.cpp,或你自己的閘道。留在你的網路內;沒有任何內容離開 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout serveur compatible OpenAI : Ollama, LM Studio, llama.cpp, ou votre propre passerelle. Reste sur votre réseau ; rien ne quitte %@." + } } } }, @@ -15458,6 +17684,12 @@ "state": "translated", "value": "任何需要 Oura 雲端的功能(網頁儀表板、Oura 的教練功能、共享圈子)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ce qui nécessite le cloud d'Oura (tableau de bord web, coaching Oura, cercles partagés)." + } } } }, @@ -15492,6 +17724,12 @@ "state": "translated", "value": "你在這裡看到的一切(包括任何並排趨勢)都是你自己的資訊回饋給你。它是一種關聯,絕非因果,也絕非醫學結論。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ce que vous voyez ici (y compris toute tendance comparée) est votre propre information qui vous est restituée. C'est une association, jamais une cause, et jamais un constat médical." + } } } }, @@ -15526,6 +17764,12 @@ "state": "translated", "value": "應用程式圖示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône de l'application" + } } } }, @@ -15560,6 +17804,12 @@ "state": "translated", "value": "外觀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apparence" + } } } }, @@ -15698,6 +17948,12 @@ "state": "translated", "value": "Apple 健康(即時)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apple Santé (En direct)" + } } } }, @@ -15732,6 +17988,12 @@ "state": "translated", "value": "Apple 健康在 %@ 上不可用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apple Santé n'est pas disponible sur %@." + } } } }, @@ -15766,6 +18028,12 @@ "state": "translated", "value": "Apple 健康在此裝置上不可用,因此這裡沒有可連接的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apple Santé n'est pas disponible sur cet appareil, il n'y a donc rien à connecter ici." + } } } }, @@ -15800,6 +18068,12 @@ "state": "translated", "value": "Apple 健康位於 iPhone 上,而非 Mac 上,因此連接你的 Apple Watch 需在那裡進行。請在 iPhone 上打開 NOOP,前往「設定」,執行同樣的這套 Apple Watch 設定流程。之後你的評分便會在各裝置間顯示。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apple Santé se trouve sur l'iPhone, pas sur le Mac, donc la connexion de votre Apple Watch se fait là-bas. Ouvrez NOOP sur votre iPhone, allez dans Paramètres et effectuez cette même configuration Apple Watch. Vos scores apparaîtront ensuite sur tous vos appareils." + } } } }, @@ -15834,6 +18108,12 @@ "state": "translated", "value": "Apple Health 同步失敗:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la synchronisation avec Apple Santé : %@" + } } } }, @@ -15868,6 +18148,12 @@ "state": "translated", "value": "Apple Watch。目前還需要更多資料才能對此評分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apple Watch. A besoin de plus de données pour évaluer cela." + } } } }, @@ -15902,6 +18188,12 @@ "state": "translated", "value": "Apple 自己的睡眠階段直接驅動休息分數。這是手錶的強項之一。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les phases de sommeil propres à Apple alimentent directement le Repos. C'est l'un des points forts de la montre." + } } } }, @@ -15936,6 +18228,12 @@ "state": "translated", "value": "Apple 的睡眠階段很可靠,它們直接驅動你的休息分數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les phases de sommeil d'Apple sont fiables et alimentent directement votre score de Repos." + } } } }, @@ -15970,6 +18268,12 @@ "state": "translated", "value": "Apple 記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré par Apple" + } } } }, @@ -16004,6 +18308,12 @@ "state": "translated", "value": "4月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avr" + } } } }, @@ -16038,6 +18348,12 @@ "state": "translated", "value": "在手環本身上設定,因此即使你的手機處於睡眠狀態或 NOOP 已關閉,它也能在你的起床時間震動。我們發送的是與官方應用程式相同的鬧鐘指令,但由手環驅動的喚醒尚未在我們這邊得到確認,因此目前請保留一個備用鬧鐘。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Armée sur le bracelet lui-même, elle peut donc vibrer à votre heure de réveil même si votre téléphone est en veille ou NOOP fermé. Nous envoyons la même commande d'alarme que l'application officielle, mais un réveil piloté par le bracelet n'a pas encore été confirmé de notre côté, alors gardez pour l'instant une alarme de secours." + } } } }, @@ -16072,6 +18388,12 @@ "state": "translated", "value": "設定手環在你的起床時間震動,即使 NOOP 已關閉。在 WHOOP 4.0 上仍屬實驗性,因此在你確認它能叫醒你之前,請保留一個備用鬧鐘。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arme le bracelet pour vibrer à votre heure de réveil, même si NOOP est fermé. Encore expérimental sur WHOOP 4.0, gardez donc une alarme de secours jusqu'à ce que vous ayez confirmé qu'il vous réveille." + } } } }, @@ -16158,6 +18480,12 @@ "state": "translated", "value": "詢問你的能量、消耗、休息和鍛鍊,一切都基於你自己的數據。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Posez des questions sur votre charge, votre effort, votre repos et vos entraînements, ancrées dans vos propres chiffres." + } } } }, @@ -16349,6 +18677,12 @@ "state": "translated", "value": "入睡時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temps de sommeil" + } } } }, @@ -16540,6 +18874,12 @@ "state": "translated", "value": "把它附加到錯誤報告中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Joignez ceci à un rapport de bug." + } } } }, @@ -16574,6 +18914,12 @@ "state": "translated", "value": "音訊提示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaux sonores" + } } } }, @@ -16608,6 +18954,12 @@ "state": "translated", "value": "8月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Août" + } } } }, @@ -16694,6 +19046,12 @@ "state": "translated", "value": "自動 · 信心度 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Auto · confiance %@" + } } } }, @@ -16780,6 +19138,12 @@ "state": "translated", "value": "自動偵測鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détecter automatiquement les entraînements" + } } } }, @@ -16814,6 +19178,12 @@ "state": "translated", "value": "自動提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rappel automatique" + } } } }, @@ -16848,6 +19218,12 @@ "state": "translated", "value": "自動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatique" + } } } }, @@ -16882,6 +19258,12 @@ "state": "translated", "value": "自動化" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatisation" + } } } }, @@ -16968,6 +19350,12 @@ "state": "translated", "value": "自主神經平衡(LF/HF)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équilibre autonome (LF/HF)" + } } } }, @@ -17106,6 +19494,12 @@ "state": "translated", "value": "全天的自主神經負荷" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge autonome au fil de la journée" + } } } }, @@ -17140,6 +19534,12 @@ "state": "translated", "value": "今日自律神經負荷:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge autonome aujourd'hui : %@" + } } } }, @@ -17174,6 +19574,12 @@ "state": "translated", "value": "從你的心率節律得出的自律神經僵硬度。數值越高,代表節律越僵硬、越有壓力。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rigidité autonome déduite du rythme de votre fréquence cardiaque. Plus c'est élevé, plus le rythme est rigide et stressé." + } } } }, @@ -17260,6 +19666,12 @@ "state": "translated", "value": "平均心率必須在 25–250 bpm 之間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La FC moyenne doit être comprise entre 25 et 250 bpm." + } } } }, @@ -17294,6 +19706,12 @@ "state": "translated", "value": "平均心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque moyenne" + } } } }, @@ -17328,6 +19746,12 @@ "state": "translated", "value": "平均心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque moyenne" + } } } }, @@ -17362,6 +19786,12 @@ "state": "translated", "value": "平均心率(每分鐘次數,可選)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque moyenne en battements par minute, facultatif" + } } } }, @@ -17448,6 +19878,12 @@ "state": "translated", "value": "平均心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC moy." + } } } }, @@ -17482,6 +19918,12 @@ "state": "translated", "value": "平均心率會以你輸入的數值顯示。心率圖、區間與消耗仍沿用記錄的訓練資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La FC moy. est affichée telle que saisie. Le graphique de FC, les zones et l'Effort restent ceux de la séance enregistrée." + } } } }, @@ -17516,6 +19958,12 @@ "state": "translated", "value": "平均配速" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Allure moy." + } } } }, @@ -17550,6 +19998,12 @@ "state": "translated", "value": "等待手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente du bracelet" + } } } }, @@ -17636,6 +20090,12 @@ "state": "translated", "value": "BANISTER TRIMP / 心率區間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "BANISTER TRIMP / ZONES DE FC" + } } } }, @@ -17670,6 +20130,12 @@ "state": "translated", "value": "逐拍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "BATTEMENT PAR BATTEMENT" + } } } }, @@ -17704,6 +20170,12 @@ "state": "translated", "value": "逐拍散點圖" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NUAGE BATTEMENT PAR BATTEMENT" + } } } }, @@ -17738,6 +20210,12 @@ "state": "translated", "value": "已讀取的心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "BATTEMENTS LUS" + } } } }, @@ -17928,6 +20406,12 @@ "state": "translated", "value": "返回標準設定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour à la configuration standard" + } } } }, @@ -17962,6 +20446,12 @@ "state": "translated", "value": "返回標記列表" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour à la liste des marqueurs" + } } } }, @@ -17996,6 +20486,12 @@ "state": "translated", "value": "立即備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarder maintenant" + } } } }, @@ -18030,6 +20526,12 @@ "state": "translated", "value": "已備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegardé" + } } } }, @@ -18064,6 +20566,12 @@ "state": "translated", "value": "背景 Bluetooth 有作業系統的限制:NOOP 不在前景時 iOS 可能會暫停它,所以同步新手環時請保持應用程式開啟。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Bluetooth en arrière-plan a des limites au niveau du système : iOS peut mettre NOOP en pause lorsqu'il n'est pas au premier plan, alors gardez-le ouvert pendant la synchronisation d'un nouveau bracelet." + } } } }, @@ -18098,6 +20606,12 @@ "state": "translated", "value": "大約每天一次備份到你的資料夾,並保留最新的 %lld 份。在此平台上,它會在你下次打開 NOOP 時執行。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde dans votre dossier environ une fois par jour et conserve les %lld plus récentes. Sur cette plateforme, cela s'exécute à la prochaine ouverture de NOOP." + } } } }, @@ -18132,6 +20646,12 @@ "state": "translated", "value": "備份與同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde et synchronisation" + } } } }, @@ -18166,6 +20686,12 @@ "state": "translated", "value": "備份並同步到資料夾…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde et synchronisation vers un dossier…" + } } } }, @@ -18252,6 +20778,12 @@ "state": "translated", "value": "備份已匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde exportée" + } } } }, @@ -18286,6 +20818,12 @@ "state": "translated", "value": "備份失敗,請重新選擇資料夾後再試一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la sauvegarde, resélectionnez le dossier et réessayez." + } } } }, @@ -18320,6 +20858,12 @@ "state": "translated", "value": "備份失敗:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la sauvegarde : %@" + } } } }, @@ -18354,6 +20898,12 @@ "state": "translated", "value": "備份資料夾" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier de sauvegarde" + } } } }, @@ -18388,6 +20938,12 @@ "state": "translated", "value": "備份已匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde importée" + } } } }, @@ -18422,6 +20978,12 @@ "state": "translated", "value": "備份問題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Problème de sauvegarde" + } } } }, @@ -18456,6 +21018,12 @@ "state": "translated", "value": "備用喚醒:你的智慧鬧鐘時間到了。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveil de secours : votre heure d'alarme intelligente est arrivée." + } } } }, @@ -18490,6 +21058,12 @@ "state": "translated", "value": "巴耶夫斯基壓力指數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indice de stress de Baevsky" + } } } }, @@ -18524,6 +21098,12 @@ "state": "translated", "value": "平衡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équilibre" + } } } }, @@ -18558,6 +21138,12 @@ "state": "translated", "value": "平衡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équilibré" + } } } }, @@ -18592,6 +21178,12 @@ "state": "translated", "value": "目前主要基於一般模式,還不是你自己的。再記錄幾天 %@,它就會屬於你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Basé principalement sur des tendances types, pas encore les vôtres. Enregistrez encore quelques jours de %@ et cela deviendra le vôtre." + } } } }, @@ -18626,6 +21218,12 @@ "state": "translated", "value": "基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Référence" + } } } }, @@ -18660,6 +21258,12 @@ "state": "translated", "value": "基線會在你最初的 14 天內於裝置本地學習得出,在那之前,採用典型範圍。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les références sont apprises sur l'appareil au cours de vos 14 premiers jours. D'ici là, des plages types s'appliquent." + } } } }, @@ -18694,6 +21298,12 @@ "state": "translated", "value": "電池" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie" + } } } }, @@ -18728,6 +21338,12 @@ "state": "translated", "value": "電量百分之 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie à %lld pour cent" + } } } }, @@ -18866,6 +21482,12 @@ "state": "translated", "value": "電池提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alertes de batterie" + } } } }, @@ -18900,6 +21522,12 @@ "state": "translated", "value": "逐拍間期會因許多平常的原因而變化:呼吸、體動、不完美的光學讀數,或大多數健康人偶爾會有的多跳或漏跳。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le rythme battement par battement varie pour de nombreuses raisons ordinaires : la respiration, le mouvement, une lecture optique imparfaite, ou le battement occasionnel en trop ou manquant que la plupart des personnes en bonne santé ont." + } } } }, @@ -18934,6 +21562,12 @@ "state": "translated", "value": "心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Battements" + } } } }, @@ -18968,6 +21602,12 @@ "state": "translated", "value": "每分鐘次數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Battements par minute" + } } } }, @@ -19002,6 +21642,12 @@ "state": "translated", "value": "整段期間的每分鐘心跳數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Battements par minute au cours de la séance" + } } } }, @@ -19036,6 +21682,12 @@ "state": "translated", "value": "就寢時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure du coucher" + } } } }, @@ -19070,6 +21722,12 @@ "state": "translated", "value": "在開啟「節律」之前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avant d'activer Rhythm" + } } } }, @@ -19104,6 +21762,12 @@ "state": "translated", "value": "在使用 NOOP 之前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avant d'utiliser NOOP" + } } } }, @@ -19138,6 +21802,12 @@ "state": "translated", "value": "行為" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comportement" + } } } }, @@ -19224,6 +21894,12 @@ "state": "translated", "value": "低於區間 1" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sous la zone 1" + } } } }, @@ -19258,6 +21934,12 @@ "state": "translated", "value": "低於所需" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sous les besoins" + } } } }, @@ -19292,6 +21974,12 @@ "state": "translated", "value": "Beta" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bêta" + } } } }, @@ -19326,6 +22014,12 @@ "state": "translated", "value": "Beta。* 為裝置端估算值。皮膚溫度是相對於你自身基線的趨勢,HRV 需要你保持靜止。戒指不會輸出 Oura 的準備度或 SpO₂ 百分比(如需這些,請匯入 Oura 檔案)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO₂ ne provient de la bague (importez un fichier Oura pour cela)." + } } } }, @@ -19360,6 +22054,12 @@ "state": "translated", "value": "Beta。* 為裝置端估算值。皮膚溫度是相對於你自身基線的趨勢,步數是原始運動計數,HRV 需要你保持靜止。戒指不會輸出 Oura 的準備度或 SpO2 百分比(如需這些,請匯入 Oura 檔案)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, les pas sont un simple comptage de mouvement, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO2 ne provient de la bague (importez un fichier Oura pour cela)." + } } } }, @@ -19394,6 +22094,12 @@ "state": "translated", "value": "Beta。請先閱讀此內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bêta. Lisez ceci d'abord." + } } } }, @@ -19480,6 +22186,12 @@ "state": "translated", "value": "血氧" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "O₂ sanguin" + } } } }, @@ -19566,6 +22278,12 @@ "state": "translated", "value": "血氧(SpO2 %)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Oxygène sanguin (SpO2 %)" + } } } }, @@ -19600,6 +22318,12 @@ "state": "translated", "value": "血氧(SpO₂)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Oxygène sanguin (SpO₂)" + } } } }, @@ -19634,6 +22358,12 @@ "state": "translated", "value": "血液檢驗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bilan sanguin" + } } } }, @@ -19668,6 +22398,12 @@ "state": "translated", "value": "血壓" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tension artérielle" + } } } }, @@ -19702,6 +22438,12 @@ "state": "translated", "value": "身體" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Corps" + } } } }, @@ -19736,6 +22478,12 @@ "state": "translated", "value": "身體年齡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Âge corporel" + } } } }, @@ -19770,6 +22518,12 @@ "state": "translated", "value": "身體年齡 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Âge corporel %lld" + } } } }, @@ -19908,6 +22662,12 @@ "state": "translated", "value": "生理時鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Horloge biologique" + } } } }, @@ -20046,6 +22806,12 @@ "state": "translated", "value": "已綁定 · 串流中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Associé · flux actif" + } } } }, @@ -20080,6 +22846,12 @@ "state": "translated", "value": "已綁定,正在串流。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Associé, flux actif." + } } } }, @@ -20114,6 +22886,12 @@ "state": "translated", "value": "已綁定,一切就緒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Associé. Vous êtes prêt." + } } } }, @@ -20148,6 +22926,12 @@ "state": "translated", "value": "NOOP 和 Oura 應用程式都可以透過金鑰使用一枚你擁有的戒指,但同一時間只能有一個持有藍牙連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP et l'application Oura peuvent tous deux utiliser une bague que vous possédez via une clé, mais un seul peut détenir la liaison Bluetooth à la fois." + } } } }, @@ -20182,6 +22966,12 @@ "state": "translated", "value": "瓶" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bouteille" + } } } }, @@ -20216,6 +23006,12 @@ "state": "translated", "value": "回彈" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération" + } } } }, @@ -20250,6 +23046,12 @@ "state": "translated", "value": "箱式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Carré" + } } } }, @@ -20284,6 +23086,12 @@ "state": "translated", "value": "箱式 4-4" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Carré 4-4" + } } } }, @@ -20370,6 +23178,12 @@ "state": "translated", "value": "如果你做過「找出我的節奏」掃描,就依照上次鎖定的節奏呼吸;否則以平穩的每分鐘 5.5 次呼吸進行。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respirez au rythme fixé par votre dernier balayage « trouver mon rythme », si vous en avez un. Sinon, un rythme calme de 5,5 respirations/min." + } } } }, @@ -20404,6 +23218,12 @@ "state": "translated", "value": "吸氣" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspirez" + } } } }, @@ -20438,6 +23258,12 @@ "state": "translated", "value": "吸氣 %lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspirez pendant %lld secondes" + } } } }, @@ -20472,6 +23298,12 @@ "state": "translated", "value": "吸氣…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspirez…" + } } } }, @@ -20506,6 +23338,12 @@ "state": "translated", "value": "呼吸模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode respiration" + } } } }, @@ -20540,6 +23378,12 @@ "state": "translated", "value": "立即呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respirez maintenant" + } } } }, @@ -20574,6 +23418,12 @@ "state": "translated", "value": "吐氣" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expirez" + } } } }, @@ -20608,6 +23458,12 @@ "state": "translated", "value": "吐氣 %lld 秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expirez pendant %lld secondes" + } } } }, @@ -20642,6 +23498,12 @@ "state": "translated", "value": "吐氣…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expirez…" + } } } }, @@ -20728,6 +23590,12 @@ "state": "translated", "value": "帶入你的歷史資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apportez votre historique" + } } } }, @@ -20762,6 +23630,12 @@ "state": "translated", "value": "廣播心率已開啟。你的手環正在持續廣播其心率,這會讓其無線電保持活躍,更快地耗盡電池。當你不與其他裝置一起使用它時,請將其關閉。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La diffusion de la FC est ACTIVÉE. Votre bracelet diffuse en continu sa fréquence cardiaque, ce qui maintient sa radio active et vide la batterie plus vite. Désactivez-la lorsque vous ne l'utilisez pas avec un autre appareil." + } } } }, @@ -20796,6 +23670,12 @@ "state": "translated", "value": "廣播心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffuser la fréquence cardiaque" + } } } }, @@ -20830,6 +23710,12 @@ "state": "translated", "value": "廣播心率(Garmin/ANT)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffuser la fréquence cardiaque (Garmin/ANT)" + } } } }, @@ -20864,6 +23750,12 @@ "state": "translated", "value": "作為藍牙感測器廣播心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffuser la fréquence cardiaque comme capteur Bluetooth" + } } } }, @@ -20898,6 +23790,12 @@ "state": "translated", "value": "廣播中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffusion en cours" + } } } }, @@ -20932,6 +23830,12 @@ "state": "translated", "value": "連接後,廣播 NOOP 從手環推算出的即時心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffuse la fréquence cardiaque en direct que NOOP déduit du bracelet une fois connecté." + } } } }, @@ -20966,6 +23870,12 @@ "state": "translated", "value": "透過 Bluetooth 廣播手環自身的即時心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffuse via Bluetooth la fréquence cardiaque en direct propre au bracelet." + } } } }, @@ -21000,6 +23910,12 @@ "state": "translated", "value": "錯誤回報" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rapport de bug" + } } } }, @@ -21034,6 +23950,12 @@ "state": "translated", "value": "自行建立" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créez le vôtre" + } } } }, @@ -21068,6 +23990,12 @@ "state": "translated", "value": "建立中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction en cours" + } } } }, @@ -21102,6 +24030,12 @@ "state": "translated", "value": "建立中,1 個窗口" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction en cours, 1 fenêtre" + } } } }, @@ -21188,6 +24122,12 @@ "state": "translated", "value": "正在建立你的基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction de votre référence" + } } } }, @@ -21229,6 +24169,12 @@ "state": "translated", "value": "正在建立你的基線。大約還需 %1$lld %2$@,你的評分才會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction de votre référence. Encore environ %1$lld %2$@ avant que vos scores ne soient personnalisés." + } } } }, @@ -21263,6 +24209,12 @@ "state": "translated", "value": "正在建立你的基線。大約還需 %lld 晚,你的評分才會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés." + } } } }, @@ -21297,6 +24249,12 @@ "state": "translated", "value": "正在建立你的基線。大約還需 1 晚,你的評分才會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés." + } } } }, @@ -21331,6 +24289,12 @@ "state": "translated", "value": "正在建立你的最佳來源記錄…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Construction de votre relevé le mieux documenté…" + } } } }, @@ -21469,6 +24433,12 @@ "state": "translated", "value": "建立中,請持續記錄。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En construction. Continuez à enregistrer." + } } } }, @@ -21503,6 +24473,12 @@ "state": "translated", "value": "建立一個已去識別化的 .zip,向你確切展示其內容,然後打開一個已預填的 GitHub issue。你將在下一畫面附上該檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Crée un .zip anonymisé, vous montre exactement son contenu, puis ouvre une issue GitHub prérempli. Vous joignez le fichier à l'écran suivant." + } } } }, @@ -21537,6 +24513,12 @@ "state": "translated", "value": "根據 Apple 的心肺適能 VO₂ max 估算值建立,與「體能訓練」應用程式顯示的數字相同。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé à partir de l'estimation du VO₂ max cardio d'Apple, le même chiffre qu'affiche l'application Fitness." + } } } }, @@ -21571,6 +24553,12 @@ "state": "translated", "value": "在 %@ 上根據已發表的方法(Nes/HUNT)建立。這是與你同齡的平均同伴的體能比較,而非生物學或醫學年齡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé selon des méthodes publiées (Nes/HUNT) sur %@. C'est une comparaison de forme physique avec une personne moyenne de votre âge, pas un âge biologique ou médical." + } } } }, @@ -21605,6 +24593,12 @@ "state": "translated", "value": "根據手錶的心肺適能 VO₂ max 建立,與「體能訓練」應用程式顯示的數字相同。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé à partir du VO₂ max cardio de la montre, le même chiffre qu'affiche l'application Fitness." + } } } }, @@ -21691,6 +24685,12 @@ "state": "translated", "value": "震動手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire vibrer le bracelet" + } } } }, @@ -21725,6 +24725,12 @@ "state": "translated", "value": "回震(確認)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibration en retour (confirmer)" + } } } }, @@ -21863,6 +24869,12 @@ "state": "translated", "value": "震動強度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intensité de la vibration" + } } } }, @@ -21897,6 +24909,12 @@ "state": "translated", "value": "震動報時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrer l'heure" + } } } }, @@ -21931,6 +24949,12 @@ "state": "translated", "value": "當你進入最高區間時震動(提醒放慢),恢復後再震動一次。使用「設定」中的最大心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibre lorsque vous atteignez votre zone maximale (ralentissez) et à nouveau lorsque vous récupérez. Utilise votre FC max des Paramètres." + } } } }, @@ -21965,6 +24989,12 @@ "state": "translated", "value": "透過 Siri、Spotlight、捷徑應用程式,或輕點背面 / 自動化來震動你的手環或標記某個時刻,無需任何設定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites vibrer votre bracelet ou marquez un instant depuis Siri, Spotlight, l'application Raccourcis, ou un Tapback / une automatisation. Aucune configuration nécessaire." + } } } }, @@ -21999,6 +25029,12 @@ "state": "translated", "value": "當這些應用程式通知你時,讓手環震動。一切都在 %@ 上執行。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites vibrer votre bracelet lorsque ces applications vous notifient. Tout s'exécute sur %@." + } } } }, @@ -22086,6 +25122,12 @@ "state": "translated", "value": "久坐超過你的門檻後震動提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibre après que vous êtes resté assis au-delà de votre seuil." + } } } }, @@ -22172,6 +25214,12 @@ "state": "translated", "value": "按天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par jour" + } } } }, @@ -22258,6 +25306,12 @@ "state": "translated", "value": "用數字說話" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En chiffres" + } } } }, @@ -22292,6 +25346,12 @@ "state": "translated", "value": "步頻" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CADENCE" + } } } }, @@ -22326,6 +25386,12 @@ "state": "translated", "value": "校準中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CALIBRAGE" + } } } }, @@ -22360,6 +25426,12 @@ "state": "translated", "value": "點雲形狀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FORME DU NUAGE" + } } } }, @@ -22394,6 +25466,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TERMINÉ" + } } } }, @@ -22428,6 +25506,12 @@ "state": "translated", "value": "已連線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CONNECTÉ" + } } } }, @@ -22462,6 +25546,12 @@ "state": "translated", "value": "連接中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CONNEXION EN COURS" + } } } }, @@ -22496,6 +25586,12 @@ "state": "translated", "value": "控制中心" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CENTRE DE CONTRÔLE" + } } } }, @@ -22530,6 +25626,12 @@ "state": "translated", "value": "CSV 已匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CSV exporté" + } } } }, @@ -22564,6 +25666,12 @@ "state": "translated", "value": "自訂" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PERSONNALISER" + } } } }, @@ -22598,6 +25706,12 @@ "state": "translated", "value": "咖啡因" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Caféine" + } } } }, @@ -22632,6 +25746,12 @@ "state": "translated", "value": "咖啡因可能仍在作用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La caféine pourrait encore être active" + } } } }, @@ -22718,6 +25838,12 @@ "state": "translated", "value": "校準步數估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrer l'estimation des pas" + } } } }, @@ -22752,6 +25878,12 @@ "state": "translated", "value": "校準步數估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrer l'estimation des pas" + } } } }, @@ -22786,6 +25918,12 @@ "state": "translated", "value": "校準你的步數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrer vos pas" + } } } }, @@ -22878,6 +26016,12 @@ "state": "translated", "value": "校準中,%1$lld / %2$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage, %1$lld sur %2$lld" + } } } }, @@ -22971,6 +26115,12 @@ "state": "translated", "value": "校準中,第 %lld / %lld 晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage, %lld nuits sur %lld" + } } } }, @@ -23005,6 +26155,12 @@ "state": "translated", "value": "校準中,尚無資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage, aucune donnée pour le moment" + } } } }, @@ -23039,6 +26195,12 @@ "state": "translated", "value": "校準中,今天尚未存入心率資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage, aucune fréquence cardiaque enregistrée aujourd'hui" + } } } }, @@ -23073,6 +26235,12 @@ "state": "translated", "value": "校準中。正在建立你的基線。大約還需 %lld 晚,你的評分才會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage. Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés." + } } } }, @@ -23107,6 +26275,12 @@ "state": "translated", "value": "校準中。正在建立你的基線。大約還需 1 晚,你的評分才會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage. Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés." + } } } }, @@ -23141,6 +26315,12 @@ "state": "translated", "value": "校準中。資料還太少,無法判讀。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage. Encore trop peu de données pour une lecture." + } } } }, @@ -23175,6 +26355,12 @@ "state": "translated", "value": "校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage" + } } } }, @@ -23209,6 +26395,12 @@ "state": "translated", "value": "平靜" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calme" + } } } }, @@ -23243,6 +26435,12 @@ "state": "translated", "value": "讓我平靜" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calmez-moi" + } } } }, @@ -23277,6 +26475,12 @@ "state": "translated", "value": "讓我平靜 · 3 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calmez-moi · 3 min" + } } } }, @@ -23415,6 +26619,12 @@ "state": "translated", "value": "攝取熱量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calories consommées" + } } } }, @@ -23449,6 +26659,12 @@ "state": "translated", "value": "卡路里(千卡,可選)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calories en kilocalories, facultatif" + } } } }, @@ -23483,6 +26699,12 @@ "state": "translated", "value": "熱量必須介於 0–20,000。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les calories doivent être comprises entre 0 et 20 000." + } } } }, @@ -23517,6 +26739,12 @@ "state": "translated", "value": "無法連接,你的手環配對已被重置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion impossible : l'appairage de votre bracelet a été réinitialisé" + } } } }, @@ -23551,6 +26779,12 @@ "state": "translated", "value": "取消" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } } } }, @@ -23585,6 +26819,12 @@ "state": "translated", "value": "擷取中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Capture en cours" + } } } }, @@ -23625,6 +26865,12 @@ "state": "translated", "value": "擷取中。剩餘 %lld 秒,已收集 %lld 次心跳。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Capture en cours. %1$lld secondes restantes, %2$lld battements collectés." + } } } }, @@ -23659,6 +26905,12 @@ "state": "translated", "value": "碳水化合物" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Glucides" + } } } }, @@ -23745,6 +26997,12 @@ "state": "translated", "value": "當天的心血管負荷,以 0–100 為量表(原為 0–21)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge cardiovasculaire de la journée, sur une échelle de 0 à 100 (auparavant 0 à 21)." + } } } }, @@ -23831,6 +27089,12 @@ "state": "translated", "value": "變更" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier" + } } } }, @@ -23865,6 +27129,12 @@ "state": "translated", "value": "變更資料夾" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer de dossier" + } } } }, @@ -23899,6 +27169,12 @@ "state": "translated", "value": "變更標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer de marqueur" + } } } }, @@ -23933,6 +27209,12 @@ "state": "translated", "value": "變更照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer la photo" + } } } }, @@ -23967,6 +27249,12 @@ "state": "translated", "value": "變更你的 WHOOP 4.0 廣播的藍牙名稱,即你配對時看到的名稱。手環會重啟以套用變更,因此新名稱會在它下次連接時出現。僅限 WHOOP 4.0。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifie le nom Bluetooth que diffuse votre WHOOP 4.0 (celui que vous voyez lors de l'appairage). Le bracelet redémarre pour appliquer le changement, le nouveau nom apparaît donc à la prochaine connexion. WHOOP 4.0 uniquement." + } } } }, @@ -24053,6 +27341,12 @@ "state": "translated", "value": "能量 %@%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge %@%%" + } } } }, @@ -24087,6 +27381,12 @@ "state": "translated", "value": "能量(全部歷史)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge (tout l'historique)" + } } } }, @@ -24121,6 +27421,12 @@ "state": "translated", "value": "能量(過去一年)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge (année écoulée)" + } } } }, @@ -24161,6 +27467,12 @@ "state": "translated", "value": "能量基線校準中。%1$@,%2$@。%3$@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibrage de la référence de charge. %1$@, %2$@. %3$@." + } } } }, @@ -24195,6 +27507,12 @@ "state": "translated", "value": "能量基線重新校準中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalibrage de la référence de charge" + } } } }, @@ -24229,6 +27547,12 @@ "state": "translated", "value": "能量偏低,睡眠規律。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est basse et le sommeil a été régulier." + } } } }, @@ -24263,6 +27587,12 @@ "state": "translated", "value": "能量偏低,且睡眠不足。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est basse et le sommeil a été insuffisant." + } } } }, @@ -24297,6 +27627,12 @@ "state": "translated", "value": "能量偏低。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est basse." + } } } }, @@ -24331,6 +27667,12 @@ "state": "translated", "value": "能量平穩,睡眠規律。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est stable et le sommeil a été régulier." + } } } }, @@ -24365,6 +27707,12 @@ "state": "translated", "value": "能量平穩,但睡眠不足。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est stable mais le sommeil a été insuffisant." + } } } }, @@ -24399,6 +27747,12 @@ "state": "translated", "value": "能量平穩。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est stable." + } } } }, @@ -24433,6 +27787,12 @@ "state": "translated", "value": "能量充沛,睡眠規律。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est élevée et le sommeil a été régulier." + } } } }, @@ -24467,6 +27827,12 @@ "state": "translated", "value": "能量充沛,但睡眠不足。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est élevée mais le sommeil a été insuffisant." + } } } }, @@ -24501,6 +27867,12 @@ "state": "translated", "value": "能量充沛。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge est élevée." + } } } }, @@ -24535,6 +27907,12 @@ "state": "translated", "value": "能量模型" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modèle de charge" + } } } }, @@ -24569,6 +27947,12 @@ "state": "translated", "value": "能量趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance de la charge" + } } } }, @@ -24603,6 +27987,12 @@ "state": "translated", "value": "能量會將你的 HRV 與你的個人基線(約 55%%)、靜息心率(約 20%%)、休息品質(約 15%%)、呼吸(約 5%%)和皮膚溫度偏差(約 5%%)進行加權。消耗是根據在各心率區間的時間得出的 0–%@ 心血管負荷。休息則根據體動和心率分期。一切都在這裡根據手環的原始資料計算,只要 NOOP 採集到原始資料流的任何一天都適用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La charge pondère votre VFC par rapport à votre référence personnelle (~55 %%), votre fréquence cardiaque au repos (~20 %%), la qualité du repos (~15 %%), la respiration (~5 %%) et l'écart de température cutanée (~5 %%). L'effort est une charge cardiovasculaire de 0 à %@ calculée à partir du temps passé dans les zones de fréquence cardiaque. Le repos est décomposé en phases à partir du mouvement et de la fréquence cardiaque. Tout est calculé ici à partir des données brutes du bracelet. Cela fonctionne pour tout jour où NOOP a collecté des flux bruts." + } } } }, @@ -24637,6 +28027,12 @@ "state": "translated", "value": "能量 · %@ 分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge · %@ pts" + } } } }, @@ -24671,6 +28067,12 @@ "state": "translated", "value": "能量 · 消耗 · 休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge · Effort · Repos" + } } } }, @@ -24705,6 +28107,12 @@ "state": "translated", "value": "能量 → 隔天能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge → Charge du lendemain" + } } } }, @@ -24739,6 +28147,12 @@ "state": "translated", "value": "能量、消耗和休息,以及它們與 WHOOP 的區別。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge, Effort et Repos (et en quoi ils diffèrent de WHOOP)." + } } } }, @@ -24773,6 +28187,12 @@ "state": "translated", "value": "能量、消耗和休息都是在你自己的裝置上根據手環資料計算的。能量大約需要四晚睡眠來學習你的基線(也就是「校準中」,在圓環上以幾分之四的晚數顯示),並在頭一兩週持續變得更精準。在 WHOOP 5 或 MG 上,手環儲存的歷史資料很少,所以在你連續佩戴幾晚之前,該計數可能停在 0 / 4。這是手環的同步限制,不是故障。在有數字之前,NOOP 會盡量顯示可顯示的內容,而不會偽造一個數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge, Effort et Repos sont calculés sur votre propre appareil à partir des données de votre bracelet. La charge nécessite environ quatre nuits de sommeil pour apprendre votre référence (c'est le « Calibrage », compté en nuits sur 4 sur la bague), et continue de s'affiner au cours de vos premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, ce compteur peut donc rester à 0 sur 4 jusqu'à ce que vous l'ayez porté quelques nuits. C'est la limite de synchronisation du bracelet, pas un défaut. Tant qu'il n'y a pas de chiffre, NOOP montre ce qu'il peut sans en inventer un." + } } } }, @@ -24807,6 +28227,12 @@ "state": "translated", "value": "佩戴幾晚後,能量、消耗和休息就會變得個人化。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge, Effort et Repos deviennent personnalisés après quelques nuits de port." + } } } }, @@ -24841,6 +28267,12 @@ "state": "translated", "value": "能量、消耗和休息會在你接下來幾晚的佩戴中逐步建立。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge, Effort et Repos se construisent au cours de vos prochaines nuits de port." + } } } }, @@ -24875,6 +28307,12 @@ "state": "translated", "value": "一眼掌握能量、消耗、休息、HRV、靜息與即時心率,以及手環電量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge, Effort, Repos, VFC, fréquence cardiaque au repos et en direct, et batterie du bracelet en un coup d'œil." + } } } }, @@ -24909,6 +28347,12 @@ "state": "translated", "value": "能量:你恢復得如何?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge : à quel point êtes-vous récupéré ?" + } } } }, @@ -24943,6 +28387,12 @@ "state": "translated", "value": "充電中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En charge" + } } } }, @@ -24977,6 +28427,12 @@ "state": "translated", "value": "圖表顏色" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleurs des graphiques" + } } } }, @@ -25011,6 +28467,12 @@ "state": "translated", "value": "檢查更新" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher des mises à jour" + } } } }, @@ -25045,6 +28507,12 @@ "state": "translated", "value": "請檢查數值後再試一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifiez les valeurs et réessayez." + } } } }, @@ -25079,6 +28547,12 @@ "state": "translated", "value": "檢查中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification…" + } } } }, @@ -25113,6 +28587,12 @@ "state": "translated", "value": "點擊時會到專案主頁(GitHub)檢查最新版本,不發送任何其他內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifie la page du projet (GitHub) pour la dernière version lorsque vous touchez. Rien d'autre n'est envoyé." + } } } }, @@ -25147,6 +28627,12 @@ "state": "translated", "value": "選擇" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir" + } } } }, @@ -25181,6 +28667,12 @@ "state": "translated", "value": "選擇 .csv…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir .csv…" + } } } }, @@ -25215,6 +28707,12 @@ "state": "translated", "value": "選擇 .gpx / .tcx / .fit…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir .gpx / .tcx / .fit…" + } } } }, @@ -25249,6 +28747,12 @@ "state": "translated", "value": "選擇淺色、深色,或跟隨系統。深色是標誌性的近黑色;淺色在明亮畫布上保持同樣簡潔的外觀。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez Clair, Sombre, ou suivez votre système. Sombre est le noir profond caractéristique ; Clair conserve le même look épuré sur un fond lumineux." + } } } }, @@ -25283,6 +28787,12 @@ "state": "translated", "value": "選擇 Mi Fitness 匯出…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir l'export Mi Fitness…" + } } } }, @@ -25317,6 +28827,12 @@ "state": "translated", "value": "選擇一個備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir une sauvegarde" + } } } }, @@ -25351,6 +28867,12 @@ "state": "translated", "value": "選擇一個存放 NOOP 備份的資料夾(例如 Google Drive 或 iCloud 資料夾)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un dossier pour les sauvegardes NOOP (par exemple un dossier Google Drive ou iCloud)." + } } } }, @@ -25385,6 +28907,12 @@ "state": "translated", "value": "選擇一個穿戴式訊號進行對比" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un signal d'appareil connecté à comparer" + } } } }, @@ -25523,6 +29051,12 @@ "state": "translated", "value": "選擇資料夾" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un dossier" + } } } }, @@ -25557,6 +29091,12 @@ "state": "translated", "value": "選擇 NOOP 的外觀。點擊後整個應用程式會即時更新。你隨時可以在「設定 → 外觀」中更改。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez l'apparence de NOOP. Toute l'application se met à jour au fur et à mesure. Vous pouvez changer cela à tout moment dans Paramètres → Apparence." + } } } }, @@ -25591,6 +29131,12 @@ "state": "translated", "value": "選擇距離、體重、身高、溫度和消耗的顯示方式。你的資料始終以相同方式儲存,這只改變顯示。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez comment les distances, poids, tailles, températures et l'Effort sont affichés. Vos données sont toujours stockées de la même manière. Cela ne change que l'affichage." + } } } }, @@ -25625,6 +29171,12 @@ "state": "translated", "value": "選擇照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir une photo" + } } } }, @@ -25711,6 +29263,12 @@ "state": "translated", "value": "選擇顯示哪些關鍵指標並重新排序" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez les indicateurs clés à afficher et réorganisez-les" + } } } }, @@ -25745,6 +29303,12 @@ "state": "translated", "value": "選擇顯示哪些卡片並重新排序" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez les cartes à afficher et réorganisez-les" + } } } }, @@ -25779,6 +29343,12 @@ "state": "translated", "value": "選擇在你的控制中心顯示哪些圖塊,並用箭頭重新排序。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez les tuiles à afficher dans votre Centre de contrôle et réorganisez-les avec les flèches." + } } } }, @@ -25813,6 +29383,12 @@ "state": "translated", "value": "清理" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nettoyer" + } } } }, @@ -25847,6 +29423,12 @@ "state": "translated", "value": "清理遺留的匯入檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nettoyer les fichiers d'import restants" + } } } }, @@ -25881,6 +29463,12 @@ "state": "translated", "value": "立即清理" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nettoyer maintenant" + } } } }, @@ -25915,6 +29503,12 @@ "state": "translated", "value": "清理中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nettoyage en cours…" + } } } }, @@ -26001,6 +29595,12 @@ "state": "translated", "value": "清除 %@ 的覆寫,使用預設起床時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer la substitution %@, utiliser l'heure de réveil par défaut" + } } } }, @@ -26035,6 +29635,12 @@ "state": "translated", "value": "全部清除" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout effacer" + } } } }, @@ -26121,6 +29727,12 @@ "state": "translated", "value": "關閉 HRV 讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer la mesure de VFC" + } } } }, @@ -26155,6 +29767,12 @@ "state": "translated", "value": "關閉「節律」" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer Rhythm" + } } } }, @@ -26293,6 +29911,12 @@ "state": "translated", "value": "教練指示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Instructions du Coach" + } } } }, @@ -26327,6 +29951,12 @@ "state": "translated", "value": "教練指示編輯器" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur des instructions du Coach" + } } } }, @@ -26569,6 +30199,12 @@ "state": "translated", "value": "教練只與你設定的伺服器 URL 通信,將它指向一個本地模型(Ollama、LM Studio、llama.cpp),即可讓一切都留在你自己的機器上。在你主動請求之前,不會發送任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coach ne parle qu'à l'URL de serveur que vous définissez. Pointez-le vers un modèle local (Ollama, LM Studio, llama.cpp) pour tout garder sur votre propre machine. Rien n'est envoyé tant que vous ne le demandez pas." + } } } }, @@ -26603,6 +30239,12 @@ "state": "translated", "value": "教練使用你自己的 API 金鑰。選擇一個服務供應商,貼上金鑰,再選擇一個模型。你的金鑰安全地儲存在鑰匙圈中,除了你發起的請求外,絕不會離開 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coach utilise votre propre clé API. Choisissez un fournisseur, collez une clé, et choisissez un modèle. Votre clé est stockée en sécurité dans le Trousseau et ne quitte jamais %@ sauf sous la forme de la requête que vous effectuez." + } } } }, @@ -26690,6 +30332,12 @@ "state": "translated", "value": "諧振" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cohérence" + } } } }, @@ -26724,6 +30372,12 @@ "state": "translated", "value": "諧振 5.5" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cohérence 5,5" + } } } }, @@ -26810,6 +30464,12 @@ "state": "translated", "value": "諧振中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cohérent" + } } } }, @@ -26844,6 +30504,12 @@ "state": "translated", "value": "收合" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduire" + } } } }, @@ -26878,6 +30544,12 @@ "state": "translated", "value": "收合教練指示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduire les instructions du Coach" + } } } }, @@ -26912,6 +30584,12 @@ "state": "translated", "value": "已收合" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduit" + } } } }, @@ -26946,6 +30624,12 @@ "state": "translated", "value": "先累積幾天有記錄的介入日再來判讀效果。基線與匯入的指標會維持不變。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez quelques jours d'intervention avant de lire l'effet. La référence et les métriques importées restent en place." + } } } }, @@ -26980,6 +30664,12 @@ "state": "translated", "value": "即將推出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bientôt disponible" + } } } }, @@ -27170,6 +30860,12 @@ "state": "translated", "value": "對比來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comparer les sources" + } } } }, @@ -27204,6 +30900,12 @@ "state": "translated", "value": "與某個訊號對比" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comparer avec un signal" + } } } }, @@ -27290,6 +30992,12 @@ "state": "translated", "value": "依從性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conformité" + } } } }, @@ -27324,6 +31032,12 @@ "state": "translated", "value": "透過「健康」從你的 Apple Watch 即時計算。恢復需要大約一週的夜晚來校準,且每個由手錶推算的評分都會標註其可信度。上方只列出你的手錶實際記錄的指標。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé en direct depuis votre Apple Watch via Santé. La récupération nécessite environ une semaine de nuits pour se calibrer, et chaque score dérivé de la montre est étiqueté avec son niveau de confiance. Seules les métriques réellement enregistrées par votre montre sont listées ci-dessus." + } } } }, @@ -27358,6 +31072,12 @@ "state": "translated", "value": "可信度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confiance" + } } } }, @@ -27398,6 +31118,12 @@ "state": "translated", "value": "可信度:%@(距離 %@)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confiance : %1$@ (distance %2$@)" + } } } }, @@ -27432,6 +31158,12 @@ "state": "translated", "value": "可信度:校準中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confiance : calibrage" + } } } }, @@ -27466,6 +31198,12 @@ "state": "translated", "value": "可信度:估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confiance : estimation" + } } } }, @@ -27500,6 +31238,12 @@ "state": "translated", "value": "可信度:可靠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confiance : fiable" + } } } }, @@ -27586,6 +31330,12 @@ "state": "translated", "value": "連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecter" + } } } }, @@ -27620,6 +31370,12 @@ "state": "translated", "value": "連接 Apple 健康" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecter Apple Santé" + } } } }, @@ -27654,6 +31410,12 @@ "state": "translated", "value": "連接 WHOOP、心率帶或健身器材,即時觀看每一次心跳:心率、變異性和區間即刻呈現。已有其他地方的歷史資料?可從 WHOOP、Apple Health、Oura、Fitbit 或 Garmin 匯入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez un WHOOP, un bracelet cardio ou un appareil de salle de sport et regardez chaque battement en temps réel : fréquence cardiaque, variabilité et zones au fur et à mesure. Vous avez déjà un historique ailleurs ? Importez-le depuis WHOOP, Apple Santé, Oura, Fitbit ou Garmin." + } } } }, @@ -27792,6 +31554,12 @@ "state": "translated", "value": "請先連接手環,然後從即時資料流中標記一次鍛鍊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez d'abord le bracelet, puis marquez un entraînement depuis le flux en direct." + } } } }, @@ -27826,6 +31594,12 @@ "state": "translated", "value": "連接以讀取佩戴狀態" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez-vous pour lire l'état de port" + } } } }, @@ -27860,6 +31634,12 @@ "state": "translated", "value": "連接到此戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter à cette bague" + } } } }, @@ -27894,6 +31674,12 @@ "state": "translated", "value": "請在 NOOP 中連接你的 WHOOP 手環,然後重試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet WHOOP dans NOOP, puis réessayez." + } } } }, @@ -27928,6 +31714,12 @@ "state": "translated", "value": "請先連接你的手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez d'abord votre bracelet." + } } } }, @@ -27962,6 +31754,12 @@ "state": "translated", "value": "請先連接你的手環,讀數需要即時 R-R 資料流。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez d'abord votre bracelet. La mesure nécessite le flux R-R en direct." + } } } }, @@ -28049,6 +31847,12 @@ "state": "translated", "value": "連接你的手環以獲得觸感引導,吸氣時一次脈衝,呼氣時兩次,讓你閉著眼也能跟著呼吸。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet pour un guidage haptique. Vous sentirez une pulsation à l'inspiration, deux à l'expiration, afin de pouvoir respirer les yeux fermés." + } } } }, @@ -28083,6 +31887,12 @@ "state": "translated", "value": "連接你的手環以獲得可感知的提示,掃掠會以吸氣一次震動、呼氣兩次的方式為你把握節奏。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet pour le signal ressenti. Le balayage vous guide avec une vibration à l'inspiration, deux à l'expiration." + } } } }, @@ -28117,6 +31927,12 @@ "state": "translated", "value": "在「即時」畫面連接你的手環以進行測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet dans l'écran En direct pour effectuer une mesure." + } } } }, @@ -28203,6 +32019,12 @@ "state": "translated", "value": "連接你的手環以同步其儲存的歷史資料。在那之前,這裡只會顯示匯入的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet pour synchroniser son historique enregistré. En attendant, seules les données importées s'affichent ici." + } } } }, @@ -28289,6 +32111,12 @@ "state": "translated", "value": "連接你的手環 ,「讓我平靜」是手腕上可感知的節律,因此需要已綁定的連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez votre bracelet. « Calmez-moi » est un rythme ressenti au poignet, il nécessite donc une connexion appairée." + } } } }, @@ -28375,6 +32203,12 @@ "state": "translated", "value": "現已連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté maintenant" + } } } }, @@ -28409,6 +32243,12 @@ "state": "translated", "value": "已連接至 %@。可在「裝置」中配對或切換手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté à %@. Appairez ou changez de bracelet dans Appareils." + } } } }, @@ -28443,6 +32283,12 @@ "state": "translated", "value": "已連接,等待進入串流狀態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté, en attente d'un état de diffusion." + } } } }, @@ -28477,6 +32323,12 @@ "state": "translated", "value": "已連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté." + } } } }, @@ -28511,6 +32363,12 @@ "state": "translated", "value": "已連接。正在完成安全配對交握…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté. Finalisation de la poignée de main d'appairage sécurisée…" + } } } }, @@ -28545,6 +32403,12 @@ "state": "translated", "value": "已連接。歷史同步在 5.0 上仍屬實驗性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté. La synchronisation de l'historique est expérimentale sur 5.0." + } } } }, @@ -28579,6 +32443,12 @@ "state": "translated", "value": "已連接。在啟動時以及你返回 NOOP 時讀取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté. Lecture au lancement et lorsque vous revenez sur NOOP." + } } } }, @@ -28665,6 +32535,12 @@ "state": "translated", "value": "連線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion" + } } } }, @@ -28699,6 +32575,12 @@ "state": "translated", "value": "連線持續時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Durée de connexion" + } } } }, @@ -28785,6 +32667,12 @@ "state": "translated", "value": "繼續" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer" + } } } }, @@ -28819,6 +32707,12 @@ "state": "translated", "value": "繼續準備你的戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuez pour préparer votre bague" + } } } }, @@ -28853,6 +32747,12 @@ "state": "translated", "value": "連續 HRV 擷取" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Capture continue de la VFC" + } } } }, @@ -28887,6 +32787,12 @@ "state": "translated", "value": "貢獻者" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contributeurs" + } } } }, @@ -28973,6 +32879,12 @@ "state": "translated", "value": "控制項已解鎖" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commandes déverrouillées" + } } } }, @@ -29007,6 +32919,12 @@ "state": "translated", "value": "低於你的基線溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus fraîche que votre référence" + } } } }, @@ -29249,6 +33167,12 @@ "state": "translated", "value": "複製環境傾印" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier le rapport d'environnement" + } } } }, @@ -29283,6 +33207,12 @@ "state": "translated", "value": "複製 report.txt" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier report.txt" + } } } }, @@ -29317,6 +33247,12 @@ "state": "translated", "value": "將已去識別化的報告複製到剪貼簿" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier le rapport caviardé dans le presse-papiers" + } } } }, @@ -29351,6 +33287,12 @@ "state": "translated", "value": "修正你的就寢和起床時間。各階段會根據你的資料重新推導;該編輯會在下次手環同步後保留。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Corrigez l'heure de coucher et de réveil. Les phases sont recalculées à partir de vos données ; la modification est conservée jusqu'à la prochaine synchronisation du bracelet." + } } } }, @@ -29385,6 +33327,12 @@ "state": "translated", "value": "無法接管" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de prendre le relais" + } } } }, @@ -29419,6 +33367,12 @@ "state": "translated", "value": "無法更改應用程式圖示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de changer l'icône de l'application" + } } } }, @@ -29453,6 +33407,12 @@ "state": "translated", "value": "無法檢查。請重試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification impossible. Réessayez." + } } } }, @@ -29487,6 +33447,12 @@ "state": "translated", "value": "找不到 NOOP 資料庫。%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Base de données NOOP introuvable. %@" + } } } }, @@ -29521,6 +33487,12 @@ "state": "translated", "value": "無法鎖定今天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de verrouiller aujourd'hui" + } } } }, @@ -29555,6 +33527,12 @@ "state": "translated", "value": "無法打開備份封存檔:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d'ouvrir l'archive de sauvegarde : %@" + } } } }, @@ -29589,6 +33567,12 @@ "state": "translated", "value": "無法打開本機儲存區。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d'ouvrir le stockage local." + } } } }, @@ -29623,6 +33607,12 @@ "state": "translated", "value": "無法清楚判讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture peu claire" + } } } }, @@ -29657,6 +33647,12 @@ "state": "translated", "value": "目前無法讀取本地儲存。請稍後重試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de lire le stockage local pour le moment. Réessayez dans un instant." + } } } }, @@ -29691,6 +33687,12 @@ "state": "translated", "value": "無法移除資料:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de supprimer les données : %@" + } } } }, @@ -29725,6 +33727,12 @@ "state": "translated", "value": "無法還原該備份。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de restaurer cette sauvegarde." + } } } }, @@ -29759,6 +33767,12 @@ "state": "translated", "value": "目前無法安全備份。最近的變更仍在預寫日誌中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées." + } } } }, @@ -29793,6 +33807,12 @@ "state": "translated", "value": "目前無法安全匯出。最近的變更仍在資料庫的預寫日誌中。請先結束任何進行中的同步,然後再試一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées de la base de données. Fermez toute synchronisation en cours, puis réessayez." + } } } }, @@ -29827,6 +33847,12 @@ "state": "translated", "value": "無法開始。需要已連接的手環和靜息心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage impossible. Un bracelet connecté et une fréquence cardiaque au repos sont nécessaires." + } } } }, @@ -29861,6 +33887,12 @@ "state": "translated", "value": "目前無法寫入手環記錄。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d'écrire le journal du bracelet pour le moment." + } } } }, @@ -29895,6 +33927,12 @@ "state": "translated", "value": "每步的計數器計數,除非你的步數偏高,否則保持為 1.0。在 WHOOP 5/MG 上,它們可能非常高(10 倍或更多),因此此值最高可到 30。走一段已知的 1,000 步,用 NOOP 的計數除以真實計數,即可得到你的數值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impulsions du compteur par pas. Laissez à 1,0 sauf si votre nombre de pas est trop élevé. Sur un WHOOP 5/MG, il peut être très élevé (10× ou plus), d'où une plage allant jusqu'à 30. Marchez 1 000 pas connus et divisez le comptage de NOOP par le nombre réel pour obtenir votre valeur." + } } } }, @@ -29981,6 +34019,12 @@ "state": "translated", "value": "杯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tasse" + } } } }, @@ -30015,6 +34059,12 @@ "state": "translated", "value": "目前校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calibration actuelle" + } } } }, @@ -30049,6 +34099,12 @@ "state": "translated", "value": "目前能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge actuelle" + } } } }, @@ -30083,6 +34139,12 @@ "state": "translated", "value": "將目前的生理狀態、手環信任度和工作階段控制集中在一個操作檢視中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Physiologie actuelle, fiabilité du bracelet et commandes de séance dans une seule vue de travail." + } } } }, @@ -30117,6 +34179,12 @@ "state": "translated", "value": "目前:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actuel : %@" + } } } }, @@ -30151,6 +34219,12 @@ "state": "translated", "value": "自訂" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisé" + } } } }, @@ -30185,6 +34259,12 @@ "state": "translated", "value": "自訂 %lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisé %lld ml" + } } } }, @@ -30219,6 +34299,12 @@ "state": "translated", "value": "自訂標記名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom du marqueur personnalisé" + } } } }, @@ -30253,6 +34339,12 @@ "state": "translated", "value": "自訂標記單位" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Unité du marqueur personnalisé" + } } } }, @@ -30339,6 +34431,12 @@ "state": "translated", "value": "自訂大小" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Taille personnalisée" + } } } }, @@ -30373,6 +34471,12 @@ "state": "translated", "value": "自訂你的卡片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisez vos cartes" + } } } }, @@ -30407,6 +34511,12 @@ "state": "translated", "value": "已自訂。你編輯的指示會影響每一則回覆。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisé. Vos instructions modifiées encadrent chaque réponse." + } } } }, @@ -30493,6 +34603,12 @@ "state": "translated", "value": "睡前截止時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Limite avant le coucher" + } } } }, @@ -30527,6 +34643,12 @@ "state": "translated", "value": "週期感知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivi du cycle" + } } } }, @@ -30561,6 +34683,12 @@ "state": "translated", "value": "週期階段:%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Phase du cycle : %@." + } } } }, @@ -30601,6 +34729,12 @@ "state": "translated", "value": "週期階段:%1$@。大約第 %2$lld 至 %3$lld 天。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Phase du cycle : %1$@. Environ du jour %2$lld au %3$lld." + } } } }, @@ -30641,6 +34775,12 @@ "state": "translated", "value": "週期階段:%1$@。大約第 %2$lld 天。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Phase du cycle : %1$@. Environ jour %2$lld." + } } } }, @@ -30675,6 +34815,12 @@ "state": "translated", "value": "日期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DATE" + } } } }, @@ -30715,6 +34861,12 @@ "state": "translated", "value": "第 %1$lld/%2$lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "JOUR %1$lld/%2$lld" + } } } }, @@ -30749,6 +34901,12 @@ "state": "translated", "value": "描述性統計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "STATISTIQUES DESCRIPTIVES" + } } } }, @@ -30783,6 +34941,12 @@ "state": "translated", "value": "診斷工具" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "OUTILS DE DIAGNOSTIC" + } } } }, @@ -30817,6 +34981,12 @@ "state": "translated", "value": "距離" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DIST" + } } } }, @@ -30851,6 +35021,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TERMINÉ" + } } } }, @@ -30885,6 +35061,12 @@ "state": "translated", "value": "時長" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DUR" + } } } }, @@ -30919,6 +35101,12 @@ "state": "translated", "value": "時長 + 效率 + 階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DURÉE + EFFICACITÉ + PHASES" + } } } }, @@ -30953,6 +35141,12 @@ "state": "translated", "value": "每日 0–3 代理指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Proxy quotidien 0–3" + } } } }, @@ -30987,6 +35181,12 @@ "state": "translated", "value": "每日自動備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde automatique quotidienne" + } } } }, @@ -31021,6 +35221,12 @@ "state": "translated", "value": "每日自動匯出手環日誌" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export automatique quotidien du journal du bracelet" + } } } }, @@ -31055,6 +35261,12 @@ "state": "translated", "value": "每日自動匯出時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure d'export automatique quotidien" + } } } }, @@ -31141,6 +35353,12 @@ "state": "translated", "value": "資料與應用程式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données et application" + } } } }, @@ -31227,6 +35445,12 @@ "state": "translated", "value": "資料來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources de données" + } } } }, @@ -31313,6 +35537,12 @@ "state": "translated", "value": "拍攝日期和時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Date et heure de la mesure" + } } } }, @@ -31347,6 +35577,12 @@ "state": "translated", "value": "測量日期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Date de la mesure" + } } } }, @@ -31381,6 +35617,12 @@ "state": "translated", "value": "天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jour" + } } } }, @@ -31415,6 +35657,12 @@ "state": "translated", "value": "第 %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jour %lld" + } } } }, @@ -31455,6 +35703,12 @@ "state": "translated", "value": "第 1 天,強光照射 %1$@–%2$@,約 %3$@ 熄燈。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jour 1 : lumière vive %1$@–%2$@, extinction des lumières vers %3$@." + } } } }, @@ -31542,6 +35796,12 @@ "state": "translated", "value": "日間壓力" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stress du jour" + } } } }, @@ -31629,6 +35889,12 @@ "state": "translated", "value": "日循環背景" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plan du cycle journalier" + } } } }, @@ -31715,6 +35981,12 @@ "state": "translated", "value": "%@ 較高的日子,往往是你心情較好的日子。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jours avec un %@ plus élevé ont tendance à être vos jours de meilleure humeur." + } } } }, @@ -31749,6 +36021,12 @@ "state": "translated", "value": "%@ 較高的日子,往往是你心情較差的日子。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jours avec un %@ plus élevé ont tendance à être vos jours de moins bonne humeur." + } } } }, @@ -31783,6 +36061,12 @@ "state": "translated", "value": "日間睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil diurne" + } } } }, @@ -31817,6 +36101,12 @@ "state": "translated", "value": "12月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déc" + } } } }, @@ -31903,6 +36193,12 @@ "state": "translated", "value": "深睡(SWS)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil profond (SWS)" + } } } }, @@ -31937,6 +36233,12 @@ "state": "translated", "value": "深度時間線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chronologie détaillée" + } } } }, @@ -31971,6 +36273,12 @@ "state": "translated", "value": "深度時間線,你一天中的每一秒,可縮放" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chronologie détaillée, chaque seconde de votre journée, avec zoom" + } } } }, @@ -32005,6 +36313,12 @@ "state": "translated", "value": "深度平靜" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calme profond" + } } } }, @@ -32039,6 +36353,12 @@ "state": "translated", "value": "深層資料(R22)需要 iPhone 或 Android。Mac 無法建立 5/MG 所需的加密綁定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les données approfondies (R22) nécessitent un iPhone ou un Android. Un Mac ne peut pas établir l'appairage chiffré requis par un 5/MG." + } } } }, @@ -32073,6 +36393,12 @@ "state": "translated", "value": "深睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil profond" + } } } }, @@ -32107,6 +36433,12 @@ "state": "translated", "value": "預設" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut" + } } } }, @@ -32141,6 +36473,12 @@ "state": "translated", "value": "刪除" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer" + } } } }, @@ -32175,6 +36513,12 @@ "state": "translated", "value": "刪除 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer %@" + } } } }, @@ -32209,6 +36553,12 @@ "state": "translated", "value": "刪除此裝置的所有資料?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer toutes les données de cet appareil ?" + } } } }, @@ -32243,6 +36593,12 @@ "state": "translated", "value": "刪除資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les données" + } } } }, @@ -32283,6 +36639,12 @@ "state": "translated", "value": "刪除記錄於 %2$@ 的 %1$lld 毫升飲品" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer la boisson de %1$lld millilitres enregistrée à %2$@" + } } } }, @@ -32317,6 +36679,12 @@ "state": "translated", "value": "刪除此自訂問題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cette question personnalisée" + } } } }, @@ -32351,6 +36719,12 @@ "state": "translated", "value": "刪除此裝置的資料…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les données de cet appareil…" + } } } }, @@ -32385,6 +36759,12 @@ "state": "translated", "value": "刪除此讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cette mesure" + } } } }, @@ -32419,6 +36799,12 @@ "state": "translated", "value": "刪除此睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer ce sommeil" + } } } }, @@ -32453,6 +36839,12 @@ "state": "translated", "value": "刪除此睡眠工作階段?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cette séance de sommeil ?" + } } } }, @@ -32539,6 +36931,12 @@ "state": "translated", "value": "已偵測到" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détecté" + } } } }, @@ -32573,6 +36971,12 @@ "state": "translated", "value": "裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appareil" + } } } }, @@ -32607,6 +37011,12 @@ "state": "translated", "value": "%@ 的裝置操作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actions sur l'appareil pour %@" + } } } }, @@ -32641,6 +37051,12 @@ "state": "translated", "value": "裝置指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Métriques de l'appareil" + } } } }, @@ -32675,6 +37091,12 @@ "state": "translated", "value": "裝置名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom de l'appareil" + } } } }, @@ -32709,6 +37131,12 @@ "state": "translated", "value": "裝置、iOS 建置版本、資料保護和側載狀態,用於錯誤報告。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appareil, version d'iOS, statut de Data Protection et de sideload, pour les rapports de bug." + } } } }, @@ -32743,6 +37171,12 @@ "state": "translated", "value": "裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appareils" + } } } }, @@ -32777,6 +37211,12 @@ "state": "translated", "value": "診斷" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diagnostic" + } } } }, @@ -32811,6 +37251,12 @@ "state": "translated", "value": "舒張壓" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diastolique" + } } } }, @@ -32845,6 +37291,12 @@ "state": "translated", "value": "舒張壓(mmHg)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pression artérielle diastolique en mmHg" + } } } }, @@ -32879,6 +37331,12 @@ "state": "translated", "value": "每天不同的起床時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure de réveil différente chaque jour" + } } } }, @@ -32913,6 +37371,12 @@ "state": "translated", "value": "略有不同" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffère légèrement" + } } } }, @@ -32999,6 +37463,12 @@ "state": "translated", "value": "中斷服務供應商連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déconnecter le fournisseur" + } } } }, @@ -33033,6 +37503,12 @@ "state": "translated", "value": "已中斷連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déconnecté" + } } } }, @@ -33067,6 +37543,12 @@ "state": "translated", "value": "忽略" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer" + } } } }, @@ -33101,6 +37583,12 @@ "state": "translated", "value": "忽略(不是鍛鍊)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer (pas un entraînement)" + } } } }, @@ -33135,6 +37623,12 @@ "state": "translated", "value": "忽略此鍛鍊建議" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer cette suggestion d'entraînement" + } } } }, @@ -33169,6 +37663,12 @@ "state": "translated", "value": "忽略並轉到「更新」" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer et classer dans Mises à jour" + } } } }, @@ -33203,6 +37703,12 @@ "state": "translated", "value": "距離" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Distance" + } } } }, @@ -33237,6 +37743,12 @@ "state": "translated", "value": "不再詢問" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne plus demander" + } } } }, @@ -33271,6 +37783,12 @@ "state": "translated", "value": "手環不在手腕上時不震動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne pas vibrer quand le bracelet n'est pas au poignet." + } } } }, @@ -33305,6 +37823,12 @@ "state": "translated", "value": "沒看到嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous ne le voyez pas ?" + } } } }, @@ -33391,6 +37915,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminé" + } } } }, @@ -33425,6 +37955,12 @@ "state": "translated", "value": "已完成儀表板自訂" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisation du tableau de bord terminée" + } } } }, @@ -33459,6 +37995,12 @@ "state": "translated", "value": "已完成關鍵指標的編輯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modification des indicateurs clés terminée" + } } } }, @@ -33493,6 +38035,12 @@ "state": "translated", "value": "完成。儲存這次讀數,即可將它保留在你的趨勢中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminé. Enregistrez cette mesure pour la conserver dans vos tendances." + } } } }, @@ -33527,6 +38075,12 @@ "state": "translated", "value": "劑量反應" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dose-réponse" + } } } }, @@ -33567,6 +38121,12 @@ "state": "translated", "value": "劑量反應曲線。每多 %1$@,大約對應 %3$@ 上 %2$@ 的變化,屬於典型模式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Courbe dose-réponse. Chaque %@ supplémentaire correspond à environ %@ sur %@, schémas typiques." + } } } }, @@ -33607,6 +38167,12 @@ "state": "translated", "value": "劑量反應曲線。每多 %1$@,大約對應 %3$@ 上 %2$@ 的變化,來自你自己的數據。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Courbe dose-réponse. Chaque %@ supplémentaire correspond à environ %@ sur %@, vos propres données." + } } } }, @@ -33641,6 +38207,12 @@ "state": "translated", "value": "雙倍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Double" + } } } }, @@ -33675,6 +38247,12 @@ "state": "translated", "value": "雙擊以收合" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Double-tapez pour réduire" + } } } }, @@ -33709,6 +38287,12 @@ "state": "translated", "value": "雙擊以展開" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Double-tapez pour développer" + } } } }, @@ -33743,6 +38327,12 @@ "state": "translated", "value": "輕點兩下" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Double-tap" + } } } }, @@ -33777,6 +38367,12 @@ "state": "translated", "value": "輕點兩下手環,即可在 %@ 上觸發一個動作。(手環只提供單一的輕點兩下手勢。)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Double-tapez sur le bracelet pour déclencher une action sur %@. (Le bracelet ne propose qu'un seul geste de double-tap.)" + } } } }, @@ -33811,6 +38407,12 @@ "state": "translated", "value": "下載" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Télécharger" + } } } }, @@ -33845,6 +38447,12 @@ "state": "translated", "value": "拖曳平移 · 輕點兩下重置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Glissez pour déplacer · double-tap pour réinitialiser" + } } } }, @@ -33879,6 +38487,12 @@ "state": "translated", "value": "拖動以重新排序。開啟或關閉某張卡片。尚無數值的卡片會顯示一個短橫。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Glissez pour réorganiser. Activez ou désactivez une carte. Les cartes sans valeur affichent un tiret." + } } } }, @@ -33913,6 +38527,12 @@ "state": "translated", "value": "驅動你的體能年齡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détermine votre âge de forme physique" + } } } }, @@ -33947,6 +38567,12 @@ "state": "translated", "value": "將過去 24 小時已解碼的逐樣本感測器資料流(心率、R-R、運動、步數、SpO₂、皮膚溫度、呼吸、事件)匯出到單個 CSV,全部在 %@ 上,不上傳任何內容。分享它有助於原型開發和測試睡眠、活動與力量演算法。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporte les 24 dernières heures des flux de capteurs décodés échantillon par échantillon (fréquence cardiaque, R-R, mouvement, pas, SpO₂, température cutanée, respiration, événements) dans un seul fichier CSV. Tout sur %@, rien n'est envoyé. Partagez-le pour aider à prototyper et tester les algorithmes de sommeil, d'activité et de force." + } } } }, @@ -33981,6 +38607,12 @@ "state": "translated", "value": "複製為手動…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dupliquer en manuel…" + } } } }, @@ -34015,6 +38647,12 @@ "state": "translated", "value": "時長" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Durée" + } } } }, @@ -34049,6 +38687,12 @@ "state": "translated", "value": "時長(分鐘)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Durée en minutes" + } } } }, @@ -34083,6 +38727,12 @@ "state": "translated", "value": "在安靜、靜止、休息的時段,NOOP 會查看你心跳之間的時間(R-R 間期)並繪製其龐加萊散點圖。它會根據點雲計算出短軸和長軸(SD1、SD2)以及幾個簡單的規律性數字。運動和有雜訊的時段會被跳過,不予顯示。這些是透明的、已發表的描述性統計,是對你時間節律的一幅圖景,絕非臨床測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pendant les périodes calmes, immobiles et au repos, NOOP examine le rythme entre vos battements cardiaques (intervalles R-R) et trace leur nuage de Poincaré. À partir du nuage, il calcule ses axes court et long (SD1, SD2) et quelques chiffres de régularité simples. Les périodes avec mouvement ou bruit sont ignorées, non affichées. Ce sont des statistiques descriptives transparentes et publiées : une image de votre rythme, jamais une mesure clinique." + } } } }, @@ -34117,6 +38767,12 @@ "state": "translated", "value": "更早" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PLUS TÔT" + } } } }, @@ -34151,6 +38807,12 @@ "state": "translated", "value": "早期訊號" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SIGNAL PRÉCOCE" + } } } }, @@ -34185,6 +38847,12 @@ "state": "translated", "value": "消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EFFORT" + } } } }, @@ -34219,6 +38887,12 @@ "state": "translated", "value": "消耗建立中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EFFORT EN CONSTRUCTION" + } } } }, @@ -34253,6 +38927,12 @@ "state": "translated", "value": "估計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EST." + } } } }, @@ -34339,6 +39019,12 @@ "state": "translated", "value": "匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EXPORTER" + } } } }, @@ -34373,6 +39059,12 @@ "state": "translated", "value": "額外 / 已跳過" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EXTRA / IGNORÉ" + } } } }, @@ -34407,6 +39099,12 @@ "state": "translated", "value": "每個點將一個心跳間期與下一個配對。沿對角線的緊密線條意味著心跳平穩;更圓、更分散的點雲意味著時間節律的變化更大。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque point associe un intervalle de battement cardiaque au suivant. Une ligne serrée le long de la diagonale indique un battement régulier ; un nuage plus rond et plus étalé indique une variation plus importante du rythme." + } } } }, @@ -34447,6 +39145,12 @@ "state": "translated", "value": "每條線在 %1$@ 內以最小–最大值正規化 · %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque ligne normalisée min–max dans %1$@ · %2$@" + } } } }, @@ -34481,6 +39185,12 @@ "state": "translated", "value": "每項指標、你的 Apple Watch 在其中所處的位置,以及原因。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque métrique, où se situe votre Apple Watch, et pourquoi." + } } } }, @@ -34515,6 +39225,12 @@ "state": "translated", "value": "每個評分都遵循一種已發表的方法,在你的裝置上計算。我們會說明方法的類別,方便你去查閱,我們絕不聲稱能精確重現另一家公司的數值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque score suit une méthode publiée, calculée sur votre appareil. Nous indiquons la famille de méthode pour que vous puissiez vous documenter, et nous ne prétendons jamais reproduire exactement le chiffre d'une autre entreprise." + } } } }, @@ -34549,6 +39265,12 @@ "state": "translated", "value": "在你佩戴手環時,每項測試都會為應用程式的某個部分記錄額外細節,然後將其打包成一份錯誤報告。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque test consigne des détails supplémentaires pour une partie de l'application pendant que vous portez le bracelet, puis les regroupe pour un rapport de bug." + } } } }, @@ -34583,6 +39305,12 @@ "state": "translated", "value": "早期警訊:放輕鬆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alerte précoce : allez-y doucement" + } } } }, @@ -34617,6 +39345,12 @@ "state": "translated", "value": "編輯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier" + } } } }, @@ -34651,6 +39385,12 @@ "state": "translated", "value": "編輯關鍵指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les indicateurs clés" + } } } }, @@ -34685,6 +39425,12 @@ "state": "translated", "value": "編輯鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier l'entraînement" + } } } }, @@ -34719,6 +39465,12 @@ "state": "translated", "value": "編輯教練指示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les instructions du Coach" + } } } }, @@ -34753,6 +39505,12 @@ "state": "translated", "value": "編輯詳情…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les détails…" + } } } }, @@ -34787,6 +39545,12 @@ "state": "translated", "value": "編輯飲品" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier la boisson" + } } } }, @@ -34821,6 +39585,12 @@ "state": "translated", "value": "編輯教練的思考與說話方式。從你的下一則訊息開始生效。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifiez la façon dont le Coach raisonne et s'exprime. Prend effet à votre prochain message." + } } } }, @@ -34855,6 +39625,12 @@ "state": "translated", "value": "編輯小睡時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les horaires de sieste" + } } } }, @@ -34889,6 +39665,12 @@ "state": "translated", "value": "編輯小睡時間(已編輯)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les horaires de sieste (modifié)" + } } } }, @@ -34923,6 +39705,12 @@ "state": "translated", "value": "編輯睡眠時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les horaires de sommeil" + } } } }, @@ -34957,6 +39745,12 @@ "state": "translated", "value": "編輯睡眠時間(已編輯)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les horaires de sommeil (modifié)" + } } } }, @@ -34991,6 +39785,12 @@ "state": "translated", "value": "編輯今天的心情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier l'humeur d'aujourd'hui" + } } } }, @@ -35025,6 +39825,12 @@ "state": "translated", "value": "編輯…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier…" + } } } }, @@ -35215,6 +40021,12 @@ "state": "translated", "value": "消耗(0–100)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effort (0–100)" + } } } }, @@ -35249,6 +40061,12 @@ "state": "translated", "value": "消耗量表" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échelle d'effort" + } } } }, @@ -35283,6 +40101,12 @@ "state": "translated", "value": "本 %@ 的消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effort sur ce %@" + } } } }, @@ -35317,6 +40141,12 @@ "state": "translated", "value": "消耗:你的心臟有多努力?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effort : à quel point votre cœur a-t-il travaillé ?" + } } } }, @@ -35351,6 +40181,12 @@ "state": "translated", "value": "符合即時生理數據條件" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éligible à la physiologie en direct" + } } } }, @@ -35490,6 +40326,12 @@ "state": "translated", "value": "啟用 Apple 健康" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer Apple Santé" + } } } }, @@ -35524,6 +40366,12 @@ "state": "translated", "value": "啟用久坐提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer le rappel d'inactivité" + } } } }, @@ -35610,6 +40458,12 @@ "state": "translated", "value": "已加密" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chiffré" + } } } }, @@ -35644,6 +40498,12 @@ "state": "translated", "value": "加密資料流:可使用深層控制與歷史同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Flux chiffré : commandes avancées et synchronisation de l'historique disponibles." + } } } }, @@ -35678,6 +40538,12 @@ "state": "translated", "value": "結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer" + } } } }, @@ -35712,6 +40578,12 @@ "state": "translated", "value": "結束實驗計劃。日誌和指標歷史保持不變。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer le plan d'expérience. Le journal et l'historique des métriques restent intacts." + } } } }, @@ -35746,6 +40618,12 @@ "state": "translated", "value": "結束鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer l'entraînement" + } } } }, @@ -35780,6 +40658,12 @@ "state": "translated", "value": "喜歡 NOOP 嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous appréciez NOOP ?" + } } } }, @@ -35814,6 +40698,12 @@ "state": "translated", "value": "進入 NOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entrer dans NOOP" + } } } }, @@ -35900,6 +40790,12 @@ "state": "translated", "value": "輸入一項運動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisissez un sport." + } } } }, @@ -35934,6 +40830,12 @@ "state": "translated", "value": "一起錄入;儲存為兩個標記,以便各自與你的訊號清晰對齊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisis ensemble ; enregistrés comme deux marqueurs afin que chacun s'aligne précisément avec vos signaux." + } } } }, @@ -35968,6 +40870,12 @@ "state": "translated", "value": "等長呼吸 · 約 5.5 次/分鐘諧振" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respiration égale · cohérence ~5,5 resp/min" + } } } }, @@ -36054,6 +40962,12 @@ "state": "translated", "value": "估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Est." + } } } }, @@ -36088,6 +41002,12 @@ "state": "translated", "value": "估計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation" + } } } }, @@ -36122,6 +41042,12 @@ "state": "translated", "value": "估算,部分資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation (données partielles)" + } } } }, @@ -36156,6 +41082,12 @@ "state": "translated", "value": "根據 PPG 推導的 R-R 得出的估算,放鬆指導,而非臨床讀數。你的節奏會漂移,因此我們會為它標註日期,你可以隨時重新測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation à partir des R-R dérivés du PPG : guide de relaxation, pas une mesure clinique. Votre rythme dérive, nous la datons donc et vous pouvez remesurer à tout moment." + } } } }, @@ -36190,6 +41122,12 @@ "state": "translated", "value": "根據今天的消耗、你通常的睡眠以及你 %lld 晚的恢復基線得出的估算,而非測量值。你真正的能量是在你醒來後根據明天的 HRV 評分得出的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation à partir de l'effort d'aujourd'hui, de votre sommeil habituel et de votre référence de récupération sur %lld nuits, pas une mesure. Votre véritable Charge est calculée à partir de la VFC de demain à votre réveil." + } } } }, @@ -36277,6 +41215,12 @@ "state": "translated", "value": "僅為估計,節律呼吸時較高的 RMSSD 通常意味著你副交感神經的「休息」分支正在發揮作用。這不是臨床讀數;一次工作階段內的趨勢比任何單一數值都更重要。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation seulement : un RMSSD plus élevé pendant le rythme guidé signifie généralement que votre branche parasympathique de « repos » s'active. Ce n'est pas une mesure clinique ; les tendances sur une séance comptent plus qu'un chiffre isolé." + } } } }, @@ -36311,6 +41255,12 @@ "state": "translated", "value": "估計生理時鐘低谷約在 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Creux estimé de l'horloge biologique vers %@" + } } } }, @@ -36345,6 +41295,12 @@ "state": "translated", "value": "根據 R-R 間期估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimé à partir de l'intervalle R-R" + } } } }, @@ -36379,6 +41335,12 @@ "state": "translated", "value": "根據你的 WHOOP 的體動估算,並以你的手機校準。並非實測的步數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimé à partir du mouvement de votre WHOOP, calibré sur votre téléphone. Ce n'est pas un nombre de pas mesuré." + } } } }, @@ -36465,6 +41427,12 @@ "state": "translated", "value": "估計大部分已代謝。你記錄的物質可能都已不再起作用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimé en grande partie éliminé. Rien de ce qui est enregistré n'est probablement encore actif." + } } } }, @@ -36499,6 +41467,12 @@ "state": "translated", "value": "估計剩餘續航" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autonomie restante estimée" + } } } }, @@ -36533,6 +41507,12 @@ "state": "translated", "value": "估算值 vs 你的手機" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimé par rapport à votre téléphone" + } } } }, @@ -36567,6 +41547,12 @@ "state": "translated", "value": "傍晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Soir" + } } } }, @@ -36601,6 +41587,12 @@ "state": "translated", "value": "傍晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Soir" + } } } }, @@ -36635,6 +41627,12 @@ "state": "translated", "value": "傍晚預報" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prévision du soir" + } } } }, @@ -36669,6 +41667,12 @@ "state": "translated", "value": "事件" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Événement" + } } } }, @@ -36755,6 +41759,12 @@ "state": "translated", "value": "每天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque jour" + } } } }, @@ -36766,6 +41776,12 @@ "state": "translated", "value": "Jede Metrik ist eine nicht validierte Näherung. Nutze NOOP nicht, um zu diagnostizieren, zu behandeln oder Gesundheitsentscheidungen zu treffen. Ziehe immer qualifiziertes Fachpersonal hinzu." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque métrique est une approximation non validée. N'utilisez pas NOOP pour diagnostiquer, traiter ou prendre une décision de santé. Consultez toujours un professionnel qualifié." + } } } }, @@ -36800,6 +41816,12 @@ "state": "translated", "value": "每個版本,皆有鏡像。當 GitHub 出現故障時的備選方案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque version, mise en miroir. Une solution de repli si GitHub venait à être indisponible." + } } } }, @@ -36834,6 +41856,12 @@ "state": "translated", "value": "每個評分都帶有一個小小的誠實標籤。「校準中」表示 NOOP 仍在學習你的基線,或資料還不夠。「建立中」表示已有足夠內容可顯示,但還很單薄。「扎實」表示所有輸入都已具備。當 NOOP 無法誠實地計算出一個評分時,它會什麼都不顯示,而不是給出一個虛假的數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque score porte une petite étiquette de transparence. « Calibration » signifie que NOOP apprend encore votre référence ou n'a pas encore assez de données. « En construction » signifie qu'il y en a assez pour afficher quelque chose, mais c'est mince. « Solide » signifie que toutes les données sont présentes. Quand NOOP ne peut pas calculer un score honnêtement, il n'affiche rien plutôt qu'un chiffre inventé." + } } } }, @@ -36868,6 +41896,12 @@ "state": "translated", "value": "你一天中的每一秒,可縮放。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque seconde de votre journée, avec zoom." + } } } }, @@ -37006,6 +42040,12 @@ "state": "translated", "value": "每個人都有一個呼吸節奏,通常在每分鐘 4.5 到 7 次之間,在這個節奏下,心臟的節律隨每次呼吸擺動得最大。我們會帶你嘗試幾個候選節奏,測量你的 HRV 如何回應,並鎖定最能與你產生共振的那個。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chacun a un rythme respiratoire (généralement entre 4,5 et 7 respirations par minute) où le rythme cardiaque oscille le plus à chaque respiration. Nous vous guidons à travers quelques rythmes candidats, mesurons la réponse de votre VFC, et fixons celui qui résonne le mieux pour vous." + } } } }, @@ -37040,6 +42080,12 @@ "state": "translated", "value": "其餘的一切,一觸即達" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout le reste, à portée d'un tap" + } } } }, @@ -37074,6 +42120,12 @@ "state": "translated", "value": "這裡的一切都是你自己記錄天數中的模式:帶有效應量和信賴度的關聯性,絕非因果或診斷。人群模式會標示為「典型」,一旦你累積足夠的資料,就永遠以你自己的數據為準。這些是近似值,不是 WHOOP 的評分;也不是醫療器材。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ici est un schéma issu de vos propres jours enregistrés : une association avec une taille d'effet et un niveau de confiance, jamais une cause ou un diagnostic. Les schémas de population sont indiqués comme « typiques » et sont toujours remplacés par vos propres données dès que vous en avez suffisamment. Des approximations, pas les scores de WHOOP ; ce n'est pas un dispositif médical." + } } } }, @@ -37108,6 +42160,12 @@ "state": "translated", "value": "一切都保存在 %@ 上。沒有帳號、沒有同步、沒有雲端。你的對話只屬於你自己。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout réside sur %@. Aucun compte, aucune synchronisation, aucun cloud. Votre conversation n'appartient qu'à vous." + } } } }, @@ -37142,6 +42200,12 @@ "state": "translated", "value": "一切都留在 %@ 上。把你的歷史匯入一次,從此便歸你所有。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout reste sur %@. Importez votre historique une seule fois, il est ensuite à vous." + } } } }, @@ -37229,6 +42293,12 @@ "state": "translated", "value": "一切都留在你的裝置上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout reste sur votre appareil" + } } } }, @@ -37263,6 +42333,12 @@ "state": "translated", "value": "確切型號未知。顯示每款 WHOOP 都能做到的功能。* 為裝置端估算 · 手環不輸出 SpO₂ %(如需請匯入 WHOOP CSV)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modèle exact inconnu. Montre ce que chaque WHOOP peut faire. * estimation sur l'appareil · pas de % SpO₂ depuis le bracelet (importez un CSV WHOOP pour cela)." + } } } }, @@ -37297,6 +42373,12 @@ "state": "translated", "value": "展開以查看完整解讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Développer pour la lecture complète" + } } } }, @@ -37331,6 +42413,12 @@ "state": "translated", "value": "已展開" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Développé" + } } } }, @@ -37365,6 +42453,12 @@ "state": "translated", "value": "實驗行為" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comportement de l'expérience" + } } } }, @@ -37399,6 +42493,12 @@ "state": "translated", "value": "實驗結果指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Métrique de résultat de l'expérience" + } } } }, @@ -37433,6 +42533,12 @@ "state": "translated", "value": "實驗進度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Progression de l'expérience" + } } } }, @@ -37467,6 +42573,12 @@ "state": "translated", "value": "實驗窗口長度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Durée de la fenêtre de l'expérience" + } } } }, @@ -37501,6 +42613,12 @@ "state": "translated", "value": "實驗性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental" + } } } }, @@ -37535,6 +42653,12 @@ "state": "translated", "value": "實驗性睡眠分期(V2)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détection des phases de sommeil expérimentale (V2)" + } } } }, @@ -37569,6 +42693,12 @@ "state": "translated", "value": "實驗性健康視覺化,並非診斷、並非心電圖,也不是醫療裝置。它無法檢測任何心臟疾病。逐拍變化有許多普通、良性的原因。如果你感覺不適或感到擔憂,請聯繫合格的專業人士;在緊急情況下,請聯繫當地急救服務。一切都在你的裝置上計算。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Visualisation expérimentale du bien-être : ce n'est pas un diagnostic, pas un ECG, et pas un dispositif médical. Elle ne peut détecter aucune affection cardiaque. La variation battement à battement a de nombreuses causes courantes et bénignes. Si vous ne vous sentez pas bien ou êtes inquiet, contactez un professionnel qualifié ; en cas d'urgence, votre service d'urgence local. Tout est calculé sur votre appareil." + } } } }, @@ -37603,6 +42733,12 @@ "state": "translated", "value": "實驗性 · 睡眠分期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental · Phases de sommeil" + } } } }, @@ -37689,6 +42825,12 @@ "state": "translated", "value": "實驗性功能,並非醫療器材" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental, et ce n'est pas un dispositif médical" + } } } }, @@ -37723,6 +42865,12 @@ "state": "translated", "value": "實驗性、盡力而為的支援。我們仍在測試這些,因此它們可能無法在每台裝置上連接。它們絕不會編造資料,並且會誠實地告訴你何時無法進行即時連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prise en charge expérimentale, au mieux des capacités. Nous testons encore ces appareils, ils pourraient donc ne pas se connecter sur tous les modèles. Ils n'inventent jamais de données et vous diront honnêtement quand le direct n'est pas possible." + } } } }, @@ -37734,6 +42882,12 @@ "state": "translated", "value": "Experimentell: auf eigenes Risiko" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental : à vos risques et périls" + } } } }, @@ -37768,6 +42922,12 @@ "state": "translated", "value": "實驗性功能:如果你的手環需要配對,我們會誠實告訴你,而不是顯示假的讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental : si votre bracelet nécessite un appairage, nous vous le dirons honnêtement plutôt que d'afficher une mesure fictive." + } } } }, @@ -37802,6 +42962,12 @@ "state": "translated", "value": "實驗性功能:在手環有提供時顯示即時心率。有些手環需要我們目前還做不到的配對,NOOP 會誠實說明,絕不顯示編造的數字。不含睡眠、恢復、皮膚溫度、SpO₂ 或步數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental : fréquence cardiaque en direct là où le bracelet l'expose. Certains bracelets nécessitent un appairage que nous ne pouvons pas encore effectuer. NOOP le dira honnêtement et n'affichera jamais de chiffre inventé. Pas de sommeil, récupération, température cutanée, SpO₂ ni pas." + } } } }, @@ -37836,6 +43002,12 @@ "state": "translated", "value": "實驗性功能:僅盡力而為。如果即時模式無法運作,你可以從 Zepp 匯出並匯入該檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérimental : ceci est au mieux des capacités. Si le direct ne fonctionne pas, vous pouvez exporter depuis Zepp et importer le fichier." + } } } }, @@ -37870,6 +43042,12 @@ "state": "translated", "value": "由以下因素解釋" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expliqué par" + } } } }, @@ -37956,6 +43134,12 @@ "state": "translated", "value": "匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter" + } } } }, @@ -37990,6 +43174,12 @@ "state": "translated", "value": "匯出 CSV…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter en CSV…" + } } } }, @@ -38024,6 +43214,12 @@ "state": "translated", "value": "匯出 NOOP 備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter la sauvegarde NOOP" + } } } }, @@ -38058,6 +43254,12 @@ "state": "translated", "value": "將 NOOP 資料匯出為 CSV" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter les données NOOP en CSV" + } } } }, @@ -38092,6 +43294,12 @@ "state": "translated", "value": "匯出 PDF" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter en PDF" + } } } }, @@ -38126,6 +43334,12 @@ "state": "translated", "value": "匯出失敗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de l'export" + } } } }, @@ -38160,6 +43374,12 @@ "state": "translated", "value": "匯出失敗:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de l'export : %@" + } } } }, @@ -38194,6 +43414,12 @@ "state": "translated", "value": "匯出到捷徑(Apple 健康)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter pour Raccourcis (Apple Santé)" + } } } }, @@ -38280,6 +43506,12 @@ "state": "translated", "value": "匯出問題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Problème d'export" + } } } }, @@ -38314,6 +43546,12 @@ "state": "translated", "value": "匯出原始資料 + 日誌" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter les données brutes + le journal" + } } } }, @@ -38348,6 +43586,12 @@ "state": "translated", "value": "匯出原始感測器資料(CSV)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter les données brutes du capteur (CSV)" + } } } }, @@ -38382,6 +43626,12 @@ "state": "translated", "value": "匯出趨勢報告" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exporter le rapport de tendances" + } } } }, @@ -38416,6 +43666,12 @@ "state": "translated", "value": "匯出中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export en cours…" + } } } }, @@ -38502,6 +43758,12 @@ "state": "translated", "value": "完全綁定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "APPAIRAGE COMPLET" + } } } }, @@ -38536,6 +43798,12 @@ "state": "translated", "value": "韌體 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FW %@" + } } } }, @@ -38570,6 +43838,12 @@ "state": "translated", "value": "一般" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Correct" + } } } }, @@ -38604,6 +43878,12 @@ "state": "translated", "value": "備選方案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Solution de repli" + } } } }, @@ -38638,6 +43918,12 @@ "state": "translated", "value": "脂肪" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Graisses" + } } } }, @@ -38672,6 +43958,12 @@ "state": "translated", "value": "燃脂" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Combustion des graisses" + } } } }, @@ -38706,6 +43998,12 @@ "state": "translated", "value": "功能" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fonctionnalités" + } } } }, @@ -38740,6 +44038,12 @@ "state": "translated", "value": "2月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fév" + } } } }, @@ -38878,6 +44182,12 @@ "state": "translated", "value": "檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichier" + } } } }, @@ -38965,6 +44275,12 @@ "state": "translated", "value": "找到你的共振節奏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouvez votre rythme de résonance" + } } } }, @@ -38999,6 +44315,12 @@ "state": "translated", "value": "尋找你的手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouvez votre bracelet" + } } } }, @@ -39033,6 +44355,12 @@ "state": "translated", "value": "微調提醒送達手腕的時機。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglez précisément quand les alertes atteignent votre poignet." + } } } }, @@ -39067,6 +44395,12 @@ "state": "translated", "value": "完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer" + } } } }, @@ -39101,6 +44435,12 @@ "state": "translated", "value": "正在完成配對交握。手環配對完成後,即可使用「立即同步」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Finalisation de la poignée de main d'appairage. La synchronisation immédiate devient disponible une fois le bracelet appairé." + } } } }, @@ -39343,6 +44683,12 @@ "state": "translated", "value": "在手環離開手腕的那一刻觸發。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se déclenche dès que le bracelet quitte votre poignet." + } } } }, @@ -39377,6 +44723,12 @@ "state": "translated", "value": "韌體版本 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version du firmware %@" + } } } }, @@ -39469,6 +44821,12 @@ "state": "translated", "value": "體能年齡 %1$lld,%2$@。點一下查看趨勢。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Âge de forme physique %1$lld, %2$@. Tapez pour voir la tendance." + } } } }, @@ -39510,6 +44868,12 @@ "state": "translated", "value": "體能年齡 %1$lld 歲,比你的年齡%4$@ %2$lld 歲%3$@。點擊查看趨勢。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Âge de forme physique %1$lld, %2$lld an%3$@ %4$@ que votre âge. Tapez pour voir la tendance." + } } } }, @@ -39544,6 +44908,12 @@ "state": "translated", "value": "擬合自" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajusté à partir de" + } } } }, @@ -39578,6 +44948,12 @@ "state": "translated", "value": "在設定中修正" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Corriger dans les Paramètres" + } } } }, @@ -39612,6 +44988,12 @@ "state": "translated", "value": "旗標已接受,但啟用序列不會啟動一個獨立的即時流,深度記錄會作為常規歷史同步的一部分到達(#494)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indicateurs acceptés, mais la séquence d'activation ne démarre pas de flux en direct séparé. Les enregistrements détaillés arrivent dans le cadre de la synchronisation normale de l'historique (#494)." + } } } }, @@ -39646,6 +45028,12 @@ "state": "translated", "value": "濾泡期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Folliculaire" + } } } }, @@ -39680,6 +45068,12 @@ "state": "translated", "value": "對於不發送步數的 WHOOP 4.0:NOOP 會根據運動來估算步數,並以你的手機進行校準。點擊查看它有多接近,並進行調整。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour un WHOOP 4.0, qui n'envoie aucun nombre de pas : NOOP estime les pas à partir du mouvement, calibré sur votre téléphone. Tapez pour voir sa précision et l'ajuster." + } } } }, @@ -39714,6 +45108,12 @@ "state": "translated", "value": "請先強制退出官方 WHOOP 應用程式,否則配對可能失敗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Forcez d'abord la fermeture de l'application officielle WHOOP, sinon l'appairage pourrait échouer." + } } } }, @@ -39800,6 +45200,12 @@ "state": "translated", "value": "幀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trame" + } } } }, @@ -39834,6 +45240,12 @@ "state": "translated", "value": "幀摘要" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résumé de la trame" + } } } }, @@ -39868,6 +45280,12 @@ "state": "translated", "value": "週五" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ven" + } } } }, @@ -39902,6 +45320,12 @@ "state": "translated", "value": "週五" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vendredi" + } } } }, @@ -39988,6 +45412,12 @@ "state": "translated", "value": "來自手環心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis la FC du bracelet" + } } } }, @@ -40022,6 +45452,12 @@ "state": "translated", "value": "來自手錶睡眠期間的手腕溫度感測器,適用於 Series 8 及之後的型號。較舊的型號沒有這個感測器,所以會顯示「無法取得」而非零。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis le capteur de température au poignet de la montre pendant le sommeil, sur Series 8 et ultérieures. Les modèles plus anciens n'ont pas le capteur, il affiche donc « non disponible » plutôt que zéro." + } } } }, @@ -40056,6 +45492,12 @@ "state": "translated", "value": "來自你的夜間感測器" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis votre capteur nocturne" + } } } }, @@ -40090,6 +45532,12 @@ "state": "translated", "value": "來自你的夜間溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis votre température nocturne" + } } } }, @@ -40124,6 +45572,12 @@ "state": "translated", "value": "手環完整資料流已啟用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le flux complet du bracelet est actif." + } } } }, @@ -40158,6 +45612,12 @@ "state": "translated", "value": "完整掃掠 · 約 13 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Balayage complet · ~13 min" + } } } }, @@ -40192,6 +45652,12 @@ "state": "translated", "value": "完全結束並重新開啟 NOOP 以載入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermez complètement puis rouvrez NOOP pour le charger." + } } } }, @@ -40226,6 +45692,12 @@ "state": "translated", "value": "功能性肌力" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Force fonctionnelle" + } } } }, @@ -40260,6 +45732,12 @@ "state": "translated", "value": "在 %@ 上融合。沒有任何內容離開它,無帳戶,無雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fusionné sur %@. Rien n'en sort : aucun compte, aucun cloud." + } } } }, @@ -40294,6 +45772,12 @@ "state": "translated", "value": "Garmin 手錶" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Montre Garmin" + } } } }, @@ -40328,6 +45812,12 @@ "state": "translated", "value": "由 NOOP 於 %@ 生成 · 全部在裝置本地,無帳戶,無雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Généré par NOOP sur %@ · tout sur l'appareil, aucun compte, aucun cloud." + } } } }, @@ -40362,6 +45852,12 @@ "state": "translated", "value": "開始使用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commencer" + } } } }, @@ -40396,6 +45892,12 @@ "state": "translated", "value": "手環電量偏低(15%)時收到通知,讓你在今晚之前充電;充電完成時也會通知你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recevez une notification quand la batterie du bracelet est faible (15 %) afin de la recharger avant ce soir, et quand elle a fini de charger." + } } } }, @@ -40482,6 +45984,12 @@ "state": "translated", "value": "先把它準備好,然後掃描。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparez-le, puis scannez." + } } } }, @@ -40569,6 +46077,12 @@ "state": "translated", "value": "準備好你的戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparez votre bague" + } } } }, @@ -40603,6 +46117,12 @@ "state": "translated", "value": "正在準備你的裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation de vos appareils" + } } } }, @@ -40637,6 +46157,12 @@ "state": "translated", "value": "GitHub,程式碼、發布、issue 和 wiki。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GitHub : code, versions, tickets et le wiki." + } } } }, @@ -40677,6 +46203,12 @@ "state": "translated", "value": "為 %1$@ %2$@ 取一個你能認出的名字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Donnez à %1$@ %2$@ un nom que vous reconnaîtrez." + } } } }, @@ -40711,6 +46243,12 @@ "state": "translated", "value": "如果電量偏低,先充個幾分鐘的電。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Laissez-le charger quelques minutes si la batterie est faible." + } } } }, @@ -40745,6 +46283,12 @@ "state": "translated", "value": "轉到 Apple 健康權限步驟" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Accède à l'étape d'autorisation Apple Santé" + } } } }, @@ -40779,6 +46323,12 @@ "state": "translated", "value": "正在入睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Endormissement" + } } } }, @@ -40813,6 +46363,12 @@ "state": "translated", "value": "良好" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bon" + } } } }, @@ -40847,6 +46403,12 @@ "state": "translated", "value": "午安" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bon après-midi" + } } } }, @@ -40881,6 +46443,12 @@ "state": "translated", "value": "晚安" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bonsoir" + } } } }, @@ -40915,6 +46483,12 @@ "state": "translated", "value": "早安" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bonjour" + } } } }, @@ -40949,6 +46523,12 @@ "state": "translated", "value": "早安。你的智慧鬧鐘剛剛叫醒了你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bonjour. Votre alarme intelligente vient de vous réveiller." + } } } }, @@ -41035,6 +46615,12 @@ "state": "translated", "value": "報告錯誤時抓取此內容。它會告訴我應用程式看到了什麼。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé." + } } } }, @@ -41069,6 +46655,12 @@ "state": "translated", "value": "報告錯誤時抓取此內容,它會告訴我應用程式看到了什麼。(完整的即時日誌也在「即時」畫面上。)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé. (Le journal complet en direct est aussi disponible dans l'écran En direct.)" + } } } }, @@ -41103,6 +46695,12 @@ "state": "translated", "value": "授予「健康」存取權" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l'accès à Santé" + } } } }, @@ -41137,6 +46735,12 @@ "state": "translated", "value": "重力覆蓋率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couverture de gravité" + } } } }, @@ -41171,6 +46775,12 @@ "state": "translated", "value": "很好" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Excellent" + } } } }, @@ -41205,6 +46815,12 @@ "state": "translated", "value": "健身器材" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équipement de salle" + } } } }, @@ -41239,6 +46855,12 @@ "state": "translated", "value": "健身器材(FTMS)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équipement de salle (FTMS)" + } } } }, @@ -41325,6 +46947,12 @@ "state": "translated", "value": "HF 功率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Puissance HF" + } } } }, @@ -41359,6 +46987,12 @@ "state": "translated", "value": "高" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ÉLEVÉ" + } } } }, @@ -41393,6 +47027,12 @@ "state": "translated", "value": "心率 %1$@ · 消耗 %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC %@ · Effort %@" + } } } }, @@ -41427,6 +47067,12 @@ "state": "translated", "value": "心率區間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ZONE FC" + } } } }, @@ -41513,6 +47159,12 @@ "state": "translated", "value": "心率區間(全部)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zones FC (toutes)" + } } } }, @@ -41547,6 +47199,12 @@ "state": "translated", "value": "心率區間 1–3" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zones FC 1–3" + } } } }, @@ -41581,6 +47239,12 @@ "state": "translated", "value": "心率區間 4–5" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zones FC 4–5" + } } } }, @@ -41615,6 +47279,12 @@ "state": "translated", "value": "心率密度(每分鐘)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Densité FC (par min)" + } } } }, @@ -41655,6 +47325,12 @@ "state": "translated", "value": "心率在 %3$@ 內從 %1$lld 緩降至 %2$lld。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC réduite de %1$lld à %2$lld sur %3$@." + } } } }, @@ -41695,6 +47371,12 @@ "state": "translated", "value": "心率保持穩定(%1$lld → %2$lld)。試試改用節律呼吸。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC restée stable (%1$lld → %2$lld). Essayez plutôt une respiration guidée." + } } } }, @@ -41735,6 +47417,12 @@ "state": "translated", "value": "心率在 %3$@ 內從 %1$lld 平穩降至 %2$lld。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC stabilisée de %1$lld à %2$lld sur %3$@." + } } } }, @@ -41769,6 +47457,12 @@ "state": "translated", "value": "心率在 %@ 內趨於平穩。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC stabilisée sur %@." + } } } }, @@ -41803,6 +47497,12 @@ "state": "translated", "value": "無法取得心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC indisponible" + } } } }, @@ -41837,6 +47537,12 @@ "state": "translated", "value": "心率區間指導" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coaching par zone de FC" + } } } }, @@ -41929,6 +47635,12 @@ "state": "translated", "value": "HRV %1$@ · 靜息心率 %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VFC %@ · FCR %@" + } } } }, @@ -41963,6 +47675,12 @@ "state": "translated", "value": "HRV 高於基線 %lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VFC %lld %% au-dessus de la référence" + } } } }, @@ -41997,6 +47715,12 @@ "state": "translated", "value": "HRV 低於基線 %lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VFC %lld %% en dessous de la référence" + } } } }, @@ -42031,6 +47755,12 @@ "state": "translated", "value": "HRV 讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure de la VFC" + } } } }, @@ -42065,6 +47795,12 @@ "state": "translated", "value": "HRV 已明顯低於你的基線,顯示壓力或疲勞升高。放慢腳步,給身體一點時間恢復。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La VFC est tombée bien en dessous de votre référence, signe de stress ou de fatigue élevés. Levez le pied et laissez à votre corps le temps de récupérer." + } } } }, @@ -42099,6 +47835,12 @@ "state": "translated", "value": "HRV 高於基線,代表神經系統放鬆且恢復良好。壓力偏低。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La VFC est au-dessus de la référence, signe d'un système nerveux détendu et bien récupéré. Le stress est faible." + } } } }, @@ -42133,6 +47875,12 @@ "state": "translated", "value": "HRV 低於你的基線,今天請放緩節奏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La VFC est en dessous de votre référence, alors abordez la journée en douceur" + } } } }, @@ -42167,6 +47915,12 @@ "state": "translated", "value": "HRV 讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure de la VFC" + } } } }, @@ -42201,6 +47955,12 @@ "state": "translated", "value": "HRV 讀取尚未開始" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure de la VFC non démarrée" + } } } }, @@ -42235,6 +47995,12 @@ "state": "translated", "value": "HRV ↔ 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VFC ↔ Charge" + } } } }, @@ -42269,6 +48035,12 @@ "state": "translated", "value": "已攝入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fait" + } } } }, @@ -42303,6 +48075,12 @@ "state": "translated", "value": "震動指導" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coaching haptique" + } } } }, @@ -42337,6 +48115,12 @@ "state": "translated", "value": "觸感節律呼吸 · 找到你的節奏 · 平靜下來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respiration rythmée par vibrations · trouvez votre rythme · calmez-vous" + } } } }, @@ -42476,6 +48260,12 @@ "state": "translated", "value": "目前難以判讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Difficile à lire pour le moment" + } } } }, @@ -42510,6 +48300,12 @@ "state": "translated", "value": "提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Attention" + } } } }, @@ -42544,6 +48340,12 @@ "state": "translated", "value": "請注意。你的 WHOOP 同一時間只與一部手機通信。請先強制退出官方 WHOOP 應用程式,否則配對可能失敗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Attention. Votre WHOOP ne communique qu'avec un seul téléphone à la fois. Forcez d'abord la fermeture de l'application WHOOP officielle, sinon le jumelage risque d'échouer." + } } } }, @@ -42682,6 +48484,12 @@ "state": "translated", "value": "健康資料庫" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Base de données Santé" + } } } }, @@ -42716,6 +48524,12 @@ "state": "translated", "value": "健康洞察" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Perspectives sur la santé" + } } } }, @@ -42854,6 +48668,12 @@ "state": "translated", "value": "心率變異性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Variabilité de la fréquence cardiaque" + } } } }, @@ -42888,6 +48708,12 @@ "state": "translated", "value": "心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque" + } } } }, @@ -42974,6 +48800,12 @@ "state": "translated", "value": "心率(即時,盡力而為)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque (en direct, sous réserve)" + } } } }, @@ -43008,6 +48840,12 @@ "state": "translated", "value": "%@ 期間的心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque pendant %@" + } } } }, @@ -43042,6 +48880,12 @@ "state": "translated", "value": "心率不可用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque non disponible" + } } } }, @@ -43076,6 +48920,12 @@ "state": "translated", "value": "心率加上活動能量能給出可靠的每日心血管負荷。在手錶上記錄鍛鍊能讓它更精準。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque et l'énergie active donnent une charge cardiovasculaire quotidienne solide. Un entraînement lancé depuis la montre l'affine encore plus." + } } } }, @@ -43110,6 +48960,12 @@ "state": "translated", "value": "心率資料流已啟用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le flux de fréquence cardiaque est actif." + } } } }, @@ -43196,6 +49052,12 @@ "state": "translated", "value": "心率 · HRV(即時)* · 強度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque · VFC (en direct)* · Effort" + } } } }, @@ -43230,6 +49092,12 @@ "state": "translated", "value": "心率 · HRV · 皮膚溫度* · 呼吸率* · 睡眠 · 強度 · 電量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque · VFC · Temp. cutanée* · Fréq. respiratoire* · Sommeil · Effort · Batterie" + } } } }, @@ -43264,6 +49132,12 @@ "state": "translated", "value": "心率 · HRV · 皮膚溫度* · 呼吸率* · 步數* · 睡眠 · 強度 · 電量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque · VFC · Temp. cutanée* · Fréq. respiratoire* · Pas* · Sommeil · Effort · Batterie" + } } } }, @@ -43298,6 +49172,12 @@ "state": "translated", "value": "心率 · HRV* · 睡眠 · 靜息心率 · 皮膚溫度* · 電量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque · VFC* · Sommeil · FC au repos · Temp. cutanée* · Batterie" + } } } }, @@ -43332,6 +49212,12 @@ "state": "translated", "value": "心率* · HRV* · 睡眠* · 靜息心率* · 皮膚溫度* · 電量*" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque* · VFC* · Sommeil* · FC au repos* · Temp. cutanée* · Batterie*" + } } } }, @@ -43366,6 +49252,12 @@ "state": "translated", "value": "心率手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet de fréquence cardiaque" + } } } }, @@ -43452,6 +49344,12 @@ "state": "translated", "value": "心率變異性是你能量分數背後的引擎。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La variabilité de la fréquence cardiaque comme moteur de votre score de charge." + } } } }, @@ -43486,6 +49384,12 @@ "state": "translated", "value": "心率區間分佈:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Répartition des zones de fréquence cardiaque : %@" + } } } }, @@ -43572,6 +49476,12 @@ "state": "translated", "value": "身高(公分)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Taille en centimètres" + } } } }, @@ -43606,6 +49516,12 @@ "state": "translated", "value": "身高(英寸)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Taille en pouces" + } } } }, @@ -43646,6 +49562,12 @@ "state": "translated", "value": "身高,%1$lld 英尺 %2$lld 英寸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Taille, %1$lld pieds %2$lld pouces" + } } } }, @@ -43680,6 +49602,12 @@ "state": "translated", "value": "幫助與聯繫" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aide et contact" + } } } }, @@ -43714,6 +49642,12 @@ "state": "translated", "value": "幫助最大:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aide le plus : %@" + } } } }, @@ -43748,6 +49682,12 @@ "state": "translated", "value": "已隱藏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masqué" + } } } }, @@ -43782,6 +49722,12 @@ "state": "translated", "value": "隱藏 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer %@" + } } } }, @@ -43816,6 +49762,12 @@ "state": "translated", "value": "隱藏資料來源詳情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer le détail de la source de données" + } } } }, @@ -43850,6 +49802,12 @@ "state": "translated", "value": "隱藏此問題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer cette question" + } } } }, @@ -43884,6 +49842,12 @@ "state": "translated", "value": "高" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Élevé" + } } } }, @@ -43970,6 +49934,12 @@ "state": "translated", "value": "歷史資料會立即填滿儀表板" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'historique remplit immédiatement le tableau de bord" + } } } }, @@ -44004,6 +49974,12 @@ "state": "translated", "value": "歷史同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisation de l'historique" + } } } }, @@ -44038,6 +50014,12 @@ "state": "translated", "value": "歷史同步在 5.0 上屬實驗性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La synchronisation de l'historique est expérimentale sur 5.0." + } } } }, @@ -44072,6 +50054,12 @@ "state": "translated", "value": "歷史已同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Historique synchronisé" + } } } }, @@ -44158,6 +50146,12 @@ "state": "translated", "value": "拖累你的:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous freine : %@" + } } } }, @@ -44192,6 +50186,12 @@ "state": "translated", "value": "在你錄製鍛鍊時保持螢幕常亮,讓你的即時心率始終可見而裝置不會變暗。僅在錄製期間適用,其餘時間螢幕會正常休眠。保持開啟確實會多耗一點電,並意味著你已解鎖的螢幕會在整個鍛鍊過程中保持可見,如果你對此有顧慮,請將其關閉。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Garde l'écran allumé pendant l'enregistrement d'un entraînement, pour que votre fréquence cardiaque en direct reste visible sans que l'appareil s'assombrisse. Ne s'applique que pendant un enregistrement. Le reste du temps, l'écran se met en veille normalement. Le laisser activé consomme un peu plus de batterie et signifie que votre écran déverrouillé reste visible pendant tout l'entraînement, alors désactivez-le si cela vous préoccupe." + } } } }, @@ -44278,6 +50278,12 @@ "state": "translated", "value": "睡眠時數趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance des heures de sommeil" + } } } }, @@ -44364,6 +50370,12 @@ "state": "translated", "value": "%@ 如何計算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment %@ est calculé" + } } } }, @@ -44450,6 +50462,12 @@ "state": "translated", "value": "能量如何計算。評分背後的方法。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment la Charge est calculée. La méthode derrière le score." + } } } }, @@ -44484,6 +50502,12 @@ "state": "translated", "value": "能量、消耗和休息是如何計算的,以及它們與 WHOOP 的區別。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment Charge, Effort et Repos sont calculés, et en quoi ils diffèrent de WHOOP." + } } } }, @@ -44570,6 +50594,12 @@ "state": "translated", "value": "NOOP 如何將一個夜晚劃分為淺睡 / 深睡 / REM。這是一套獨立的、需主動開啟的演算法,除非你開啟它,否則你的預設分期保持不變。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment NOOP répartit une nuit en léger / profond / paradoxal. C'est une méthode distincte et facultative. Votre répartition par défaut reste inchangée tant que vous ne l'activez pas." + } } } }, @@ -44604,6 +50634,12 @@ "state": "translated", "value": "NOOP 的運作原理" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment fonctionne NOOP" + } } } }, @@ -44794,6 +50830,12 @@ "state": "translated", "value": "這有多準確?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelle est la précision de ces données ?" + } } } }, @@ -44829,6 +50871,12 @@ "state": "translated", "value": "這有多準確?%@ 你體能年齡背後的資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelle est la précision de ces données ? %@ les données derrière votre âge fitness" + } } } }, @@ -44863,6 +50911,12 @@ "state": "translated", "value": "這有多準確?隱藏你體能年齡背後的數據" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelle est la précision de ces données ? Masquer les données derrière votre âge fitness" + } } } }, @@ -44897,6 +50951,12 @@ "state": "translated", "value": "這有多準確?顯示你體能年齡背後的數據" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelle est la précision de ces données ? Afficher les données derrière votre âge fitness" + } } } }, @@ -44931,6 +50991,12 @@ "state": "translated", "value": "一夜好眠與隔天早晨能量的關聯程度。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À quel point une bonne nuit reflète la charge du lendemain matin." + } } } }, @@ -44965,6 +51031,12 @@ "state": "translated", "value": "一天的能量會延續到隔天的程度。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans quelle mesure la charge d'un jour se répercute sur le suivant." + } } } }, @@ -44999,6 +51071,12 @@ "state": "translated", "value": "你的恢復程度,主要由 HRV 相對於你個人基線的表現決定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre niveau de récupération, déterminé par la VFC par rapport à votre référence personnelle." + } } } }, @@ -45033,6 +51111,12 @@ "state": "translated", "value": "你的睡眠恢復效果如何:時長、效率、深睡+REM、時間點。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le caractère réparateur de votre sommeil : durée, efficacité, profond+paradoxal, horaires." + } } } }, @@ -45067,6 +51151,12 @@ "state": "translated", "value": "震動的強度。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'intensité de la vibration." + } } } }, @@ -45101,6 +51191,12 @@ "state": "translated", "value": "NOOP 有多確定? · 扎實 · 建立中 · 校準中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quel est le niveau de confiance de NOOP ? · Solide · En construction · Calibrage" + } } } }, @@ -45187,6 +51283,12 @@ "state": "translated", "value": "這是如何測量的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment cela est mesuré" + } } } }, @@ -45221,6 +51323,12 @@ "state": "translated", "value": "此評分如何計算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment ce score est calculé" + } } } }, @@ -45307,6 +51415,12 @@ "state": "translated", "value": "如何解讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment lire ceci" + } } } }, @@ -45341,6 +51455,12 @@ "state": "translated", "value": "如何解讀:HRV、靜息心率、睡眠時長、呼吸率和皮膚溫度都是從手環測量的(皮膚溫度顯示為相對於你自身基線的偏差)。鍛鍊是你記錄或被偵測到的活動次數。恢復、強度和壓力是 NOOP 自己的裝置端分數,並非臨床測量:恢復是每日準備度綜合值(HRV、靜息心率、睡眠和皮膚溫度趨勢),強度是由心率推導的心血管負荷,壓力則是由靜息心率和 HRV 得出的 0–3 自主神經負荷指數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment lire ceci : la VFC, la FC au repos, la durée du sommeil, la fréquence respiratoire et la température cutanée sont mesurées par le bracelet (la température cutanée est affichée comme un écart par rapport à votre propre référence). Entraînements correspond au nombre d'activités que vous avez enregistrées ou qui ont été détectées. Récupération, Effort et Stress sont des scores propres à NOOP calculés sur l'appareil, pas des mesures cliniques : la Récupération est un indice composite de forme quotidienne (VFC, FC au repos, sommeil et tendance de température cutanée), l'Effort est la charge cardiovasculaire dérivée de la fréquence cardiaque, et le Stress est un indice de charge autonome de 0 à 3 basé sur la FC au repos et la VFC." + } } } }, @@ -45375,6 +51495,12 @@ "state": "translated", "value": "你的評分是如何計算的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment vos scores sont calculés" + } } } }, @@ -45409,6 +51535,12 @@ "state": "translated", "value": "你的評分如何運作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment fonctionnent vos scores" + } } } }, @@ -45443,6 +51575,12 @@ "state": "translated", "value": "你的睡眠如何被分類" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment votre sommeil est classé" + } } } }, @@ -45477,6 +51615,12 @@ "state": "translated", "value": "我的能量趨勢如何?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment évolue ma charge ?" + } } } }, @@ -45511,6 +51655,12 @@ "state": "translated", "value": "你今天心情如何?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment est votre humeur aujourd'hui ?" + } } } }, @@ -45597,6 +51747,12 @@ "state": "translated", "value": "今日水分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hydratation aujourd'hui" + } } } }, @@ -45631,6 +51787,12 @@ "state": "translated", "value": "水分追蹤" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivi de l'hydratation" + } } } }, @@ -45665,6 +51827,12 @@ "state": "translated", "value": "睡眠圖" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hypnogramme" + } } } }, @@ -45699,6 +51867,12 @@ "state": "translated", "value": "我已閱讀並接受這些條款,並且我是在自擔風險的情況下,使用我自己的裝置和我自己的資料來使用 NOOP。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "J'ai lu et j'accepte ces conditions, et j'utilise NOOP avec mon propre appareil et mes propres données, à mes risques et périls." + } } } }, @@ -45733,6 +51907,12 @@ "state": "translated", "value": "我瞭解了" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "J'ai compris" + } } } }, @@ -45767,6 +51947,12 @@ "state": "translated", "value": "我明白這會將戒指從 Oura 中斷連接,並且 NOOP 無法為我撤銷此操作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi." + } } } }, @@ -45801,6 +51987,12 @@ "state": "translated", "value": "我明白這會將戒指從 Oura 中斷連接,並且 NOOP 無法為我撤銷此操作。若要回到 Oura,我需要再次將戒指恢復原廠設定,並在 Oura 應用程式中重新設定它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi. Pour revenir à Oura, je devrai réinitialiser la bague aux paramètres d'usine et la configurer à nouveau dans l'application Oura." + } } } }, @@ -45835,6 +52027,12 @@ "state": "translated", "value": "我明白這是一項實驗性的健康功能,而非醫療裝置或診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Je comprends qu'il s'agit d'une fonctionnalité de bien-être expérimentale, et non d'un dispositif médical ni d'un diagnostic." + } } } }, @@ -45869,6 +52067,12 @@ "state": "translated", "value": "我醒了" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Je suis réveillé" + } } } }, @@ -45903,6 +52107,12 @@ "state": "translated", "value": "我已戴上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Je le porte" + } } } }, @@ -45937,6 +52147,12 @@ "state": "translated", "value": "閒置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "INACTIF" + } } } }, @@ -45971,6 +52187,12 @@ "state": "translated", "value": "收件匣" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "BOÎTE DE RÉCEPTION" + } } } }, @@ -46005,6 +52227,12 @@ "state": "translated", "value": "閒置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inactif" + } } } }, @@ -46039,6 +52267,12 @@ "state": "translated", "value": "如果 Oura 應用程式仍在執行,它會佔用戒指,接管將會失敗。請強制退出 Oura,然後掃描。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si l'application Oura est encore en cours d'exécution, elle retiendra la bague et l'adoption échouera. Forcez la fermeture d'Oura, puis lancez le scan." + } } } }, @@ -46073,6 +52307,12 @@ "state": "translated", "value": "如果你沒有看到提示,請在「設定 › 健康 › 資料存取與裝置」下啟用 NOOP。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils." + } } } }, @@ -46107,6 +52347,12 @@ "state": "translated", "value": "如果你沒有看到提示,請在「設定 › 健康 › 資料存取與裝置」下打開 NOOP。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils." + } } } }, @@ -46141,6 +52387,12 @@ "state": "translated", "value": "如果你從之前的 Oura 設定中提取了戒指的 16 位元組金鑰,NOOP 可以用該金鑰與戒指通信而無需重置它,因此 Oura 應用程式也能繼續工作。NOOP 不會為你提取金鑰,也無法幫你找到金鑰。如果你不明白這是什麼意思,請返回並使用標準設定或檔案匯入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si vous avez extrait la clé de 16 octets de votre bague depuis une configuration Oura précédente, NOOP peut communiquer avec la bague grâce à cette clé sans la réinitialiser, si bien que l'application Oura continue elle aussi de fonctionner. NOOP n'extrait pas de clé pour vous et ne peut pas vous aider à en trouver une. Si vous ne savez pas ce que cela signifie, revenez en arrière et utilisez la configuration standard ou l'import de fichier." + } } } }, @@ -46175,6 +52427,12 @@ "state": "translated", "value": "如果你感到不適或擔心你的心臟,請聯絡合格的專業人員;緊急情況下請聯絡當地的緊急服務。請勿依賴 NOOP。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si vous vous sentez mal ou si vous êtes inquiet pour votre cœur, contactez un professionnel qualifié ; en cas d'urgence, appelez les services d'urgence locaux. Ne comptez pas sur NOOP." + } } } }, @@ -46209,6 +52467,12 @@ "state": "translated", "value": "如果你仍坐著,每隔這麼久再震動一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Si vous êtes toujours assis, vibrer à nouveau à cette fréquence." + } } } }, @@ -46243,6 +52507,12 @@ "state": "translated", "value": "疾病早期預警" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alerte précoce de maladie" + } } } }, @@ -46277,6 +52547,12 @@ "state": "translated", "value": "影像檢查" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Imagerie" + } } } }, @@ -46311,6 +52587,12 @@ "state": "translated", "value": "英制" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impérial" + } } } }, @@ -46345,6 +52627,12 @@ "state": "translated", "value": "匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer" + } } } }, @@ -46379,6 +52667,12 @@ "state": "translated", "value": "匯入 Apple Health 匯出檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer un export Apple Santé" + } } } }, @@ -46413,6 +52707,12 @@ "state": "translated", "value": "匯入 NOOP 備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer une sauvegarde NOOP" + } } } }, @@ -46447,6 +52747,12 @@ "state": "translated", "value": "匯入 WHOOP 匯出檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer un export WHOOP" + } } } }, @@ -46481,6 +52787,12 @@ "state": "translated", "value": "匯入一份 WHOOP 匯出檔案、Apple 健康或第二條手環,你的最佳來源記錄便會在這裡建立,就在這台裝置上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez un export WHOOP, Apple Santé ou un second bracelet, et votre historique le plus complet se construit ici, sur cet appareil." + } } } }, @@ -46515,6 +52827,12 @@ "state": "translated", "value": "直接從你的裝置匯入任何品牌(Garmin、Coros、Suunto、Wahoo、Polar、Strava、Apple)匯出的單一鍛鍊檔案。只要檔案裡有,GPS 路線、距離、心率和卡路里都會匯入。完全離線;沒有任何內容離開 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez un seul fichier d'entraînement exporté depuis n'importe quelle marque (Garmin, Coros, Suunto, Wahoo, Polar, Strava, Apple) directement depuis votre appareil. Le tracé GPS, la distance, la fréquence cardiaque et les calories sont importés lorsque le fichier les contient. Entièrement hors ligne ; rien ne quitte %@." + } } } }, @@ -46549,6 +52867,12 @@ "state": "translated", "value": "匯入 Apple Health 匯出檔案(健康 App → 個人檔案 → 匯出所有健康資料 → export.zip)。7 年的心率、HRV、睡眠、SpO₂、步數等,在本機串流處理。較大的匯出檔案需要一兩分鐘。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez un export Apple Santé (application Santé → profil → Exporter toutes les données de santé → export.zip). 7 ans de FC, VFC, sommeil, SpO₂, pas et plus, traités localement. Les exports volumineux prennent une minute ou deux." + } } } }, @@ -46583,6 +52907,12 @@ "state": "translated", "value": "從 Cronometer 或 MacroFactor 的 CSV 匯出檔案匯入每日營養總量:攝取卡路里、蛋白質、碳水化合物、脂肪(若有也包含體重)。只要檔案有日期欄和每日總量,其他追蹤器也適用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez les totaux nutritionnels quotidiens depuis un export CSV Cronometer ou MacroFactor : calories consommées, protéines, glucides, lipides (et poids si présent). D'autres applications de suivi fonctionnent aussi si le fichier contient une colonne date et des totaux quotidiens." + } } } }, @@ -46617,6 +52947,12 @@ "state": "translated", "value": "匯入失敗。你現有的資料已被保留。%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de l'import. Vos données existantes ont été conservées. %@" + } } } }, @@ -46651,6 +52987,12 @@ "state": "translated", "value": "匯入失敗:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de l'import : %@" + } } } }, @@ -46685,6 +53027,12 @@ "state": "translated", "value": "匯入讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer des mesures" + } } } }, @@ -46719,6 +53067,12 @@ "state": "translated", "value": "匯入暫存檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer des fichiers temporaires" + } } } }, @@ -46753,6 +53107,12 @@ "state": "translated", "value": "直接從 Mi Fitness 應用程式匯入你的 Mi Band 歷史資料(步數、心率、靜息心率、睡眠階段、SpO₂、壓力和睡眠分數)。在你的 iPhone 上:檔案 → 我的 iPhone → Mi Fitness,長按資料夾 → 壓縮,然後在這裡選擇該 .zip 檔案。完全離線;不需要小米帳號或 Bluetooth。適用 Smart Band 8/9/10。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez l'historique de votre Mi Band (pas, fréquence cardiaque, FC au repos, phases de sommeil, SpO₂, stress et score de sommeil) directement depuis l'application Mi Fitness. Sur votre iPhone : Fichiers → Sur mon iPhone → Mi Fitness, appui long sur le dossier → Compresser, puis choisissez le fichier .zip ici. Entièrement hors ligne ; aucun compte Xiaomi ni Bluetooth requis. Smart Band 8/9/10." + } } } }, @@ -46787,6 +53147,12 @@ "state": "translated", "value": "從資料匯出檔案(.zip)匯入你完整的 WHOOP 歷史(恢復、強度、睡眠、鍛鍊)。適用於 WHOOP 4.0、5.0 和 MG。可在 app.whoop.com → Data Management 取得。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez tout votre historique WHOOP (récupération, effort, sommeil, entraînements) depuis un export de données (.zip). Fonctionne pour WHOOP 4.0, 5.0 et MG. Obtenez-en un sur app.whoop.com → Data Management." + } } } }, @@ -46873,6 +53239,12 @@ "state": "translated", "value": "匯入你自己從 Oura、Fitbit 或 Garmin 匯出的資料:睡眠、靜息心率、HRV、步數等,只要匯出檔案裡有。從該品牌的應用程式下載(Oura:帳號 → 匯出資料;Fitbit:Google Takeout;Garmin:匯出你的資料),然後在這裡選擇檔案。完全離線;沒有任何內容離開 %@。各品牌自己的準備度或睡眠分數僅保留作參考。你的分數始終是你自己的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez votre propre export de données depuis Oura, Fitbit ou Garmin : sommeil, fréquence cardiaque au repos, VFC, pas et plus, lorsque l'export les contient. Téléchargez-le depuis l'application de la marque (Oura : Compte → Export Data ; Fitbit : Google Takeout ; Garmin : Export Your Data), puis choisissez le fichier ici. Entièrement hors ligne ; rien ne quitte %@. Le score de forme ou de sommeil propre à chaque marque est conservé à titre de référence uniquement. Vos scores restent les vôtres." + } } } }, @@ -46907,6 +53279,12 @@ "state": "translated", "value": "從 Hevy 的 CSV 匯出檔案或 Liftosaur 的 JSON 匯出檔案匯入你的力量訓練歷史。每次鍛鍊都會成為一個力量訓練課次,並帶有訓練量估算(重量 × 次數)。這是一個訓練量數字,不是實測的強度,它絕不會改變你的消耗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez votre historique de musculation depuis un export CSV Hevy ou un export JSON Liftosaur. Chaque entraînement devient une séance de Force avec une estimation du volume d'entraînement (poids × répétitions). C'est un chiffre de volume, pas un effort mesuré. Cela ne modifie jamais votre Effort." + } } } }, @@ -46941,6 +53319,12 @@ "state": "translated", "value": "已匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importé" + } } } }, @@ -46981,6 +53365,12 @@ "state": "translated", "value": "已匯入 %lld 天(%lld 個數值)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld jours importés (%2$lld valeurs)" + } } } }, @@ -47015,6 +53405,12 @@ "state": "translated", "value": "已匯入 %lld 次鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld entraînements importés" + } } } }, @@ -47049,6 +53445,12 @@ "state": "translated", "value": "已匯入 1 次鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 entraînement importé" + } } } }, @@ -47083,6 +53485,12 @@ "state": "translated", "value": "匯入會覆蓋 %@ 上目前的所有內容。以防萬一,你的舊資料會保存在一個備用檔案中。匯入生效需要重新啟動 NOOP。匯出 CSV 會寫出一個 WHOOP 格式的 zip,包含你的每日資料、睡眠、鍛鍊和日誌,可重新匯入到 Mac、iPhone 或 Android 上的 NOOP 中,裝置端計算的列會在其「來源」欄中標為 APPROXIMATE(近似);完整備份仍是無損的還原路徑。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'import écrase tout ce qui se trouve actuellement sur %@. Vos anciennes données sont conservées dans un fichier annexe par précaution. NOOP doit être relancé pour que l'import prenne effet. « Exporter en CSV » écrit un zip au format WHOOP de vos jours, sommeils, entraînements et journal, qui peut être réimporté dans NOOP sur Mac, iPhone ou Android. Les lignes calculées sur l'appareil sont marquées APPROXIMATIVE dans la colonne Source ; la sauvegarde complète reste le moyen de restauration sans perte." + } } } }, @@ -47274,6 +53682,12 @@ "state": "translated", "value": "到目前為止,在你的資料中,這對你 %@ 的影響不像通常那樣。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans vos données actuelles, cela ne fait pas varier votre %@ comme d'habitude." + } } } }, @@ -47308,6 +53722,12 @@ "state": "translated", "value": "在你的範圍內" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans votre plage" + } } } }, @@ -47342,6 +53762,12 @@ "state": "translated", "value": "久坐提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rappel d'inactivité" + } } } }, @@ -47376,6 +53802,12 @@ "state": "translated", "value": "含 Helio。只要手環有開放即可顯示即時心率。幫我們測試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Incl. Helio. Fréquence cardiaque en direct lorsque le bracelet la fournit. Aidez-nous à tester." + } } } }, @@ -47410,6 +53842,12 @@ "state": "translated", "value": "獨立,且屬實驗性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indépendant, et expérimental" + } } } }, @@ -47421,6 +53859,12 @@ "state": "translated", "value": "Unabhängig: nicht mit WHOOP verbunden" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indépendant : non affilié à WHOOP" + } } } }, @@ -47455,6 +53899,12 @@ "state": "translated", "value": "僅供參考,並非醫療建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À titre informatif uniquement, pas un avis médical." + } } } }, @@ -47594,6 +54044,12 @@ "state": "translated", "value": "安裝於 App Store 之外" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installé en dehors de l'App Store" + } } } }, @@ -47628,6 +54084,12 @@ "state": "translated", "value": "正在安裝 NOOP 的金鑰,並確認戒指只回應 NOOP。請讓戒指保持在附近,且不要打開 Oura 應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installation de la clé de NOOP et vérification que la bague ne répond qu'à NOOP. Gardez la bague à proximité et n'ouvrez pas l'application Oura." + } } } }, @@ -47714,6 +54176,12 @@ "state": "translated", "value": "高強度分鐘數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minutes d'intensité" + } } } }, @@ -47748,6 +54216,12 @@ "state": "translated", "value": "強度分鐘數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minutes d'intensité" + } } } }, @@ -47938,6 +54412,12 @@ "state": "translated", "value": "干預" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intervention" + } } } }, @@ -47972,6 +54452,12 @@ "state": "translated", "value": "日內" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intrajournalier" + } } } }, @@ -48006,6 +54492,12 @@ "state": "translated", "value": "一切都留在你的手環和 %@ 上:沒有帳號,沒有雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout reste sur votre bracelet et %@ : pas de compte, pas de cloud." + } } } }, @@ -48040,6 +54532,12 @@ "state": "translated", "value": "它是一幅圖像,不是一個判決" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est un instantané, pas un verdict" + } } } }, @@ -48074,6 +54572,12 @@ "state": "translated", "value": "它不能替代專業人員" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela ne remplace pas un professionnel" + } } } }, @@ -48108,6 +54612,12 @@ "state": "translated", "value": "它沒有被 WHOOP 手機應用程式佔用。一次只能有一個主機:請關閉該應用程式或關閉其 Bluetooth。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'est pas retenu par l'application WHOOP du téléphone. Un seul hôte à la fois : fermez l'application ou désactivez son Bluetooth." + } } } }, @@ -48142,6 +54652,12 @@ "state": "translated", "value": "它顯示你心跳時序的形態,以及用平實語言描述它看起來有多穩定。它不會告訴你任何事情是對是錯。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela montre la forme du rythme de vos battements et une description en langage simple de sa régularité apparente. Cela ne vous dit pas si quelque chose est normal ou anormal." + } } } }, @@ -48176,6 +54692,12 @@ "state": "translated", "value": "它是一個筆記本,而非實驗室。NOOP 只是把你錄入的數字排列對齊,它不檢測、不解讀、也不評判它們。並非醫療建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est un carnet, pas un laboratoire. NOOP aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse ni ne les juge. Ce n'est pas un avis médical." + } } } }, @@ -48210,6 +54732,12 @@ "state": "translated", "value": "它已充電並戴上。感測器需要接觸皮膚才能喚醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il est chargé et porté. Le capteur a besoin d'un contact avec la peau pour se réveiller." + } } } }, @@ -48244,6 +54772,12 @@ "state": "translated", "value": "它在 %@ 約一公尺範圍內。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il se trouve à environ un mètre de %@." + } } } }, @@ -48278,6 +54812,12 @@ "state": "translated", "value": "1月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Janv." + } } } }, @@ -48312,6 +54852,12 @@ "state": "translated", "value": "日誌" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journal" + } } } }, @@ -48346,6 +54892,12 @@ "state": "translated", "value": "7月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Juil." + } } } }, @@ -48380,6 +54932,12 @@ "state": "translated", "value": "6月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Juin" + } } } }, @@ -48414,6 +54972,12 @@ "state": "translated", "value": "大卡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "KCAL" + } } } }, @@ -48448,6 +55012,12 @@ "state": "translated", "value": "請將它保持在 %@ 約一公尺範圍內。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gardez-le à environ un mètre de %@." + } } } }, @@ -48482,6 +55052,12 @@ "state": "translated", "value": "鍛鍊期間保持螢幕常亮" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Garder l'écran allumé pendant un entraînement" + } } } }, @@ -48516,6 +55092,12 @@ "state": "translated", "value": "改為保留 Oura 應用程式(匯入檔案)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserver l'application Oura à la place (importer un fichier)" + } } } }, @@ -48550,6 +55132,12 @@ "state": "translated", "value": "改為保留 Oura 應用程式,匯入檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserver l'application Oura à la place, importer un fichier" + } } } }, @@ -48584,6 +55172,12 @@ "state": "translated", "value": "請讓戒指留在充電器上或戴在手指上,讓它保持喚醒狀態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gardez la bague sur le chargeur ou à votre doigt pour qu'elle reste éveillée." + } } } }, @@ -48618,6 +55212,12 @@ "state": "translated", "value": "讓你的血液、血壓和身體數據保持私密,留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gardez vos résultats sanguins, votre tension et vos données corporelles privés, sur %@." + } } } }, @@ -48652,6 +55252,12 @@ "state": "translated", "value": "在這裡保存你自己的數字" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conservez vos propres données ici" + } } } }, @@ -48686,6 +55292,12 @@ "state": "translated", "value": "讓詳細的逐拍心率資料流全天候執行,而不僅僅是在即時畫面打開時,這樣 NOOP 就能為夜間 HRV、恢復和睡眠擷取更多資料。會更耗電,你的手環在連接期間會持續串流心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintient le flux détaillé battement par battement de la fréquence cardiaque actif jour et nuit, pas seulement lorsqu'un écran en direct est ouvert, afin que NOOP capture bien plus de données pour la VFC nocturne, la récupération et le sommeil. Utilise plus de batterie : votre bracelet transmet en continu la fréquence cardiaque tant qu'il est connecté." + } } } }, @@ -48772,6 +55384,12 @@ "state": "translated", "value": "昨晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NUIT DERNIÈRE" + } } } }, @@ -48806,6 +55424,12 @@ "state": "translated", "value": "僅即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC EN DIRECT UNIQUEMENT" + } } } }, @@ -48840,6 +55464,12 @@ "state": "translated", "value": "即時生理狀態" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PHYSIOLOGIE EN DIRECT" + } } } }, @@ -48874,6 +55504,12 @@ "state": "translated", "value": "僅本地" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LOCAL UNIQUEMENT" + } } } }, @@ -48908,6 +55544,12 @@ "state": "translated", "value": "長軸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GRAND AXE" + } } } }, @@ -48942,6 +55584,12 @@ "state": "translated", "value": "低" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FAIBLE" + } } } }, @@ -48976,6 +55624,12 @@ "state": "translated", "value": "訊號弱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SIGNAL FAIBLE" + } } } }, @@ -49010,6 +55664,12 @@ "state": "translated", "value": "實驗手記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Carnet de labo" + } } } }, @@ -49044,6 +55704,12 @@ "state": "translated", "value": "實驗手記是一個私密的筆記本,而非醫療服務。NOOP 儲存並排列對齊你錄入的數字,它不檢測、不解讀、不診斷、也不建議。你的記錄絕不會離開 %@;沒有帳戶或雲端,因此它不受「HIPAA 保護」。請始終依靠你的醫生或藥劑師來解讀結果。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Carnet de labo est un carnet privé, pas un service médical. NOOP stocke et aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse, ne les diagnostique ni ne les commente. Vos données ne quittent jamais %@ ; il n'y a ni compte ni cloud, donc ce n'est pas « couvert par HIPAA ». Fiez-vous toujours à votre médecin ou pharmacien pour interpréter les résultats." + } } } }, @@ -49078,6 +55744,12 @@ "state": "translated", "value": "實驗手記保存你自己的數字,它不檢測、不解讀、也不評判它們,也不是醫療建議。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Carnet de labo conserve vos propres données. Il ne les teste, ne les analyse ni ne les juge, et ce n'est pas un avis médical. Tout reste sur %@." + } } } }, @@ -49112,6 +55784,12 @@ "state": "translated", "value": "最近 %lld 次心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld derniers battements" + } } } }, @@ -49146,6 +55824,12 @@ "state": "translated", "value": "過去 14 晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "14 dernières nuits" + } } } }, @@ -49180,6 +55864,12 @@ "state": "translated", "value": "最近 30 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30 derniers jours" + } } } }, @@ -49214,6 +55904,12 @@ "state": "translated", "value": "最近 6 個月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "6 derniers mois" + } } } }, @@ -49248,6 +55944,12 @@ "state": "translated", "value": "過去 7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7 derniers jours" + } } } }, @@ -49282,6 +55984,12 @@ "state": "translated", "value": "最近 90 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "90 derniers jours" + } } } }, @@ -49316,6 +56024,12 @@ "state": "translated", "value": "最近一次能量分解" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier détail de la Charge" + } } } }, @@ -49350,6 +56064,12 @@ "state": "translated", "value": "最近一次 HRV 讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière mesure de VFC" + } } } }, @@ -49436,6 +56156,12 @@ "state": "translated", "value": "上次備份:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière sauvegarde : %@" + } } } }, @@ -49470,6 +56196,12 @@ "state": "translated", "value": "上次觸發的閘門" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier déclencheur activé" + } } } }, @@ -49504,6 +56236,12 @@ "state": "translated", "value": "上次匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier import" + } } } }, @@ -49590,6 +56328,12 @@ "state": "translated", "value": "昨晚 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit dernière · %@" + } } } }, @@ -49624,6 +56368,12 @@ "state": "translated", "value": "昨晚,%@。今晚的分數會在你戴著手環入睡後出現。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit dernière, %@. Le résultat de ce soir arrive une fois que vous aurez dormi avec le bracelet." + } } } }, @@ -49710,6 +56460,12 @@ "state": "translated", "value": "上次卸載完成時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier déchargement terminé" + } } } }, @@ -49744,6 +56500,12 @@ "state": "translated", "value": "上次卸載結果" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résultat du dernier déchargement" + } } } }, @@ -49778,6 +56540,12 @@ "state": "translated", "value": "手環上次回報" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière donnée signalée par le bracelet" + } } } }, @@ -49812,6 +56580,12 @@ "state": "translated", "value": "上次偵測到於 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vu pour la dernière fois %@" + } } } }, @@ -49846,6 +56620,12 @@ "state": "translated", "value": "上次訓練" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière séance" + } } } }, @@ -49880,6 +56660,12 @@ "state": "translated", "value": "上次訓練:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière séance : %@" + } } } }, @@ -49914,6 +56700,12 @@ "state": "translated", "value": "上次睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier sommeil" + } } } }, @@ -49948,6 +56740,12 @@ "state": "translated", "value": "上次同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière synchronisation" + } } } }, @@ -49982,6 +56780,12 @@ "state": "translated", "value": "上次同步 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière synchronisation %@." + } } } }, @@ -50016,6 +56820,12 @@ "state": "translated", "value": "上次同步於 %lld 分鐘前。重新連接以拉取最新資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière synchronisation il y a %lld minutes. Reconnectez-vous pour récupérer les dernières données." + } } } }, @@ -50050,6 +56860,12 @@ "state": "translated", "value": "上次同步於 %lld 分鐘前" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière synchronisation il y a %lldmin" + } } } }, @@ -50084,6 +56900,12 @@ "state": "translated", "value": "上週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La semaine dernière" + } } } }, @@ -50118,6 +56940,12 @@ "state": "translated", "value": "去年" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'année dernière" + } } } }, @@ -50152,6 +56980,12 @@ "state": "translated", "value": "稍後" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus tard" + } } } }, @@ -50238,6 +57072,12 @@ "state": "translated", "value": "最新記錄:%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière entrée : %@." + } } } }, @@ -50324,6 +57164,12 @@ "state": "translated", "value": "最近睡眠 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier sommeil · %@" + } } } }, @@ -50358,6 +57204,12 @@ "state": "translated", "value": "最近睡眠,%@。這是你最後一次評分的睡眠。整夜佩戴手環即可獲得新分數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier sommeil, %@. C'est votre dernière séance notée. Portez le bracelet pendant la nuit pour obtenir un nouveau score." + } } } }, @@ -50392,6 +57244,12 @@ "state": "translated", "value": "去脂體重" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masse maigre" + } } } }, @@ -50530,6 +57388,12 @@ "state": "translated", "value": "學習中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apprentissage" + } } } }, @@ -50616,6 +57480,12 @@ "state": "translated", "value": "正在學習你的模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apprentissage de votre schéma" + } } } }, @@ -50650,6 +57520,12 @@ "state": "translated", "value": "不保留任何活躍項" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne laisser aucune active" + } } } }, @@ -50684,6 +57560,12 @@ "state": "translated", "value": "主要依據你的心率變異性(HRV)相對於你個人基線的表現,再加上靜息心率、昨晚的休息、呼吸率,以及皮膚溫度訊號(疾病或過度訓練的早期標誌)。HRV 相對基線越高,能量就越多。NOOP 需要先花幾晚學習你的基線,在那之前你會看到「校準中」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déterminée par votre variabilité de fréquence cardiaque (VFC) mesurée par rapport à votre référence personnelle, ainsi que par la fréquence cardiaque au repos, le Repos de la nuit dernière, la fréquence respiratoire et un signal de température cutanée (un indicateur précoce de maladie ou de surcharge). Une VFC plus élevée par rapport à votre référence signifie plus de Charge. NOOP a d'abord besoin de quelques nuits pour apprendre votre référence. Jusque-là, vous verrez « Calibrage »." + } } } }, @@ -50718,6 +57600,12 @@ "state": "translated", "value": "主要依據你的心率變異性相對於自身基線的表現。手錶是抽樣測量 HRV 而非持續串流,因此這需要大約一週的夜晚來校準。在那之前 NOOP 會顯示「需要更多資料」,絕不顯示猜測的數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déterminée par votre variabilité de fréquence cardiaque par rapport à votre propre référence. La montre échantillonne la VFC au lieu de la transmettre en continu, ce qui nécessite environ une semaine de nuits pour se calibrer. D'ici là, NOOP affiche « besoin de plus de données », jamais un chiffre estimé." + } } } }, @@ -50752,6 +57640,12 @@ "state": "translated", "value": "遺留的匯入副本" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copies d'import restantes" + } } } }, @@ -50786,6 +57680,12 @@ "state": "translated", "value": "讓 NOOP 為你在下方開啟的項目輕拍你的手腕,這樣你就能放下手機,仍能感受到重要的事。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Laissez NOOP tapoter votre poignet pour les fonctions que vous activez ci-dessous, afin que vous puissiez laisser votre téléphone tout en ressentant ce qui compte." + } } } }, @@ -50820,6 +57720,12 @@ "state": "translated", "value": "讓自我檢查自動觸發。關閉則保持手動:由你自己從「呼吸」開始一次呼吸練習。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Laisser le check-in se déclencher tout seul. Désactivé, cela reste manuel : vous démarrez vous-même une respiration depuis Respirer." + } } } }, @@ -50906,6 +57812,12 @@ "state": "translated", "value": "力量訓練" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Musculation" + } } } }, @@ -50940,6 +57852,12 @@ "state": "translated", "value": "力量訓練記錄(Hevy / Liftosaur)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journal de musculation (Hevy / Liftosaur)" + } } } }, @@ -51026,6 +57944,12 @@ "state": "translated", "value": "僅光照 + 睡眠時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lumière + horaires de sommeil uniquement" + } } } }, @@ -51060,6 +57984,12 @@ "state": "translated", "value": "淺睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil léger" + } } } }, @@ -51094,6 +58024,12 @@ "state": "translated", "value": "很可能是你的 Oura 保固和支援,因為戒指已不再與 Oura 配對。請將此視為永久性的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Probablement votre garantie et le support Oura, car la bague n'est plus jumelée à Oura. Considérez ceci comme permanent." + } } } }, @@ -51128,6 +58064,12 @@ "state": "translated", "value": "正在對齊它們…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alignement en cours…" + } } } }, @@ -51214,6 +58156,12 @@ "state": "translated", "value": "即時身體控制台" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Console corporelle en direct" + } } } }, @@ -51248,6 +58196,12 @@ "state": "translated", "value": "即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC en direct" + } } } }, @@ -51282,6 +58236,12 @@ "state": "translated", "value": "即時心率(未完全配對)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC en direct (jumelage incomplet)" + } } } }, @@ -51316,6 +58276,12 @@ "state": "translated", "value": "僅即時心率 + R-R · 沒有睡眠、恢復、皮膚溫度、SpO₂、步數或電量(那些僅限 WHOOP)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC en direct + R-R uniquement · pas de sommeil, récupération, température cutanée, SpO₂, pas ou batterie (réservés à WHOOP)." + } } } }, @@ -51350,6 +58316,12 @@ "state": "translated", "value": "即時心率可用,釋放手環以解鎖震動、鬧鐘和同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La FC en direct fonctionne. Libérez le bracelet pour débloquer vibrations, alarmes et synchro" + } } } }, @@ -51384,6 +58356,12 @@ "state": "translated", "value": "即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque en direct" + } } } }, @@ -51471,6 +58449,12 @@ "state": "translated", "value": "即時心率已能在 WHOOP 5/MG 手環上運作。這些探測會更進一步,嘗試從中挖掘更多。它們只是猜測,預設關閉,並且只會作用於 5/MG 手環,WHOOP 4.0 絕不受影響。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque en direct fonctionne déjà sur un bracelet WHOOP 5/MG. Ces sondes vont plus loin et tentent d'en tirer davantage. Ce sont des suppositions, désactivées par défaut, qui ne touchent jamais qu'un bracelet 5/MG. WHOOP 4.0 n'est jamais concerné." + } } } }, @@ -51505,6 +58489,12 @@ "state": "translated", "value": "即時心率和能量消耗,記錄在你的手腕上。留在裝置上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque et énergie en direct, enregistrées au poignet. Reste sur l'appareil." + } } } }, @@ -51539,6 +58529,12 @@ "state": "translated", "value": "連接手環後,即時心率會顯示在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque en direct apparaît ici une fois qu'un bracelet est connecté." + } } } }, @@ -51573,6 +58569,12 @@ "state": "translated", "value": "在動態島中顯示即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque en direct dans la Dynamic Island" + } } } }, @@ -51607,6 +58609,12 @@ "state": "translated", "value": "即時心率正在傳輸;完整的手環控制需要加密綁定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque en direct circule ; les contrôles complets du bracelet nécessitent une liaison chiffrée." + } } } }, @@ -51641,6 +58649,12 @@ "state": "translated", "value": "即時心率是即刻的。恢復、強度和睡眠會隨著 NOOP 在你最初幾晚的佩戴中學習你的基線而越來越準。想現在就看到歷史?在「資料來源」中匯入你的 WHOOP 匯出檔案,大約一分鐘就能回填。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque en direct est instantanée. Récupération, effort et sommeil s'affinent à mesure que NOOP apprend votre référence pendant vos premières nuits de port. Vous voulez votre historique tout de suite ? Importez votre export WHOOP dans Sources de données, il se recharge en environ une minute." + } } } }, @@ -51675,6 +58689,12 @@ "state": "translated", "value": "在無需配對的手環上顯示即時心率。幫我們測試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de jumelage. Aidez-nous à tester." + } } } }, @@ -51709,6 +58729,12 @@ "state": "translated", "value": "隨時間變化的即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque en direct dans le temps" + } } } }, @@ -51743,6 +58769,12 @@ "state": "translated", "value": "即時心率,以及戒指能測量時的 HRV。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque en direct, et VFC quand la bague peut la mesurer." + } } } }, @@ -51777,6 +58809,12 @@ "state": "translated", "value": "透過 Bluetooth FTMS 傳輸即時器械資料。沒有睡眠、恢復、皮膚溫度或 SpO₂。消耗需要器械的心率;沒有的話,該課次只記錄器械指標。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données de machine en direct via Bluetooth FTMS. Pas de sommeil, récupération, température cutanée ou SpO₂. L'Effort nécessite la fréquence cardiaque de la machine ; sans elle, la séance n'enregistre que les métriques de la machine." + } } } }, @@ -51915,6 +58953,12 @@ "state": "translated", "value": "正在載入當天…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chargement de la journée…" + } } } }, @@ -52105,6 +59149,12 @@ "state": "translated", "value": "本地 Oura 支援是新功能,我們無法在這裡測試每一枚戒指。它可能無法連接你的戒指,並且可能在不同更新之間發生變化。NOOP 絕不編造數字。如果有什麼無法運作,它會如實告訴你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le support Oura local est récent et nous ne pouvons pas tester chaque bague ici. Il se peut qu'il ne se connecte pas à votre bague, et cela peut changer d'une mise à jour à l'autre. NOOP n'invente jamais de chiffre. Si quelque chose ne fonctionne pas, il vous le dira clairement." + } } } }, @@ -52139,6 +59189,12 @@ "state": "translated", "value": "鎖定 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Verrouiller %@" + } } } }, @@ -52173,6 +59229,12 @@ "state": "translated", "value": "當我取下手環時鎖定 Mac" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Verrouiller le Mac quand je retire le bracelet" + } } } }, @@ -52207,6 +59269,12 @@ "state": "translated", "value": "已鎖定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Verrouillé" + } } } }, @@ -52241,6 +59309,12 @@ "state": "translated", "value": "已鎖定 %@ · 節奏會漂移,可隨時重新測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Verrouillé %@ · les rythmes évoluent, remesurez à tout moment." + } } } }, @@ -52275,6 +59349,12 @@ "state": "translated", "value": "已鎖定 %@。切換到「呼吸」以使用它,或在上方重新測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Verrouillé %@. Passez à Respirer pour l'utiliser, ou remesurez ci-dessus." + } } } }, @@ -52361,6 +59441,12 @@ "state": "translated", "value": "記錄 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer %@" + } } } }, @@ -52395,6 +59481,12 @@ "state": "translated", "value": "記錄一杯咖啡、茶或能量飲料,NOOP 會大致估算還有多少可能仍在起作用。這是基於典型的 5 到 6 小時半衰期的一個參考,而非測量值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez un café, un thé ou une boisson énergisante, et NOOP affiche une estimation approximative de ce qui pourrait encore être actif. C'est un repère basé sur une demi-vie type de 5 à 6 heures, pas une mesure." + } } } }, @@ -52429,6 +59521,12 @@ "state": "translated", "value": "記錄一次你在別處追蹤的工作階段。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez une séance suivie ailleurs." + } } } }, @@ -52463,6 +59561,12 @@ "state": "translated", "value": "記錄一個睡眠標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer un repère de sommeil" + } } } }, @@ -52497,6 +59601,12 @@ "state": "translated", "value": "記錄酒精或深夜咖啡因並附上份量,NOOP 會擬合出個人劑量曲線:每多一單位往往會讓你的數字移動多少。在那之前它會顯示典型模式,並清楚標示這還不是你自己的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez de l'alcool ou de la caféine tardive avec une quantité, et NOOP ajuste une courbe de dose personnelle : dans quelle mesure chaque unité supplémentaire a tendance à faire varier vos chiffres. En attendant, il affiche des schémas types, clairement indiqués comme n'étant pas encore les vôtres." + } } } }, @@ -52531,6 +59641,12 @@ "state": "translated", "value": "在開始實驗之前,請在上方至少記錄一項行為。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez au moins un comportement ci-dessus avant de lancer une expérience." + } } } }, @@ -52565,6 +59681,12 @@ "state": "translated", "value": "在上方記錄各項行為,在積累幾天的回答後,NOOP 會排列出每一項如何影響你的能量、HRV 和休息。匯入一份 WHOOP 匯出檔案(其中包含日誌)可立即回填歷史。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez des comportements ci-dessus. Après quelques jours de réponses, NOOP classe l'influence de chacun sur votre charge, votre VFC et votre repos. Importer un export WHOOP (qui inclut son journal) recharge instantanément l'historique." + } } } }, @@ -52599,6 +59721,12 @@ "state": "translated", "value": "記錄自訂 %lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer %lld millilitres personnalisés" + } } } }, @@ -52633,6 +59761,12 @@ "state": "translated", "value": "記錄入睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer l'endormissement" + } } } }, @@ -52667,6 +59801,12 @@ "state": "translated", "value": "記錄日誌" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer le journal" + } } } }, @@ -52701,6 +59841,12 @@ "state": "translated", "value": "記錄經期開始" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer le début des règles" + } } } }, @@ -52735,6 +59881,12 @@ "state": "translated", "value": "記錄醒來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer le réveil" + } } } }, @@ -52775,6 +59927,12 @@ "state": "translated", "value": "已於 %2$@ 記錄 %1$lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld millilitres enregistrés à %2$@" + } } } }, @@ -52809,6 +59967,12 @@ "state": "translated", "value": "已記錄為小睡。不對?點擊「編輯」以調整你的入睡和起床時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré comme sieste. Une erreur ? Touchez Modifier pour ajuster vos heures de coucher et de réveil." + } } } }, @@ -52843,6 +60007,12 @@ "state": "translated", "value": "已於 %@ 記錄就寢時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure de coucher enregistrée à %@." + } } } }, @@ -52877,6 +60047,12 @@ "state": "translated", "value": "跨 %@ 記錄的工作階段。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séances enregistrées sur %@." + } } } }, @@ -52911,6 +60087,12 @@ "state": "translated", "value": "今日已記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré aujourd'hui" + } } } }, @@ -52945,6 +60127,12 @@ "state": "translated", "value": "已於 %@ 記錄起床時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveil enregistré à %@." + } } } }, @@ -52979,6 +60167,12 @@ "state": "translated", "value": "為明天提前記錄:今天的活動會影響明天的恢復,正如昨天的活動反映在今天一樣。明天的回答與明天早晨對齊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrement anticipé pour demain : les activités d'aujourd'hui influencent la récupération de demain, tout comme celles d'hier se reflètent dans celle d'aujourd'hui. Les réponses de demain correspondront au matin de demain." + } } } }, @@ -53013,6 +60207,12 @@ "state": "translated", "value": "長" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Long" + } } } }, @@ -53047,6 +60247,12 @@ "state": "translated", "value": "長呼氣 · 降檔至休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expiration longue · bascule vers le repos" + } } } }, @@ -53081,6 +60287,12 @@ "state": "translated", "value": "看起來像一次鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressemble à un entraînement" + } } } }, @@ -53121,6 +60333,12 @@ "state": "translated", "value": "看起來像是 %@–%@ 左右的一次鍛鍊(平均心率 %lld,%lld 分鐘)。要儲存嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressemble à un entraînement vers %@–%@ (FC moy. %lld, %lld min). L'enregistrer ?" + } } } }, @@ -53161,6 +60379,12 @@ "state": "translated", "value": "看起來像是 %@ 當天 %@–%@ 左右的一次鍛鍊(平均心率 %lld,%lld 分鐘)。要儲存嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressemble à un entraînement le %1$@ vers %2$@–%3$@ (FC moy. %4$lld, %5$lld min). L'enregistrer ?" + } } } }, @@ -53201,6 +60425,12 @@ "state": "translated", "value": "看起來像是昨天 %@–%@ 左右的一次鍛鍊(平均心率 %lld,%lld 分鐘)。要儲存嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressemble à un entraînement hier vers %@–%@ (FC moy. %lld, %lld min). L'enregistrer ?" + } } } }, @@ -53287,6 +60517,12 @@ "state": "translated", "value": "電量不足" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie faible" + } } } }, @@ -53321,6 +60557,12 @@ "state": "translated", "value": "低信賴度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faible confiance" + } } } }, @@ -53355,6 +60597,12 @@ "state": "translated", "value": "低信賴度分期。這一晚的效率評分很高,但深睡或 REM 極少,更可能是估算失誤,而非真正的修復不足。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Répartition à faible confiance. Cette nuit affiche une efficacité élevée mais très peu de sommeil profond ou paradoxal, plus probablement une erreur d'estimation qu'un véritable déficit réparateur." + } } } }, @@ -53389,6 +60637,12 @@ "state": "translated", "value": "黃體期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lutéale" + } } } }, @@ -53423,6 +60677,12 @@ "state": "translated", "value": "M" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L" + } } } }, @@ -53457,6 +60717,12 @@ "state": "translated", "value": "中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MOYEN" + } } } }, @@ -53491,6 +60757,12 @@ "state": "translated", "value": "方法" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MÉTHODE" + } } } }, @@ -53525,6 +60797,12 @@ "state": "translated", "value": "主睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil principal" + } } } }, @@ -53559,6 +60837,12 @@ "state": "translated", "value": "維持" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir" + } } } }, @@ -53593,6 +60877,12 @@ "state": "translated", "value": "現在將 %@ 設為你的活躍裝置?它將提供你的即時資料。你可以隨時更改。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire de %@ votre appareil actif maintenant ? Il fournira vos données en direct. Vous pouvez changer cela à tout moment." + } } } }, @@ -53633,6 +60923,12 @@ "state": "translated", "value": "將 %1$@ 設為你的活躍手環?從現在起,它將提供你的即時資料。%2$@ 的歷史保持原樣,只有新的日期來自 %3$@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire de %1$@ votre bracelet actif ? Dès maintenant, il fournira vos données en direct. L'historique de %2$@ reste exactement tel quel. Seuls les nouveaux jours proviendront de %3$@." + } } } }, @@ -53667,6 +60963,12 @@ "state": "translated", "value": "設為活躍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rendre actif" + } } } }, @@ -53701,6 +61003,12 @@ "state": "translated", "value": "打造屬於你的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites-en le vôtre" + } } } }, @@ -53735,6 +61043,12 @@ "state": "translated", "value": "請確認它沒有已連接到其他 App(Zwift、健身房的 App、自行車碼錶…)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assurez-vous qu'il n'est pas déjà connecté à une autre application (Zwift, l'application de la salle, un compteur vélo…)." + } } } }, @@ -53769,6 +61083,12 @@ "state": "translated", "value": "請確認它沒有連接到其他 App(自行車碼錶、品牌自家的 App…)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assurez-vous qu'il n'est pas connecté à une autre application (un compteur vélo, l'application propre à la marque…)." + } } } }, @@ -53803,6 +61123,12 @@ "state": "translated", "value": "請確認它此刻「沒有」連接到官方 WHOOP App。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assurez-vous qu'il n'est PAS connecté à l'application WHOOP officielle en ce moment." + } } } }, @@ -53837,6 +61163,12 @@ "state": "translated", "value": "確保它已喚醒且未在別處連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assurez-vous qu'il est éveillé et non connecté ailleurs." + } } } }, @@ -53871,6 +61203,12 @@ "state": "translated", "value": "請確認 Oura App 已完全關閉。戒指一次只回應一個持有者。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assurez-vous que l'application Oura est complètement fermée. Une bague ne répond qu'à un seul propriétaire à la fois." + } } } }, @@ -53958,6 +61296,12 @@ "state": "translated", "value": "讓手環為你做事,輕點執行,走開即鎖定,憑感覺訓練。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites faire des choses à votre bracelet : touchez pour agir, éloignez-vous pour verrouiller, entraînez-vous au feeling." + } } } }, @@ -53992,6 +61336,12 @@ "state": "translated", "value": "將此設為你的活躍裝置?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire de cet appareil votre appareil actif ?" + } } } }, @@ -54026,6 +61376,12 @@ "state": "translated", "value": "將此設為你的活躍手環?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire de ce bracelet votre bracelet actif ?" + } } } }, @@ -54060,6 +61416,12 @@ "state": "translated", "value": "讓你的 WHOOP 5.0/MG 以標準藍牙心率感測器的形式廣播其心率,這樣 Garmin(Edge/手錶)、Zwift 或健身房器械就能在鍛鍊期間使用它。在下次連接時套用(若已連接則立即套用);會寫入手環的 whoop_live_hr_in_adv_ind_pkt 旗標。可逆。僅在 iPhone 端,Mac 無法向 5/MG 寫入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fait en sorte que votre WHOOP 5.0/MG diffuse sa fréquence cardiaque comme un capteur FC Bluetooth standard, afin qu'un Garmin (Edge/montre), Zwift ou un équipement de salle puisse l'utiliser pendant un entraînement. Appliqué à la prochaine connexion (et immédiatement si déjà connecté) ; modifie l'indicateur whoop_live_hr_in_adv_ind_pkt du bracelet. Réversible. Côté iPhone uniquement. Un Mac ne peut pas écrire sur un 5/MG." + } } } }, @@ -54146,6 +61508,12 @@ "state": "translated", "value": "管理裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gérer les appareils" + } } } }, @@ -54180,6 +61548,12 @@ "state": "translated", "value": "手動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manuel" + } } } }, @@ -54214,6 +61588,12 @@ "state": "translated", "value": "手動(由你親自設定)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manuel (défini manuellement)" + } } } }, @@ -54300,6 +61680,12 @@ "state": "translated", "value": "手動步數係數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coefficient manuel des pas" + } } } }, @@ -54340,6 +61726,12 @@ "state": "translated", "value": "你的 %@ 路線地圖,%@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Carte de votre parcours de %1$@, %2$@." + } } } }, @@ -54374,6 +61766,12 @@ "state": "translated", "value": "3月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mars" + } } } }, @@ -54408,6 +61806,12 @@ "state": "translated", "value": "標記一個時刻" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marquer un moment" + } } } }, @@ -54442,6 +61846,12 @@ "state": "translated", "value": "標記一個時刻" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marquer un moment" + } } } }, @@ -54476,6 +61886,12 @@ "state": "translated", "value": "全部標為已讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout marquer comme lu" + } } } }, @@ -54510,6 +61926,12 @@ "state": "translated", "value": "標記今天已完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marquer comme fait aujourd'hui" + } } } }, @@ -54544,6 +61966,12 @@ "state": "translated", "value": "標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Repère" + } } } }, @@ -54630,6 +62058,12 @@ "state": "translated", "value": "匹配" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Correspondance" + } } } }, @@ -54820,6 +62254,12 @@ "state": "translated", "value": "最大心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque maximale" + } } } }, @@ -55010,6 +62450,12 @@ "state": "translated", "value": "最大值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maximum" + } } } }, @@ -55044,6 +62490,12 @@ "state": "translated", "value": "5月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mai" + } } } }, @@ -55078,6 +62530,12 @@ "state": "translated", "value": "平均值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyenne" + } } } }, @@ -55165,6 +62623,12 @@ "state": "translated", "value": "平均心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC moyenne" + } } } }, @@ -55199,6 +62663,12 @@ "state": "translated", "value": "度量系統" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Système de mesure" + } } } }, @@ -55233,6 +62703,12 @@ "state": "translated", "value": "測量中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure en cours…" + } } } }, @@ -55267,6 +62743,12 @@ "state": "translated", "value": "中等" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moyen" + } } } }, @@ -55301,6 +62783,12 @@ "state": "translated", "value": "選單和設定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Menu et paramètres" + } } } }, @@ -55491,6 +62979,12 @@ "state": "translated", "value": "小米手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mi Band" + } } } }, @@ -55525,6 +63019,12 @@ "state": "translated", "value": "週期中段變化" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Décalage en milieu de cycle" + } } } }, @@ -55611,6 +63111,12 @@ "state": "translated", "value": "心智" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Esprit" + } } } }, @@ -55645,6 +63151,12 @@ "state": "translated", "value": "久坐多少分鐘後發出第一次提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minutes assis avant la première relance." + } } } }, @@ -55685,6 +63197,12 @@ "state": "translated", "value": "最小–最大正規化 · 稀疏序列在 %@ 之後已加寬 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Normalisé min-max · séries clairsemées élargies au-delà de %1$@ · %2$@" + } } } }, @@ -55719,6 +63237,12 @@ "state": "translated", "value": "位於 noop dot fans 的鏡像,當 GitHub 故障時的備選方案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Miroir sur noop point fans, une solution de secours si GitHub est indisponible" + } } } }, @@ -55753,6 +63277,12 @@ "state": "translated", "value": "鏡像,noop.fans" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Miroir : noop.fans" + } } } }, @@ -55787,6 +63317,12 @@ "state": "translated", "value": "缺少" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manquant" + } } } }, @@ -55873,6 +63409,12 @@ "state": "translated", "value": "中等" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modéré" + } } } }, @@ -55907,6 +63449,12 @@ "state": "translated", "value": "時刻已標記。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moment marqué." + } } } }, @@ -55941,6 +63489,12 @@ "state": "translated", "value": "週一" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lun" + } } } }, @@ -55975,6 +63529,12 @@ "state": "translated", "value": "星期一" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lundi" + } } } }, @@ -56009,6 +63569,12 @@ "state": "translated", "value": "心情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Humeur" + } } } }, @@ -56043,6 +63609,12 @@ "state": "translated", "value": "心情,與你身體的訊號並列" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Humeur, aux côtés des signaux de votre corps" + } } } }, @@ -56077,6 +63649,12 @@ "state": "translated", "value": "更多" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus" + } } } }, @@ -56111,6 +63689,12 @@ "state": "translated", "value": "早晨" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Matin" + } } } }, @@ -56203,6 +63787,12 @@ "state": "translated", "value": "主要是 %1$@,%2$@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Principalement %1$@ (%2$@)." + } } } }, @@ -56237,6 +63827,12 @@ "state": "translated", "value": "大致平穩,只有少數零星的多跳或漏跳。非常常見,通常沒什麼。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Globalement régulier, avec quelques battements supplémentaires ou manqués isolés. Très courant et généralement sans gravité." + } } } }, @@ -56271,6 +63867,12 @@ "state": "translated", "value": "運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mouvement" + } } } }, @@ -56305,6 +63907,12 @@ "state": "translated", "value": "移動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bouger" + } } } }, @@ -56339,6 +63947,12 @@ "state": "translated", "value": "將 %@ 下移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacer %@ vers le bas" + } } } }, @@ -56373,6 +63987,12 @@ "state": "translated", "value": "將 %@ 上移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacer %@ vers le haut" + } } } }, @@ -56407,6 +64027,12 @@ "state": "translated", "value": "將你所有的 NOOP 資料遷移到另一台機器。匯出會把一切,歷史、睡眠、鍛鍊、設定,保存到一個可供複製的單一檔案中;匯入則用備份替換 %@ 的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacez toutes vos données NOOP vers un autre appareil. L'export enregistre tout (historique, sommeils, entraînements, paramètres) dans un seul fichier que vous pouvez copier ; l'import remplace les données de %@ par une sauvegarde." + } } } }, @@ -56494,6 +64120,12 @@ "state": "translated", "value": "活動提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rappel de mouvement" + } } } }, @@ -56580,6 +64212,12 @@ "state": "translated", "value": "夜間靜音手腕提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couper les alertes au poignet pendant la nuit." + } } } }, @@ -56614,6 +64252,12 @@ "state": "translated", "value": "我的儀表板" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mon tableau de bord" + } } } }, @@ -56648,6 +64292,12 @@ "state": "translated", "value": "我的 WHOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mon WHOOP" + } } } }, @@ -56682,6 +64332,12 @@ "state": "translated", "value": "單人 (N-of-1) 試驗方案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Protocole N-de-1" + } } } }, @@ -56716,6 +64372,12 @@ "state": "translated", "value": "NES / HUNT VO2MAX" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NES / HUNT VO2MAX" + } } } }, @@ -56750,6 +64412,12 @@ "state": "translated", "value": "新" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOUVEAU" + } } } }, @@ -56940,6 +64608,12 @@ "state": "translated", "value": "NOOP %@ 來了。點擊查看新功能。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP %@ est arrivé. Appuyez pour découvrir les nouveautés." + } } } }, @@ -56974,6 +64648,12 @@ "state": "translated", "value": "NOOP 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge NOOP" + } } } }, @@ -57008,6 +64688,12 @@ "state": "translated", "value": "NOOP 可以從你的夜間皮膚溫度中讀取粗略的月經週期階段,完全在你的裝置上。它僅供了解參考:不是避孕手段,不是生育力預測工具,也不是醫療服務。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP peut estimer une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur votre appareil. C'est uniquement informatif : ni contraception, ni prédicteur de fertilité, ni service médical." + } } } }, @@ -57042,6 +64728,12 @@ "state": "translated", "value": "NOOP 可以僅靠一塊 Apple Watch 運行,無需胸帶。手錶是感測器;NOOP 在你的手機上做運算,從你的健康資料中計算出能量、休息、消耗和你的體能年齡,全部在裝置本地。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP peut fonctionner avec une simple Apple Watch, sans ceinture pectorale. La montre est le capteur ; NOOP fait les calculs sur votre téléphone, en calculant Charge, Repos, Effort et votre âge de forme physique à partir de vos données Santé, le tout sur l'appareil." + } } } }, @@ -57076,6 +64768,12 @@ "state": "translated", "value": "當你的 %@ 需要你時,NOOP 可以輕拍你的手腕。無需看螢幕。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP peut tapoter votre poignet quand votre %@ a besoin de vous. Pas besoin de regarder l'écran." + } } } }, @@ -57110,6 +64808,12 @@ "state": "translated", "value": "由 NOOP 計算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé par NOOP" + } } } }, @@ -57144,6 +64848,12 @@ "state": "translated", "value": "NOOP 會根據你 WHOOP 的運動來估算步數,並以你手機的步數進行校準。這是一個估算值,而非計步器,WHOOP 4.0 不傳輸步數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP estime vos pas à partir du mouvement de votre WHOOP, calibré sur le nombre de pas de votre téléphone. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas." + } } } }, @@ -57178,6 +64888,12 @@ "state": "translated", "value": "NOOP 為你提供三個每日評分,能量、消耗和休息,每個都採用 0–100 的量表。它們根據你手環的原始訊號,運用已發表的、經同行評審的運動科學構建,並完全在你的裝置上計算。它們不是 WHOOP 的評分:我們沒有 WHOOP 的私有演算法,也不假裝擁有。它們運用開放科學瞄準同樣的三個問題,因此通常在方向上會與 WHOOP 的一致,但不會數字對數字地吻合,而這正是重點所在。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP vous donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de votre bracelet en utilisant une science du sport publiée et évaluée par des pairs, et calculés entièrement sur votre appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les mêmes trois questions avec une science ouverte, donc ils suivront généralement la direction de ceux de WHOOP, mais ne correspondront pas chiffre pour chiffre. Et c'est bien le but." + } } } }, @@ -57212,6 +64928,12 @@ "state": "translated", "value": "NOOP 是一個個人開放專案:它不是 WHOOP App,也與 WHOOP 沒有任何關聯。它在你自己的裝置上讀取你擁有的手環。請把它當作一個有實力但仍在進行中的作品,而非成品。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP est un projet personnel et ouvert : ce n'est pas l'application WHOOP et il n'est pas affilié à WHOOP. Il lit un bracelet que vous possédez, sur votre propre appareil. Considérez-le comme un travail en cours capable plutôt qu'un produit fini." + } } } }, @@ -57223,6 +64945,12 @@ "state": "translated", "value": "NOOP ist ein inoffizielles Projekt: nicht mit WHOOP, Inc. verbunden, nicht von ihr unterstützt oder gesponsert. „WHOOP“ ist deren Marke und wird nur verwendet, um die Hardware zu benennen, mit der NOOP arbeitet." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP est un projet non officiel : il n'est ni affilié, ni approuvé, ni sponsorisé par WHOOP, Inc. « WHOOP » est leur marque déposée, utilisée uniquement pour nommer le matériel avec lequel NOOP fonctionne." + } } } }, @@ -57234,6 +64962,12 @@ "state": "translated", "value": "NOOP ist kostenlos und wird „wie besehen“ bereitgestellt, ohne Gewährleistung. Die Haftung ist im maximal zulässigen Umfang des für dich geltenden Rechts begrenzt, und nichts hier nimmt dir Schutz, den dein lokales Recht uns nicht zu nehmen erlaubt." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP est gratuit et fourni « tel quel », sans garantie. La responsabilité est limitée dans la mesure maximale permise par la loi qui vous est applicable, et rien ici ne supprime les protections que votre législation locale ne nous permet pas de supprimer." + } } } }, @@ -57320,6 +65054,12 @@ "state": "translated", "value": "NOOP 正在打開你裝置上的資料。你已配對的手環稍後會出現在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP ouvre vos données sur l'appareil. Vos bracelets couplés apparaîtront ici dans un instant." + } } } }, @@ -57354,6 +65094,12 @@ "state": "translated", "value": "NOOP 會在 %@ 上尋找已知的通知 App:Mail、Outlook、WhatsApp、Teams、訊息、Slack 等等。安裝其中一個,它就會自動出現在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP recherche les applications de notification connues sur %@ : Mail, Outlook, WhatsApp, Teams, Messages, Slack et similaires. Installez-en une et elle apparaîtra ici automatiquement." + } } } }, @@ -57388,6 +65134,12 @@ "state": "translated", "value": "NOOP 會尋找廣播標準 Bluetooth 健身器材服務的機器。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP recherche les appareils qui diffusent le service Bluetooth Fitness Machine standard." + } } } }, @@ -57422,6 +65174,12 @@ "state": "translated", "value": "NOOP 絕不判定數值是「正常」、「偏高」還是「偏低」。顯示的任何參考範圍都完全來自你從自己報告中輸入的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP ne décide jamais si une valeur est « normale », « élevée » ou « basse ». Toute plage de référence affichée est exactement ce que vous avez saisi depuis votre propre rapport." + } } } }, @@ -57456,6 +65214,12 @@ "state": "translated", "value": "NOOP 絕不會填寫此項,它只會原樣顯示你根據自己的報告所輸入的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP ne remplit jamais ce champ lui-même. Il affiche uniquement ce que vous saisissez depuis votre propre rapport." + } } } }, @@ -57490,6 +65254,12 @@ "state": "translated", "value": "NOOP 絕不編造數字。當它無法誠實地計算出一個數字時,它會告訴你缺少什麼以及該怎麼做,而不是顯示一個虛假的數值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP n'invente jamais de chiffre. Quand il ne peut pas en calculer un honnêtement, il vous dit ce qui manque et quoi faire, plutôt que d'afficher une fausse valeur." + } } } }, @@ -57524,6 +65294,12 @@ "state": "translated", "value": "NOOP 絕不會向你顯示它不得不編造的數字。如果某個評分尚未就緒,它會告訴你原因以及下一步該怎麼做。這裡的一切都在你的裝置上運行,源自你的手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP ne vous montre jamais un chiffre qu'il a dû inventer. Si un score n'est pas prêt, il vous explique pourquoi et quoi faire ensuite. Tout ici fonctionne sur votre appareil, à partir de votre bracelet." + } } } }, @@ -57611,6 +65387,12 @@ "state": "translated", "value": "NOOP 透過藍牙直接與你的 WHOOP 配對,無需 WHOOP 應用程式,無需雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP se couple directement à votre WHOOP via Bluetooth : pas d'application WHOOP, pas de cloud." + } } } }, @@ -57645,6 +65427,12 @@ "state": "translated", "value": "NOOP 會挑選來源最佳的數字,並向你顯示每一個的來處。它用於健康和求知,它不診斷,也不替代醫療建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP choisit le chiffre le mieux sourcé et vous montre d'où vient chacun. C'est pour le bien-être et la curiosité. Il ne diagnostique pas et ne remplace pas un avis médical." + } } } }, @@ -57679,6 +65467,12 @@ "state": "translated", "value": "NOOP 會把你最長的真實睡眠時段選為主要睡眠,而在學會你的作息之後,會選最接近你平常入睡時間的那段。當天其餘的都算小睡。你隨時可以編輯就寢與起床時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP choisit comme sommeil principal votre plus long bloc réel, et (une fois qu'il a appris vos horaires habituels) celui le plus proche de votre heure de sommeil normale. Tout le reste ce jour-là est une sieste. Vous pouvez toujours modifier les heures de coucher et de réveil." + } } } }, @@ -57713,6 +65507,12 @@ "state": "translated", "value": "NOOP 可以在不需配對的手環上讀取即時心率。較新的手環需要我們目前還做不到的驗證交握。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP lit la fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de couplage. Les bracelets plus récents nécessitent une authentification que nous ne pouvons pas encore effectuer." + } } } }, @@ -57747,6 +65547,12 @@ "state": "translated", "value": "只要手環有開放,NOOP 就能讀取即時心率。有些手環需要我們目前還做不到的配對。如果是這樣,我們會誠實告訴你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP lit la fréquence cardiaque en direct quand le bracelet l'expose. Certains bracelets nécessitent un couplage que nous ne pouvons pas encore effectuer. Si c'est le cas, nous vous le dirons honnêtement." + } } } }, @@ -57781,6 +65587,12 @@ "state": "translated", "value": "NOOP 透過 Apple 健康在你的手機上讀取你的 Apple Watch 資料,沒有任何內容離開裝置。你可以確切地選擇要分享什麼。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP lit les données de votre Apple Watch via Apple Santé, sur votre téléphone, rien ne quitte l'appareil. Vous choisissez exactement ce que vous partagez." + } } } }, @@ -57815,6 +65627,12 @@ "state": "translated", "value": "NOOP 從 Apple 健康讀取你的心率、HRV、靜息心率、睡眠、步數、能量和最大攝氧量來計算你的評分。一切都留在這台 iPhone 上,你可以在下一畫面確切地選擇要分享什麼。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP lit votre fréquence cardiaque, VFC, fréquence cardiaque au repos, sommeil, pas, énergie et VO₂ max depuis Apple Santé pour calculer vos scores. Tout reste sur cet iPhone, et vous choisissez exactement quoi partager à l'écran suivant." + } } } }, @@ -57849,6 +65667,12 @@ "state": "translated", "value": "NOOP 自行計算你的能量、消耗和休息評分,在裝置本地,無需雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP calcule lui-même votre charge, votre effort et votre repos : sur l'appareil, sans cloud." + } } } }, @@ -57942,6 +65766,12 @@ "state": "translated", "value": "NOOP 顯示 %1$@ 的讀數,因為對於此項指標它 %2$@,在這裡是一個更可信的來源,而非判定其他來源是錯的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP affiche la mesure de %1$@ car il %2$@ pour cette métrique : une source plus fiable ici, pas un verdict que les autres se trompent." + } } } }, @@ -57976,6 +65806,12 @@ "state": "translated", "value": "NOOP 只儲存並整理你自己輸入的數字。它不會為你檢測、解讀你的結果、提供醫療建議或做任何診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP stocke et aligne les chiffres que vous saisissez vous-même. Il ne vous teste pas, ne lit pas vos résultats, ne donne pas d'avis médical et ne diagnostique rien." + } } } }, @@ -58010,6 +65846,12 @@ "state": "translated", "value": "NOOP 只將此金鑰儲存在這台裝置上,與它儲存你已配對手環的位置相同。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP stocke cette clé uniquement sur cet appareil, au même endroit que vos bracelets couplés." + } } } }, @@ -58044,6 +65886,12 @@ "state": "translated", "value": "NOOP 透過低功耗 Bluetooth 直接與你的手環通信。中間沒有任何伺服器。連接是本機的,讀進來的每一筆資料也是。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP communique directement avec votre bracelet via Bluetooth Low Energy. Il n'y a aucun serveur intermédiaire. La connexion est locale, tout comme chaque mesure qu'il récupère." + } } } }, @@ -58055,6 +65903,12 @@ "state": "translated", "value": "NOOP spricht mit der Firmware deines Straps über ein inoffizielles, unabhängig rekonstruiertes Protokoll. Es bleibt ein Restrisiko für das Gerät, seine Daten und seine Verbindung zu offiziellen Diensten. Dieses Risiko übernimmst du." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP communique avec le micrologiciel de votre bracelet via un protocole non officiel, cartographié de manière indépendante. Il subsiste un risque résiduel pour l'appareil, ses données et sa connexion aux services officiels. Vous assumez ce risque." + } } } }, @@ -58089,6 +65943,12 @@ "state": "translated", "value": "NOOP 會輕拍你的手環,讓提醒落在手腕上而不是螢幕上。不必伸手拿手機。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP tapote votre bracelet pour qu'une alerte arrive sur votre poignet plutôt que sur votre écran. Pas besoin de le prendre en main. Tout reste sur %@." + } } } }, @@ -58123,6 +65983,12 @@ "state": "translated", "value": "NOOP 會在戒指上安裝自己的金鑰並成為其持有者。Oura 應用程式將不再控制這枚戒指。這是有意為之,且無法從 NOOP 撤銷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP installera sa propre clé sur la bague et en deviendra le propriétaire. L'application Oura ne contrôlera plus cette bague. C'est voulu et cela ne peut pas être annulé depuis NOOP." + } } } }, @@ -58157,6 +66023,12 @@ "state": "translated", "value": "NOOP 會在附近尋找它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP le recherchera à proximité." + } } } }, @@ -58191,6 +66063,12 @@ "state": "translated", "value": "NOOP 會從今晚的資料開始重新學習你的基線。你的歷史記錄會保留,需要幾個晚上才能穩定下來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP réapprendra votre référence à partir des données de cette nuit. Votre historique est conservé, et il faut quelques nuits pour se stabiliser." + } } } }, @@ -58225,6 +66103,12 @@ "state": "translated", "value": "NOOP 自己的能量、消耗與休息,依據公開發表的方法在你的裝置上計算。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La Charge, l'Effort et le Repos propres à NOOP, calculés sur votre appareil selon des méthodes publiées." + } } } }, @@ -58259,6 +66143,12 @@ "state": "translated", "value": "NOOP 主要、完整支援的手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le bracelet principal de NOOP, entièrement pris en charge" + } } } }, @@ -58399,6 +66289,12 @@ "state": "translated", "value": "NOOP,你的全部資料,不上雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP : toutes vos données, aucun cloud." + } } } }, @@ -58433,6 +66329,12 @@ "state": "translated", "value": "名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom" + } } } }, @@ -58467,6 +66369,12 @@ "state": "translated", "value": "命名並確認" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nommer et confirmer" + } } } }, @@ -58501,6 +66409,12 @@ "state": "translated", "value": "小睡結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sieste terminée" + } } } }, @@ -58535,6 +66449,12 @@ "state": "translated", "value": "小睡開始" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sieste commencée" + } } } }, @@ -58569,6 +66489,12 @@ "state": "translated", "value": "小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sieste(s)" + } } } }, @@ -58603,6 +66529,12 @@ "state": "translated", "value": "小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Siestes" + } } } }, @@ -58637,6 +66569,12 @@ "state": "translated", "value": "藏青色" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marine" + } } } }, @@ -58671,6 +66609,12 @@ "state": "translated", "value": "需要至少 14 天的歷史記錄。當兩個以上的訊號同時偏移時,你會在儀表板上看到橫幅並收到通知,每天最多一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite au moins 14 jours d'historique. Quand deux signaux ou plus dérivent ensemble, vous recevez une bannière sur le tableau de bord et une notification, au maximum une fois par jour." + } } } }, @@ -58705,6 +66649,12 @@ "state": "translated", "value": "需要間隔幀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite des trames d'intervalle" + } } } }, @@ -58739,6 +66689,12 @@ "state": "translated", "value": "需要更多資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite plus de données" + } } } }, @@ -58773,6 +66729,12 @@ "state": "translated", "value": "需要完整的加密綁定:請先關閉官方 WHOOP App,並將手環與 NOOP 配對(僅即時心率的連接無法承載解鎖)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite le couplage chiffré complet : fermez l'application WHOOP officielle et couplez d'abord le bracelet à NOOP (une liaison FC en direct seule ne peut pas transporter le déverrouillage)." + } } } }, @@ -58859,6 +66821,12 @@ "state": "translated", "value": "需要手環。今天沒有資料。昨晚你的手環有戴著並保持連接嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite le bracelet. Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?" + } } } }, @@ -58893,6 +66861,12 @@ "state": "translated", "value": "中性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Neutre" + } } } }, @@ -58927,6 +66901,12 @@ "state": "translated", "value": "永不" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jamais" + } } } }, @@ -58961,6 +66941,12 @@ "state": "translated", "value": "已新增資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvelles données ajoutées" + } } } }, @@ -58995,6 +66981,12 @@ "state": "translated", "value": "初次使用?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau ici ?" + } } } }, @@ -59029,6 +67021,12 @@ "state": "translated", "value": "新的版本說明和最新資料會出現在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les nouvelles notes de version et les données récentes arriveront ici." + } } } }, @@ -59063,6 +67061,12 @@ "state": "translated", "value": "新的手環名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau nom du bracelet" + } } } }, @@ -59097,6 +67101,12 @@ "state": "translated", "value": "較新的 WHOOP 手環,在 NOOP 中為實驗性支援" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet WHOOP plus récent, expérimental dans NOOP" + } } } }, @@ -59131,6 +67141,12 @@ "state": "translated", "value": "次日" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jour suivant" + } } } }, @@ -59165,6 +67181,12 @@ "state": "translated", "value": "次日早晨" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Matin suivant" + } } } }, @@ -59199,6 +67221,12 @@ "state": "translated", "value": "次日夜晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit suivante" + } } } }, @@ -59233,6 +67261,12 @@ "state": "translated", "value": "下週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Semaine suivante" + } } } }, @@ -59371,6 +67405,12 @@ "state": "translated", "value": "否" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non" + } } } }, @@ -59405,6 +67445,12 @@ "state": "translated", "value": "這裡沒有 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun %@ ici" + } } } }, @@ -59439,6 +67485,12 @@ "state": "translated", "value": "尚無能量明細" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun détail de Charge pour l'instant" + } } } }, @@ -59473,6 +67525,12 @@ "state": "translated", "value": "無資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée" + } } } }, @@ -59507,6 +67565,12 @@ "state": "translated", "value": "無 HRV 數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune valeur de VFC" + } } } }, @@ -59541,6 +67605,12 @@ "state": "translated", "value": "無 RMSSD · R-R 資料不足" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun RMSSD · données R-R insuffisantes" + } } } }, @@ -59575,6 +67645,12 @@ "state": "translated", "value": "無 SpO₂ 匯入或健康數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun import SpO₂ ni valeur Santé" + } } } }, @@ -59609,6 +67685,12 @@ "state": "translated", "value": "沒有 WHOOP?你仍然可以繼續。在「裝置」下配對一條心率帶(Polar、Wahoo、Coospo、Garmin HRM…)或一台健身房器械,或在「資料來源」下從 WHOOP、Apple 健康、Oura、Fitbit、Garmin 等匯入。兩者你都可以隨時進行。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas de WHOOP ? Vous pouvez quand même continuer. Couplez une ceinture cardio (Polar, Wahoo, Coospo, Garmin HRM…) ou une machine de salle dans Appareils, ou importez depuis WHOOP, Apple Santé, Oura, Fitbit, Garmin et plus dans Sources de données. Vous pouvez faire l'un ou l'autre à tout moment." + } } } }, @@ -59643,6 +67725,12 @@ "state": "translated", "value": "沒有帳號、沒有雲端、沒有同步。NOOP 只與你的手環通信,一切都留在本機。你的資料只屬於你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun compte, aucun cloud, aucune synchronisation. NOOP ne communique qu'avec votre bracelet et garde tout en local. Vos données n'appartiennent qu'à vous." + } } } }, @@ -59677,6 +67765,12 @@ "state": "translated", "value": "無進行中的串流" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun flux actif" + } } } }, @@ -59711,6 +67805,12 @@ "state": "translated", "value": "尚無備份。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune sauvegarde pour l'instant." + } } } }, @@ -59745,6 +67845,12 @@ "state": "translated", "value": "找不到備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune sauvegarde trouvée" + } } } }, @@ -59831,6 +67937,12 @@ "state": "translated", "value": "未記錄咖啡因。記錄一次攝入以查看估算。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune caféine enregistrée. Enregistrez une prise pour voir une estimation." + } } } }, @@ -59865,6 +67977,12 @@ "state": "translated", "value": "尚無心肺負荷。當你的心率進入消耗區間(約為心率儲備的 50%)後,消耗才會累積。平靜的一天老實說接近零。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune charge cardio pour l'instant. L'Effort augmente quand votre fréquence cardiaque atteint votre zone d'effort (environ 50 % de votre réserve de fréquence cardiaque). Une journée calme affiche honnêtement une valeur proche de zéro." + } } } }, @@ -59899,6 +68017,12 @@ "state": "translated", "value": "沒有胸帶?沒問題。NOOP 可以僅靠你的 Apple Watch 運行。它透過 Apple 健康讀取你手錶的資料,並就在你的手機上計算出你的能量、休息、消耗和體能年齡。一切都留在裝置上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas de ceinture pectorale ? Aucun problème. NOOP peut fonctionner avec votre seule Apple Watch. Il lit les données de votre montre via Apple Santé et calcule votre Charge, Repos, Effort et âge de forme physique directement sur votre téléphone. Tout reste sur l'appareil." + } } } }, @@ -59933,6 +68057,12 @@ "state": "translated", "value": "沒有明顯模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun schéma clair" + } } } }, @@ -59967,6 +68097,12 @@ "state": "translated", "value": "尚無清晰讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune mesure claire pour l'instant" + } } } }, @@ -60001,6 +68137,12 @@ "state": "translated", "value": "尚無已完成的卸載" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun déchargement terminé pour l'instant" + } } } }, @@ -60139,6 +68281,12 @@ "state": "translated", "value": "今天沒有資料。你的手環整晚都佩戴並保持連接了嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?" + } } } }, @@ -60173,6 +68321,12 @@ "state": "translated", "value": "還沒有 NOOP 和你手機同時統計步數的日期。一旦你的手機與手環一起記錄了幾天,它們就會出現在這裡,讓你看到估算有多接近。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jour pour l'instant où NOOP et votre téléphone ont tous deux compté les pas. Une fois que votre téléphone enregistre quelques jours en parallèle du bracelet, ils apparaîtront ici pour voir la précision de l'estimation." + } } } }, @@ -60207,6 +68361,12 @@ "state": "translated", "value": "尚未記錄任何飲品。點擊「一口」「一杯」或「一瓶」開始。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune boisson enregistrée pour l'instant. Appuyez sur Gorgée, Tasse ou Bouteille pour commencer." + } } } }, @@ -60241,6 +68401,12 @@ "state": "translated", "value": "尚未儲存更早的夜晚。更早的夜晚會在早晨同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune nuit antérieure stockée pour l'instant. Les nuits précédentes se synchronisent le matin." + } } } }, @@ -60275,6 +68441,12 @@ "state": "translated", "value": "今晚沒有額外的。你的 %@ 預測維持不變。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien d'extra ce soir. Votre prévision de %@ reste inchangée." + } } } }, @@ -60309,6 +68481,12 @@ "state": "translated", "value": "尚未選擇資料夾。選擇一個你的雲端應用程式已在同步的資料夾,或任意本地資料夾。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun dossier choisi pour l'instant. Choisissez-en un que votre application cloud synchronise déjà, ou n'importe quel dossier local." + } } } }, @@ -60343,6 +68521,12 @@ "state": "translated", "value": "這一天沒有心率資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune fréquence cardiaque pour ce jour" + } } } }, @@ -60377,6 +68561,12 @@ "state": "translated", "value": "尚無歷史。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun historique pour l'instant." + } } } }, @@ -60411,6 +68601,12 @@ "state": "translated", "value": "無可用的 iOS 診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun diagnostic iOS disponible." + } } } }, @@ -60445,6 +68641,12 @@ "state": "translated", "value": "今天尚無日間壓力資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée de stress intrajournalière aujourd'hui pour l'instant." + } } } }, @@ -60479,6 +68681,12 @@ "state": "translated", "value": "尚無即時心率。打開「即時」以配對你的手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune fréquence cardiaque en direct pour l'instant. Ouvrez En direct pour coupler votre bracelet." + } } } }, @@ -60513,6 +68721,12 @@ "state": "translated", "value": "無即時串流。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun flux en direct." + } } } }, @@ -60547,6 +68761,12 @@ "state": "translated", "value": "無匹配項。請在下方將其新增為自訂標記。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune correspondance. Ajoutez-le comme marqueur personnalisé ci-dessous." + } } } }, @@ -60581,6 +68801,12 @@ "state": "translated", "value": "尚無指標。匯入你的 Whoop 匯出檔,或戴上手環開始。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune métrique pour l'instant. Importez votre export Whoop ou portez le bracelet pour commencer." + } } } }, @@ -60615,6 +68841,12 @@ "state": "translated", "value": "尚未同步運動資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun mouvement synchronisé pour l'instant" + } } } }, @@ -60649,6 +68881,12 @@ "state": "translated", "value": "這一晚沒有體動詳情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun détail de mouvement pour cette nuit" + } } } }, @@ -60683,6 +68921,12 @@ "state": "translated", "value": "這一晚未記錄體動詳情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun détail de mouvement enregistré pour cette nuit" + } } } }, @@ -60717,6 +68961,12 @@ "state": "translated", "value": "這一天未記錄小睡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune sieste enregistrée pour ce jour." + } } } }, @@ -60751,6 +69001,12 @@ "state": "translated", "value": "無夜間皮膚溫度數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune valeur de température cutanée nocturne" + } } } }, @@ -60837,6 +69093,12 @@ "state": "translated", "value": "尚無有睡眠資料的夜晚,隨著你佩戴手環入睡,你的記錄會逐漸填滿。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune nuit avec données de sommeil pour l'instant. Votre registre se remplit à mesure que vous portez le bracelet pour dormir." + } } } }, @@ -60871,6 +69133,12 @@ "state": "translated", "value": "尚無數值讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune mesure numérique pour l'instant." + } } } }, @@ -60905,6 +69173,12 @@ "state": "translated", "value": "尚無裝置上的儲存。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun stockage sur l'appareil pour l'instant." + } } } }, @@ -60939,6 +69213,12 @@ "state": "translated", "value": "此標記與 %@ 之間尚無重疊。請多記錄幾筆讀數(並繼續佩戴你的手環)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun recoupement pour l'instant entre ce marqueur et %@. Enregistrez quelques mesures supplémentaires (et continuez à porter votre bracelet)." + } } } }, @@ -60973,6 +69253,12 @@ "state": "translated", "value": "未設定頭像照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune photo de profil définie" + } } } }, @@ -61007,6 +69293,12 @@ "state": "translated", "value": "本次工作階段尚未記錄任何原始擷取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune capture brute n'a été enregistrée pendant cette session pour l'instant." + } } } }, @@ -61093,6 +69385,12 @@ "state": "translated", "value": "本週無讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune mesure cette semaine" + } } } }, @@ -61127,6 +69425,12 @@ "state": "translated", "value": "本週暫無讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune mesure cette semaine pour l'instant" + } } } }, @@ -61161,6 +69465,12 @@ "state": "translated", "value": "無呼吸頻率數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune valeur de fréquence respiratoire" + } } } }, @@ -61195,6 +69505,12 @@ "state": "translated", "value": "無靜息心率數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune valeur de fréquence cardiaque au repos" + } } } }, @@ -61229,6 +69545,12 @@ "state": "translated", "value": "此視窗內沒有已評分的日期。請擴大範圍或匯入更多歷史。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jour avec score dans cette période. Élargissez la plage ou importez plus d'historique." + } } } }, @@ -61263,6 +69585,12 @@ "state": "translated", "value": "尚無來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune source pour l'instant" + } } } }, @@ -61297,6 +69625,12 @@ "state": "translated", "value": "這一晚未記錄階段資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée de phase enregistrée pour cette nuit." + } } } }, @@ -61331,6 +69665,12 @@ "state": "translated", "value": "未連接手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun bracelet connecté" + } } } }, @@ -61365,6 +69705,12 @@ "state": "translated", "value": "未連接手環。將你的 WHOOP 放在附近,然後點擊「重新掃描」以配對。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun bracelet connecté. Placez votre WHOOP à proximité et appuyez sur Rescanner pour le coupler." + } } } }, @@ -61451,6 +69797,12 @@ "state": "translated", "value": "找不到支援的 App" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune application prise en charge trouvée" + } } } }, @@ -61485,6 +69837,12 @@ "state": "translated", "value": "找不到可用的活動。請指向 .gpx、.tcx 或 .fit 鍛鍊檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune activité utilisable trouvée. Indiquez un fichier d'entraînement .gpx, .tcx ou .fit." + } } } }, @@ -61519,6 +69877,12 @@ "state": "translated", "value": "找不到可用的列。請確認檔案有日期欄(yyyy-MM-dd)與每日總計。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune ligne utilisable trouvée. Vérifiez que le fichier a une colonne de date (yyyy-MM-dd) et des totaux journaliers." + } } } }, @@ -61530,6 +69894,12 @@ "state": "translated", "value": "Keine Gewährleistung; Haftung begrenzt" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune garantie ; responsabilité limitée" + } } } }, @@ -61564,6 +69934,12 @@ "state": "translated", "value": "在 %@ 內沒有任何鍛鍊、壓力、恢復、睡眠、HRV、靜息心率、強度、呼吸頻率或皮膚溫度讀數。請再佩戴手環幾天,或選擇更寬的範圍,然後重新匯出。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune mesure d'entraînement, de stress, de récupération, de sommeil, de VFC, de fréquence cardiaque au repos, d'effort, de fréquence respiratoire ou de température cutanée ne se situait dans %@. Portez votre bracelet quelques jours de plus, ou choisissez une plage plus large, puis exportez à nouveau." + } } } }, @@ -61598,6 +69974,12 @@ "state": "translated", "value": "找不到鍛鍊。請指向 Hevy CSV 匯出檔或 Liftosaur JSON 匯出檔。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun entraînement trouvé. Indiquez un export CSV Hevy ou un export JSON Liftosaur." + } } } }, @@ -61632,6 +70014,12 @@ "state": "translated", "value": "暫無鍛鍊記錄。它們來自你的 WHOOP 和 Apple 健康歷史。請在「資料來源」中匯入以引入它們,或新增一次你在別處追蹤的鍛鍊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun entraînement pour l'instant. Ils proviennent de l'historique de votre WHOOP et d'Apple Santé. Importez dans Sources de données pour les récupérer, ou ajoutez-en un que vous avez suivi ailleurs." + } } } }, @@ -61771,6 +70159,12 @@ "state": "translated", "value": "中午" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Midi" + } } } }, @@ -61886,6 +70280,12 @@ "state": "translated", "value": "Kein Medizinprodukt, kein medizinischer Rat" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas un dispositif médical, pas un avis médical" + } } } }, @@ -61920,6 +70320,12 @@ "state": "translated", "value": "不是鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas un entraînement" + } } } }, @@ -62006,6 +70412,12 @@ "state": "translated", "value": "無法使用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non disponible" + } } } }, @@ -62040,6 +70452,12 @@ "state": "translated", "value": "未校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non calibré" + } } } }, @@ -62074,6 +70492,12 @@ "state": "translated", "value": "尚未校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore calibré" + } } } }, @@ -62108,6 +70532,12 @@ "state": "translated", "value": "未連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non connecté" + } } } }, @@ -62142,6 +70572,12 @@ "state": "translated", "value": "未連接,打開「即時」以配對。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non connecté. Ouvrez En direct pour coupler." + } } } }, @@ -62176,6 +70612,12 @@ "state": "translated", "value": "今天沒有足夠乾淨的心跳資料來鎖定一個節奏,請在休息好、坐姿靜止、手環貼合時重試。目前我們會以 5.5 次/分鐘(一致性)為你把握節奏。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas assez de données de battements propres pour fixer un rythme aujourd'hui. Réessayez reposé, assis immobile avec le bracelet bien ajusté. Pour l'instant, nous vous guiderons à 5,5 resp/min (cohérence)." + } } } }, @@ -62210,6 +70652,12 @@ "state": "translated", "value": "乾淨的心跳不足。請坐著不動再試一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas assez de battements propres. Restez immobile et réessayez." + } } } }, @@ -62250,6 +70698,12 @@ "state": "translated", "value": "乾淨的心跳不足,請坐姿靜止後重試。%1$lld / %2$lld 次心跳通過了過濾(需要 %3$lld 次)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas assez de battements propres. Restez immobile et réessayez. %1$lld battements sur %2$lld ont survécu au filtrage (il en faut %3$lld)." + } } } }, @@ -62284,6 +70738,12 @@ "state": "translated", "value": "資料不足" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données insuffisantes" + } } } }, @@ -62370,6 +70830,12 @@ "state": "translated", "value": "此範圍內的資料尚不足" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données insuffisantes dans cette plage pour l'instant" + } } } }, @@ -62404,6 +70870,12 @@ "state": "translated", "value": "資料尚不足" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données insuffisantes pour l'instant" + } } } }, @@ -62490,6 +70962,12 @@ "state": "translated", "value": "你的日誌回答與 %@ 之間的重疊還不夠,尚無法測量效果。請持續記錄。每個行為都需要有做與沒做的日子才能看出效果。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas assez de recoupement entre vos réponses au journal et %@ pour mesurer un effet pour l'instant. Continuez à enregistrer. Les effets nécessitent des jours avec et sans chaque comportement." + } } } }, @@ -62524,6 +71002,12 @@ "state": "translated", "value": "你的日誌回答與 %@ 之間的重疊還不夠。請持續記錄。每個行為都需要有做與沒做的日子,NOOP 才能讀出它的效果。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore assez de recoupement entre vos réponses au journal et %@. Continuez à enregistrer. Chaque comportement nécessite des jours avec et sans avant que NOOP puisse en lire l'effet." + } } } }, @@ -62714,6 +71198,12 @@ "state": "translated", "value": "暫不" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas maintenant" + } } } }, @@ -62748,6 +71238,12 @@ "state": "translated", "value": "未在錄製" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun enregistrement" + } } } }, @@ -62782,6 +71278,12 @@ "state": "translated", "value": "未在記錄。手環未連接。點擊以連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun enregistrement. Bracelet non connecté. Appuyez pour connecter." + } } } }, @@ -62816,6 +71318,12 @@ "state": "translated", "value": "未設定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non défini" + } } } }, @@ -62850,6 +71358,12 @@ "state": "translated", "value": "沒有顯示出來?請確保你已在 Oura 應用程式中重置戒指並強制退出它,然後點擊「重新掃描」。仍歸 Oura 持有的戒指不會在此列出。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Elle n'apparaît pas ? Assurez-vous d'avoir réinitialisé la bague dans l'application Oura et de l'avoir forcée à quitter, puis appuyez sur Rescanner. Une bague encore possédée par Oura n'apparaîtra pas ici." + } } } }, @@ -62884,6 +71398,12 @@ "state": "translated", "value": "沒有顯示出來?官方 WHOOP 應用程式可能仍在佔用它。請強制退出該應用程式,然後點擊「重新掃描」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'apparaît pas ? L'application WHOOP officielle le retient peut-être encore. Forcez la fermeture de cette application, puis appuyez sur Rescanner." + } } } }, @@ -62970,6 +71490,12 @@ "state": "translated", "value": "備註(選填)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Note (facultatif)" + } } } }, @@ -63004,6 +71530,12 @@ "state": "translated", "value": "備註" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Notes" + } } } }, @@ -63038,6 +71570,12 @@ "state": "translated", "value": "無" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien" + } } } }, @@ -63072,6 +71610,12 @@ "state": "translated", "value": "這裡還沒有內容。點擊上方的「啟用 Apple 健康」以即時讀取你的資料,或在「資料來源」中匯入一個健康匯出的 .zip 檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien pour l'instant. Appuyez sur Activer Apple Santé ci-dessus pour lire vos données en direct, ou importez un fichier .zip d'export Santé dans Sources de données." + } } } }, @@ -63106,6 +71650,12 @@ "state": "translated", "value": "這裡還沒有內容。此側載的安裝無法直接讀取 Apple 健康,請在「資料來源」中匯入一個健康匯出的 .zip 檔案,或開啟「捷徑匯出」以將你的手環資料帶入「健康」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien pour l'instant. Cette installation sideloadée ne peut pas lire Apple Santé directement. Importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export via Raccourcis pour faire entrer les données de votre bracelet dans Santé." + } } } }, @@ -63140,6 +71690,12 @@ "state": "translated", "value": "未匯入任何內容" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien d'importé" + } } } }, @@ -63174,6 +71730,12 @@ "state": "translated", "value": "尚未匯入任何內容。在「資料來源」中,選擇你的 Mi Fitness 匯出檔案(來自「檔案」應用程式的 Mi Fitness 應用程式資料夾的 .zip)以引入你的步數、心率、睡眠階段、SpO₂ 和壓力。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien d'importé pour l'instant. Dans Sources de données, choisissez votre export Mi Fitness (un .zip du dossier de l'application Mi Fitness depuis l'application Fichiers) pour récupérer vos pas, votre fréquence cardiaque, vos phases de sommeil, votre SpO₂ et votre stress." + } } } }, @@ -63312,6 +71874,12 @@ "state": "translated", "value": "任何資料都不會離開你的 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien ne quitte votre %@" + } } } }, @@ -63346,6 +71914,12 @@ "state": "translated", "value": "沒有值得注意的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien de notable" + } } } }, @@ -63380,6 +71954,12 @@ "state": "translated", "value": "此視窗尚無已卸載的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien de déchargé pour cette période pour l'instant." + } } } }, @@ -63466,6 +72046,12 @@ "state": "translated", "value": "沒有可匯出的內容" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien à exporter" + } } } }, @@ -63500,6 +72086,12 @@ "state": "translated", "value": "尚無可融合的內容" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien à fusionner pour l'instant" + } } } }, @@ -63534,6 +72126,12 @@ "state": "translated", "value": "目前沒有可回收的空間,NOOP 已自動清理匯入的暫存空間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien à récupérer pour l'instant. NOOP nettoie déjà automatiquement l'espace temporaire d'import." + } } } }, @@ -63620,6 +72218,12 @@ "state": "translated", "value": "通知已關閉,因此這還無法震動,請在「通知」中打開總開關以放行它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les notifications sont désactivées, donc cela ne peut pas encore vibrer. Activez l'interrupteur principal dans Notifications pour le laisser passer." + } } } }, @@ -63654,6 +72258,12 @@ "state": "translated", "value": "電量低與充飽時通知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Notifier en cas de batterie faible ou pleine" + } } } }, @@ -63688,6 +72298,12 @@ "state": "translated", "value": "11月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nov" + } } } }, @@ -63722,6 +72338,12 @@ "state": "translated", "value": "現在" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenant" + } } } }, @@ -63756,6 +72378,12 @@ "state": "translated", "value": "營養" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nutrition" + } } } }, @@ -63790,6 +72418,12 @@ "state": "translated", "value": "營養(.csv)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nutrition (.csv)" + } } } }, @@ -63824,6 +72458,12 @@ "state": "translated", "value": "離線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "HORS LIGNE" + } } } }, @@ -63910,6 +72550,12 @@ "state": "translated", "value": "開啟" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ON" + } } } }, @@ -63944,6 +72590,12 @@ "state": "translated", "value": "10月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Oct" + } } } }, @@ -64030,6 +72682,12 @@ "state": "translated", "value": "偏離基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hors référence" + } } } }, @@ -64064,6 +72722,12 @@ "state": "translated", "value": "未戴在手腕上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hors du poignet" + } } } }, @@ -64151,6 +72815,12 @@ "state": "translated", "value": "關閉,只分享你的核心指標,不包括你的規律或實驗手記。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivé : seules vos métriques principales sont partagées, pas vos tendances ni votre Carnet de labo." + } } } }, @@ -64185,6 +72855,12 @@ "state": "translated", "value": "關閉,教練只作一般性回答,不發送你的任何指標。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivé : le Coach répond de manière générale et n'envoie aucune de vos métriques." + } } } }, @@ -64219,6 +72895,12 @@ "state": "translated", "value": "當它發現持續升高的心率時,會建議保存一次鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Propose d'enregistrer un entraînement quand il détecte une fréquence cardiaque élevée soutenue" + } } } }, @@ -64253,6 +72935,12 @@ "state": "translated", "value": "離線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hors ligne" + } } } }, @@ -64287,6 +72975,12 @@ "state": "translated", "value": "好" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "OK" + } } } }, @@ -64321,6 +73015,12 @@ "state": "translated", "value": "在 %@ 上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur %@" + } } } }, @@ -64407,6 +73107,12 @@ "state": "translated", "value": "在 iOS 測試版上,在正式版能正常運作的功能也可能出問題。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur une version bêta d'iOS, des choses qui fonctionnent sur la version finale peuvent casser." + } } } }, @@ -64441,6 +73147,12 @@ "state": "translated", "value": "在 iPhone 上這是側載版本。使用免費 Apple ID 大約每 7 天要重新簽署一次(付費帳號可以更久)。手機重新開機後,請解鎖一次,讓 NOOP 能讀取並同步它的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur iPhone, il s'agit d'une version sideloadée. Resignez-la environ tous les 7 jours avec un identifiant Apple gratuit (plus longtemps avec un compte payant). Après le redémarrage de votre téléphone, déverrouillez-le une fois pour que NOOP puisse lire et synchroniser ses données." + } } } }, @@ -64475,6 +73187,12 @@ "state": "translated", "value": "在 iPhone 上這是盡力而為的(由 iOS 決定何時執行背景任務)。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur iPhone, cela se fait au mieux (c'est iOS qui décide quand les tâches en arrière-plan s'exécutent). Tout reste sur %@." + } } } }, @@ -64509,6 +73227,12 @@ "state": "translated", "value": "達標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans l'objectif" + } } } }, @@ -64543,6 +73267,12 @@ "state": "translated", "value": "在你手機也統計步數的那些日子裡,NOOP 會學習你的運動對應多少步數,然後將其套用到僅有手環的日子。匹配的日子越多,它對估算就越信任。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jours où votre téléphone comptait aussi les pas, NOOP apprend comment votre mouvement se traduit en pas, puis applique cela aux jours avec seulement le bracelet. Plus il a de jours correspondants, plus il fait confiance à l'estimation." + } } } }, @@ -64577,6 +73307,12 @@ "state": "translated", "value": "戴在手腕上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Au poignet" + } } } }, @@ -64664,6 +73400,12 @@ "state": "translated", "value": "裝置本地" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur l'appareil" + } } } }, @@ -64698,6 +73440,12 @@ "state": "translated", "value": "裝置上的估算(近似值),並非診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation sur l'appareil (approximative), pas un diagnostic." + } } } }, @@ -64732,6 +73480,12 @@ "state": "translated", "value": "裝置本地佔用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Empreinte sur l'appareil" + } } } }, @@ -64766,6 +73520,12 @@ "state": "translated", "value": "開啟,會附上你最顯著的規律和已記錄健康數字的簡短摘要。僅摘要,絕不包含原始讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé : un court résumé de vos tendances les plus marquées et de vos données de santé enregistrées est ajouté. Uniquement des résumés, jamais de mesures brutes." + } } } }, @@ -64800,6 +73560,12 @@ "state": "translated", "value": "開啟,你的能量、休息、HRV 和鍛鍊會分享給服務供應商,以獲得量身定制的指導。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé : votre charge, repos, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé." + } } } }, @@ -64834,6 +73600,12 @@ "state": "translated", "value": "一旦 NOOP 擁有 14 晚的歷史,「在範圍內」會將每項生命徵象與你自己的基線進行比較(近似值,並非醫療建議);在那之前,採用典型成人範圍。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une fois que NOOP a 14 nuits d'historique, « dans la norme » compare chaque signe vital à votre propre référence (approximatif, pas un avis médical) ; jusque-là, les plages typiques pour un adulte s'appliquent." + } } } }, @@ -64868,6 +73640,12 @@ "state": "translated", "value": "一旦本週有一兩天的資料,你的每週回顧就會出現在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une fois que cette semaine a un ou deux jours de données, votre bilan de la semaine apparaît ici." + } } } }, @@ -64908,6 +73686,12 @@ "state": "translated", "value": "目前只有一筆讀數:%@ %@。再記錄幾筆就能看到趨勢。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une seule mesure pour l'instant : %@ %@. Enregistrez-en quelques-unes de plus pour voir une tendance." + } } } }, @@ -64942,6 +73726,12 @@ "state": "translated", "value": "僅在佩戴時震動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrer uniquement quand porté" + } } } }, @@ -64976,6 +73766,12 @@ "state": "translated", "value": "僅在活動時段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement pendant les heures actives" + } } } }, @@ -65010,6 +73806,12 @@ "state": "translated", "value": "僅當你的伺服器需要時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement si votre serveur en exige une" + } } } }, @@ -65044,6 +73846,12 @@ "state": "translated", "value": "只在你的活動時段提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne relancer que pendant vos heures actives." + } } } }, @@ -65078,6 +73886,12 @@ "state": "translated", "value": "僅在佩戴時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement quand porté" + } } } }, @@ -65112,6 +73926,12 @@ "state": "translated", "value": "打開" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir" + } } } }, @@ -65146,6 +73966,12 @@ "state": "translated", "value": "打開「備份並同步到資料夾」" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir Sauvegarde et synchronisation vers un dossier" + } } } }, @@ -65180,6 +74006,12 @@ "state": "translated", "value": "請先打開 NOOP,以便它能連接你的手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez d'abord NOOP pour qu'il puisse atteindre votre bracelet." + } } } }, @@ -65214,6 +74046,12 @@ "state": "translated", "value": "在你的手環附近打開 NOOP,讓它追上進度(首次執行時,一次完整的歷史同步可能需要一段時間)。一旦一兩天的運動資料到位,你的步數估算和下面的校準就會開始填充。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez NOOP près de votre bracelet et laissez-le se mettre à jour (une synchronisation complète de l'historique peut prendre du temps au premier lancement). Une fois qu'un ou deux jours de mouvement sont arrivés, votre estimation de pas et la calibration ci-dessous commenceront à se remplir." + } } } }, @@ -65248,6 +74086,12 @@ "state": "translated", "value": "在你的 iPhone 上打開 NOOP 以同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez NOOP sur votre iPhone pour synchroniser" + } } } }, @@ -65282,6 +74126,12 @@ "state": "translated", "value": "打開「節律」" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir Rhythm" + } } } }, @@ -65316,6 +74166,12 @@ "state": "translated", "value": "打開測試中心" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le Centre de test" + } } } }, @@ -65350,6 +74206,12 @@ "state": "translated", "value": "打開測試中心以報告錯誤" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez le Centre de test pour signaler un bug" + } } } }, @@ -65384,6 +74246,12 @@ "state": "translated", "value": "打開即時檢視" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la vue en direct" + } } } }, @@ -65418,6 +74286,12 @@ "state": "translated", "value": "打開官方 Oura App 並移除此戒指(Oura 稱之為「恢復原廠設定」或「取消配對並重置」)。這會清除戒指的持有者,讓 NOOP 可以接手。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez l'application Oura officielle et retirez cette bague (Oura appelle cela « réinitialisation d'usine » ou « désassocier et réinitialiser »). Cela efface le propriétaire de la bague pour que NOOP puisse en prendre le contrôle." + } } } }, @@ -65504,6 +74378,12 @@ "state": "translated", "value": "打開 Apple Watch 設定和健康權限" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvre la configuration de l'Apple Watch et l'autorisation Santé" + } } } }, @@ -65538,6 +74418,12 @@ "state": "translated", "value": "打開「裝置」畫面,你可以在其中配對和切換手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvre l'écran Appareils, où vous couplez et changez de bracelets." + } } } }, @@ -65625,6 +74511,12 @@ "state": "translated", "value": "打開鍛鍊詳情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvre le détail de l'entraînement" + } } } }, @@ -65659,6 +74551,12 @@ "state": "translated", "value": "最佳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Optimal" + } } } }, @@ -65693,6 +74591,12 @@ "state": "translated", "value": "可選" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif" + } } } }, @@ -65727,6 +74631,12 @@ "state": "translated", "value": "可選備註" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Note facultative" + } } } }, @@ -65761,6 +74671,12 @@ "state": "translated", "value": "可選的追蹤器,預設關閉。開啟它們以新增其卡片。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trackers facultatifs, désactivés par défaut. Activez-les pour ajouter leurs cartes. Tout reste sur %@." + } } } }, @@ -65795,6 +74711,12 @@ "state": "translated", "value": "選用功能,在裝置上從你的夜間訊號讀取。每一項預設都是關閉的:僅供參考,絕非診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lectures facultatives, sur l'appareil, à partir de vos signaux nocturnes. Chacune est désactivée par défaut : uniquement à titre informatif, jamais un diagnostic." + } } } }, @@ -65829,6 +74751,12 @@ "state": "translated", "value": "可選。為左上角的頭像新增一張照片。僅儲存在 %@ 上,NOOP 是離線的,因此絕不會上傳。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif. Ajoutez une photo pour l'avatar en haut à gauche. Stockée uniquement sur %@. NOOP est hors ligne, elle n'est donc jamais téléversée." + } } } }, @@ -65863,6 +74791,12 @@ "state": "translated", "value": "可選。僅儲存在這台裝置上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif. Stocké uniquement sur cet appareil." + } } } }, @@ -65897,6 +74831,12 @@ "state": "translated", "value": "可選,會加上你的最大攝氧量估算。體能年齡本身並不需要它。請在腰部、肚臍位置測量。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif : ajoute votre estimation de VO₂max. L'âge de forme physique lui-même n'en a pas besoin. Mesurez autour de votre taille, au niveau du nombril." + } } } }, @@ -65931,6 +74871,12 @@ "state": "translated", "value": "選用:現在匯入,或先繼續,之後再回到「資料來源」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif : importez maintenant, ou continuez et revenez à Sources de données plus tard." + } } } }, @@ -65965,6 +74911,12 @@ "state": "translated", "value": "其他" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autre" + } } } }, @@ -65999,6 +74951,12 @@ "state": "translated", "value": "其他指標(R-R、幀、電量、歷史)需要一次完整同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les autres métriques (R-R, trames, batterie, historique) nécessitent une synchronisation complète." + } } } }, @@ -66033,6 +74991,12 @@ "state": "translated", "value": "其他來源不會在裝置上卸載原始的逐秒資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les autres sources ne déchargent pas de données brutes à la seconde sur l'appareil." + } } } }, @@ -66067,6 +75031,12 @@ "state": "translated", "value": "Oura / Fitbit / Garmin 匯出檔" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export Oura / Fitbit / Garmin" + } } } }, @@ -66101,6 +75071,12 @@ "state": "translated", "value": "Oura 準備度 / 睡眠分數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Score Préparation / Sommeil d'Oura" + } } } }, @@ -66135,6 +75111,12 @@ "state": "translated", "value": "Oura 戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bague Oura" + } } } }, @@ -66169,6 +75151,12 @@ "state": "translated", "value": "Oura 自己的準備度與睡眠分數。NOOP 不會照抄它們,而是自行計算。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les scores Préparation et Sommeil propres à Oura. NOOP ne les copie pas. Il calcule les siens." + } } } }, @@ -66203,6 +75191,12 @@ "state": "translated", "value": "結果" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résultat" + } } } }, @@ -66289,6 +75283,12 @@ "state": "translated", "value": "超出範圍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hors plage" + } } } }, @@ -66329,6 +75329,12 @@ "state": "translated", "value": "從你的讀數來看,%@ 和 %@ 大致上各走各的。沒有明顯關係。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sur vos mesures, %1$@ et %2$@ évoluent largement de manière indépendante. Aucune relation claire." + } } } }, @@ -66519,6 +75525,12 @@ "state": "translated", "value": "夜間計算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calculé pendant la nuit" + } } } }, @@ -66553,6 +75565,12 @@ "state": "translated", "value": "僅限夜間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit uniquement" + } } } }, @@ -66587,6 +75605,12 @@ "state": "translated", "value": "夜間睡眠分期、靜息心率、皮膚溫度趨勢、運動與電量,直接從戒指讀取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Phases de sommeil nocturne, fréquence cardiaque au repos, tendance de température cutanée, mouvement et batterie, lus directement depuis la bague." + } } } }, @@ -66621,6 +75645,12 @@ "state": "translated", "value": "用你自己的每運動步數值覆寫自動擬合。當你的手機沒有可供學習的步數歷史,或估算持續偏高或偏低時會很有用。將滑桿拖到最左端即可恢復為自動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacez l'ajustement automatique par votre propre valeur de pas par mouvement. Utile si votre téléphone n'a pas d'historique de pas pour apprendre, ou si l'estimation est systématiquement trop haute ou trop basse. Remettez sur auto en faisant glisser complètement à gauche." + } } } }, @@ -66655,6 +75685,12 @@ "state": "translated", "value": "離線擁有你的資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Possédez vos données, hors ligne" + } } } }, @@ -66689,6 +75725,12 @@ "state": "translated", "value": "已擁有" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Possédé" + } } } }, @@ -66723,6 +75765,12 @@ "state": "translated", "value": "已配對" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "COUPLÉ" + } } } }, @@ -66757,6 +75805,12 @@ "state": "translated", "value": "已暫停" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EN PAUSE" + } } } }, @@ -66791,6 +75845,12 @@ "state": "translated", "value": "巔峰" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PIC" + } } } }, @@ -66825,6 +75885,12 @@ "state": "translated", "value": "功率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PUISSANCE" + } } } }, @@ -66859,6 +75925,12 @@ "state": "translated", "value": "資料來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PROVENANCE" + } } } }, @@ -66893,6 +75965,12 @@ "state": "translated", "value": "配速" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rythme" + } } } }, @@ -66927,6 +76005,12 @@ "state": "translated", "value": "配對並管理 NOOP 讀取資料的手環。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couplez et gérez les bracelets que NOOP lit." + } } } }, @@ -66961,6 +76045,12 @@ "state": "translated", "value": "已配對" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couplé" + } } } }, @@ -66995,6 +76085,12 @@ "state": "translated", "value": "已在本地配對。只要 NOOP 持有金鑰,它就是這枚戒指的持有者。如果你再次重置它,或在 Oura 應用程式中設定它,NOOP 便不再持有它,你需要重新新增它以再次接管。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couplée localement. NOOP possède cette bague tant qu'il détient la clé. Si vous la réinitialisez à nouveau ou la configurez dans l'application Oura, NOOP ne la possède plus et vous devrez la rajouter pour en reprendre le contrôle." + } } } }, @@ -67029,6 +76125,12 @@ "state": "translated", "value": "已配對 · 閒置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couplé · inactif" + } } } }, @@ -67063,6 +76165,12 @@ "state": "translated", "value": "配對幫助:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aide au couplage : %@" + } } } }, @@ -67097,6 +76205,12 @@ "state": "translated", "value": "配對中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couplage en cours…" + } } } }, @@ -67131,6 +76245,12 @@ "state": "translated", "value": "透過 Bluetooth 直接與你的手環配對:不需 WHOOP App,不需雲端。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se couple directement à votre bracelet via Bluetooth : pas d'application WHOOP, pas de cloud." + } } } }, @@ -67165,6 +76285,12 @@ "state": "translated", "value": "你 HRV 的副交感(休息)頻段。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bande parasympathique (repos) de votre VFC." + } } } }, @@ -67199,6 +76325,12 @@ "state": "translated", "value": "部分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partiel" + } } } }, @@ -67233,6 +76365,12 @@ "state": "translated", "value": "被動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passif" + } } } }, @@ -67319,6 +76457,12 @@ "state": "translated", "value": "你自己資料中的規律,關聯,而非因果。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendances dans vos propres données : association, pas cause." + } } } }, @@ -67509,6 +76653,12 @@ "state": "translated", "value": "峰值心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque de pointe" + } } } }, @@ -67751,6 +76901,12 @@ "state": "translated", "value": "每額外 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par %@ supplémentaire" + } } } }, @@ -67785,6 +76941,12 @@ "state": "translated", "value": "每晚,過去 30 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par nuit, sur les 30 derniers jours" + } } } }, @@ -67819,6 +76981,12 @@ "state": "translated", "value": "每晚所需" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Besoin par nuit" + } } } }, @@ -67859,6 +77027,12 @@ "state": "translated", "value": "每晚睡眠平衡:%1$lld 晚,淨 %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bilan de sommeil par nuit : %1$lld nuits, net %2$@" + } } } }, @@ -67893,6 +77067,12 @@ "state": "translated", "value": "個人實驗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expérience personnelle" + } } } }, @@ -67927,6 +77107,12 @@ "state": "translated", "value": "個人曲線 · 先驗收縮" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Courbe personnelle · réduite par prior" + } } } }, @@ -67961,6 +77147,12 @@ "state": "translated", "value": "第 1 階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Phase 1" + } } } }, @@ -67995,6 +77187,12 @@ "state": "translated", "value": "手機" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléphone" + } } } }, @@ -68029,6 +77227,12 @@ "state": "translated", "value": "選擇 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir %@" + } } } }, @@ -68063,6 +77267,12 @@ "state": "translated", "value": "選擇一個新的活躍手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un nouveau bracelet actif" + } } } }, @@ -68097,6 +77307,12 @@ "state": "translated", "value": "選擇一個運動項目,NOOP 會從即時資料流中記錄心率、峰值、平均值和消耗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un sport. NOOP enregistre la FC, le pic, la moyenne et l'effort à partir du flux en direct." + } } } }, @@ -68131,6 +77347,12 @@ "state": "translated", "value": "選擇一個穿戴式訊號(靜息心率、HRV、睡眠、能量、體重…)以將其與此標記對齊。NOOP 會對每次讀數之前 %@ 內的訊號取平均值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un signal du dispositif porté (FC au repos, VFC, sommeil, Charge, poids…) à comparer avec ce marqueur. NOOP fait la moyenne du signal sur les %@ précédant chaque mesure." + } } } }, @@ -68165,6 +77387,12 @@ "state": "translated", "value": "選擇一項你記錄的行為、一個結果和一個短窗口。NOOP 會將你記錄該行為的日子與開始之前你沒有該行為的日子進行比較。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un comportement que vous enregistrez, un résultat et une courte période. NOOP compare les jours où vous enregistrez le comportement aux jours sans ce comportement avant le début." + } } } }, @@ -68199,6 +77427,12 @@ "state": "translated", "value": "選擇戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir la bague" + } } } }, @@ -68233,6 +77467,12 @@ "state": "translated", "value": "選擇小睡的開始和結束時間。NOOP 會將其作為獨立於夜間睡眠的單獨工作階段,根據你的資料進行分期。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez quand la sieste a commencé et fini. NOOP la traite à partir de vos données comme une session à part, séparée du sommeil nocturne." + } } } }, @@ -68267,6 +77507,12 @@ "state": "translated", "value": "在「設定 → 通知」中選擇哪些應用程式可以送達你的手腕。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez quelles applications atteignent votre poignet dans Paramètres → Notifications." + } } } }, @@ -68301,6 +77547,12 @@ "state": "translated", "value": "選擇你的裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre appareil" + } } } }, @@ -68335,6 +77587,12 @@ "state": "translated", "value": "在下方選擇你的手環,然後點按「掃描」,NOOP 會找到它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre bracelet ci-dessous, puis appuyez sur Scanner. NOOP le trouvera." + } } } }, @@ -68369,6 +77627,12 @@ "state": "translated", "value": "被選為你的主要睡眠,因為它的開始時間接近你通常的入睡時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisi comme sommeil principal car il a commencé près de votre heure de sommeil habituelle." + } } } }, @@ -68403,6 +77667,12 @@ "state": "translated", "value": "被選為你的主要睡眠,因為它是你最長的睡眠區段(%@),且接近你通常的就寢時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisi comme sommeil principal car c'était votre plus long bloc (%@), près de votre heure de coucher habituelle." + } } } }, @@ -68437,6 +77707,12 @@ "state": "translated", "value": "被選為你的主要睡眠,因為它是你最長的睡眠區段(%@)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisi comme sommeil principal car c'était votre plus long bloc (%@)." + } } } }, @@ -68471,6 +77747,12 @@ "state": "translated", "value": "捏合以縮放 · 拖動以平移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pincer pour zoomer · glisser pour déplacer" + } } } }, @@ -68505,6 +77787,12 @@ "state": "translated", "value": "計劃一次旅行或作息調整" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Planifier un voyage ou un décalage" + } } } }, @@ -68539,6 +77827,12 @@ "state": "translated", "value": "計劃 · %lld 天的調整" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plan · décalage de %lld jours" + } } } }, @@ -68573,6 +77867,12 @@ "state": "translated", "value": "請閱讀並接受下面的各項。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Veuillez lire et accepter les points ci-dessous." + } } } }, @@ -68659,6 +77959,12 @@ "state": "translated", "value": "Polar、Wahoo、Coospo、Garmin HRM、Amazfit Helio 廣播" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffusion Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio" + } } } }, @@ -68693,6 +77999,12 @@ "state": "translated", "value": "不佳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faible" + } } } }, @@ -68727,6 +78039,12 @@ "state": "translated", "value": "僅限高級使用者。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réservé aux utilisateurs avancés." + } } } }, @@ -68761,6 +78079,12 @@ "state": "translated", "value": "為能量、消耗、休息和睡眠提供資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimente Charge, Effort, Repos et Sommeil" + } } } }, @@ -68795,6 +78119,12 @@ "state": "translated", "value": "為能量、消耗、休息、睡眠與健康監測提供資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimente Charge, Effort, Repos, Sommeil + Moniteur de santé" + } } } }, @@ -68829,6 +78159,12 @@ "state": "translated", "value": "目前為消耗提供資料;累積足夠夜晚並確認解碼後,也會支援能量和休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimente l'Effort dès maintenant ; Charge et Repos une fois assez de nuits et le décodage confirmés" + } } } }, @@ -68863,6 +78199,12 @@ "state": "translated", "value": "為休息、消耗、體能年齡和步數提供資料,恢復校準完成後也會支援能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimente Repos, Effort, âge de forme physique et pas, plus Charge une fois la récupération calibrée" + } } } }, @@ -68897,6 +78239,12 @@ "state": "translated", "value": "為即時主控台與消耗提供資料。不支援能量、休息或睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimente la console en direct + Effort. Aucune Charge, Repos ou Sommeil" + } } } }, @@ -68931,6 +78279,12 @@ "state": "translated", "value": "準備中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation en cours…" + } } } }, @@ -68965,6 +78319,12 @@ "state": "translated", "value": "在場自動化:設定專注模式、暫停媒體、標記離開⋯⋯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatisation de présence : définir un mode Focus, mettre en pause les contenus, se marquer absent…" + } } } }, @@ -68999,6 +78359,12 @@ "state": "translated", "value": "預覽" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aperçu" + } } } }, @@ -69033,6 +78399,12 @@ "state": "translated", "value": "預覽劑量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aperçu de la dose" + } } } }, @@ -69067,6 +78439,12 @@ "state": "translated", "value": "前一天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jour précédent" + } } } }, @@ -69101,6 +78479,12 @@ "state": "translated", "value": "前一晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit précédente" + } } } }, @@ -69135,6 +78519,12 @@ "state": "translated", "value": "上一週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Semaine précédente" + } } } }, @@ -69169,6 +78559,12 @@ "state": "translated", "value": "先前已配對但目前未連接。重新掃描以再次連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Précédemment couplé mais non connecté actuellement. Rescannez pour reconnecter." + } } } }, @@ -69203,6 +78599,12 @@ "state": "translated", "value": "就緒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt" + } } } }, @@ -69237,6 +78639,12 @@ "state": "translated", "value": "可能不是生病" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Probablement pas une maladie" + } } } }, @@ -69323,6 +78731,12 @@ "state": "translated", "value": "頭像照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Photo de profil" + } } } }, @@ -69357,6 +78771,12 @@ "state": "translated", "value": "專案主頁與原始碼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Page et code source du projet" + } } } }, @@ -69391,6 +78811,12 @@ "state": "translated", "value": "GitHub 上的專案主頁和原始碼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Page et code source du projet sur GitHub" + } } } }, @@ -69425,6 +78851,12 @@ "state": "translated", "value": "控制台為最新的證明" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Preuve que la console est à jour" + } } } }, @@ -69459,6 +78891,12 @@ "state": "translated", "value": "蛋白質" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Protéines" + } } } }, @@ -69597,6 +79035,12 @@ "state": "translated", "value": "正在拉取你手環儲存的歷史資料。這會從最舊的開始下載;大量積壓現在會跨多輪自動繼續,而不必在同步之間等待。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération de l'historique stocké sur votre bracelet. Il se vide en commençant par le plus ancien ; un arriéré important se poursuit désormais automatiquement sur plusieurs passages au lieu d'attendre entre les synchronisations." + } } } }, @@ -69631,6 +79075,12 @@ "state": "translated", "value": "立即拉取你手環儲存的歷史,無需等待下一次自動同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupère immédiatement l'historique stocké sur votre bracelet, sans attendre la prochaine synchronisation automatique." + } } } }, @@ -69665,6 +79115,12 @@ "state": "translated", "value": "推播" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Push" + } } } }, @@ -69699,6 +79155,12 @@ "state": "translated", "value": "讓手環進入配對模式,戴在你的手腕上並保持喚醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez le bracelet en mode couplage, au poignet et actif." + } } } }, @@ -69733,6 +79195,12 @@ "state": "translated", "value": "請先戴上手環,深度資料流僅在佩戴時可用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez d'abord le bracelet. Le flux approfondi fonctionne uniquement au poignet." + } } } }, @@ -69767,6 +79235,12 @@ "state": "translated", "value": "將你的 WHOOP 4.0 戴在手腕上,並確保它處於喚醒狀態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez votre WHOOP 4.0 au poignet et assurez-vous qu'il est actif." + } } } }, @@ -69801,6 +79275,12 @@ "state": "translated", "value": "戴上你的手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez votre bracelet" + } } } }, @@ -69835,6 +79315,12 @@ "state": "translated", "value": "快速操作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ACTIONS RAPIDES" + } } } }, @@ -69974,6 +79460,12 @@ "state": "translated", "value": "問題、意見回饋、錯誤回報:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Questions, retours, bugs : %@" + } } } }, @@ -70008,6 +79500,12 @@ "state": "translated", "value": "快速操作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actions rapides" + } } } }, @@ -70042,6 +79540,12 @@ "state": "translated", "value": "快速掃掠 · 約 7 分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Balayage rapide · ~7 min" + } } } }, @@ -70076,6 +79580,12 @@ "state": "translated", "value": "勿擾時段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heures de silence" + } } } }, @@ -70214,6 +79724,12 @@ "state": "translated", "value": "R-R 間期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intervalles R-R" + } } } }, @@ -70248,6 +79764,12 @@ "state": "translated", "value": "記錄中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ENREGISTREMENT" + } } } }, @@ -70282,6 +79804,12 @@ "state": "translated", "value": "正在錄製鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ENREGISTREMENT DE L'ENTRAÎNEMENT" + } } } }, @@ -70316,6 +79844,12 @@ "state": "translated", "value": "相對" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "REL." + } } } }, @@ -70402,6 +79936,12 @@ "state": "translated", "value": "REM 睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil paradoxal" + } } } }, @@ -70436,6 +79976,12 @@ "state": "translated", "value": "REM 睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil paradoxal" + } } } }, @@ -70470,6 +80016,12 @@ "state": "translated", "value": "休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "REPOS" + } } } }, @@ -70504,6 +80056,12 @@ "state": "translated", "value": "靜息心率 + 睡眠 + HRV" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC AU REPOS + SOMMEIL + VFC" + } } } }, @@ -70538,6 +80096,12 @@ "state": "translated", "value": "RMSSD" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD" + } } } }, @@ -70578,6 +80142,12 @@ "state": "translated", "value": "目前 RMSSD %.0f 毫秒,你的基線約為 %.0f 毫秒(根據 PPG 推導的 R-R 估算)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD %.0f ms maintenant contre votre référence de ~%.0f ms (estimation à partir des R-R dérivés du PPG)." + } } } }, @@ -70612,6 +80182,12 @@ "state": "translated", "value": "RMSSD %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD %@" + } } } }, @@ -70646,6 +80222,12 @@ "state": "translated", "value": "RMSSD %lld 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD %lld millisecondes" + } } } }, @@ -70680,6 +80262,12 @@ "state": "translated", "value": "RMSSD %lld 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD %lld ms" + } } } }, @@ -70766,6 +80354,12 @@ "state": "translated", "value": "按節奏的 RSA 回應" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RÉPONSE RSA PAR ALLURE" + } } } }, @@ -70800,6 +80394,12 @@ "state": "translated", "value": "按節奏的 RSA 回應" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réponse RSA par allure" + } } } }, @@ -70834,6 +80434,12 @@ "state": "translated", "value": "無線電已連接,資料流尚未受信任。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Radio connectée, flux pas encore fiable." + } } } }, @@ -70868,6 +80474,12 @@ "state": "translated", "value": "範圍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plage" + } } } }, @@ -70902,6 +80514,12 @@ "state": "translated", "value": "已排名 · 你的數據" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Classé · vos données" + } } } }, @@ -70936,6 +80554,12 @@ "state": "translated", "value": "已排名、考慮滯後:你的哪些習慣真正影響你的能量,外加你個人對酒精/咖啡因的劑量反應。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Classé, avec prise en compte du délai : quelles habitudes influencent réellement votre Charge, plus votre réponse personnelle à la dose d'alcool/caféine." + } } } }, @@ -70970,6 +80594,12 @@ "state": "translated", "value": "原始 · 每秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Brut · par seconde" + } } } }, @@ -71004,6 +80634,12 @@ "state": "translated", "value": "將所有為能量提供資料的基線重新錨定到你近期的夜晚。不會刪除任何已儲存的日期。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réancre chaque référence qui alimente la Charge sur vos nuits récentes. Aucun jour enregistré n'est supprimé." + } } } }, @@ -71038,6 +80674,12 @@ "state": "translated", "value": "重新標註為" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réétiqueter comme" + } } } }, @@ -71072,6 +80714,12 @@ "state": "translated", "value": "重新提醒間隔" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Relancer toutes les" + } } } }, @@ -71158,6 +80806,12 @@ "state": "translated", "value": "透過 Bluetooth 將你手環的即時心率以標準心率感測器的形式再分享出去,讓健身房的跑步機、單車、Zwift、Peloton 或附近的任何健身應用程式都能讀取。僅限本機 Bluetooth,不會有任何資料離開 %@。預設關閉。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Repartagez la fréquence cardiaque en direct de votre bracelet via Bluetooth comme un capteur de fréquence cardiaque standard, afin qu'un tapis de course, un vélo, Zwift, Peloton ou toute application fitness à proximité puisse la lire. Bluetooth local uniquement. Rien ne quitte %@. Désactivé par défaut." + } } } }, @@ -71192,6 +80846,12 @@ "state": "translated", "value": "在手環取下或戴上時作出反應。注意:macOS 將真正的自動「解鎖」保留給 Apple Watch,因此這裡只能鎖定,無法解鎖。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réagissez lorsque le bracelet est retiré ou remis. Remarque : macOS réserve le vrai déverrouillage automatique à l'Apple Watch, ceci peut donc verrouiller, pas déverrouiller." + } } } }, @@ -71226,6 +80886,12 @@ "state": "translated", "value": "在手環取下或戴上時作出反應。執行捷徑以設定專注模式、暫停媒體、將自己標記為離開。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réagissez lorsque le bracelet est retiré ou remis. Exécutez un raccourci pour activer un mode Focus, mettre en pause les médias ou vous marquer absent." + } } } }, @@ -71260,6 +80926,12 @@ "state": "translated", "value": "閱讀完整的實驗手記說明" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lire la note complète du Cahier de labo" + } } } }, @@ -71294,6 +80966,12 @@ "state": "translated", "value": "閱讀完整說明" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lire la note complète" + } } } }, @@ -71328,6 +81006,12 @@ "state": "translated", "value": "直接從 Apple 健康讀取你的心率、HRV、血氧、呼吸頻率、睡眠、步數和能量,並將 NOOP 從手環推導出的指標寫回。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lisez votre fréquence cardiaque, VFC, oxygène sanguin, fréquence respiratoire, sommeil, pas et énergie directement depuis Apple Santé, et réécrivez les métriques de NOOP issues du bracelet. Tout reste sur %@." + } } } }, @@ -71414,6 +81098,12 @@ "state": "translated", "value": "準備度:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation : %@" + } } } }, @@ -71454,6 +81144,12 @@ "state": "translated", "value": "準備度:%1$@。%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation : %1$@. %2$@" + } } } }, @@ -71488,6 +81184,12 @@ "state": "translated", "value": "讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure" + } } } }, @@ -71522,6 +81224,12 @@ "state": "translated", "value": "讀數完成" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure terminée" + } } } }, @@ -71556,6 +81264,12 @@ "state": "translated", "value": "讀數不完整" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure incomplète" + } } } }, @@ -71694,6 +81408,12 @@ "state": "translated", "value": "正在讀取你的體能年齡…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture de votre âge de forme…" + } } } }, @@ -71728,6 +81448,12 @@ "state": "translated", "value": "正在讀取你的小米手環歷史…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture de l'historique de votre Mi Band…" + } } } }, @@ -71762,6 +81488,12 @@ "state": "translated", "value": "正在讀取你的活力…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture de votre Vitality…" + } } } }, @@ -71952,6 +81684,12 @@ "state": "translated", "value": "正在讀取你的日誌…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture de votre journal…" + } } } }, @@ -71986,6 +81724,12 @@ "state": "translated", "value": "正在讀取你的來源…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture de vos sources…" + } } } }, @@ -72020,6 +81764,12 @@ "state": "translated", "value": "根據你的夜間皮膚溫度粗略判讀月經週期階段,完全在 %@ 上進行。僅供參考:不是避孕方法、不是生育力預測工具,也不是醫療服務。該卡片會出現在「健康」中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lit une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur %@. À titre informatif uniquement : ni contraception, ni prédicteur de fertilité, ni service médical. La carte apparaît dans Santé." + } } } }, @@ -72106,6 +81856,12 @@ "state": "translated", "value": "已準備好迎接一次顯著的消耗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt pour un effort marqué." + } } } }, @@ -72140,6 +81896,12 @@ "state": "translated", "value": "準備好呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à respirer" + } } } }, @@ -72174,6 +81936,12 @@ "state": "translated", "value": "準備好連接你的手錶了嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à connecter votre montre ?" + } } } }, @@ -72260,6 +82028,12 @@ "state": "translated", "value": "準備好同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à synchroniser" + } } } }, @@ -72294,6 +82068,12 @@ "state": "translated", "value": "現成的操作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actions prêtes à l'emploi" + } } } }, @@ -72328,6 +82108,12 @@ "state": "translated", "value": "重新校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalibrer" + } } } }, @@ -72362,6 +82148,12 @@ "state": "translated", "value": "重新校準能量基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalibrer la référence de Charge" + } } } }, @@ -72396,6 +82188,12 @@ "state": "translated", "value": "重新校準你的能量基線?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalibrer votre référence de Charge ?" + } } } }, @@ -72430,6 +82228,12 @@ "state": "translated", "value": "最近" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récent" + } } } }, @@ -72464,6 +82268,12 @@ "state": "translated", "value": "最近的間期:%@ 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intervalles récents : %@ ms" + } } } }, @@ -72550,6 +82360,12 @@ "state": "translated", "value": "今晚之前為你的 WHOOP 充電。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechargez votre WHOOP avant ce soir." + } } } }, @@ -72584,6 +82400,12 @@ "state": "translated", "value": "可回收" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupérable" + } } } }, @@ -72618,6 +82440,12 @@ "state": "translated", "value": "已釋放 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ récupérés." + } } } }, @@ -72704,6 +82532,12 @@ "state": "translated", "value": "重新連接幫助:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aide à la reconnexion : %@" + } } } }, @@ -72738,6 +82572,12 @@ "state": "translated", "value": "重新連接以拉取最新內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reconnectez-vous pour récupérer les dernières données." + } } } }, @@ -72772,6 +82612,12 @@ "state": "translated", "value": "本次執行的重新連接次數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reconnexions de cette session" + } } } }, @@ -72806,6 +82652,12 @@ "state": "translated", "value": "記錄一個帶時間戳的時刻(如果手環已連接則讓其震動)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez un moment horodaté (et faites vibrer le bracelet s'il est connecté)." + } } } }, @@ -72840,6 +82692,12 @@ "state": "translated", "value": "在 NOOP 中記錄一個帶時間戳的時刻。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez un moment horodaté dans NOOP." + } } } }, @@ -72874,6 +82732,12 @@ "state": "translated", "value": "記錄或檢查當前資料流" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer ou inspecter le flux actuel" + } } } }, @@ -72960,6 +82824,12 @@ "state": "translated", "value": "記錄在裝置上" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré sur l'appareil" + } } } }, @@ -72994,6 +82864,12 @@ "state": "translated", "value": "錄製中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrement" + } } } }, @@ -73028,6 +82904,12 @@ "state": "translated", "value": "記錄狀態,過去的日期不顯示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "État d'enregistrement, non affiché pour un jour passé" + } } } }, @@ -73062,6 +82944,12 @@ "state": "translated", "value": "記錄中。你的手環已連接並正在儲存資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrement en cours. Votre bracelet est connecté et enregistre les données." + } } } }, @@ -73096,6 +82984,12 @@ "state": "translated", "value": "記錄與來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrements et sources" + } } } }, @@ -73130,6 +83024,12 @@ "state": "translated", "value": "記錄一次即時器械鍛鍊,當器械回報心率時以消耗評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistre un entraînement sur machine en direct, l'Effort étant calculé à partir de la FC lorsque la machine la transmet" + } } } }, @@ -73216,6 +83116,12 @@ "state": "translated", "value": "恢復 / 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération / Charge" + } } } }, @@ -73250,6 +83156,12 @@ "state": "translated", "value": "恢復大約需要一週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La récupération prend environ une semaine" + } } } }, @@ -73337,6 +83249,12 @@ "state": "translated", "value": "恢復,即 NOOP 的能量評分,主要由你的心率變異性相對於你個人基線的測量來主導。胸帶整晚密集地串流逐拍資料,因此能快速學會該基線。而 Apple Watch 則是對 HRV 進行取樣,全天加上夜間只有零星幾次讀數,所以訊號是真實的,但更稀疏。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La Récupération, le score de Charge de NOOP, est principalement déterminée par votre variabilité de fréquence cardiaque mesurée par rapport à votre référence personnelle. Un bracelet pectoral transmet des données battement par battement de manière dense toute la nuit, il peut donc apprendre cette référence rapidement. Une Apple Watch échantillonne quant à elle la VFC, avec quelques mesures dans la journée et pendant la nuit, le signal est donc réel mais plus épars." + } } } }, @@ -73371,6 +83289,12 @@ "state": "translated", "value": "恢復、測試中心、實驗性探測和備份。收納於此,讓日常畫面保持整潔。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération, Centre de test, sondes expérimentales et sauvegarde. Rangés pour garder l'écran du quotidien épuré." + } } } }, @@ -73405,6 +83329,12 @@ "state": "translated", "value": "我報告中的參考範圍(選填)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plage de référence de mon bilan (facultatif)" + } } } }, @@ -73439,6 +83369,12 @@ "state": "translated", "value": "來自你自己報告的參考範圍,可選" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plage de référence de votre propre bilan, facultatif" + } } } }, @@ -73473,6 +83409,12 @@ "state": "translated", "value": "重新整理" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser" + } } } }, @@ -73611,6 +83553,12 @@ "state": "translated", "value": "重新整理手環電量和連接狀態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser la batterie et l'état de connexion du bracelet." + } } } }, @@ -73645,6 +83593,12 @@ "state": "translated", "value": "放鬆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détente" + } } } }, @@ -73679,6 +83633,12 @@ "state": "translated", "value": "放鬆 4-6" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détente 4-6" + } } } }, @@ -73713,6 +83673,12 @@ "state": "translated", "value": "來自你自己數據的放鬆指導,並非健康警報,也不是診斷。趨勢比任何單一數值都更重要。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conseils de détente basés sur vos propres chiffres : ni alerte santé, ni diagnostic. Les tendances comptent plus qu'un chiffre isolé." + } } } }, @@ -73747,6 +83713,12 @@ "state": "translated", "value": "版本說明" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Notes de version" + } } } }, @@ -73781,6 +83753,12 @@ "state": "translated", "value": "剩餘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restant" + } } } }, @@ -73815,6 +83793,12 @@ "state": "translated", "value": "提醒我放鬆下來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Me rappeler de me détendre" + } } } }, @@ -73849,6 +83833,12 @@ "state": "translated", "value": "移除" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer" + } } } }, @@ -73935,6 +83925,12 @@ "state": "translated", "value": "移除 %@?NOOP 將停止連接它。它已記錄的資料會保留,你可以隨時重新新增它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer %@ ? NOOP cessera de s'y connecter. Ses données enregistrées sont conservées et vous pouvez le rajouter à tout moment." + } } } }, @@ -73969,6 +83965,12 @@ "state": "translated", "value": "移除 Apple 健康匯入的資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les données importées d'Apple Santé" + } } } }, @@ -74003,6 +84005,12 @@ "state": "translated", "value": "移除 Apple 健康匯入的資料?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les données importées d'Apple Santé ?" + } } } }, @@ -74037,6 +84045,12 @@ "state": "translated", "value": "移除一個問題以整理你的列表。自訂問題會被刪除;內建問題會被隱藏,可在下方恢復。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimez une question pour épurer votre liste. Les questions personnalisées sont supprimées ; les questions intégrées sont masquées et peuvent être restaurées ci-dessous." + } } } }, @@ -74071,6 +84085,12 @@ "state": "translated", "value": "移除 %@ 的咖啡因攝入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer la prise de caféine à %@" + } } } }, @@ -74105,6 +84125,12 @@ "state": "translated", "value": "移除匯入的資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les données importées" + } } } }, @@ -74139,6 +84165,12 @@ "state": "translated", "value": "移除照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer la photo" + } } } }, @@ -74173,6 +84205,12 @@ "state": "translated", "value": "移除此裝置?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cet appareil ?" + } } } }, @@ -74207,6 +84245,12 @@ "state": "translated", "value": "已移除" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimé" + } } } }, @@ -74241,6 +84285,12 @@ "state": "translated", "value": "已移除所有從 Apple Health 匯入的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Toutes les données importées d'Apple Santé ont été supprimées." + } } } }, @@ -74275,6 +84325,12 @@ "state": "translated", "value": "已移除 · 資料保留" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimé · données conservées" + } } } }, @@ -74309,6 +84365,12 @@ "state": "translated", "value": "移除這筆已記錄的睡眠,並在沒有它的情況下重新計算當天。此操作無法撤銷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprime ce sommeil enregistré et recalcule le jour sans lui. Cette action est irréversible." + } } } }, @@ -74343,6 +84405,12 @@ "state": "translated", "value": "移除中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suppression…" + } } } }, @@ -74377,6 +84445,12 @@ "state": "translated", "value": "重新命名" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommer" + } } } }, @@ -74411,6 +84485,12 @@ "state": "translated", "value": "重新命名裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommer l'appareil" + } } } }, @@ -74445,6 +84525,12 @@ "state": "translated", "value": "要用備份 %@ 取代目前的所有資料嗎?此操作無法復原。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer toutes les données actuelles par la sauvegarde %@ ? Cette action est irréversible." + } } } }, @@ -74479,6 +84565,12 @@ "state": "translated", "value": "用來自 %@ 的備份替換目前所有資料?此操作無法撤銷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer toutes les données actuelles par la sauvegarde du %@ ? Cette action est irréversible." + } } } }, @@ -74513,6 +84605,12 @@ "state": "translated", "value": "替換所有資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer toutes les données" + } } } }, @@ -74547,6 +84645,12 @@ "state": "translated", "value": "用你資料夾中的某個備份替換這台裝置的資料。這會覆蓋目前資料,如果你不確定,請先備份。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacez les données de cet appareil par l'une des sauvegardes de votre dossier. Cela écrase les données actuelles, faites donc d'abord une sauvegarde en cas de doute." + } } } }, @@ -74581,6 +84685,12 @@ "state": "translated", "value": "報告" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaler" + } } } }, @@ -74615,6 +84725,12 @@ "state": "translated", "value": "報告一個 %@ 錯誤" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaler un bug de %@" + } } } }, @@ -74649,6 +84765,12 @@ "state": "translated", "value": "附上我的日誌報告一個錯誤" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaler un bug avec mon journal" + } } } }, @@ -74683,6 +84805,12 @@ "state": "translated", "value": "重新掃描" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rescanner" + } } } }, @@ -74822,6 +84950,12 @@ "state": "translated", "value": "將關鍵指標重設為預設" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser les métriques clés par défaut" + } } } }, @@ -74856,6 +84990,12 @@ "state": "translated", "value": "將教練指示重設為預設值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser les instructions du Coach par défaut" + } } } }, @@ -74890,6 +85030,12 @@ "state": "translated", "value": "將儀表板卡片重設為預設" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser les cartes du tableau de bord par défaut" + } } } }, @@ -74924,6 +85070,12 @@ "state": "translated", "value": "請先在 Oura 應用程式中重置它,然後掃描。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialisez-le d'abord dans l'application Oura, puis scannez." + } } } }, @@ -74958,6 +85110,12 @@ "state": "translated", "value": "請先在 Oura 應用程式中重置它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialisez-le d'abord dans l'application Oura." + } } } }, @@ -75045,6 +85203,12 @@ "state": "translated", "value": "重設為預設值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser par défaut" + } } } }, @@ -75079,6 +85243,12 @@ "state": "translated", "value": "共振" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résonance" + } } } }, @@ -75113,6 +85283,12 @@ "state": "translated", "value": "呼吸頻率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréq. resp." + } } } }, @@ -75147,6 +85323,12 @@ "state": "translated", "value": "遵守勿擾時段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respecter les heures calmes" + } } } }, @@ -75181,6 +85363,12 @@ "state": "translated", "value": "呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respiration" + } } } }, @@ -75267,6 +85455,12 @@ "state": "translated", "value": "呼吸頻率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence respiratoire" + } } } }, @@ -75405,6 +85599,12 @@ "state": "translated", "value": "休息基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Référence de repos" + } } } }, @@ -75439,6 +85639,12 @@ "state": "translated", "value": "休息品質" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Qualité du repos" + } } } }, @@ -75473,6 +85679,12 @@ "state": "translated", "value": "好好休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reposez-vous" + } } } }, @@ -75507,6 +85719,12 @@ "state": "translated", "value": "休息 ↔ 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Repos ↔ Charge" + } } } }, @@ -75541,6 +85759,12 @@ "state": "translated", "value": "休息:你的睡眠有多具修復性?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Repos : à quel point votre sommeil a-t-il été réparateur ?" + } } } }, @@ -75627,6 +85851,12 @@ "state": "translated", "value": "從今晚起,重新開始能量和你 HRV 基線大約 4 晚的建立過程。如果糟糕的第一週使你的基線出現偏差,可以使用它。你的歷史會保留。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Relance la montée en charge d'environ 4 nuits pour la Charge et votre référence de VFC à partir de ce soir. Utilisez-le si une mauvaise première semaine a faussé votre référence. Votre historique reste intact." + } } } }, @@ -75713,6 +85943,12 @@ "state": "translated", "value": "靜息心率偏高且 HRV 低於你的基線,兩者都是高度激活的典型跡象。優先安排休息、補水,度過輕鬆的一天。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La FC au repos est élevée et la VFC est en dessous de votre référence, deux signes classiques d'une forte activation. Privilégiez le repos, l'hydratation et une journée légère." + } } } }, @@ -75747,6 +85983,12 @@ "state": "translated", "value": "靜息心率 ↔ 能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FC au repos ↔ Charge" + } } } }, @@ -75781,6 +86023,12 @@ "state": "translated", "value": "靜息心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence cardiaque au repos" + } } } }, @@ -75867,6 +86115,12 @@ "state": "translated", "value": "靜息心率和 HRV 都處於基線或以下:生理壓力低。你正處於平靜、已恢復的狀態。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque au repos et la VFC sont au niveau ou en dessous de la référence : faible stress physiologique. Vous êtes dans un état calme et récupéré." + } } } }, @@ -75901,6 +86155,12 @@ "state": "translated", "value": "靜息心率偏低且 HRV 上升。你的神經系統看起來恢復良好且平靜。如果你想的話,今天很適合加把勁。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque au repos est basse et la VFC est en hausse. Votre système nerveux semble bien récupéré et calme. Une excellente journée pour pousser si vous le souhaitez." + } } } }, @@ -75935,6 +86195,12 @@ "state": "translated", "value": "靜息心率相較你的常態偏高。你的身體今天正承受負荷,請保持輕度活動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La fréquence cardiaque au repos est élevée par rapport à votre norme. Votre corps est sous charge aujourd'hui. Gardez un effort léger." + } } } }, @@ -75969,6 +86235,12 @@ "state": "translated", "value": "靜息壓力提醒(實驗性)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rappel de stress au repos (expérimental)" + } } } }, @@ -76055,6 +86327,12 @@ "state": "translated", "value": "修復性睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil réparateur" + } } } }, @@ -76089,6 +86367,12 @@ "state": "translated", "value": "還原" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer" + } } } }, @@ -76123,6 +86407,12 @@ "state": "translated", "value": "還原備份 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer la sauvegarde %@" + } } } }, @@ -76157,6 +86447,12 @@ "state": "translated", "value": "從 %@ 還原備份" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer la sauvegarde du %@" + } } } }, @@ -76191,6 +86487,12 @@ "state": "translated", "value": "從備份還原…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer à partir d'une sauvegarde…" + } } } }, @@ -76225,6 +86527,12 @@ "state": "translated", "value": "還原問題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Problème de restauration" + } } } }, @@ -76259,6 +86567,12 @@ "state": "translated", "value": "還原此備份?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer cette sauvegarde ?" + } } } }, @@ -76293,6 +86607,12 @@ "state": "translated", "value": "還原到今天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer dans Aujourd'hui" + } } } }, @@ -76327,6 +86647,12 @@ "state": "translated", "value": "已還原" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restauré" + } } } }, @@ -76361,6 +86687,12 @@ "state": "translated", "value": "繼續" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reprendre" + } } } }, @@ -76395,6 +86727,12 @@ "state": "translated", "value": "返回鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour à l'entraînement" + } } } }, @@ -76481,6 +86819,12 @@ "state": "translated", "value": "當你返回時,反向執行上述動作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inversez ce qui précède à votre retour." + } } } }, @@ -76515,6 +86859,12 @@ "state": "translated", "value": "恢復為預設的頭像圖示" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revient à l'icône de profil par défaut" + } } } }, @@ -76549,6 +86899,12 @@ "state": "translated", "value": "分享前查看" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier avant de partager" + } } } }, @@ -76583,6 +86939,12 @@ "state": "translated", "value": "節律" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rythme" + } } } }, @@ -76617,6 +86979,12 @@ "state": "translated", "value": "「節律」只在安靜、靜止、休息的時段進行觀察,因此它需要一個平靜夜晚中足量的平穩心跳。一旦有了乾淨的時段,散點圖及其說明就會顯示在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rythme n'observe que pendant les fenêtres calmes, immobiles et de repos, il a donc besoin d'une nuit calme aux battements réguliers. Une fois qu'une fenêtre propre existe, le nuage de points et sa description s'affichent ici." + } } } }, @@ -76651,6 +87019,12 @@ "state": "translated", "value": "節律視覺化(實驗性)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Visualisation du rythme (expérimental)" + } } } }, @@ -76685,6 +87059,12 @@ "state": "translated", "value": "戒指金鑰(32 個十六進位字元)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé de la bague (32 caractères hexadécimaux)" + } } } }, @@ -76719,6 +87099,12 @@ "state": "translated", "value": "戒指金鑰,32 個十六進位字元" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé de la bague, 32 caractères hexadécimaux" + } } } }, @@ -76753,6 +87139,12 @@ "state": "translated", "value": "滾動 RMSSD %lld 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RMSSD glissant %lld millisecondes" + } } } }, @@ -76787,6 +87179,12 @@ "state": "translated", "value": "粗略" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Difficile" + } } } }, @@ -76821,6 +87219,12 @@ "state": "translated", "value": "僅為粗略參考,根據你記錄的內容得出。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Simple indication approximative, basée sur ce que vous avez consigné." + } } } }, @@ -76855,6 +87259,12 @@ "state": "translated", "value": "第 %lld 輪,共 %lld 輪" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Round %1$lld sur %2$lld" + } } } }, @@ -76889,6 +87299,12 @@ "state": "translated", "value": "輪數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rounds" + } } } }, @@ -76923,6 +87339,12 @@ "state": "translated", "value": "路線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Itinéraire" + } } } }, @@ -76957,6 +87379,12 @@ "state": "translated", "value": "免動手執行 NOOP 操作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécutez des actions NOOP en mains libres." + } } } }, @@ -76991,6 +87419,12 @@ "state": "translated", "value": "戴回時執行捷徑" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter un raccourci lorsqu'il est remis" + } } } }, @@ -77025,6 +87459,12 @@ "state": "translated", "value": "取下時執行捷徑" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter un raccourci lorsqu'il est retiré" + } } } }, @@ -77059,6 +87499,12 @@ "state": "translated", "value": "執行捷徑⋯⋯" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter un raccourci…" + } } } }, @@ -77093,6 +87539,12 @@ "state": "translated", "value": "執行一次乾淨的個人測試" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réaliser un test personnel propre" + } } } }, @@ -77127,6 +87579,12 @@ "state": "translated", "value": "疲憊不堪" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Épuisé" + } } } }, @@ -77161,6 +87619,12 @@ "state": "translated", "value": "立即執行" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter maintenant" + } } } }, @@ -77247,6 +87711,12 @@ "state": "translated", "value": "僅在你的勿擾時段(預設為 22:00 至 07:00)執行資料流,大約可將電池消耗減半。由於壓力讀取的是此即時資料流,白天的壓力讀數會較稀疏。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "N'exécute le flux que pendant votre fenêtre d'heures calmes (de 22h00 à 07h00 par défaut), ce qui réduit environ de moitié le coût en batterie. Les mesures de Stress en journée seront plus rares, car Stress lit ce flux en direct." + } } } }, @@ -77281,6 +87751,12 @@ "state": "translated", "value": "評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SCORES" + } } } }, @@ -77315,6 +87791,12 @@ "state": "translated", "value": "SD1 · 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SD1 · ms" + } } } }, @@ -77349,6 +87831,12 @@ "state": "translated", "value": "SD1:SD2 比值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rapport SD1:SD2" + } } } }, @@ -77383,6 +87871,12 @@ "state": "translated", "value": "SD2 · 毫秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SD2 · ms" + } } } }, @@ -77417,6 +87911,12 @@ "state": "translated", "value": "秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SEC" + } } } }, @@ -77503,6 +88003,12 @@ "state": "translated", "value": "感測器" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CAPTEUR" + } } } }, @@ -77589,6 +88095,12 @@ "state": "translated", "value": "訓練場次" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SÉANCES" + } } } }, @@ -77623,6 +88135,12 @@ "state": "translated", "value": "短軸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AXE COURT" + } } } }, @@ -77709,6 +88227,12 @@ "state": "translated", "value": "睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SOMMEIL" + } } } }, @@ -77743,6 +88267,12 @@ "state": "translated", "value": "來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SOURCE" + } } } }, @@ -77777,6 +88307,12 @@ "state": "translated", "value": "速度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VITESSE" + } } } }, @@ -77811,6 +88347,12 @@ "state": "translated", "value": "運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SPORT" + } } } }, @@ -77845,6 +88387,12 @@ "state": "translated", "value": "步數估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ESTIMATION DES PAS" + } } } }, @@ -77931,6 +88479,12 @@ "state": "translated", "value": "串流中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DIFFUSION" + } } } }, @@ -77965,6 +88519,12 @@ "state": "translated", "value": "訊號更強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SIGNAL PLUS FORT" + } } } }, @@ -77999,6 +88559,12 @@ "state": "translated", "value": "正在同步 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SYNCHRONISATION %lld" + } } } }, @@ -78033,6 +88599,12 @@ "state": "translated", "value": "與 WHOOP 的 Day Strain(0–21)採用相同的心血管負荷概念。我們將刻度的頂端從 21 重新調整為 100,讓三項評分共用同一個尺度。刻度本身沒有移動,因此 100 分和過去的 21.0 一樣罕見。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Même principe de charge cardiovasculaire que l'Effort du jour de WHOOP (0-21). Nous avons redimensionné le haut de l'échelle de 21 à 100 pour que les trois scores partagent une seule échelle. Les échelons n'ont pas bougé, un 100 est donc aussi rare que l'était un 21,0." + } } } }, @@ -78067,6 +88639,12 @@ "state": "translated", "value": "與 WHOOP 的 Recovery %(以 HRV 為主的恢復)採用相同的核心概念,但我們的加權與基線計算是自己的,並且公開記錄。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Même principe de base que le % de Récupération de WHOOP (récupération pilotée par la VFC), mais notre pondération et nos calculs de référence sont les nôtres, et documentés ouvertement." + } } } }, @@ -78101,6 +88679,12 @@ "state": "translated", "value": "週六" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sam" + } } } }, @@ -78135,6 +88719,12 @@ "state": "translated", "value": "星期六" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Samedi" + } } } }, @@ -78169,6 +88759,12 @@ "state": "translated", "value": "儲存" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } } } }, @@ -78203,6 +88799,12 @@ "state": "translated", "value": "儲存並繼續" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer et continuer" + } } } }, @@ -78237,6 +88839,12 @@ "state": "translated", "value": "將完整備份保存到你選擇的資料夾,將它指向 Google Drive、iCloud 或 Dropbox 以進行裝置外同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrez une sauvegarde complète dans un dossier de votre choix - pointez-le vers Google Drive, iCloud ou Dropbox pour une synchronisation hors appareil." + } } } }, @@ -78271,6 +88879,12 @@ "state": "translated", "value": "儲存它" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } } } }, @@ -78357,6 +88971,12 @@ "state": "translated", "value": "儲存讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer la mesure" + } } } }, @@ -78391,6 +89011,12 @@ "state": "translated", "value": "儲存鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer l'entraînement" + } } } }, @@ -78425,6 +89051,12 @@ "state": "translated", "value": "已儲存" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré" + } } } }, @@ -78459,6 +89091,12 @@ "state": "translated", "value": "已將 %@ 儲存到「檔案」應用程式中 NOOP 的資料夾。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ enregistré dans le dossier de NOOP dans l'application Fichiers." + } } } }, @@ -78493,6 +89131,12 @@ "state": "translated", "value": "已將 %@ 儲存到你的「文件」資料夾。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ enregistré dans votre dossier Documents." + } } } }, @@ -78527,6 +89171,12 @@ "state": "translated", "value": "已將備份儲存到你的資料夾。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde enregistrée dans votre dossier." + } } } }, @@ -78567,6 +89217,12 @@ "state": "translated", "value": "已儲存到 %@。將此檔案複製到你的另一台 %@,並在那裡使用「匯入」以還原所有內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré dans %1$@. Copiez ce fichier sur votre autre %2$@ et utilisez Importer là-bas pour tout restaurer." + } } } }, @@ -78601,6 +89257,12 @@ "state": "translated", "value": "已儲存到 %@。這個 zip 可在任何 Mac、iPhone 或 Android 裝置上重新匯入 NOOP(資料來源 → WHOOP 匯出)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré dans %@. Le zip se réimporte dans NOOP (Sources de données → Export WHOOP) sur n'importe quel Mac, iPhone ou appareil Android." + } } } }, @@ -78635,6 +89297,12 @@ "state": "translated", "value": "將每一個原始 5/MG 幀(帶時間戳和即時心率)保存到一個 JSON 檔案,你可以分享它來幫助繪製生物數據佈局。它只記錄手環已經發送的幀,絕不會向你的手環寫入任何內容,因此可以放心保持開啟。匯出該檔案並將其附加到協議對映的 issue 中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistre chaque trame brute 5/MG (avec un horodatage et la fréquence cardiaque en direct) dans un fichier JSON que vous pouvez partager pour aider à cartographier la structure biométrique. Cela n'enregistre que les trames déjà envoyées par le bracelet (rien n'est jamais écrit sur votre bracelet), il est donc sans risque de le laisser activé. Exportez le fichier et joignez-le à un signalement de cartographie de protocole." + } } } }, @@ -78722,6 +89390,12 @@ "state": "translated", "value": "將原始擷取和手環日誌作為一對配對檔案一起保存,請將兩者都附加到協議對映的 issue 中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistre la capture brute et le journal du bracelet ensemble, comme une paire assortie. Joignez les deux à un signalement de cartographie de protocole." + } } } }, @@ -78808,6 +89482,12 @@ "state": "translated", "value": "正在儲存到:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrement dans : %@" + } } } }, @@ -78842,6 +89522,12 @@ "state": "translated", "value": "儲存中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrement…" + } } } }, @@ -78981,6 +89667,12 @@ "state": "translated", "value": "掃描並連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Scanner et connecter" + } } } }, @@ -79015,6 +89707,12 @@ "state": "translated", "value": "掃描會連接到 %@。要配對或切換手環,請打開「裝置」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "« Scanner » se connecte à %@. Pour appairer ou changer de bracelet, ouvrez Appareils." + } } } }, @@ -79049,6 +89747,12 @@ "state": "translated", "value": "掃描 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher %@" + } } } }, @@ -79083,6 +89787,12 @@ "state": "translated", "value": "掃描你的 Oura 戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher votre bague Oura" + } } } }, @@ -79117,6 +89827,12 @@ "state": "translated", "value": "掃描你的戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher votre bague" + } } } }, @@ -79151,6 +89867,12 @@ "state": "translated", "value": "正在掃描你的資料…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyse de vos données…" + } } } }, @@ -79237,6 +89959,12 @@ "state": "translated", "value": "評分仍在校準中,尚無單一的當日主導來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Scores encore en calibration, pas encore de propriétaire unique du jour" + } } } }, @@ -79271,6 +89999,12 @@ "state": "translated", "value": "滾動以縮放 · 拖動以平移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Défiler pour zoomer · glisser pour déplacer" + } } } }, @@ -79305,6 +90039,12 @@ "state": "translated", "value": "搜尋" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher" + } } } }, @@ -79339,6 +90079,12 @@ "state": "translated", "value": "搜尋標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher des marqueurs" + } } } }, @@ -79373,6 +90119,12 @@ "state": "translated", "value": "搜尋標記(例如 LDL、鐵蛋白)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher des marqueurs (ex. LDL, ferritine)" + } } } }, @@ -79407,6 +90159,12 @@ "state": "translated", "value": "搜尋運動項目" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher un sport" + } } } }, @@ -79493,6 +90251,12 @@ "state": "translated", "value": "搜尋中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche…" + } } } }, @@ -79527,6 +90291,12 @@ "state": "translated", "value": "了解能量、消耗和休息如何計算,以及它們與 WHOOP 的區別。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez comment la Charge, l'Effort et le Repos sont calculés, et en quoi ils diffèrent de WHOOP." + } } } }, @@ -79561,6 +90331,12 @@ "state": "translated", "value": "以優美的方式看見恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Visualisez la récupération, avec élégance" + } } } }, @@ -79595,6 +90371,12 @@ "state": "translated", "value": "查看是什麼塑造了你的能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez ce qui a façonné votre Charge" + } } } }, @@ -79629,6 +90411,12 @@ "state": "translated", "value": "查看你的完整準備度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir votre préparation complète" + } } } }, @@ -79663,6 +90451,12 @@ "state": "translated", "value": "自我追蹤,而非臨床評估。如果低落情緒持續存在,請諮詢專業人士,你值得獲得支持。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Auto-suivi, pas une évaluation clinique. Si une humeur basse persiste, parlez-en à un professionnel. Vous méritez du soutien." + } } } }, @@ -79749,6 +90543,12 @@ "state": "translated", "value": "向你的 WHOOP 手環發送一次觸感震動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Envoyez une vibration haptique à votre bracelet WHOOP." + } } } }, @@ -79783,6 +90583,12 @@ "state": "translated", "value": "向手環發送啟用序列" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Envoyer la séquence d'activation au bracelet" + } } } }, @@ -79817,6 +90623,12 @@ "state": "translated", "value": "9月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sept" + } } } }, @@ -79851,6 +90663,12 @@ "state": "translated", "value": "伺服器 URL" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "URL du serveur" + } } } }, @@ -79937,6 +90755,12 @@ "state": "translated", "value": "工作階段結束" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séance terminée" + } } } }, @@ -79971,6 +90795,12 @@ "state": "translated", "value": "訓練已結束。改試試節律呼吸吧。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séance terminée. Essayez plutôt une respiration cadencée." + } } } }, @@ -80057,6 +90887,12 @@ "state": "translated", "value": "工作階段進度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Progression de la séance" + } } } }, @@ -80091,6 +90927,12 @@ "state": "translated", "value": "工作階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séances" + } } } }, @@ -80125,6 +90967,12 @@ "state": "translated", "value": "為特定的日子設定起床時間,比如週末睡個懶覺。你未作設定的日子會使用上方的時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Définissez une heure de réveil pour des jours précis (une grasse matinée le week-end, par exemple). Les jours non modifiés utilisent l'heure ci-dessus." + } } } }, @@ -80159,6 +91007,12 @@ "state": "translated", "value": "設定自訂容器大小" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Définir une taille de récipient personnalisée" + } } } }, @@ -80193,6 +91047,12 @@ "state": "translated", "value": "在你的 iPhone 上設定此項" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configurez ceci sur votre iPhone" + } } } }, @@ -80227,6 +91087,12 @@ "state": "translated", "value": "設定 Apple Watch" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configurer l'Apple Watch" + } } } }, @@ -80313,6 +91179,12 @@ "state": "translated", "value": "正在平復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stabilisation" + } } } }, @@ -80347,6 +91219,12 @@ "state": "translated", "value": "設定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configuration" + } } } }, @@ -80433,6 +91311,12 @@ "state": "translated", "value": "分享" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partager" + } } } }, @@ -80467,6 +91351,12 @@ "state": "translated", "value": "匯入的各區間時間佔比,按各工作階段的時長加權,近似值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Part du temps en zone importé, pondérée par la durée sur l'ensemble des séances (approximative)." + } } } }, @@ -80554,6 +91444,12 @@ "state": "translated", "value": "正在分享 %lld bpm。正在等待裝置配對。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partage de %lld bpm. En attente d'un appareil à appairer." + } } } }, @@ -80588,6 +91484,12 @@ "state": "translated", "value": "捷徑名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom du raccourci" + } } } }, @@ -80622,6 +91524,12 @@ "state": "translated", "value": "捷徑匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export Raccourcis" + } } } }, @@ -80656,6 +91564,12 @@ "state": "translated", "value": "捷徑檔案匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export de fichier Raccourcis" + } } } }, @@ -80742,6 +91656,12 @@ "state": "translated", "value": "顯示 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher %@" + } } } }, @@ -80776,6 +91696,12 @@ "state": "translated", "value": "顯示與重新排序" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher et réorganiser" + } } } }, @@ -80810,6 +91736,12 @@ "state": "translated", "value": "顯示所有指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher toutes les métriques" + } } } }, @@ -80844,6 +91776,12 @@ "state": "translated", "value": "顯示所有指標,還有 %lld 項" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher toutes les métriques, %lld de plus" + } } } }, @@ -80878,6 +91816,12 @@ "state": "translated", "value": "顯示更少" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher moins" + } } } }, @@ -80912,6 +91856,12 @@ "state": "translated", "value": "顯示更少指標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher moins de métriques" + } } } }, @@ -80946,6 +91896,12 @@ "state": "translated", "value": "顯示 NOOP 從何處同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher les sources de synchronisation de NOOP" + } } } }, @@ -80980,6 +91936,12 @@ "state": "translated", "value": "在「今天」畫面背後顯示柔和的日出、白天、黃昏和夜晚場景。將它關閉可獲得純粹的深色畫布,你的卡片依然同樣清晰易讀。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche une douce scène de lever de soleil, jour, crépuscule et nuit derrière l'écran Aujourd'hui. Désactivez-le pour un fond sombre uni. Vos cartes restent tout aussi lisibles." + } } } }, @@ -81014,6 +91976,12 @@ "state": "translated", "value": "顯示 Apple 健康權限面板" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche la fenêtre d'autorisation Apple Santé" + } } } }, @@ -81048,6 +92016,12 @@ "state": "translated", "value": "顯示進階設定的各部分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche les sections des réglages avancés" + } } } }, @@ -81082,6 +92056,12 @@ "state": "translated", "value": "在手環連接期間,於鎖定畫面和動態島上顯示你的即時心率。將它關閉可讓你的即時心率不出現在動態島中。(任何已在顯示的都會在片刻內清除。)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche votre fréquence cardiaque en direct sur l'écran verrouillé et dans la Dynamic Island tant que le bracelet est connecté. Désactivez-le pour garder votre FC en direct hors de la Dynamic Island. (Une FC déjà affichée disparaît sous peu.)" + } } } }, @@ -81116,6 +92096,12 @@ "state": "translated", "value": "訊號信任度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fiabilité du signal" + } } } }, @@ -81150,6 +92136,12 @@ "state": "translated", "value": "訊號上升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaux en hausse" + } } } }, @@ -81237,6 +92229,12 @@ "state": "translated", "value": "靜默觸感 HIIT,手環以震動提示各階段切換" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "HIIT haptique silencieux : le bracelet vibre aux transitions" + } } } }, @@ -81271,6 +92269,12 @@ "state": "translated", "value": "與 WHOOP 的 Sleep Performance % 理念相近;我們的綜合評分是自己的。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Similaire dans l'esprit au % de Performance de sommeil de WHOOP ; notre composite nous est propre." + } } } }, @@ -81305,6 +92309,12 @@ "state": "translated", "value": "單次" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Unique" + } } } }, @@ -81339,6 +92349,12 @@ "state": "translated", "value": "範圍內的單次讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mesure unique dans la plage" + } } } }, @@ -81373,6 +92389,12 @@ "state": "translated", "value": "一口" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gorgée" + } } } }, @@ -81413,6 +92435,12 @@ "state": "translated", "value": "一口 %1$lld 毫升 · 一杯 %2$lld 毫升 · 一瓶 %3$lld 毫升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gorgée %1$lld ml · Tasse %2$lld ml · Bouteille %3$lld ml" + } } } }, @@ -81447,6 +92475,12 @@ "state": "translated", "value": "Siri 與捷徑" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Siri et Raccourcis" + } } } }, @@ -81481,6 +92515,12 @@ "state": "translated", "value": "靜坐並正常呼吸。點按下方進行 60 秒讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restez immobile et respirez normalement. Touchez ci-dessous pour effectuer une mesure de 60 secondes." + } } } }, @@ -81515,6 +92555,12 @@ "state": "translated", "value": "坐姿靜止,跟著震動呼吸。你可以隨時停止;被停止的掃掠不會鎖定節奏。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restez immobile et respirez au rythme de la vibration. Vous pouvez arrêter à tout moment ; un balayage interrompu ne fixera pas d'allure." + } } } }, @@ -81549,6 +92595,12 @@ "state": "translated", "value": "靜坐,正常呼吸。保持手腕放鬆且穩定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restez immobile, respirez normalement. Gardez votre poignet détendu et stable." + } } } }, @@ -81583,6 +92635,12 @@ "state": "translated", "value": "久坐已" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assis depuis" + } } } }, @@ -81669,6 +92727,12 @@ "state": "translated", "value": "皮膚溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Température cutanée" + } } } }, @@ -81761,6 +92825,12 @@ "state": "translated", "value": "皮膚溫度 %1$@。%2$@。可靠。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Température cutanée %1$@. %2$@. Fiable." + } } } }, @@ -81795,6 +92865,12 @@ "state": "translated", "value": "皮膚溫度偏差" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écart de température cutanée" + } } } }, @@ -81829,6 +92905,12 @@ "state": "translated", "value": "皮膚溫度趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance de la température cutanée" + } } } }, @@ -81863,6 +92945,12 @@ "state": "translated", "value": "手環未佩戴在手腕上時跳過提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer les alertes lorsque le bracelet n'est pas au poignet." + } } } }, @@ -81950,6 +93038,12 @@ "state": "translated", "value": "跳過今天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer aujourd'hui" + } } } }, @@ -82036,6 +93130,12 @@ "state": "translated", "value": "睡眠與休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil et Repos" + } } } }, @@ -82070,6 +93170,12 @@ "state": "translated", "value": "睡眠 / 休息" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil / Repos" + } } } }, @@ -82104,6 +93210,12 @@ "state": "translated", "value": "睡眠規律性" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Régularité du sommeil" + } } } }, @@ -82190,6 +93302,12 @@ "state": "translated", "value": "睡眠效率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Efficacité du sommeil" + } } } }, @@ -82224,6 +93342,12 @@ "state": "translated", "value": "睡眠需求" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Besoin de sommeil" + } } } }, @@ -82311,6 +93435,12 @@ "state": "translated", "value": "睡眠評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Score de sommeil" + } } } }, @@ -82345,6 +93475,12 @@ "state": "translated", "value": "平均睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil moy." + } } } }, @@ -82379,6 +93515,12 @@ "state": "translated", "value": "睡眠標記" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marqueurs de sommeil" + } } } }, @@ -82413,6 +93555,12 @@ "state": "translated", "value": "睡眠表現" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Performance de sommeil" + } } } }, @@ -82447,6 +93595,12 @@ "state": "translated", "value": "睡眠表現 %lld / 100" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Performance de sommeil %lld sur 100" + } } } }, @@ -82481,6 +93635,12 @@ "state": "translated", "value": "睡眠品質" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Qualité du sommeil" + } } } }, @@ -82515,6 +93675,12 @@ "state": "translated", "value": "睡眠評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Score de sommeil" + } } } }, @@ -82549,6 +93715,12 @@ "state": "translated", "value": "睡眠評分依據:你的睡眠時長相對於所需時長、整晚的效率,以及修復性睡眠(深睡和 REM)所佔的比例。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil évalué à partir de la durée dormie par rapport à votre besoin, de l'efficacité de la nuit et de la part réparatrice (sommeil profond et paradoxal)." + } } } }, @@ -82583,6 +93755,12 @@ "state": "translated", "value": "睡眠分類、評分、錄製,以及你的數字來自何處。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tri du sommeil, scores, enregistrement et provenance de vos chiffres." + } } } }, @@ -82669,6 +93847,12 @@ "state": "translated", "value": "睡眠分期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Répartition des phases de sommeil" + } } } }, @@ -82703,6 +93887,12 @@ "state": "translated", "value": "睡眠穩定性:休息在各晚之間波動 ±%@ 分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stabilité du sommeil : le Repos a varié de ±%@ pts d'une nuit à l'autre." + } } } }, @@ -82737,6 +93927,12 @@ "state": "translated", "value": "睡眠 · 評分 · 錄製 · 你的數字來自何處" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sommeil · scores · enregistrement · provenance de vos chiffres" + } } } }, @@ -82771,6 +93967,12 @@ "state": "translated", "value": "睡眠負債帳本" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Registre de dette de sommeil" + } } } }, @@ -82805,6 +94007,12 @@ "state": "translated", "value": "略微偏離基線(HRV 稍低),所以你處於中度激活狀態。沒什麼好擔心的,只是別過度勉強。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Légèrement écarté de la référence (la VFC est un peu basse), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire." + } } } }, @@ -82839,6 +94047,12 @@ "state": "translated", "value": "略微偏離基線(靜息心率稍高),所以你處於中度激活狀態。沒什麼好擔心的,只是別過度勉強。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Légèrement écarté de la référence (la FC au repos est un peu élevée), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire." + } } } }, @@ -82873,6 +94087,12 @@ "state": "translated", "value": "斜率來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source de la pente" + } } } }, @@ -82959,6 +94179,12 @@ "state": "translated", "value": "智慧鬧鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveil intelligent" + } } } }, @@ -82993,6 +94219,12 @@ "state": "translated", "value": "確保你的區間、卡路里和基線準確無誤。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour que vos zones, calories et références soient exactes." + } } } }, @@ -83027,6 +94259,12 @@ "state": "translated", "value": "每個階段發出柔和提示音 · 遵從靜音模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tonalité douce à chaque phase · respecte le mode silencieux" + } } } }, @@ -83061,6 +94299,12 @@ "state": "translated", "value": "扎實" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Solide" + } } } }, @@ -83095,6 +94339,12 @@ "state": "translated", "value": "訊號穩定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signal solide." + } } } }, @@ -83129,6 +94379,12 @@ "state": "translated", "value": "偶爾出現的額外或漏跳心搏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quelques battements supplémentaires ou manqués occasionnels" + } } } }, @@ -83163,6 +94419,12 @@ "state": "translated", "value": "來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source" + } } } }, @@ -83301,6 +94563,12 @@ "state": "translated", "value": "來源:匯入的活動檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : fichier d'activité importé" + } } } }, @@ -83335,6 +94603,12 @@ "state": "translated", "value": "來源:匯入的重訓記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : journal de musculation importé" + } } } }, @@ -83369,6 +94643,12 @@ "state": "translated", "value": "來源:手動輸入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : saisie manuelle" + } } } }, @@ -83403,6 +94683,12 @@ "state": "translated", "value": "來源:裝置端偵測" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : détecté sur l'appareil" + } } } }, @@ -83437,6 +94723,12 @@ "state": "translated", "value": "來源:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : %@" + } } } }, @@ -83471,6 +94763,12 @@ "state": "translated", "value": "來源:Apple Watch" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Source : Apple Watch" + } } } }, @@ -83505,6 +94803,12 @@ "state": "translated", "value": "來源不一致" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sources diffèrent" + } } } }, @@ -83545,6 +94849,12 @@ "state": "translated", "value": "資料稀疏,已放寬至 %@ · %lld 筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Épars, élargi à %1$@ · %2$lld mesures" + } } } }, @@ -83579,6 +94889,12 @@ "state": "translated", "value": "速度 · 踏頻 · 功率 · 距離 · 能量 · 心率(如果器材有傳送)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vitesse · Cadence · Puissance · Distance · Énergie · Fréquence cardiaque (si la machine l'envoie)" + } } } }, @@ -83613,6 +94929,12 @@ "state": "translated", "value": "運動項目" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sport" + } } } }, @@ -83647,6 +94969,12 @@ "state": "translated", "value": "方形呼吸 · 穩定專注" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respiration carrée · concentration stable" + } } } }, @@ -83785,6 +95113,12 @@ "state": "translated", "value": "標準心率並非完整綁定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La FC standard n'est pas un appairage complet" + } } } }, @@ -83819,6 +95153,12 @@ "state": "translated", "value": "標準心率模式(低頻寬)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode FC standard (faible bande passante)" + } } } }, @@ -83853,6 +95193,12 @@ "state": "translated", "value": "標準心率模式,低頻寬。%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode FC standard, faible bande passante. %@" + } } } }, @@ -83939,6 +95285,12 @@ "state": "translated", "value": "開始 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer %@" + } } } }, @@ -83973,6 +95325,12 @@ "state": "translated", "value": "開始一次呼吸工作階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer une séance de Respiration" + } } } }, @@ -84007,6 +95365,12 @@ "state": "translated", "value": "開始一個即時流" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer un flux en direct" + } } } }, @@ -84041,6 +95405,12 @@ "state": "translated", "value": "以今天的日期作為第一天,開始一次本地實驗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrez une expérience locale en utilisant la date d'aujourd'hui comme jour un." + } } } }, @@ -84075,6 +95445,12 @@ "state": "translated", "value": "開始一次鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer un entraînement" + } } } }, @@ -84109,6 +95485,12 @@ "state": "translated", "value": "當資料流很重要時,開始一次鍛鍊。NOOP 會從同一個即時資料流中記錄時段、心率、峰值、平均值和消耗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrez un entraînement quand le flux compte. NOOP enregistre l'intervalle, la FC, le pic, la moyenne et l'effort à partir du même flux en direct." + } } } }, @@ -84143,6 +95525,12 @@ "state": "translated", "value": "開始一次鍛鍊、記錄你的日誌,或呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrez un entraînement, remplissez votre journal ou respirez" + } } } }, @@ -84177,6 +95565,12 @@ "state": "translated", "value": "開始時間不能在未來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le début ne peut pas être dans le futur." + } } } }, @@ -84211,6 +95605,12 @@ "state": "translated", "value": "開始日期和時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Date et heure de début" + } } } }, @@ -84245,6 +95645,12 @@ "state": "translated", "value": "開始實驗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer l'expérience" + } } } }, @@ -84331,6 +95737,12 @@ "state": "translated", "value": "開始鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer l'entraînement" + } } } }, @@ -84371,6 +95783,12 @@ "state": "translated", "value": "已開始 %1$@ · 正在測試 %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commencé %@ · test %@" + } } } }, @@ -84405,6 +95823,12 @@ "state": "translated", "value": "從 %lld bpm 開始 · 節律會引領你的心率下降。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commencé à %lld bpm · le rythme accompagne votre cœur vers le bas." + } } } }, @@ -84439,6 +95863,12 @@ "state": "translated", "value": "開始中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage…" + } } } }, @@ -84525,6 +95955,12 @@ "state": "translated", "value": "掌握最新動態" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restez informé" + } } } }, @@ -84559,6 +95995,12 @@ "state": "translated", "value": "平穩" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stable" + } } } }, @@ -84593,6 +96035,12 @@ "state": "translated", "value": "回到有佩戴手環的一天。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revenez à un jour où le bracelet était porté." + } } } }, @@ -84627,6 +96075,12 @@ "state": "translated", "value": "步數校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étalonnage des pas" + } } } }, @@ -84661,6 +96115,12 @@ "state": "translated", "value": "步數校準,每步 %@ 次計數器計數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étalonnage des pas, %@ impulsions du compteur par pas" + } } } }, @@ -84695,6 +96155,12 @@ "state": "translated", "value": "用上方的箭頭切換到另一週以查看其回顧。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passez à une autre semaine avec les flèches ci-dessus pour voir son bilan." + } } } }, @@ -84781,6 +96247,12 @@ "state": "translated", "value": "步數與鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas et entraînements" + } } } }, @@ -84815,6 +96287,12 @@ "state": "translated", "value": "步數(估算)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas (estimés)" + } } } }, @@ -84849,6 +96327,12 @@ "state": "translated", "value": "步數 / 體動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas / mouvement" + } } } }, @@ -84883,6 +96367,12 @@ "state": "translated", "value": "步數估算" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation des pas" + } } } }, @@ -84917,6 +96407,12 @@ "state": "translated", "value": "步數估算校準。%@。打開校準畫面。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étalonnage de l'estimation des pas. %@. Ouvre l'écran d'étalonnage." + } } } }, @@ -84951,6 +96447,12 @@ "state": "translated", "value": "今日步數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas aujourd'hui" + } } } }, @@ -84985,6 +96487,12 @@ "state": "translated", "value": "步數、活動能量和已記錄的鍛鍊會提供給消耗。密集且可靠。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les pas, l'énergie active et les entraînements enregistrés alimentent l'Effort. Dense et fiable." + } } } }, @@ -85019,6 +96527,12 @@ "state": "translated", "value": "步數、活動能量和已記錄的鍛鍊會提供給你的消耗。密集且可靠。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les pas, l'énergie active et les entraînements enregistrés alimentent votre Effort. Dense et fiable." + } } } }, @@ -85053,6 +96567,12 @@ "state": "translated", "value": "步數、心率、睡眠、SpO₂ 和壓力,從 Mi Fitness 匯入,在 %@ 上本地讀取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas, fréquence cardiaque, sommeil, SpO₂ et stress, importés depuis Mi Fitness, lus localement sur %@." + } } } }, @@ -85087,6 +96607,12 @@ "state": "translated", "value": "步數、心率、睡眠、身體組成和 VO₂ max,在 %@ 上本地讀取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas, cœur, sommeil, composition corporelle et VO₂ max, lus localement sur %@." + } } } }, @@ -85121,6 +96647,12 @@ "state": "translated", "value": "靜止" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Immobile" + } } } }, @@ -85155,6 +96687,12 @@ "state": "translated", "value": "停止" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrêter" + } } } }, @@ -85195,6 +96733,12 @@ "state": "translated", "value": "在大約 %1$@ 之後停止攝入咖啡因,以便在 %2$@ 前將其大部分代謝掉。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrêtez la caféine vers %1$@ pour qu'elle soit en grande partie éliminée d'ici %2$@." + } } } }, @@ -85281,6 +96825,12 @@ "state": "translated", "value": "停止掃掠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrêter le balayage" + } } } }, @@ -85315,6 +96865,12 @@ "state": "translated", "value": "停止從你的夜間溫度中讀取週期階段。你可以隨時在這裡重新開啟它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrête de déduire une phase du cycle à partir de votre température nocturne. Vous pouvez le réactiver ici à tout moment." + } } } }, @@ -85349,6 +96905,12 @@ "state": "translated", "value": "在鍛鍊錄製期間阻止螢幕變暗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Empêche l'écran de s'assombrir pendant l'enregistrement d'un entraînement" + } } } }, @@ -85383,6 +96945,12 @@ "state": "translated", "value": "儲存空間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stockage" + } } } }, @@ -85417,6 +96985,12 @@ "state": "translated", "value": "儲存空間不可用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stockage indisponible" + } } } }, @@ -85457,6 +97031,12 @@ "state": "translated", "value": "儲存為 %1$@。%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré sous %1$@. %2$@" + } } } }, @@ -85491,6 +97071,12 @@ "state": "translated", "value": "強度提示和你的智慧鬧鐘會在觸發的那一刻輕拍你的手腕。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet dès qu'ils se déclenchent." + } } } }, @@ -85525,6 +97111,12 @@ "state": "translated", "value": "強度提示和你的智慧鬧鐘以同樣的方式輕拍你的手腕。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet de la même manière." + } } } }, @@ -85559,6 +97151,12 @@ "state": "translated", "value": "緊繃" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sous tension" + } } } }, @@ -85645,6 +97243,12 @@ "state": "translated", "value": "手環已接受 %lld/15 個 R22 旗標…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le bracelet a accepté %lld/15 indicateurs R22…" + } } } }, @@ -85679,6 +97283,12 @@ "state": "translated", "value": "手環已接受全部 15 個 R22 旗標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le bracelet a accepté les 15 indicateurs R22" + } } } }, @@ -85713,6 +97323,12 @@ "state": "translated", "value": "手環電量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie du bracelet" + } } } }, @@ -85753,6 +97369,12 @@ "state": "translated", "value": "手環電量百分之 %1$lld%2$@%3$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie du bracelet %1$lld pour cent%2$@%3$@" + } } } }, @@ -85839,6 +97461,12 @@ "state": "translated", "value": "手環已連接" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet connecté" + } } } }, @@ -85873,6 +97501,12 @@ "state": "translated", "value": "無需 HealthKit 即可將手環資料匯入 Apple 健康,適用於側載安裝。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données du bracelet vers Apple Santé sans HealthKit, pour les installations en sideload." + } } } }, @@ -85907,6 +97541,12 @@ "state": "translated", "value": "手環已充滿電" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet complètement chargé" + } } } }, @@ -85941,6 +97581,12 @@ "state": "translated", "value": "手環歷史" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Historique du bracelet" + } } } }, @@ -85975,6 +97621,12 @@ "state": "translated", "value": "手環閒置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet inactif" + } } } }, @@ -86009,6 +97661,12 @@ "state": "translated", "value": "手環即時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet en direct" + } } } }, @@ -86043,6 +97701,12 @@ "state": "translated", "value": "手環日誌已匯出" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journal du bracelet exporté" + } } } }, @@ -86077,6 +97741,12 @@ "state": "translated", "value": "手環名稱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom du bracelet" + } } } }, @@ -86111,6 +97781,12 @@ "state": "translated", "value": "需要手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet requis" + } } } }, @@ -86197,6 +97873,12 @@ "state": "translated", "value": "手環未連接。點擊以連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet non connecté. Touchez pour connecter." + } } } }, @@ -86231,6 +97913,12 @@ "state": "translated", "value": "手環未佩戴" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet non porté" + } } } }, @@ -86317,6 +98005,12 @@ "state": "translated", "value": "手環喚醒鬧鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveil du bracelet" + } } } }, @@ -86351,6 +98045,12 @@ "state": "translated", "value": "即時串流中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diffusion en direct" + } } } }, @@ -86385,6 +98085,12 @@ "state": "translated", "value": "正在串流" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En cours de diffusion" + } } } }, @@ -86419,6 +98125,12 @@ "state": "translated", "value": "力量訓練時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temps d'activité de force" + } } } }, @@ -86609,6 +98321,12 @@ "state": "translated", "value": "平均壓力" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stress moy." + } } } }, @@ -86643,6 +98361,12 @@ "state": "translated", "value": "壓力簽到" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bilan de stress" + } } } }, @@ -86677,6 +98401,12 @@ "state": "translated", "value": "壓力簽到(觸覺回饋)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bilans de stress (haptiques)" + } } } }, @@ -86815,6 +98545,12 @@ "state": "translated", "value": "壓力趨勢" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance du stress" + } } } }, @@ -86954,6 +98690,12 @@ "state": "translated", "value": "強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fort" + } } } }, @@ -87040,6 +98782,12 @@ "state": "translated", "value": "摘要" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résumé" + } } } }, @@ -87074,6 +98822,12 @@ "state": "translated", "value": "週日" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dim" + } } } }, @@ -87108,6 +98862,12 @@ "state": "translated", "value": "星期日" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dimanche" + } } } }, @@ -87194,6 +98954,12 @@ "state": "translated", "value": "支持 NOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Soutenir NOOP" + } } } }, @@ -87280,6 +99046,12 @@ "state": "translated", "value": "夜間(晚上 10 點至早上 7 點)暫停自動提示。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactiver les alertes automatiques la nuit (22h–7h)." + } } } }, @@ -87314,6 +99086,12 @@ "state": "translated", "value": "持續高壓力" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stress élevé prolongé" + } } } }, @@ -87348,6 +99126,12 @@ "state": "translated", "value": "掃掠進度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Progression du balayage" + } } } }, @@ -87382,6 +99166,12 @@ "state": "translated", "value": "掃描中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Balayage en cours…" + } } } }, @@ -87416,6 +99206,12 @@ "state": "translated", "value": "由頻域 HRV 得出的交感與副交感神經張力。數值越高越偏向交感神經(壓力方向)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tonus sympathique contre parasympathique d'après la VFC dans le domaine fréquentiel. Plus élevé penche vers le sympathique (côté stress)." + } } } }, @@ -87450,6 +99246,12 @@ "state": "translated", "value": "同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchroniser" + } } } }, @@ -87484,6 +99286,12 @@ "state": "translated", "value": "立即同步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchroniser maintenant" + } } } }, @@ -87518,6 +99326,12 @@ "state": "translated", "value": "同步來源" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisé depuis" + } } } }, @@ -87552,6 +99366,12 @@ "state": "translated", "value": "同步自:%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisé depuis : %@" + } } } }, @@ -87586,6 +99406,12 @@ "state": "translated", "value": "同步中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisation" + } } } }, @@ -87672,6 +99498,12 @@ "state": "translated", "value": "同步中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisation…" + } } } }, @@ -87706,6 +99538,12 @@ "state": "translated", "value": "立即同步手環中儲存的歷史資料,而不是等待下一次自動同步。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronise immédiatement l'historique enregistré de votre bracelet, au lieu d'attendre la prochaine synchronisation automatique." + } } } }, @@ -87740,6 +99578,12 @@ "state": "translated", "value": "綜覽" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synthèse" + } } } }, @@ -87774,6 +99618,12 @@ "state": "translated", "value": "綜覽。%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synthèse. %@" + } } } }, @@ -87808,6 +99658,12 @@ "state": "translated", "value": "系統會跟隨你 %@ 的淺色或深色設定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le système suit le réglage clair ou sombre de votre %@." + } } } }, @@ -87842,6 +99698,12 @@ "state": "translated", "value": "收縮壓" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Systolique" + } } } }, @@ -87876,6 +99738,12 @@ "state": "translated", "value": "收縮壓(mmHg)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pression artérielle systolique en mmHg" + } } } }, @@ -87910,6 +99778,12 @@ "state": "translated", "value": "測試模式" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MODES DE TEST" + } } } }, @@ -87944,6 +99818,12 @@ "state": "translated", "value": "基礎資訊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LES BASES" + } } } }, @@ -87978,6 +99858,12 @@ "state": "translated", "value": "唯一的規則" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LA RÈGLE UNIQUE" + } } } }, @@ -88012,6 +99898,12 @@ "state": "translated", "value": "三個評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LES TROIS SCORES" + } } } }, @@ -88046,6 +99938,12 @@ "state": "translated", "value": "時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TEMPS" + } } } }, @@ -88080,6 +99978,12 @@ "state": "translated", "value": "典型消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EFFORT TYPIQUE" + } } } }, @@ -88114,6 +100018,12 @@ "state": "translated", "value": "為幾次相同活動的工作階段打上標籤,NOOP 便會學習它對你個人的恢復消耗。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étiquetez quelques séances de la même activité et NOOP apprendra son coût de récupération personnel." + } } } }, @@ -88148,6 +100058,12 @@ "state": "translated", "value": "從即時 R-R 資料流中進行一次 60 秒的靜坐 HRV 讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites une mesure de VFC assise de 60 secondes à partir du flux R-R en direct." + } } } }, @@ -88182,6 +100098,12 @@ "state": "translated", "value": "進行一次 HRV 讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire une mesure de VFC" + } } } }, @@ -88216,6 +100138,12 @@ "state": "translated", "value": "再讀一次" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faire une nouvelle mesure" + } } } }, @@ -88250,6 +100178,12 @@ "state": "translated", "value": "在本地接管它。Beta。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prenez-en le contrôle en local. Bêta." + } } } }, @@ -88284,6 +100218,12 @@ "state": "translated", "value": "接管" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prendre le contrôle" + } } } }, @@ -88318,6 +100258,12 @@ "state": "translated", "value": "接管這枚戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prendre le contrôle de cet anneau" + } } } }, @@ -88352,6 +100298,12 @@ "state": "translated", "value": "接管這枚戒指?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prendre le contrôle de cet anneau ?" + } } } }, @@ -88386,6 +100338,12 @@ "state": "translated", "value": "在本地接管你的戒指。Beta。這會取代 Oura 應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prenez le contrôle de votre anneau en local. Bêta. Cela remplace l'application Oura." + } } } }, @@ -88420,6 +100378,12 @@ "state": "translated", "value": "將在下一次分析處理時生效(在下次同步之後)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prend effet lors de la prochaine analyse (après la prochaine synchronisation)." + } } } }, @@ -88454,6 +100418,12 @@ "state": "translated", "value": "正在接管你的戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prise de contrôle de votre anneau" + } } } }, @@ -88488,6 +100458,12 @@ "state": "translated", "value": "點擊某個飲品以編輯它,或用垃圾桶刪除。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez une boisson pour la modifier, ou utilisez la corbeille pour la supprimer." + } } } }, @@ -88522,6 +100498,12 @@ "state": "translated", "value": "點擊某次鍛鍊以查看其詳情 · 點擊 ••• 以重新標註、編輯或刪除它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez un entraînement pour ses détails · touchez ••• pour le renommer, le modifier ou le supprimer." + } } } }, @@ -88556,6 +100538,12 @@ "state": "translated", "value": "點擊屬於你的那一個。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez celui qui vous appartient." + } } } }, @@ -88590,6 +100578,12 @@ "state": "translated", "value": "點擊以比較" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez pour comparer" + } } } }, @@ -88624,6 +100618,12 @@ "state": "translated", "value": "點擊以編輯數量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez pour modifier la quantité" + } } } }, @@ -88658,6 +100658,12 @@ "state": "translated", "value": "點擊以記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez pour enregistrer" + } } } }, @@ -88692,6 +100698,12 @@ "state": "translated", "value": "在你準備就寢或醒來時點擊。每次點擊都會連同時間一起記錄,它不會改變今晚偵測到的睡眠。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touchez quand vous allez vous coucher ou quand vous vous réveillez. Chaque appui est enregistré avec l'heure. Cela ne modifie pas le sommeil détecté cette nuit." + } } } }, @@ -88726,6 +100738,12 @@ "state": "translated", "value": "溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Température" + } } } }, @@ -88760,6 +100778,12 @@ "state": "translated", "value": "溫度單位" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Unité de température" + } } } }, @@ -88846,6 +100870,12 @@ "state": "translated", "value": "測試中心" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Centre de test" + } } } }, @@ -88984,6 +101014,12 @@ "state": "translated", "value": "正在測試 %@ 次/分鐘…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Test à %@ resp/min…" + } } } }, @@ -89018,6 +101054,12 @@ "state": "translated", "value": "該檔案太大,無法匯入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce fichier est trop volumineux pour être importé." + } } } }, @@ -89052,6 +101094,12 @@ "state": "translated", "value": "該檔案不是 NOOP 備份,它看起來不像 SQLite 資料庫。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce fichier n'est pas une sauvegarde NOOP. Il ne ressemble pas à une base de données SQLite." + } } } }, @@ -89086,6 +101134,12 @@ "state": "translated", "value": "那不是一個 32 字元的十六進位金鑰。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n'est pas une clé hexadécimale de 32 caractères." + } } } }, @@ -89120,6 +101174,12 @@ "state": "translated", "value": "這很正常,當單靠節拍器無法平復時,節律呼吸往往能讓一切安定下來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est normal. Une respiration cadencée apaise souvent les choses là où un métronome seul n'y parvient pas." + } } } }, @@ -89154,6 +101214,12 @@ "state": "translated", "value": "這就是為什麼僅靠手錶的能量一開始會顯示「校準中」。NOOP 需要大約七晚的 HRV 來了解對你而言正常是什麼樣子。在擁有這些資料之前,它會保留評分而不是去猜測。一旦基線設定完成,你的能量便會連同其信賴度一起出現,採用與手環相同的 0–100 量表。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est pourquoi une Charge basée uniquement sur la montre commence par « Étalonnage ». NOOP a besoin d'environ sept nuits de votre VFC pour apprendre ce qui est normal pour vous. Tant qu'il ne les a pas, il préfère ne pas afficher de score plutôt que de deviner. Une fois la référence établie, votre Charge apparaît avec son niveau de confiance, sur la même échelle de 0 à 100 qu'un bracelet." + } } } }, @@ -89188,6 +101254,12 @@ "state": "translated", "value": "Oura 應用程式和你的 Oura 帳號將無法再與這枚戒指配合使用。這正是重點所在:你正在取代 Oura。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'application Oura et votre compte Oura cessent de fonctionner avec cet anneau. C'est le but recherché. Vous remplacez Oura." + } } } }, @@ -89222,6 +101294,12 @@ "state": "translated", "value": "上方的平均值已被編輯。圖表、區間和消耗仍來自記錄的訓練。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La moyenne ci-dessus a été modifiée. Le graphique, les zones et l'Effort restent ceux de la séance enregistrée." + } } } }, @@ -89256,6 +101334,12 @@ "state": "translated", "value": "備份壓縮檔中不包含資料庫檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'archive de sauvegarde ne contient pas de fichier de base de données." + } } } }, @@ -89290,6 +101374,12 @@ "state": "translated", "value": "在你使用的所有裝置中,每項指標取來源最佳的數值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le chiffre le mieux documenté pour chaque métrique, parmi tous les bracelets que vous utilisez." + } } } }, @@ -89324,6 +101414,12 @@ "state": "translated", "value": "完整條款見隨 NOOP 一同提供的 TERMS.md。這不構成法律建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les conditions complètes se trouvent dans TERMS.md, fourni avec NOOP. Ceci n'est pas un avis juridique." + } } } }, @@ -89358,6 +101454,12 @@ "state": "translated", "value": "誠實的不足之處:手錶不是胸帶。它在睡眠、步數、鍛鍊和健身方面表現出色,但在胸帶整晚測量的密集心率變異性方面較弱。因此恢復大約需要一週來校準,並且有幾項指標取決於你的手錶型號。NOOP 對這一切都坦誠相告。每一個源自手錶的數字都帶有信賴度,而當手錶無法誠實反映時,NOOP 會什麼都不顯示,而不是給出一個編造的數字。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le hic honnête : une montre n'est pas un bracelet pectoral. Elle excelle pour le sommeil, les pas, les entraînements et la forme physique, et est plus légère sur la VFC dense qu'un bracelet mesure toute la nuit. La récupération met donc environ une semaine à s'étalonner, et quelques métriques dépendent du modèle de votre montre. NOOP est transparent sur tout cela. Chaque chiffre dérivé de la montre porte un niveau de confiance, et là où la montre ne peut pas être honnête, NOOP n'affiche rien plutôt qu'un chiffre inventé." + } } } }, @@ -89392,6 +101494,12 @@ "state": "translated", "value": "這條線是每個清醒小時的 0–3 代理值,以你今天自己的平靜時段為基準評分。下方的長條將你的一天劃分為平靜、中度和高壓力時間。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La ligne représente l'indicateur 0 à 3 de chaque heure éveillée, évalué par rapport à vos propres heures calmes du jour. La barre ci-dessous répartit votre journée en temps de stress calme, modéré et élevé." + } } } }, @@ -89426,6 +101534,12 @@ "state": "translated", "value": "所有手腕震動(久坐、壓力、提醒)的總開關。關閉後無論其他設定如何,手環都會保持安靜。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'interrupteur principal de chaque vibration au poignet (inactivité, stress, alertes). Désactivé, il garde le bracelet silencieux quoi qu'il arrive d'autre." + } } } }, @@ -89460,6 +101574,12 @@ "state": "translated", "value": "評分背後的方法,而非今天的數值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La méthode derrière le score, pas les valeurs du jour." + } } } }, @@ -89500,6 +101620,12 @@ "state": "translated", "value": "提醒會在此之前 %1$lld 小時 %2$lld 分鐘觸發。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'alerte se déclenche %1$lldh %2$lldm avant cela." + } } } }, @@ -89534,6 +101660,12 @@ "state": "translated", "value": "各項數字" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les chiffres" + } } } }, @@ -89568,6 +101700,12 @@ "state": "translated", "value": "散點圖看起來比緊湊、穩定的心跳更圓、更分散。這有許多常見的原因,並非診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La dispersion semblait plus ronde et plus étalée qu'un battement compact et régulier. Cela a de nombreuses causes ordinaires et n'est pas un diagnostic." + } } } }, @@ -89602,6 +101740,12 @@ "state": "translated", "value": "手環鬧鐘是無聲的震動,而非聲音" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'alarme du bracelet est une vibration silencieuse, pas un son" + } } } }, @@ -89636,6 +101780,12 @@ "state": "translated", "value": "手環會以略低於你當前心率的柔和節律震動,一個可感知的節拍器,讓你朝著它放鬆。它會引領你的心率緩緩下降,而非猛拉,並會自行停止。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le bracelet vibre selon un rythme doux juste en dessous de votre fréquence cardiaque actuelle, un métronome ressenti vers lequel vous détendre. Il accompagne votre cœur vers le bas plutôt que de le tirer brusquement, et s'arrête de lui-même." + } } } }, @@ -89723,6 +101873,12 @@ "state": "translated", "value": "上方的喚醒鬧鐘透過手環自身的韌體震動你的手腕。它無法發出響亮的鬧鐘聲,而側載的應用程式在此裝置上也無法發出可靠的背景喚醒(那需要此版本所不具備的關鍵提醒權限)。請保留你手機內建的「時鐘」鬧鐘作為備用。NOOP 基於手機的智慧喚醒(淺睡偵測)在 Android 應用程式上可用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore, et une application en sideload ne peut pas non plus déclencher un réveil en arrière-plan fiable sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas). Gardez le réveil intégré de l'application Horloge de votre téléphone en secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android." + } } } }, @@ -89757,6 +101913,12 @@ "state": "translated", "value": "上方的喚醒鬧鐘由手環自身的韌體震動你的手腕,它無法發出響亮的鬧鈴。我們也會在你的起床時間安排一則備用通知,但側載應用程式無法在此裝置上保證發出喚醒聲音(那需要此建置版本沒有的重要警示權限),因此專注模式或靜音模式仍可能將其靜音。請保留手機內建的時鐘鬧鐘作為你真正的備援。NOOP 基於手機的智慧喚醒(淺睡偵測)在 Android 應用程式上可用。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore. Nous programmons aussi une notification de secours à l'heure de votre réveil, mais une application en sideload ne peut pas garantir un réveil sonore sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas), donc le mode Ne pas déranger ou le mode silencieux peuvent quand même la couper. Gardez le réveil intégré de l'application Horloge de votre téléphone comme véritable secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android." + } } } }, @@ -89791,6 +101953,12 @@ "state": "translated", "value": "主題" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème" + } } } }, @@ -89825,6 +101993,12 @@ "state": "translated", "value": "你的資料夾中還沒有 NOOP 備份。請先使用「立即備份」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'y a pas encore de sauvegarde NOOP dans votre dossier. Utilisez d'abord Sauvegarder maintenant." + } } } }, @@ -89859,6 +102033,12 @@ "state": "translated", "value": "沒有足夠乾淨的平靜、靜止時段可供描述。請在安穩的一晚之後再試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'y a pas eu de fenêtre calme et immobile assez nette à décrire. Réessayez après une nuit paisible." + } } } }, @@ -89893,6 +102073,12 @@ "state": "translated", "value": "大約有 %@ 的遺留匯入暫存空間可回收。此操作絕不會移除你匯入的資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il reste environ %@ d'espace temporaire d'importation à récupérer. Cela ne supprime jamais vos données importées." + } } } }, @@ -89927,6 +102113,12 @@ "state": "translated", "value": "還沒有可匯出的 NOOP 資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'y a pas encore de données NOOP à exporter." + } } } }, @@ -89961,6 +102153,12 @@ "state": "translated", "value": "還沒有可匯出的 NOOP 資料。請先匯入或記錄一些。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n'y a pas encore de données NOOP à exporter. Importez ou enregistrez-en d'abord." + } } } }, @@ -89995,6 +102193,12 @@ "state": "translated", "value": "這些是實驗性探測,預設關閉。更完整的 WHOOP 5/MG 控制項和原始感測器 CSV 匯出仍位於「設定」的「診斷」下。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont des sondes expérimentales, désactivées par défaut. Les commandes WHOOP 5/MG plus complètes et l'export CSV des capteurs bruts se trouvent toujours dans Paramètres, sous Diagnostics." + } } } }, @@ -90029,6 +102233,12 @@ "state": "translated", "value": "這些是根據今天的 R-R 間期計算的額外、按需的 HRV 視角。它們僅供參考,不會改變上方的壓力評分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont des perspectives VFC supplémentaires, à la demande, calculées à partir des intervalles R-R du jour. Elles sont informatives et ne modifient pas le score de stress ci-dessus." + } } } }, @@ -90063,6 +102273,12 @@ "state": "translated", "value": "這些是來自消費級手環的獨立近似值,基於開放科學構建,並非醫療建議,也不是 WHOOP 的官方評分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont des approximations indépendantes issues d'un bracelet grand public, basées sur la science ouverte : ni un avis médical, ni les scores officiels de WHOOP." + } } } }, @@ -90097,6 +102313,12 @@ "state": "translated", "value": "這些是在你的裝置上根據你的 Apple 健康資料計算的獨立估算值,並非醫療建議。信賴度標籤會如實反映 NOOP 目前了解多少。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont des estimations indépendantes calculées sur votre appareil à partir de vos données Apple Santé, pas un avis médical. Les niveaux de confiance indiquent honnêtement ce que NOOP sait pour l'instant." + } } } }, @@ -90131,6 +102353,12 @@ "state": "translated", "value": "這些是你手機也統計了步數的日子,因此 NOOP 可以學習你的運動如何對應步數。或者在下方手動設定係數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont les jours où votre téléphone a aussi compté les pas, afin que NOOP puisse apprendre comment votre mouvement se traduit en pas. Ou réglez le coefficient manuellement ci-dessous." + } } } }, @@ -90165,6 +102393,12 @@ "state": "translated", "value": "這些是原樣呈現給你的、你自己的數字。NOOP 不會判定任何數值是正常、偏高還是偏低。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce sont vos propres chiffres qui vous sont montrés. NOOP ne décide pas si une valeur est normale, élevée ou basse." + } } } }, @@ -90199,6 +102433,12 @@ "state": "translated", "value": "這些日子被排除在估算之外(改為顯示你手機的真實計數), 它們出現在這裡只是為了讓你判斷估算的準確性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces jours sont exclus de l'estimation (le comptage réel de votre téléphone est affiché à la place). Ils sont là uniquement pour que vous puissiez juger de la précision de l'estimation." + } } } }, @@ -90285,6 +102525,12 @@ "state": "translated", "value": "這些生命徵象來自 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces signes vitaux proviennent de %@" + } } } }, @@ -90371,6 +102617,12 @@ "state": "translated", "value": "這會隨著手環同步逐步建立。在你佩戴並睡過一晚後,消耗和休息便會出現。能量需要大約四晚的睡眠來學習你的基線(在那之前你會看到「校準中」),並會在你最初的幾週裡持續變得更精準。在 WHOOP 5 或 MG 上,手環儲存的歷史很少,因此夜數可能上升緩慢,或在你連續佩戴幾晚之前一直停留在 0 / 4。這是它的同步限制,而非故障。匯入你的 WHOOP 匯出檔案以跳過等待。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela se construit à partir du bracelet au fil de sa synchronisation. L'Effort et le Repos apparaissent après l'avoir porté et dormi une nuit. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre votre référence (vous verrez \"Étalonnage\" jusque-là), et continue de s'affiner pendant vos deux premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, donc le compteur de nuits peut progresser lentement ou rester à 0 sur 4 tant que vous ne l'avez pas porté quelques nuits. C'est sa limite de synchronisation, pas un défaut. Importez votre export WHOOP pour éviter l'attente." + } } } }, @@ -90459,6 +102711,12 @@ "state": "translated", "value": "此安裝無法直接連接 Apple 健康。它是用免費簽名設定檔側載的,其中不包含 Apple 的健康權限,因此沒有可啟用的項,NOOP 也不會出現在「設定 › 健康」下。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé." + } } } }, @@ -90494,6 +102752,12 @@ "state": "translated", "value": "此安裝無法直接連接 Apple 健康。它是用免費簽名設定檔側載的,其中不包含 Apple 的健康權限,因此這裡沒有可授予的項。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici." + } } } }, @@ -90528,6 +102792,12 @@ "state": "translated", "value": "此安裝版本無法直接連接 Apple 健康。它簽署時使用的描述檔不包含 Apple 的健康權限,因此沒有可啟用的項目,NOOP 也不會出現在「設定 › 健康」中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé." + } } } }, @@ -90562,6 +102832,12 @@ "state": "translated", "value": "此安裝版本無法直接連接 Apple 健康。它簽署時使用的描述檔不包含 Apple 的健康權限,因此這裡沒有可授予的項目。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici." + } } } }, @@ -90596,6 +102872,12 @@ "state": "translated", "value": "這是一個側載的建置版本,安裝於 App Store 之外。它需要定期重新簽署:免費 Apple ID 大約每 7 天一次,付費開發者帳號大約一年一次。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ceci est une version en sideload, installée en dehors de l'App Store. Elle doit être resignée périodiquement : environ tous les 7 jours avec un identifiant Apple gratuit, environ un an avec un compte développeur payant." + } } } }, @@ -90630,6 +102912,12 @@ "state": "translated", "value": "這是一種健康視覺化,而非篩檢測試。它不會告訴你去看臨床醫生,也不指明任何疾病。這不構成法律或醫療建議。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ceci est une visualisation de bien-être, pas un test de dépistage. Elle ne vous dit pas de consulter un médecin et ne nomme aucune affection. Ceci n'est pas un avis juridique ou médical." + } } } }, @@ -90664,6 +102952,12 @@ "state": "translated", "value": "這是一個實驗性的健康視覺化,呈現你的逐拍心跳時序。它不是心電圖,無法診斷、偵測或排除任何心臟疾病。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ceci est une visualisation expérimentale de bien-être de la cadence battement par battement. Ce n'est PAS un ECG, et cela ne peut ni diagnostiquer, ni détecter, ni écarter une affection cardiaque." + } } } }, @@ -90698,6 +102992,12 @@ "state": "translated", "value": "這正是你的報告將包含的內容。在你點擊「分享」之前,沒有任何內容離開 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est exactement ce que contiendra votre rapport. Rien ne quitte %@ tant que vous ne touchez pas Partager." + } } } }, @@ -90738,6 +103038,12 @@ "state": "translated", "value": "這是唯一會離開 %1$@ 的功能,它使用你自己的金鑰,將你各項指標的摘要發送到 %2$@。在你主動請求之前,不會發送任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est la seule fonctionnalité qui quitte %1$@. Elle envoie un résumé de vos métriques à %2$@ en utilisant votre propre clé. Rien n'est envoyé tant que vous ne le demandez pas." + } } } }, @@ -90825,6 +103131,12 @@ "state": "translated", "value": "這是你上一次已評分的工作階段。佩戴手環過夜以獲得新的評分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voici votre dernière séance notée. Portez le bracelet une nuit pour obtenir un nouveau score." + } } } }, @@ -90859,6 +103171,12 @@ "state": "translated", "value": "這是你今天唯一的睡眠時段。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C'est votre seul bloc de sommeil aujourd'hui." + } } } }, @@ -90893,6 +103211,12 @@ "state": "translated", "value": "這不是來自此應用程式的 NOOP 備份。它缺少 NOOP 備份所帶有的遷移記錄(它看起來像 Android 備份或另一個應用程式的資料庫),還原它會讓你的資料庫陷入困境。若要跨平台轉移你的歷史資料,請在另一台裝置上匯出 WHOOP 格式的 CSV(設定 → 匯出資料)並在這裡匯入,或匯入你原始的 WHOOP / Apple Health 匯出檔案。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n'est pas une sauvegarde NOOP provenant de cette application. Il lui manque les informations de migration qu'une sauvegarde NOOP contient (elle ressemble à une sauvegarde Android ou à la base de données d'une autre application), et la restaurer bloquerait votre stockage. Pour transférer votre historique entre plateformes, exportez le CSV au format WHOOP sur l'autre appareil (Paramètres → Exporter les données) et importez-le ici, ou importez votre export WHOOP / Apple Santé d'origine." + } } } }, @@ -90927,6 +103251,12 @@ "state": "translated", "value": "這一晚的效率評分很高,但深睡或 REM 極少,更可能是分期估算失誤,而非真正的修復不足。總計保持原樣;請謹慎解讀各階段佔比。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette nuit a obtenu une efficacité élevée mais très peu de sommeil profond ou paradoxal, ce qui ressemble plus à une erreur d'estimation des phases qu'à un réel déficit de récupération. Les totaux sont conservés tels quels ; interprétez la répartition avec prudence." + } } } }, @@ -90961,6 +103291,12 @@ "state": "translated", "value": "這將永久刪除從 %@ 記錄的所有資料。此操作無法撤銷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela supprime définitivement toutes les données enregistrées depuis %@. Cette action est irréversible." + } } } }, @@ -90995,6 +103331,12 @@ "state": "translated", "value": "這將永久刪除從 Apple 健康匯入的一切,心率、HRV、睡眠、步數、鍛鍊等。你的即時手環資料不受影響。此操作無法撤銷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, sommeil, pas, entraînements et plus encore. Les données en direct de votre bracelet ne sont pas concernées. Cette action est irréversible." + } } } }, @@ -91029,6 +103371,12 @@ "state": "translated", "value": "這會重新開始能量和你 HRV 基線大約 4 晚的建立過程。你的歷史會保留。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé." + } } } }, @@ -91063,6 +103411,12 @@ "state": "translated", "value": "這會重新開始能量和你 HRV 基線大約 4 晚的建立過程。你的歷史會保留。如果糟糕的第一週(比如生病時佩戴)使你的基線出現偏差,可以使用它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé. Utilisez cette option si une mauvaise première semaine, par exemple portée pendant une maladie, a faussé votre référence." + } } } }, @@ -91097,6 +103451,12 @@ "state": "translated", "value": "本次工作階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette séance" + } } } }, @@ -91131,6 +103491,12 @@ "state": "translated", "value": "本次工作階段對當天消耗的貢獻,即在鍛鍊期間所擷取的部分。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La contribution de cette séance à l'Effort du jour, telle qu'enregistrée pendant l'entraînement." + } } } }, @@ -91165,6 +103531,12 @@ "state": "translated", "value": "這個側載建置版本已於 %lld 天前過期。請重新簽署以維持運作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version en sideload a expiré il y a %lld jours. Resignez-la pour continuer à l'utiliser." + } } } }, @@ -91199,6 +103571,12 @@ "state": "translated", "value": "這個側載建置版本已於 1 天前過期。請重新簽署以維持運作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version en sideload a expiré il y a 1 jour. Resignez-la pour continuer à l'utiliser." + } } } }, @@ -91233,6 +103611,12 @@ "state": "translated", "value": "這個側載建置版本將在 %lld 天後過期。請重新簽署以維持運作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version en sideload expire dans %lld jours. Resignez-la pour continuer à l'utiliser." + } } } }, @@ -91267,6 +103651,12 @@ "state": "translated", "value": "這個側載建置版本將在 1 天後過期。請重新簽署以維持運作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version en sideload expire dans 1 jour. Resignez-la pour continuer à l'utiliser." + } } } }, @@ -91353,6 +103743,12 @@ "state": "translated", "value": "這些資料留在你的裝置上,永遠不會上傳、不會同步、不會分享。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela reste sur votre appareil. Ce n'est jamais téléversé, jamais synchronisé, jamais partagé." + } } } }, @@ -91387,6 +103783,12 @@ "state": "translated", "value": "本週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette semaine" + } } } }, @@ -91421,6 +103823,12 @@ "state": "translated", "value": "三個安靜的承諾。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trois promesses discrètes." + } } } }, @@ -91455,6 +103863,12 @@ "state": "translated", "value": "閾值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Seuil" + } } } }, @@ -91489,6 +103903,12 @@ "state": "translated", "value": "週四" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu" + } } } }, @@ -91523,6 +103943,12 @@ "state": "translated", "value": "星期四" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeudi" + } } } }, @@ -91557,6 +103983,12 @@ "state": "translated", "value": "時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure" + } } } }, @@ -91591,6 +104023,12 @@ "state": "translated", "value": "睡眠時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temps de sommeil" + } } } }, @@ -91625,6 +104063,12 @@ "state": "translated", "value": "臥床時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temps au lit" + } } } }, @@ -91659,6 +104103,12 @@ "state": "translated", "value": "在每個 %HRmax 區間的時間,根據此視窗內手環的心率推導得出,近似值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temps dans chaque zone %HRmax, calculé à partir de la fréquence cardiaque du bracelet sur cette période (approximatif)." + } } } }, @@ -91693,6 +104143,12 @@ "state": "translated", "value": "一天中的時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moment de la journée" + } } } }, @@ -91779,6 +104235,12 @@ "state": "translated", "value": "該動一動了,你已經坐了好一陣子。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il est temps de bouger. Vous êtes assis depuis un moment." + } } } }, @@ -91813,6 +104275,12 @@ "state": "translated", "value": "該放鬆下來了" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il est temps de se détendre" + } } } }, @@ -91847,6 +104315,12 @@ "state": "translated", "value": "時間預測" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prévision des horaires" + } } } }, @@ -91881,6 +104355,12 @@ "state": "translated", "value": "提示:選擇 iCloud 雲碟中的一個資料夾,你的備份就會自動同步到你所有的 Apple 裝置,無需設定帳戶。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Astuce : choisissez un dossier dans iCloud Drive et vos sauvegardes se synchroniseront automatiquement sur tous vos appareils Apple, sans configuration de compte." + } } } }, @@ -91915,6 +104395,12 @@ "state": "translated", "value": "提示:分享面板可以將 PDF 保存到「檔案」、透過 AirDrop 發送,或轉發出去。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Astuce : le menu de partage permet d'enregistrer le PDF dans Fichiers, de l'envoyer par AirDrop ou de le transférer." + } } } }, @@ -91949,6 +104435,12 @@ "state": "translated", "value": "無論如何都要引入你的 Apple 健康資料:在「資料來源」中匯入一個健康匯出的 .zip 檔案,或開啟「捷徑匯出」以在沒有權限的情況下將你的手環資料饋送到「健康」。(從 App Store 安裝或用付費 Apple 開發者帳戶簽名的版本可直接連接。)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour intégrer quand même vos données Apple Santé : importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export Raccourcis pour envoyer les données de votre bracelet vers Santé sans l'entitlement. (Une version installée depuis l'App Store ou signée avec un compte Apple Developer payant se connecte directement.)" + } } } }, @@ -92035,6 +104527,12 @@ "state": "translated", "value": "今日時間線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chronologie du jour" + } } } }, @@ -92069,6 +104567,12 @@ "state": "translated", "value": "今日飲品" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Boissons du jour" + } } } }, @@ -92155,6 +104659,12 @@ "state": "translated", "value": "今日心情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Humeur du jour" + } } } }, @@ -92195,6 +104705,12 @@ "state": "translated", "value": "今日心情:%1$@,%2$lld / 5" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Humeur du jour : %1$@, %2$lld sur 5" + } } } }, @@ -92229,6 +104745,12 @@ "state": "translated", "value": "今日評分由 %@ 主導" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Scores du jour gérés par %@" + } } } }, @@ -92269,6 +104791,12 @@ "state": "translated", "value": "今日壓力分佈:平靜 %@,中等 %@,高 %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Répartition du stress du jour : calme %1$@, modéré %2$@, élevé %3$@." + } } } }, @@ -92407,6 +104935,12 @@ "state": "translated", "value": "明天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Demain" + } } } }, @@ -92441,6 +104975,12 @@ "state": "translated", "value": "明天的能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Charge de demain" + } } } }, @@ -92481,6 +105021,12 @@ "state": "translated", "value": "明天的能量估算 %1$lld 加減 %2$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Estimation de la Charge de demain %1$lld plus ou moins %2$lld" + } } } }, @@ -92515,6 +105061,12 @@ "state": "translated", "value": "明天的 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ de demain" + } } } }, @@ -92549,6 +105101,12 @@ "state": "translated", "value": "今晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette nuit" + } } } }, @@ -92583,6 +105141,12 @@ "state": "translated", "value": "今晚的會在你佩戴手環入睡後出現。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le score de cette nuit arrivera après avoir dormi avec le bracelet." + } } } }, @@ -92617,6 +105181,12 @@ "state": "translated", "value": "今晚預報" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prévision pour cette nuit" + } } } }, @@ -92651,6 +105221,12 @@ "state": "translated", "value": "最常運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sport principal" + } } } }, @@ -92685,6 +105261,12 @@ "state": "translated", "value": "總計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Total" + } } } }, @@ -92927,6 +105509,12 @@ "state": "translated", "value": "手動追蹤一次鍛鍊,記錄心率和消耗,直到你結束它。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivez un entraînement manuellement. Enregistre la fréquence cardiaque et l'effort jusqu'à ce que vous le terminiez." + } } } }, @@ -92961,6 +105549,12 @@ "state": "translated", "value": "過去 %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%lld derniers jours" + } } } }, @@ -92995,6 +105589,12 @@ "state": "translated", "value": "回溯視窗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fenêtre glissante" + } } } }, @@ -93029,6 +105629,12 @@ "state": "translated", "value": "憑感覺訓練。手環會震動,你不必盯著螢幕。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînez-vous au ressenti. Le bracelet vibre pour que vous n'ayez pas à regarder un écran." + } } } }, @@ -93063,6 +105669,12 @@ "state": "translated", "value": "跑步機、室內單車、划船機或橢圓機(Bluetooth FTMS)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tapis de course, vélo d'intérieur, rameur ou vélo elliptique (Bluetooth FTMS)" + } } } }, @@ -93149,6 +105761,12 @@ "state": "translated", "value": "僅在支援的機型上顯示趨勢,而 Apple 已從最新的美國版機型中移除 SpO₂ 感測器,所以那些機型上根本沒有這項數據。NOOP 寧可不顯示,也不會給出假讀數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tendance uniquement là où c'est pris en charge, et Apple a retiré le capteur SpO₂ des tout derniers modèles américains, donc il est tout simplement absent sur ceux-ci. NOOP n'affiche rien plutôt qu'une mesure factice." + } } } }, @@ -93287,6 +105905,12 @@ "state": "translated", "value": "趨勢報告" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rapport de tendances" + } } } }, @@ -93321,6 +105945,12 @@ "state": "translated", "value": "三倍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Triple" + } } } }, @@ -93459,6 +106089,12 @@ "state": "translated", "value": "週二" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mar" + } } } }, @@ -93493,6 +106129,12 @@ "state": "translated", "value": "星期二" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mardi" + } } } }, @@ -93527,6 +106169,12 @@ "state": "translated", "value": "關閉" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactiver" + } } } }, @@ -93561,6 +106209,12 @@ "state": "translated", "value": "關閉週期感知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactiver le suivi du cycle" + } } } }, @@ -93595,6 +106249,12 @@ "state": "translated", "value": "開啟「節律」" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer Rythme" + } } } }, @@ -93629,6 +106289,12 @@ "state": "translated", "value": "為出問題的部分開啟一項測試,佩戴手環,然後點擊「報告」。全部在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Tout sur %@." + } } } }, @@ -93663,6 +106329,12 @@ "state": "translated", "value": "為出問題的部分開啟一項測試,佩戴手環,然後點擊「報告」。你的手環日誌、重新校準、定時匯出和實驗性探測也都在這裡。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Le journal du bracelet, le réétalonnage, l'export planifié et les sondes expérimentales se trouvent aussi ici." + } } } }, @@ -93697,6 +106369,12 @@ "state": "translated", "value": "開啟週期感知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer le suivi du cycle" + } } } }, @@ -93731,6 +106409,12 @@ "state": "translated", "value": "開啟下方的放鬆提醒,讓你在起床時間精神飽滿地醒來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activez le rappel de détente ci-dessous pour arriver reposé à l'heure de votre réveil." + } } } }, @@ -93765,6 +106449,12 @@ "state": "translated", "value": "輸入你上次就診時的一個血壓讀數或膽固醇數值。它會留在 %@ 上,隨著時間推移,你會看到它如何與你的睡眠、心率和恢復對齊。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisissez une mesure de tension artérielle ou une valeur de cholestérol issue de votre dernière consultation. Elle reste sur %@, et avec le temps vous verrez comment elle s'accorde avec votre sommeil, votre fréquence cardiaque et votre récupération." + } } } }, @@ -93799,6 +106489,12 @@ "state": "translated", "value": "輸入一個來自你自己報告的數字。它會留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisissez un chiffre issu de votre propre rapport. Il reste sur %@." + } } } }, @@ -93833,6 +106529,12 @@ "state": "translated", "value": "對你而言屬正常" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Typique pour vous" + } } } }, @@ -93867,6 +106569,12 @@ "state": "translated", "value": "典型範圍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plage typique" + } } } }, @@ -93901,6 +106609,12 @@ "state": "translated", "value": "單位" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Unité" + } } } }, @@ -93935,6 +106649,12 @@ "state": "translated", "value": "單位" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Unités" + } } } }, @@ -93969,6 +106689,12 @@ "state": "translated", "value": "未知" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inconnu" + } } } }, @@ -94003,6 +106729,12 @@ "state": "translated", "value": "解鎖 WHOOP 5/MG 深度數據(R22)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Débloquer les données approfondies WHOOP 5/MG (R22)" + } } } }, @@ -94037,6 +106769,12 @@ "state": "translated", "value": "解鎖你的最大攝氧量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Débloque votre VO₂max" + } } } }, @@ -94077,6 +106815,12 @@ "state": "translated", "value": "未讀。%1$@。%2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non lu. %1$@. %2$@" + } } } }, @@ -94163,6 +106907,12 @@ "state": "translated", "value": "更新" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mises à jour" + } } } }, @@ -94197,6 +106947,12 @@ "state": "translated", "value": "更新,%lld 則未讀" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mises à jour, %lld non lues" + } } } }, @@ -94283,6 +107039,12 @@ "state": "translated", "value": "僅用一塊 Apple Watch 使用 NOOP。它擅長什麼,以及在哪些方面比手環更薄弱。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisez NOOP avec une simple Apple Watch. Ce en quoi elle excelle, et où elle est plus limitée qu'un bracelet." + } } } }, @@ -94317,6 +107079,12 @@ "state": "translated", "value": "配合你的手錶使用 NOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser NOOP avec votre montre" + } } } }, @@ -94403,6 +107171,12 @@ "state": "translated", "value": "使用檔案匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser l'import de fichier" + } } } }, @@ -94437,6 +107211,12 @@ "state": "translated", "value": "對 Oura 使用檔案匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser l'import de fichier pour Oura" + } } } }, @@ -94448,6 +107228,12 @@ "state": "translated", "value": "Nutze es nur mit einem Gerät, das dir gehört, um deine eigenen Daten zu lesen. Ob du es nutzt (und jede Auswirkung auf dein WHOOP-Konto, Abo, Gerät oder deine Garantie), ist deine Entscheidung und allein dein Risiko." } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisez-le uniquement avec un appareil qui vous appartient, pour lire vos propres données. L'utiliser (et tout effet sur votre compte WHOOP, votre abonnement, votre appareil ou votre garantie) relève de votre seule décision et de votre seul risque." + } } } }, @@ -94482,6 +107268,12 @@ "state": "translated", "value": "使用我的共振節奏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser mon rythme de résonance" + } } } }, @@ -94516,6 +107308,12 @@ "state": "translated", "value": "使用手錶的廣播心率功能。我們會示範怎麼做。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilise la diffusion de la fréquence cardiaque de la montre. Nous vous montrerons comment faire." + } } } }, @@ -94550,6 +107348,12 @@ "state": "translated", "value": "正在使用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisation" + } } } }, @@ -94561,6 +107365,12 @@ "state": "translated", "value": "Die Nutzung von NOOP kann gegen WHOOPs Nutzungsbedingungen verstoßen" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'utilisation de NOOP peut enfreindre les conditions d'utilisation de WHOOP" + } } } }, @@ -94595,6 +107405,12 @@ "state": "translated", "value": "在 iPhone 上使用 NOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser NOOP sur iPhone" + } } } }, @@ -94681,6 +107497,12 @@ "state": "translated", "value": "VO₂ Max(估算)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VO₂ Max (estimé)" + } } } }, @@ -94715,6 +107537,12 @@ "state": "translated", "value": "最大攝氧量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VO₂max" + } } } }, @@ -94749,6 +107577,12 @@ "state": "translated", "value": "數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Valeur" + } } } }, @@ -94783,6 +107617,12 @@ "state": "translated", "value": "變異是正常的,且通常無害" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La variation est normale et souvent bénigne" + } } } }, @@ -94817,6 +107657,12 @@ "state": "translated", "value": "版本 %@ 可用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La version %@ est disponible" + } } } }, @@ -94851,6 +107697,12 @@ "state": "translated", "value": "非常強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Très fort" + } } } }, @@ -94885,6 +107737,12 @@ "state": "translated", "value": "讓你已連接的 WHOOP 手環震動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Faites vibrer votre bracelet WHOOP connecté." + } } } }, @@ -94919,6 +107777,12 @@ "state": "translated", "value": "查看進行中的鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir l'entraînement actif" + } } } }, @@ -94953,6 +107817,12 @@ "state": "translated", "value": "查看詳情" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir le détail" + } } } }, @@ -94987,6 +107857,12 @@ "state": "translated", "value": "查看進行中的鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir l'entraînement actif" + } } } }, @@ -95021,6 +107897,12 @@ "state": "translated", "value": "查看完整計劃" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir le plan complet" + } } } }, @@ -95211,6 +108093,12 @@ "state": "translated", "value": "W" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "W" + } } } }, @@ -95245,6 +108133,12 @@ "state": "translated", "value": "它擅長什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CE EN QUOI ELLE EXCELLE" + } } } }, @@ -95279,6 +108173,12 @@ "state": "translated", "value": "什麼在影響你 ›" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CE QUI VOUS FAIT BOUGER ›" + } } } }, @@ -95313,6 +108213,12 @@ "state": "translated", "value": "手錶能做什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CE QUE LA MONTRE PEUT FAIRE" + } } } }, @@ -95399,6 +108305,12 @@ "state": "translated", "value": "新增內容" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOUVEAUTÉS" + } } } }, @@ -95433,6 +108345,12 @@ "state": "translated", "value": "在哪些方面比手環更薄弱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "OÙ ELLE EST PLUS LIMITÉE QU'UN BRACELET" + } } } }, @@ -95467,6 +108385,12 @@ "state": "translated", "value": "WHOOP 4.0 已通過測試,可完整運作。WHOOP 5.0/MG 較新:即時心率現在可用,但 5/MG 的深層指標(恢復、強度、睡眠)仍在破解中。NOOP 永遠會告訴你哪些已可用、哪些還在建置。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 4.0 est testé et fonctionne de bout en bout. WHOOP 5.0/MG est plus récent : la fréquence cardiaque en direct fonctionne déjà, mais les métriques plus approfondies (récupération, effort, sommeil) pour 5/MG sont encore en cours d'étude. NOOP vous indique toujours ce qui est actif et ce qui est encore en construction." + } } } }, @@ -95501,6 +108425,12 @@ "state": "translated", "value": "WHOOP 4.0 是受支援的路徑" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 4.0 est la voie prise en charge" + } } } }, @@ -95535,6 +108465,12 @@ "state": "translated", "value": "WHOOP 4.0 協定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Protocole WHOOP 4.0" + } } } }, @@ -95569,6 +108505,12 @@ "state": "translated", "value": "WHOOP 4.0 · 運動 → 步數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 4.0 · mouvement → pas" + } } } }, @@ -95603,6 +108545,12 @@ "state": "translated", "value": "WHOOP 5 / MG" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 5 / MG" + } } } }, @@ -95637,6 +108585,12 @@ "state": "translated", "value": "WHOOP 5.0 / MG 一次只能綁定一台裝置。請先在官方 WHOOP 應用程式中解除配對。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 5.0 / MG se couple à un seul appareil à la fois. Dissociez-le d'abord de l'application officielle WHOOP." + } } } }, @@ -95671,6 +108625,12 @@ "state": "translated", "value": "WHOOP 5.0 / MG 支援較新,在 NOOP 中仍屬實驗性。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La prise en charge de WHOOP 5.0 / MG est plus récente et encore expérimentale dans NOOP." + } } } }, @@ -95705,6 +108665,12 @@ "state": "translated", "value": "WHOOP 5.0 協定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Protocole WHOOP 5.0" + } } } }, @@ -95739,6 +108705,12 @@ "state": "translated", "value": "WHOOP 5.0/MG 同一時間只與一個應用程式配對。如果掃描沒有找到任何東西,請在官方 WHOOP 應用程式中取消配對並完全關閉該應用程式,然後再次掃描。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si un scan ne trouve rien, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis relancez le scan." + } } } }, @@ -95773,6 +108745,12 @@ "state": "translated", "value": "WHOOP 5.0/MG 同一時間只與一個應用程式配對。如果沒有找到任何東西,請在官方 WHOOP 應用程式中取消配對並完全關閉該應用程式,然後掃描。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si rien n'est trouvé, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis lancez le scan." + } } } }, @@ -95807,6 +108785,12 @@ "state": "translated", "value": "WHOOP 5/MG 手環只向一個全新的應用程式提供即時心率。官方應用程式透過寫入一組功能旗標來開啟更深層的資料流(高速率心率 + 運動 + 歷史), 這是兩個獨立專案都已記錄的一個序列。開啟此項後,下面的按鈕會將那個確切的序列發送到你的手環。與這裡的其他一切不同,它確實會向手環寫入,但它是可逆的(它只改變手環選擇發出哪些資料),並且與官方應用程式所做的相同。實驗性:在你的韌體上它可能什麼都不做。僅限 iPhone/Android,Mac 無法向 5/MG 寫入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les bracelets WHOOP 5/MG ne fournissent à une nouvelle application que la fréquence cardiaque en direct. L'application officielle active les flux plus approfondis (FC haute fréquence + mouvement + historique) en écrivant un ensemble d'indicateurs de fonctionnalités, une séquence documentée par deux projets indépendants. Une fois cette option activée, le bouton ci-dessous envoie exactement cette séquence à votre bracelet. Contrairement à tout le reste ici, cela écrit bien sur le bracelet, mais c'est réversible (cela ne change que les données que le bracelet choisit d'émettre) et c'est exactement ce que fait l'application officielle. Expérimental : cela peut n'avoir aucun effet sur votre micrologiciel. iPhone/Android uniquement. Un Mac ne peut pas écrire sur un 5/MG." + } } } }, @@ -95841,6 +108825,12 @@ "state": "translated", "value": "WHOOP 匯出檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Export WHOOP" + } } } }, @@ -95875,6 +108865,12 @@ "state": "translated", "value": "WHOOP 手環(即時 BLE)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet WHOOP (BLE en direct)" + } } } }, @@ -95909,6 +108905,12 @@ "state": "translated", "value": "WHOOP 匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Import WHOOP" + } } } }, @@ -95943,6 +108945,12 @@ "state": "translated", "value": "WHOOP 是 NOOP 主要的、完全支援的手環。其他心率帶是一項早期的、開發中的新增功能,它們串流即時心率和 HRV,但不包括 WHOOP 更深層的睡眠和恢復資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque sont un ajout récent, encore en développement : ils diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP." + } } } }, @@ -95977,6 +108985,12 @@ "state": "translated", "value": "WHOOP 是 NOOP 主要的、完全支援的手環。其他心率帶串流即時心率和 HRV,但不包括 WHOOP 更深層的睡眠和恢復資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP." + } } } }, @@ -96064,6 +109078,12 @@ "state": "translated", "value": "WHOOP 手環不會出現在你 %@ 的藍牙設定中。它們透過一個只有 NOOP 這類應用程式才能找到的自訂設定檔進行廣播,所以那裡沒有可配對的裝置,你也不應嘗試。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les bracelets WHOOP n'apparaissent pas dans les réglages Bluetooth de votre %@. Ils s'annoncent sur un profil personnalisé que seules des applications comme NOOP peuvent détecter : il n'y a donc rien à associer là-bas, et vous ne devriez pas essayer." + } } } }, @@ -96098,6 +109118,12 @@ "state": "translated", "value": "本次工作階段中 WHOOP 匯入的各區間佔比。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Répartition par zone importée de WHOOP pour cette séance." + } } } }, @@ -96132,6 +109158,12 @@ "state": "translated", "value": "運動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TRAVAIL" + } } } }, @@ -96166,6 +109198,12 @@ "state": "translated", "value": "鍛鍊進行中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ENTRAÎNEMENT EN COURS" + } } } }, @@ -96200,6 +109238,12 @@ "state": "translated", "value": "腰圍(可選)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille (facultatif)" + } } } }, @@ -96234,6 +109278,12 @@ "state": "translated", "value": "腰圍(公分)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille en centimètres" + } } } }, @@ -96268,6 +109318,12 @@ "state": "translated", "value": "腰圍(英寸)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille en pouces" + } } } }, @@ -96302,6 +109358,12 @@ "state": "translated", "value": "未設定腰圍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille non défini" + } } } }, @@ -96336,6 +109398,12 @@ "state": "translated", "value": "腰圍,%lld 公分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille, %lld centimètres" + } } } }, @@ -96370,6 +109438,12 @@ "state": "translated", "value": "腰圍,%lld 英寸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour de taille, %lld pouces" + } } } }, @@ -96404,6 +109478,12 @@ "state": "translated", "value": "等待 R-R 中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente de R-R" + } } } }, @@ -96438,6 +109518,12 @@ "state": "translated", "value": "正在等待 R-R 間期。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente des intervalles R-R." + } } } }, @@ -96472,6 +109558,12 @@ "state": "translated", "value": "正在等待靜息心率,請先開始一次即時讀數,或在你靜止時再回來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente d'une fréquence cardiaque au repos. Démarrez d'abord une mesure en direct, ou revenez lorsque vous êtes immobile." + } } } }, @@ -96506,6 +109598,12 @@ "state": "translated", "value": "醒來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveil" + } } } }, @@ -96592,6 +109690,12 @@ "state": "translated", "value": "用手環震動叫醒我" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Me réveiller avec une vibration du bracelet" + } } } }, @@ -96626,6 +109730,12 @@ "state": "translated", "value": "喚醒機器。開始踩踏、走動或划船,讓它啟動 Bluetooth。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveillez l'appareil. Commencez à pédaler, marcher ou ramer pour qu'il active son Bluetooth." + } } } }, @@ -96660,6 +109770,12 @@ "state": "translated", "value": "起床時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Heure de réveil" + } } } }, @@ -96694,6 +109810,12 @@ "state": "translated", "value": "喚醒你的 Amazfit / Zepp 手環,並確認它目前沒有連線到 Zepp 應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveillez votre bracelet Amazfit / Zepp et assurez-vous qu'il n'est pas connecté à l'application Zepp en ce moment." + } } } }, @@ -96728,6 +109850,12 @@ "state": "translated", "value": "喚醒你的 Mi Band,並確認它目前沒有連線到 Mi Fitness / Zepp Life 應用程式。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveillez votre Mi Band et assurez-vous qu'elle n'est pas connectée à l'application Mi Fitness / Zepp Life en ce moment." + } } } }, @@ -96762,6 +109890,12 @@ "state": "translated", "value": "喚醒你的手環。戴上它,或沾濕接觸點。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveillez votre bracelet. Portez-le, ou humidifiez les contacts." + } } } }, @@ -96796,6 +109930,12 @@ "state": "translated", "value": "步行" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Marche" + } } } }, @@ -96830,6 +109970,12 @@ "state": "translated", "value": "想要每項指標的完整明細以及 NOOP 對每一項的確定程度嗎?「設定」中的「關於 Apple Watch 資料」頁面有一張誠實的表格。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous voulez le détail complet de chaque métrique et le degré de certitude de NOOP pour chacune ? La page « À propos des données Apple Watch » dans Paramètres contient le tableau honnête." + } } } }, @@ -96864,6 +110010,12 @@ "state": "translated", "value": "高於你的基線溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus chaude que votre référence" + } } } }, @@ -96898,6 +110050,12 @@ "state": "translated", "value": "熱身中,繼續活動以進入區間 1。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échauffement. Continuez à bouger pour atteindre la Zone 1." + } } } }, @@ -96932,6 +110090,12 @@ "state": "translated", "value": "在臨近就寢時提醒我注意咖啡因" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "M'avertir en cas de caféine proche du coucher" + } } } }, @@ -96966,6 +110130,12 @@ "state": "translated", "value": "當我在過於臨近就寢時記錄咖啡因時提醒我。這是基於你自己就寢時間的時間參考,而非測量值。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "M'avertir lorsque j'enregistre de la caféine trop près de l'heure du coucher. Un repère basé sur votre propre heure de coucher, pas une mesure." + } } } }, @@ -97000,6 +110170,12 @@ "state": "translated", "value": "手錶" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Montre" + } } } }, @@ -97034,6 +110210,12 @@ "state": "translated", "value": "留意疾病早期徵兆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Surveiller les premiers signes de maladie" + } } } }, @@ -97068,6 +110250,12 @@ "state": "translated", "value": "即時看見你的心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Observez votre cœur, en direct" + } } } }, @@ -97102,6 +110290,12 @@ "state": "translated", "value": "將你的靜息心率、HRV、皮膚溫度和呼吸與你自己的 28 天基線做比對。在裝置上運算且僅為近似值:僅供參考,並非診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Surveille votre FC au repos, votre VFC, votre température cutanée et votre respiration par rapport à votre propre référence de 28 jours. Sur l'appareil et approximatif : à titre informatif uniquement, pas un diagnostic." + } } } }, @@ -97188,6 +110382,12 @@ "state": "translated", "value": "我們無法接管這枚戒指。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous n'avons pas pu prendre le contrôle de cet anneau." + } } } }, @@ -97222,6 +110422,12 @@ "state": "translated", "value": "我們還沒有看到你手環的任何運動資料。步數是根據你 WHOOP 儲存的運動歷史估算的,因此你的手環需要先同步該歷史,NOOP 才有可供統計的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous ne voyons encore aucun mouvement provenant de votre bracelet. Les pas sont estimés à partir de l'historique de mouvement stocké par votre WHOOP, donc votre bracelet doit d'abord synchroniser cet historique avant que NOOP ait quoi que ce soit à compter." + } } } }, @@ -97256,6 +110462,12 @@ "state": "translated", "value": "佩戴與偵測" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Port et présence" + } } } }, @@ -97290,6 +110502,12 @@ "state": "translated", "value": "貼合地戴在手腕或上臂,感測器緊貼皮膚。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Portez-le ajusté au poignet ou au biceps, capteur contre la peau." + } } } }, @@ -97324,6 +110542,12 @@ "state": "translated", "value": "佩戴狀態" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "État de port" + } } } }, @@ -97358,6 +110582,12 @@ "state": "translated", "value": "佩戴手環以進行評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Portez le bracelet pour obtenir un score" + } } } }, @@ -97392,6 +110622,12 @@ "state": "translated", "value": "整晚佩戴手環,這些資料便會從你的夜間皮膚溫度中讀取。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Portez le bracelet pendant la nuit et ces valeurs seront lues à partir de votre température cutanée nocturne." + } } } }, @@ -97426,6 +110662,12 @@ "state": "translated", "value": "戴上手環,點一下,等它同步後分享你的手環記錄檔。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Portez le bracelet, touchez une fois, laissez-le se synchroniser, puis partagez le journal du bracelet." + } } } }, @@ -97460,6 +110702,12 @@ "state": "translated", "value": "週三" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mer" + } } } }, @@ -97494,6 +110742,12 @@ "state": "translated", "value": "星期三" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mercredi" + } } } }, @@ -97528,6 +110782,12 @@ "state": "translated", "value": "每週回顧" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bilan de la semaine" + } } } }, @@ -97562,6 +110822,12 @@ "state": "translated", "value": "平日" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jours de semaine" + } } } }, @@ -97596,6 +110862,12 @@ "state": "translated", "value": "週末" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Week-ends" + } } } }, @@ -97630,6 +110902,12 @@ "state": "translated", "value": "每週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hebdomadaire" + } } } }, @@ -97716,6 +110994,12 @@ "state": "translated", "value": "體重(公斤)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Poids en kilogrammes" + } } } }, @@ -97750,6 +111034,12 @@ "state": "translated", "value": "體重(磅)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Poids en livres" + } } } }, @@ -97784,6 +111074,12 @@ "state": "translated", "value": "體重,%lld 磅" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Poids, %lld livres" + } } } }, @@ -97818,6 +111114,12 @@ "state": "translated", "value": "身心健康" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bien-être" + } } } }, @@ -97904,6 +111206,12 @@ "state": "translated", "value": "「記錄中」是什麼意思" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce que signifie « enregistrement »" + } } } }, @@ -97938,6 +111246,12 @@ "state": "translated", "value": "實驗手記是什麼,又不是什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qu'est le Lab Book (et ce qu'il n'est pas)" + } } } }, @@ -97972,6 +111286,12 @@ "state": "translated", "value": "什麼在影響你" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui vous fait bouger" + } } } }, @@ -98006,6 +111326,12 @@ "state": "translated", "value": "NOOP 做些什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce que fait NOOP" + } } } }, @@ -98040,6 +111366,12 @@ "state": "translated", "value": "你要新增什麼?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Qu'ajoutez-vous ?" + } } } }, @@ -98074,6 +111406,12 @@ "state": "translated", "value": "有何變化" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui a changé" + } } } }, @@ -98160,6 +111498,12 @@ "state": "translated", "value": "每項活動對你恢復的消耗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce que chaque activité coûte à votre récupération" + } } } }, @@ -98194,6 +111538,12 @@ "state": "translated", "value": "什麼在影響你。你自己資料中已排名的規律,以及你的劑量反應。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui vous fait bouger. Des tendances classées dans vos propres données, et votre réponse à la dose." + } } } }, @@ -98280,6 +111630,12 @@ "state": "translated", "value": "是什麼塑造了它" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui l'a façonné" + } } } }, @@ -98314,6 +111670,12 @@ "state": "translated", "value": "是什麼塑造了你的能量" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui a façonné votre Charge" + } } } }, @@ -98348,6 +111710,12 @@ "state": "translated", "value": "今天的訓練該怎麼安排?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À quoi devrait ressembler l'entraînement du jour ?" + } } } }, @@ -98382,6 +111750,12 @@ "state": "translated", "value": "預期內容" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À quoi s'attendre" + } } } }, @@ -98416,6 +111790,12 @@ "state": "translated", "value": "什麼在追蹤你的心情,%lld 次簽到" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce qui suit votre humeur (%lld check-ins)" + } } } }, @@ -98450,6 +111830,12 @@ "state": "translated", "value": "你會得到什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce que vous obtenez" + } } } }, @@ -98484,6 +111870,12 @@ "state": "translated", "value": "你會失去什麼" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce que vous perdez" + } } } }, @@ -98518,6 +111910,12 @@ "state": "translated", "value": "你的手錶擅長什麼,在哪些方面比胸帶更薄弱,以及 NOOP 有多確定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce en quoi votre montre excelle, où elle est plus légère qu'une ceinture pectorale, et le niveau de confiance de NOOP." + } } } }, @@ -98604,6 +112002,12 @@ "state": "translated", "value": "NOOP %@ 的新功能" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveautés de NOOP %@" + } } } }, @@ -98638,6 +112042,12 @@ "state": "translated", "value": "應用程式與你的資料有哪些新變化" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveautés dans l'application et vos données" + } } } }, @@ -98678,6 +112088,12 @@ "state": "translated", "value": "當%@較高時,%@往往也較高。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand %1$@ est plus élevé, %2$@ a tendance à l'être aussi." + } } } }, @@ -98718,6 +112134,12 @@ "state": "translated", "value": "當%@較高時,%@往往較低。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand %1$@ est plus élevé, %2$@ a tendance à être plus bas." + } } } }, @@ -98804,6 +112226,12 @@ "state": "translated", "value": "當你靜止時偵測到新的、非運動造成的 HRV 下降,NOOP 會提供一分鐘的引導呼吸:一次確認震動加上一張可關閉的卡片。絕不是警報,也絕非診斷。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand une chute récente de la VFC hors exercice est détectée pendant que vous êtes immobile, NOOP propose une respiration guidée d'une minute : une seule vibration de confirmation et une carte que vous pouvez ignorer. Jamais une alarme, jamais un diagnostic." + } } } }, @@ -98838,6 +112266,12 @@ "state": "translated", "value": "開啟後,NOOP 會為你在下方挑選的應用程式輕震你的手腕,讓你即使離開 %@ 也能感知重要的事。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une fois activé, NOOP fait vibrer votre poignet pour les applications que vous choisissez ci-dessous, afin que vous puissiez laisser %@ de côté tout en ressentant ce qui compte." + } } } }, @@ -98878,6 +112312,12 @@ "state": "translated", "value": "當你選擇的 %@ 應用程式發出通知時,NOOP 會輕震你的手環:Slack、行事曆、訊息,任何重要的都行。一切都留在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand les applications de %1$@ que vous choisissez envoient une notification, NOOP fait vibrer votre bracelet : Slack, Calendrier, Messages, tout ce qui compte. Tout reste sur %2$@." + } } } }, @@ -98912,6 +112352,12 @@ "state": "translated", "value": "當戒指已重置並喚醒後,點擊下方的「掃描」。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand l'anneau est réinitialisé et se réveille, appuyez sur « Scanner » ci-dessous." + } } } }, @@ -98998,6 +112444,12 @@ "state": "translated", "value": "開啟此項後,每次你離開應用程式時,NOOP 都會重寫一個純文字檔案 ,「在我的 iPhone 上 › NOOP › noop_sync.txt」, 每 15 分鐘一行,包含直接從你手環讀取的心率、HRV 和步數。將它與那個讀取該檔案並將一切記錄到 Apple 健康的 Siri 捷徑搭配使用,無需 HealthKit 權限,因此它在側載安裝上也能工作。設定指南和預製的捷徑位於 GitHub 上的專案 wiki 中。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand c'est activé, NOOP réécrit un fichier texte brut (Sur mon iPhone › NOOP › noop_sync.txt) chaque fois que vous quittez l'application : une ligne par tranche de 15 minutes de fréquence cardiaque, VFC et pas, lue directement depuis votre bracelet. Associez-le au raccourci Siri qui lit le fichier et enregistre tout dans Apple Santé (aucune autorisation HealthKit requise), pour que cela fonctionne aussi sur les installations en sideload. Le guide de configuration et le raccourci prêt à l'emploi se trouvent dans le wiki du projet sur GitHub." + } } } }, @@ -99032,6 +112484,12 @@ "state": "translated", "value": "當你匯入 Apple 健康或 WHOOP 匯出檔案時,iOS 會把該檔案的一份私有副本交給 NOOP。NOOP 讀取它,將你的資料保存到健康資料庫中,然後刪除該副本。較舊的版本沒有刪除每一份副本,此畫面會回收任何被遺留下來的副本。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand vous importez un export Apple Santé ou WHOOP, iOS remet à NOOP une copie privée du fichier. NOOP la lit, enregistre vos données dans la base de santé, puis supprime la copie. Les anciennes versions ne supprimaient pas toutes les copies. Cet écran récupère celles qui sont restées en place." + } } } }, @@ -99066,6 +112524,12 @@ "state": "translated", "value": "手環連線時,NOOP 會即時保存資料。「上次同步」告訴你資料有多新。如果顯示「未記錄」,請重新連線。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand votre bracelet est connecté, NOOP enregistre les données en direct. « Dernière synchro » indique leur fraîcheur. S'il indique « Aucun enregistrement », reconnectez-vous." + } } } }, @@ -99100,6 +112564,12 @@ "state": "translated", "value": "NOOP 的裝置本地空間用在了哪裡,以及一鍵清理。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Où va l'espace de NOOP sur l'appareil, et un nettoyage en un geste." + } } } }, @@ -99134,6 +112604,12 @@ "state": "translated", "value": "當你的手錶上沒有某個感測器時,NOOP 會將該指標讀作「不可用」,絕不會讀作零,也絕不會讀作編造的數字。其他一切照常工作。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Là où un capteur est absent de votre montre, NOOP affiche « non disponible » pour cette mesure, jamais un zéro, jamais un chiffre inventé. Tout le reste continue de fonctionner." + } } } }, @@ -99168,6 +112644,12 @@ "state": "translated", "value": "你的數字從何而來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "D'où viennent vos chiffres" + } } } }, @@ -99202,6 +112684,12 @@ "state": "translated", "value": "你正在配對哪一個手環?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quel bracelet associez-vous ?" + } } } }, @@ -99393,6 +112881,12 @@ "state": "translated", "value": "我為什麼這麼疲憊?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi suis-je épuisé ?" + } } } }, @@ -99427,6 +112921,12 @@ "state": "translated", "value": "這為什麼會增長?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi cela augmente-t-il ?" + } } } }, @@ -99461,6 +112961,12 @@ "state": "translated", "value": "為什麼恢復需要大約一週來校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi la récupération se calibre en une semaine environ" + } } } }, @@ -99495,6 +113001,12 @@ "state": "translated", "value": "為什麼這被記錄為小睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi c'est enregistré comme une sieste" + } } } }, @@ -99529,6 +113041,12 @@ "state": "translated", "value": "為什麼這是你的主睡眠" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi c'est votre sommeil principal" + } } } }, @@ -99563,6 +113081,12 @@ "state": "translated", "value": "為什麼是這段睡眠?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pourquoi ce sommeil ?" + } } } }, @@ -99597,6 +113121,12 @@ "state": "translated", "value": "放鬆" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se détendre" + } } } }, @@ -99631,6 +113161,12 @@ "state": "translated", "value": "放鬆提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rappel de détente" + } } } }, @@ -99717,6 +113253,12 @@ "state": "translated", "value": "分段 rMSSD" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "rMSSD par fenêtre" + } } } }, @@ -99751,6 +113293,12 @@ "state": "translated", "value": "將 NOOP 的操作接入輕點背面、專注模式自動化或更長的捷徑,例如,雙擊你 iPhone 的背面來讓手環震動。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reliez les actions de NOOP à un Toucher au dos, une automatisation de Focus ou un raccourci plus élaboré. Par exemple, appuyez deux fois à l'arrière de votre iPhone pour faire vibrer le bracelet." + } } } }, @@ -99889,6 +113437,12 @@ "state": "translated", "value": "已醒來" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réveillé" + } } } }, @@ -99923,6 +113477,12 @@ "state": "translated", "value": "工作" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Travail" + } } } }, @@ -100009,6 +113569,12 @@ "state": "translated", "value": "鍛鍊" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînement" + } } } }, @@ -100043,6 +113609,12 @@ "state": "translated", "value": "運動檔案(GPX / TCX / FIT)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichier d'entraînement (GPX / TCX / FIT)" + } } } }, @@ -100077,6 +113649,12 @@ "state": "translated", "value": "運動已保存" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînement enregistré" + } } } }, @@ -100111,6 +113689,12 @@ "state": "translated", "value": "鍛鍊已儲存 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Entraînement enregistré · %@" + } } } }, @@ -100197,6 +113781,12 @@ "state": "translated", "value": "已佩戴" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Porté" + } } } }, @@ -100231,6 +113821,12 @@ "state": "translated", "value": "手腕提醒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alertes au poignet" + } } } }, @@ -100318,6 +113914,12 @@ "state": "translated", "value": "手腕推送尚未上線,它需要一個小型的裝置端監聽程式(將在後續更新中推出)來讀取 macOS 通知。一切都留在這台 Mac 上。你的選擇現已保存,一旦推送功能上線便會自動生效。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La diffusion au poignet n'est pas encore active. Elle nécessite un petit observateur sur l'appareil (à venir dans une mise à jour) pour lire les notifications macOS. Tout reste sur ce Mac. Vos choix sont enregistrés dès maintenant et s'appliqueront automatiquement une fois la fonction disponible." + } } } }, @@ -100352,6 +113954,12 @@ "state": "translated", "value": "手腕溫度" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Temp. poignet" + } } } }, @@ -100386,6 +113994,12 @@ "state": "translated", "value": "手腕溫度需要 Series 8 或更新的型號,而最新的美國版機型移除了血氧感測器。感測器不存在時,NOOP 會顯示「無法使用」而不是零。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La température au poignet nécessite une Series 8 ou ultérieure, et les derniers modèles américains ont perdu le capteur d'oxygène sanguin. Là où un capteur est absent, NOOP affiche « non disponible » plutôt que zéro." + } } } }, @@ -100420,6 +114034,12 @@ "state": "translated", "value": "每天將帶時間戳記的手環記錄檔副本寫入「檔案」應用程式中 NOOP 的資料夾,方便你回報錯誤時不必記得手動抓取。在 iPhone 上,它會在你選定的時間附近、iOS 下次喚醒 NOOP 時執行,無法保證精確到分鐘(整夜保持 NOOP 開啟成功率最高)。一切都留在 %@ 上,不會上傳任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écrit une copie horodatée du journal de votre bracelet dans le dossier de NOOP de l'app Fichiers, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur iPhone, cela se déclenche au prochain réveil de NOOP par iOS proche de l'heure choisie, sans garantie à la minute près (laissez NOOP ouvert la nuit pour de meilleures chances). Tout reste sur %@ ; rien n'est envoyé en ligne." + } } } }, @@ -100454,6 +114074,12 @@ "state": "translated", "value": "每天將帶時間戳記的手環記錄檔副本寫入你的「文件」資料夾,方便你回報錯誤時不必記得手動抓取。在 Mac 上,它會在 NOOP 開啟時執行(若關閉期間錯過了時間,會在啟動時補上)。一切都留在 %@ 上,不會上傳任何內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écrit une copie horodatée du journal de votre bracelet dans votre dossier Documents, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur Mac, cela s'exécute pendant que NOOP est ouvert (et rattrape au lancement si l'heure est passée pendant sa fermeture). Tout reste sur %@ ; rien n'est envoyé en ligne." + } } } }, @@ -100488,6 +114114,12 @@ "state": "translated", "value": "你的每日評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VOS SCORES QUOTIDIENS" + } } } }, @@ -100522,6 +114154,12 @@ "state": "translated", "value": "你的讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "VOTRE MESURE" + } } } }, @@ -100556,6 +114194,12 @@ "state": "translated", "value": "是" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Oui" + } } } }, @@ -100590,6 +114234,12 @@ "state": "translated", "value": "昨天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hier" + } } } }, @@ -100624,6 +114274,12 @@ "state": "translated", "value": "昨日綜覽" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synthèse d'hier" + } } } }, @@ -100658,6 +114314,12 @@ "state": "translated", "value": "你可以隨時在「設定 › 健康 › 資料存取與裝置」中更改你分享的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous pouvez modifier ce que vous partagez à tout moment dans Paramètres › Santé › Accès aux données et appareils." + } } } }, @@ -100692,6 +114354,12 @@ "state": "translated", "value": "你仍然可以透過在「資料來源」中匯入一個健康匯出檔案來引入你的資料。從 App Store 安裝的版本,或用付費 Apple 開發者帳戶簽名的版本,可直接連接。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous pouvez tout de même importer vos données via un export Santé depuis les Sources de données. Une version issue de l'App Store, ou signée avec un compte Apple Developer payant, se connecte directement." + } } } }, @@ -100726,6 +114394,12 @@ "state": "translated", "value": "你移除了你的活躍手環。請選擇哪一個已配對的手環來提供你的即時資料,或者不設定任何活躍項,稍後再配對一個。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous avez retiré votre bracelet actif. Choisissez quel bracelet associé fournira vos données en direct, ou n'en laissez aucun actif et associez-en un plus tard." + } } } }, @@ -100812,6 +114486,12 @@ "state": "translated", "value": "你會在 %@ 左右收到提醒。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous recevrez un rappel vers %@." + } } } }, @@ -100852,6 +114532,12 @@ "state": "translated", "value": "如果你今晚睡大約 %3$@,你醒來時的能量可能在 %1$lld ± %2$lld 左右。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous vous réveillerez probablement avec une Charge d'environ %1$lld ± %2$lld si vous dormez environ %3$@ cette nuit." + } } } }, @@ -100886,6 +114572,12 @@ "state": "translated", "value": "你已全部看完。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes à jour." + } } } }, @@ -100926,6 +114618,12 @@ "state": "translated", "value": "你累積了約 %@ 的睡眠盈餘(期間:%@)。整體而言你睡得超過所需,狀態領先。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous portez un surplus d'environ %@ sur %@. Vous avez dormi au-delà de vos besoins, en solde. Bien en avance." + } } } }, @@ -101012,6 +114710,12 @@ "state": "translated", "value": "你已連接。NOOP 現在正在讀取你的 Apple Watch 資料。你的能量評分將在最初一週左右進行校準,然後穩定下來。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes connecté. NOOP lit désormais les données de votre Apple Watch. Votre score de Charge passera sa première semaine environ à se calibrer, puis se stabilisera." + } } } }, @@ -101046,6 +114750,12 @@ "state": "translated", "value": "你與基線保持平衡,中等強度是明智的選擇" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes en équilibre avec votre référence, un effort modéré est donc bien choisi" + } } } }, @@ -101080,6 +114790,12 @@ "state": "translated", "value": "你已是最新版本(%@)。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous avez la dernière version (%@)." + } } } }, @@ -101114,6 +114830,12 @@ "state": "translated", "value": "在%@內你的睡眠大致跟上了需求,睡眠分鐘數與所需相抵。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes globalement à jour sur votre sommeil sur %@. Les minutes dormies compensent vos besoins." + } } } }, @@ -101148,6 +114870,12 @@ "state": "translated", "value": "你目前處於典型的自律神經基線附近:中等壓力,一個正常、平衡的日子。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes proche de votre référence autonome habituelle : stress modéré, une journée normale et équilibrée." + } } } }, @@ -101188,6 +114916,12 @@ "state": "translated", "value": "你累積了約 %@ 的睡眠負債(期間:%@)。有盈餘的夜晚會抵銷它。早睡一兩晚就能還清。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous avez accumulé environ %@ de dette de sommeil sur %@. Les nuits en surplus viennent la réduire. Une ou deux nuits plus tôt suffiraient à l'effacer." + } } } }, @@ -101222,6 +114956,12 @@ "state": "translated", "value": "你已經坐了大約 %lld 分鐘,該動一動了。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes assis depuis environ %lld min. Il est temps de bouger." + } } } }, @@ -101256,6 +114996,12 @@ "state": "translated", "value": "作為一台裝置的你的 Apple Watch" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre Apple Watch en tant qu'appareil" + } } } }, @@ -101290,6 +115036,12 @@ "state": "translated", "value": "錶面上顯示你的能量(恢復),矩形卡片中則是消耗與休息。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre Charge (récupération) sur le cadran, avec Effort et Repos dans la carte rectangulaire." + } } } }, @@ -101324,6 +115076,12 @@ "state": "translated", "value": "你的能量評分會隨著時間從你的心率變異性、靜息心率等中學習出一個個人基線。如果糟糕的第一週使它出現偏差,你可以從今晚起重新學習它。你的歷史會保留。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre score de Charge apprend une référence personnelle à partir de votre variabilité de fréquence cardiaque, de votre fréquence cardiaque au repos et plus encore, au fil du temps. Si une mauvaise première semaine l'a faussée, vous pouvez la faire réapprendre dès ce soir. Votre historique reste." + } } } }, @@ -101358,6 +115116,12 @@ "state": "translated", "value": "你的資料,已融合" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos données, fusionnées" + } } } }, @@ -101392,6 +115156,12 @@ "state": "translated", "value": "本次工作階段的 GPS 路線,記錄並儲存在你的裝置上。沒有任何內容離開你的手機。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre trajet GPS pour cette séance, enregistré et stocké sur votre appareil. Rien ne quitte votre téléphone." + } } } }, @@ -101426,6 +115196,12 @@ "state": "translated", "value": "你靜止時 HRV 出現了下降,想花一分鐘呼吸一下嗎?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre VFC a chuté pendant que vous étiez immobile. Envie d'une minute pour respirer ?" + } } } }, @@ -101460,6 +115236,12 @@ "state": "translated", "value": "你從週一到週日的情況,一眼看懂。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre semaine du lundi au dimanche, lue en un coup d'œil." + } } } }, @@ -101494,6 +115276,12 @@ "state": "translated", "value": "在實驗模式開啟之前(設定,實驗),你的 WHOOP 5/MG 不會設定此項。目前你的起床時間已保存,但手環尚未設定。即使開啟了實驗模式,由 5/MG 手環驅動的喚醒在我們這邊仍未得到確認,因此請保留一個備用鬧鐘。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre WHOOP 5/MG n'activera pas cette fonction tant que le mode Expérimental n'est pas activé (Paramètres, Expérimental). Pour l'instant, votre heure de réveil est enregistrée mais le bracelet n'est PAS armé. Même avec le mode Expérimental activé, un réveil piloté par le bracelet 5/MG reste non confirmé de notre côté, gardez donc un réveil de secours." + } } } }, @@ -101528,6 +115316,12 @@ "state": "translated", "value": "你的 WHOOP 電量已達 100%。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre WHOOP est à 100 %." + } } } }, @@ -101562,6 +115356,12 @@ "state": "translated", "value": "你的 WHOOP 同一時間只與一部手機通信。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre WHOOP ne communique qu'avec un seul téléphone à la fois." + } } } }, @@ -101596,6 +115396,12 @@ "state": "translated", "value": "你今天的自律神經指標偏向壓力,把今天當作以恢復為主的一天。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos marqueurs autonomes penchent vers le stress aujourd'hui. Considérez-la comme une journée axée sur la récupération." + } } } }, @@ -101630,6 +115436,12 @@ "state": "translated", "value": "你的手環報告了不同的數字。這裡是每一個來源,以及 NOOP 正在使用的那一個。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos bracelets rapportent des chiffres différents. Voici chaque source, et celle que NOOP utilise." + } } } }, @@ -101664,6 +115476,12 @@ "state": "translated", "value": "你的血液檢查、血壓和身體數據,私密保存,在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos analyses sanguines, votre tension et vos données corporelles. Gardées privées, sur %@." + } } } }, @@ -101698,6 +115516,12 @@ "state": "translated", "value": "你的心血管負荷。NOOP 使用心率儲備區間(Karvonen)將每一秒的心率轉換為訓練衝量,對較高強度區間的時間給予更大權重(Edwards / Banister),並將其放在 0–100 的對數刻度上:輕鬆的日子分數低,全力以赴的一天接近 100,而這種情況確實罕見。透過步數/活動能量的下限機制,心肺負荷不高的長距離步行也會計入。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre charge cardiovasculaire. NOOP transforme chaque seconde de fréquence cardiaque en impulsion d'entraînement à l'aide des zones de réserve de fréquence cardiaque (Karvonen), pondère plus fortement le temps passé dans les zones difficiles (Edwards / Banister), et place le résultat sur une échelle logarithmique de 0 à 100, si bien que les jours faciles restent bas et qu'une journée à fond approche 100, ce qui reste vraiment rare. Une longue marche avec peu de cardio compte tout de même, grâce à un seuil minimal basé sur les pas et l'énergie active." + } } } }, @@ -101732,6 +115556,12 @@ "state": "translated", "value": "你的卡片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos cartes" + } } } }, @@ -101766,6 +115596,12 @@ "state": "translated", "value": "你目前的手環" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre bracelet actuel" + } } } }, @@ -101800,6 +115636,12 @@ "state": "translated", "value": "隨著手環卸載歷史資料,你的曲線會逐漸填滿。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre courbe se remplit à mesure que le bracelet transfère son historique." + } } } }, @@ -101834,6 +115676,12 @@ "state": "translated", "value": "你的資料已還原。請結束並重新開啟 NOOP 以生效。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos données ont été restaurées. Quittez et rouvrez NOOP pour que cela prenne effet." + } } } }, @@ -101868,6 +115716,12 @@ "state": "translated", "value": "你今天的水分攝入,僅在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre apport en liquides aujourd'hui, sur %@ uniquement." + } } } }, @@ -101902,6 +115756,12 @@ "state": "translated", "value": "你過去 %lld 小時一直處於高位區間。幾分鐘的節律呼吸有助於讓你的神經系統降檔。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos dernières %lld heures sont restées dans la bande haute. Quelques minutes de respiration rythmée peuvent aider à apaiser votre système nerveux." + } } } }, @@ -101942,6 +115802,12 @@ "state": "translated", "value": "你最近 %lld 筆讀數:%@ %@,保持穩定。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos %1$lld dernières mesures : %2$@ %3$@, stables." + } } } }, @@ -101982,6 +115848,12 @@ "state": "translated", "value": "你最近 %lld 筆讀數:%@ %@,呈下降趨勢。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la baisse." + } } } }, @@ -102022,6 +115894,12 @@ "state": "translated", "value": "你最近 %lld 筆讀數:%@ %@,呈上升趨勢。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la hausse." + } } } }, @@ -102056,6 +115934,12 @@ "state": "translated", "value": "你的即時心率正透過手環工作,而能量、消耗和休息會在你接下來幾晚的佩戴中逐步建立,並隨著它學習你的基線而愈發精準。想立即獲得完整歷史?請在「資料來源」中匯入你的 WHOOP 匯出檔案,約一分鐘即可回填。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre fréquence cardiaque en direct fonctionne déjà depuis le bracelet, et la charge, l'effort et le repos s'en construisent au fil de vos prochaines nuits de port, s'affinant à mesure qu'ils apprennent votre référence. Vous voulez tout votre historique instantanément ? Importez votre export WHOOP dans les Sources de données, il se complète en une minute environ." + } } } }, @@ -102143,6 +116027,12 @@ "state": "translated", "value": "你鎖定的節奏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre rythme verrouillé" + } } } }, @@ -102177,6 +116067,12 @@ "state": "translated", "value": "你鎖定的節奏 · 每分鐘 %@ 次呼吸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre rythme verrouillé · %@ resp/min" + } } } }, @@ -102211,6 +116107,12 @@ "state": "translated", "value": "你的神經系統已充分恢復,正是全力衝刺的好時機" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre système nerveux est bien récupéré, vous êtes donc prêt à pousser" + } } } }, @@ -102245,6 +116147,12 @@ "state": "translated", "value": "你的數值、你的手環,以及 NOOP 的運作原理。全部都在 %@ 上。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos chiffres, votre bracelet et le fonctionnement de NOOP. Tout sur %@." + } } } }, @@ -102332,6 +116240,12 @@ "state": "translated", "value": "你的頭像照片" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre photo de profil" + } } } }, @@ -102366,6 +116280,12 @@ "state": "translated", "value": "你的記錄永遠不會離開 %@。沒有帳號、沒有雲端、沒有 NOOP 伺服器。因為 NOOP 是你自己運行的獨立應用程式(不是醫療服務提供者),它不屬於「HIPAA 涵蓋範圍」,該保護在此並不適用;安全來自資料僅存於本機且完全屬於你。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos données ne quittent jamais %@. Il n'y a ni compte, ni cloud, ni serveur NOOP. Comme NOOP est une application indépendante que vous exécutez vous-même (pas un prestataire de santé), elle n'est pas « couverte par HIPAA », et cette protection ne s'applique pas ici ; la sécurité vient du fait que les données restent uniquement locales et vous appartiennent." + } } } }, @@ -102400,6 +116320,12 @@ "state": "translated", "value": "你的共振節奏" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre rythme de résonance" + } } } }, @@ -102434,6 +116360,12 @@ "state": "translated", "value": "你的心律看起來穩定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre rythme semblait stable" + } } } }, @@ -102468,6 +116400,12 @@ "state": "translated", "value": "你的心律變化比平常大" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre rythme a varié plus que d'habitude" + } } } }, @@ -102502,6 +116440,12 @@ "state": "translated", "value": "你的戒指" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre anneau" + } } } }, @@ -102536,6 +116480,12 @@ "state": "translated", "value": "你的戒指只與 NOOP 通訊,完全離線,不需要 Oura 帳號。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre anneau ne communique qu'avec NOOP, entièrement hors ligne, sans compte Oura." + } } } }, @@ -102570,6 +116520,12 @@ "state": "translated", "value": "你的分數會在幾個夜晚間逐步建立" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos scores se construisent en quelques nuits" + } } } }, @@ -102657,6 +116613,12 @@ "state": "translated", "value": "你的手環已配對綁定,準備開始串流。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre bracelet est appairé et prêt à transmettre." + } } } }, @@ -102691,6 +116653,12 @@ "state": "translated", "value": "你的手環已配對綁定 · 電量 %lld%%。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre bracelet est appairé · %lld %% de batterie." + } } } }, @@ -102725,6 +116693,12 @@ "state": "translated", "value": "你的手環已連接並正在保存資料。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre bracelet est connecté et enregistre les données." + } } } }, @@ -102759,6 +116733,12 @@ "state": "translated", "value": "你的手環已配對並正在傳送資料。開啟「即時」查看即時心率。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre bracelet est associé et envoie des données. Ouvrez En direct pour une fréquence cardiaque en temps réel." + } } } }, @@ -102793,6 +116773,12 @@ "state": "translated", "value": "你的手環喚醒鬧鐘和傍晚的放鬆提醒,集中在一處。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre alarme de réveil par bracelet et le rappel de détente du soir, au même endroit." + } } } }, @@ -102879,6 +116865,12 @@ "state": "translated", "value": "你的手錶,NOOP 的大腦" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre montre, le cerveau de NOOP" + } } } }, @@ -102913,6 +116905,12 @@ "state": "translated", "value": "Z%lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Z%lld" + } } } }, @@ -102947,6 +116945,12 @@ "state": "translated", "value": "區間 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ZONE %lld" + } } } }, @@ -103039,6 +117043,12 @@ "state": "translated", "value": "區間 %1$lld · %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zone %1$lld · %2$@" + } } } }, @@ -103079,6 +117089,12 @@ "state": "translated", "value": "區間 %lld · %lld%%" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zone %1$lld · %2$lld %%" + } } } }, @@ -103119,6 +117135,12 @@ "state": "translated", "value": "區間 %1$lld:%2$lld–%3$lld bpm(最大心率的 %4$lld–%5$lld%%)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zone %1$lld : %2$lld–%3$lld bpm (%4$lld–%5$lld %% FC max)" + } } } }, @@ -103153,6 +117175,12 @@ "state": "translated", "value": "已放大 · 拖曳以平移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zoom activé · glissez pour déplacer" + } } } }, @@ -103187,6 +117215,12 @@ "state": "translated", "value": "已放大,拖動以平移" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zoom activé. Glissez pour déplacer" + } } } }, @@ -103221,6 +117255,12 @@ "state": "translated", "value": "中等" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "un modéré" + } } } }, @@ -103255,6 +117295,12 @@ "state": "translated", "value": "強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "un fort" + } } } }, @@ -103289,6 +117335,12 @@ "state": "translated", "value": "睡得很好,有助於恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "une nuit forte, favorise la récupération" + } } } }, @@ -103323,6 +117375,12 @@ "state": "translated", "value": "一個平常的夜晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "une nuit typique" + } } } }, @@ -103357,6 +117415,12 @@ "state": "translated", "value": "非常強的" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "un très fort" + } } } }, @@ -103391,6 +117455,12 @@ "state": "translated", "value": "弱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "un faible" + } } } }, @@ -103425,6 +117495,12 @@ "state": "translated", "value": "與你的年齡相仿" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "environ votre âge" + } } } }, @@ -103459,6 +117535,12 @@ "state": "translated", "value": "高於基線,限制恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au-dessus de la référence, limite la récupération" + } } } }, @@ -103493,6 +117575,12 @@ "state": "translated", "value": "高於基線,有助於恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au-dessus de la référence, favorise la récupération" + } } } }, @@ -103527,6 +117615,12 @@ "state": "translated", "value": "活動" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "actif" + } } } }, @@ -103561,6 +117655,12 @@ "state": "translated", "value": "全部歷史" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "tout l'historique" + } } } }, @@ -103595,6 +117695,12 @@ "state": "translated", "value": "全部時間" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "depuis toujours" + } } } }, @@ -103681,6 +117787,12 @@ "state": "translated", "value": "截至 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au %@" + } } } }, @@ -103715,6 +117827,12 @@ "state": "translated", "value": "昨晚已入睡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "endormi la nuit dernière" + } } } }, @@ -103749,6 +117867,12 @@ "state": "translated", "value": "處於基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au niveau de référence" + } } } }, @@ -103783,6 +117907,12 @@ "state": "translated", "value": "平均 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moy. %@" + } } } }, @@ -103823,6 +117953,12 @@ "state": "translated", "value": "平均 %@ · %lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moy. %1$@ · %2$lldh" + } } } }, @@ -103857,6 +117993,12 @@ "state": "translated", "value": "平均 %lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moy. %lld" + } } } }, @@ -103891,6 +118033,12 @@ "state": "translated", "value": "平均 · %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moy. · %lldj" + } } } }, @@ -103925,6 +118073,12 @@ "state": "translated", "value": "平衡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "équilibré" + } } } }, @@ -103959,6 +118113,12 @@ "state": "translated", "value": "不及一夜好眠,限制恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en dessous d'une bonne nuit, limite la récupération" + } } } }, @@ -103993,6 +118153,12 @@ "state": "translated", "value": "低於基線,限制恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en dessous de la référence, limite la récupération" + } } } }, @@ -104027,6 +118193,12 @@ "state": "translated", "value": "低於基線,有助於恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en dessous de la référence, favorise la récupération" + } } } }, @@ -104113,6 +118285,12 @@ "state": "translated", "value": "次/分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "resp/min" + } } } }, @@ -104147,6 +118325,12 @@ "state": "translated", "value": "建立中" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en construction" + } } } }, @@ -104181,6 +118365,12 @@ "state": "translated", "value": "卡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cal" + } } } }, @@ -104215,6 +118405,12 @@ "state": "translated", "value": "乾淨間期" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "intervalles propres" + } } } }, @@ -104249,6 +118445,12 @@ "state": "translated", "value": "cm" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cm" + } } } }, @@ -104283,6 +118485,12 @@ "state": "translated", "value": "低於基線溫度,限制恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "plus fraîche que la référence, limite la récupération" + } } } }, @@ -104317,6 +118525,12 @@ "state": "translated", "value": "已涵蓋" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "couvert" + } } } }, @@ -104351,6 +118565,12 @@ "state": "translated", "value": "下降 %lld 點" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en baisse de %lld points" + } } } }, @@ -104385,6 +118605,12 @@ "state": "translated", "value": "下降 1 點" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en baisse de 1 point" + } } } }, @@ -104419,6 +118645,12 @@ "state": "translated", "value": "喝水" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "boisson" + } } } }, @@ -104453,6 +118685,12 @@ "state": "translated", "value": "例如 120" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. 120" + } } } }, @@ -104487,6 +118725,12 @@ "state": "translated", "value": "例如 2.0–5.0(你報告自身的範圍)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. 2,0–5,0 (la plage de votre rapport)" + } } } }, @@ -104521,6 +118765,12 @@ "state": "translated", "value": "例如 3.1" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. 3,1" + } } } }, @@ -104555,6 +118805,12 @@ "state": "translated", "value": "例如 80" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. 80" + } } } }, @@ -104589,6 +118845,12 @@ "state": "translated", "value": "例如鎂" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. Magnésium" + } } } }, @@ -104623,6 +118885,12 @@ "state": "translated", "value": "例如跑步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. Course à pied" + } } } }, @@ -104657,6 +118925,12 @@ "state": "translated", "value": "例如空腹、晨間採血" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. à jeun, prélèvement matinal" + } } } }, @@ -104691,6 +118965,12 @@ "state": "translated", "value": "例如 mmol/L" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ex. mmol/L" + } } } }, @@ -104725,6 +119005,12 @@ "state": "translated", "value": "消耗 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "effort %@" + } } } }, @@ -104759,6 +119045,12 @@ "state": "translated", "value": "估計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "est." + } } } }, @@ -104793,6 +119085,12 @@ "state": "translated", "value": "估計 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "est. · %@" + } } } }, @@ -104827,6 +119125,12 @@ "state": "translated", "value": "你錄入的每一筆讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "chaque mesure que vous avez saisie" + } } } }, @@ -104861,6 +119165,12 @@ "state": "translated", "value": "來自你手機的 fit 資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ajustée depuis votre téléphone" + } } } }, @@ -104895,6 +119205,12 @@ "state": "translated", "value": "持平" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "stable" + } } } }, @@ -104929,6 +119245,12 @@ "state": "translated", "value": "來自 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "depuis %@" + } } } }, @@ -104963,6 +119285,12 @@ "state": "translated", "value": "來自 R-R" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à partir des R-R" + } } } }, @@ -104997,6 +119325,12 @@ "state": "translated", "value": "來自個人檔案" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "depuis le profil" + } } } }, @@ -105031,6 +119365,12 @@ "state": "translated", "value": "來自你的報告" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "depuis votre rapport" + } } } }, @@ -105065,6 +119405,12 @@ "state": "translated", "value": "斜線 = 典型範圍" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "hachures = typique" + } } } }, @@ -105151,6 +119497,12 @@ "state": "translated", "value": "游標懸停以查看實際數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "survolez pour les valeurs réelles" + } } } }, @@ -105185,6 +119537,12 @@ "state": "translated", "value": "http://localhost:11434/v1" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "http://localhost:11434/v1" + } } } }, @@ -105219,6 +119577,12 @@ "state": "translated", "value": "英寸" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "in" + } } } }, @@ -105253,6 +119617,12 @@ "state": "translated", "value": "剛剛" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à l'instant" + } } } }, @@ -105287,6 +119657,12 @@ "state": "translated", "value": "大" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "grand" + } } } }, @@ -105321,6 +119697,12 @@ "state": "translated", "value": "最近 30 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30 derniers jours" + } } } }, @@ -105355,6 +119737,12 @@ "state": "translated", "value": "最近 7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "7 derniers jours" + } } } }, @@ -105389,6 +119777,12 @@ "state": "translated", "value": "最近 90 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "90 derniers jours" + } } } }, @@ -105423,6 +119817,12 @@ "state": "translated", "value": "上次攝取於 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "dernière prise %@" + } } } }, @@ -105457,6 +119857,12 @@ "state": "translated", "value": "去年" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "l'année dernière" + } } } }, @@ -105491,6 +119897,12 @@ "state": "translated", "value": "晚間咖啡因" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "caféine tardive" + } } } }, @@ -105525,6 +119937,12 @@ "state": "translated", "value": "之後的步驟" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "étape ultérieure" + } } } }, @@ -105559,6 +119977,12 @@ "state": "translated", "value": "最新" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "dernier" + } } } }, @@ -105593,6 +120017,12 @@ "state": "translated", "value": "磅" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "lb" + } } } }, @@ -105679,6 +120109,12 @@ "state": "translated", "value": "記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "enregistrer" + } } } }, @@ -105713,6 +120149,12 @@ "state": "translated", "value": "今天已記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "enregistré aujourd'hui" + } } } }, @@ -105799,6 +120241,12 @@ "state": "translated", "value": "低壓力天數 · %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "jours à faible stress · %lldj" + } } } }, @@ -105833,6 +120281,12 @@ "state": "translated", "value": "mg" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "mg" + } } } }, @@ -105919,6 +120373,12 @@ "state": "translated", "value": "分鐘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "min" + } } } }, @@ -105953,6 +120413,12 @@ "state": "translated", "value": "缺少" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "manquant" + } } } }, @@ -105987,6 +120453,12 @@ "state": "translated", "value": "ml/kg/min" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ml/kg/min" + } } } }, @@ -106021,6 +120493,12 @@ "state": "translated", "value": "中等" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "modéré" + } } } }, @@ -106055,6 +120533,12 @@ "state": "translated", "value": "月" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "mois" + } } } }, @@ -106089,6 +120573,12 @@ "state": "translated", "value": "需要更多夜間佩戴才能解鎖你的%@基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "davantage de nuits portées pour débloquer votre référence de %@" + } } } }, @@ -106129,6 +120619,12 @@ "state": "translated", "value": "最近一次攝取約在 %@ 前 · 估算中仍包含 %lld 筆攝取記錄。僅為粗略參考,依據你記錄的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prise la plus récente il y a environ %1$@ · %2$lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez enregistré." + } } } }, @@ -106163,6 +120659,12 @@ "state": "translated", "value": "最近一次攝取約在 %@ 前。僅為粗略參考,依據你記錄的內容。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prise la plus récente il y a environ %@. Simple indication, basée sur ce que vous avez enregistré." + } } } }, @@ -106249,6 +120751,12 @@ "state": "translated", "value": "接近基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "proche de la référence" + } } } }, @@ -106283,6 +120791,12 @@ "state": "translated", "value": "需要近期的一天資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nécessite un jour récent" + } } } }, @@ -106317,6 +120831,12 @@ "state": "translated", "value": "需要一個有追蹤的夜晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nécessite une nuit suivie" + } } } }, @@ -106351,6 +120871,12 @@ "state": "translated", "value": "需要基線 + 已記錄的天數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nécessite une référence + des jours enregistrés" + } } } }, @@ -106385,6 +120911,12 @@ "state": "translated", "value": "需要歷史資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nécessite un historique" + } } } }, @@ -106419,6 +120951,12 @@ "state": "translated", "value": "負向" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "négatif" + } } } }, @@ -106453,6 +120991,12 @@ "state": "translated", "value": "可忽略" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "négligeable" + } } } }, @@ -106487,6 +121031,12 @@ "state": "translated", "value": "新" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nouveau" + } } } }, @@ -106521,6 +121071,12 @@ "state": "translated", "value": "否" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "non" + } } } }, @@ -106555,6 +121111,12 @@ "state": "translated", "value": "尚未校準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de calibration" + } } } }, @@ -106589,6 +121151,12 @@ "state": "translated", "value": "無變化" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aucun changement" + } } } }, @@ -106623,6 +121191,12 @@ "state": "translated", "value": "無資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aucune donnée" + } } } }, @@ -106657,6 +121231,12 @@ "state": "translated", "value": "尚無估計" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore d'estimation" + } } } }, @@ -106691,6 +121271,12 @@ "state": "translated", "value": "無額外" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aucun extra" + } } } }, @@ -106725,6 +121311,12 @@ "state": "translated", "value": "尚未匯入" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore d'import" + } } } }, @@ -106759,6 +121351,12 @@ "state": "translated", "value": "尚無即時心率" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de FC en direct" + } } } }, @@ -106793,6 +121391,12 @@ "state": "translated", "value": "尚無即時重力資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de gravité en direct" + } } } }, @@ -106827,6 +121431,12 @@ "state": "translated", "value": "尚無已評分的夜晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de nuit évaluée" + } } } }, @@ -106861,6 +121471,12 @@ "state": "translated", "value": "尚無夜晚資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de nuit" + } } } }, @@ -106895,6 +121511,12 @@ "state": "translated", "value": "尚未卸載" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de transfert" + } } } }, @@ -106929,6 +121551,12 @@ "state": "translated", "value": "尚無先前的%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aucun %@ précédent" + } } } }, @@ -106963,6 +121591,12 @@ "state": "translated", "value": "尚無讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de mesure" + } } } }, @@ -106997,6 +121631,12 @@ "state": "translated", "value": "尚無讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de mesures" + } } } }, @@ -107031,6 +121671,12 @@ "state": "translated", "value": "尚無工作階段" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de séance" + } } } }, @@ -107065,6 +121711,12 @@ "state": "translated", "value": "尚無視窗" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas encore de fenêtre" + } } } }, @@ -107099,6 +121751,12 @@ "state": "translated", "value": "未連線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "non connecté" + } } } }, @@ -107133,6 +121791,12 @@ "state": "translated", "value": "今天尚未記錄" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "non enregistré aujourd'hui" + } } } }, @@ -107167,6 +121831,12 @@ "state": "translated", "value": "不在 7 天內" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas dans les 7 derniers j" + } } } }, @@ -107201,6 +121871,12 @@ "state": "translated", "value": "/ %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sur %@" + } } } }, @@ -107235,6 +121911,12 @@ "state": "translated", "value": "/ %@ 公升" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sur %@ L" + } } } }, @@ -107269,6 +121951,12 @@ "state": "translated", "value": "/ 100" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sur 100" + } } } }, @@ -107303,6 +121991,12 @@ "state": "translated", "value": "/ 3" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sur 3" + } } } }, @@ -107389,6 +122083,12 @@ "state": "translated", "value": "的心跳" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "des battements" + } } } }, @@ -107423,6 +122123,12 @@ "state": "translated", "value": "按需 · 今天的 R-R" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à la demande · R-R du jour" + } } } }, @@ -107457,6 +122163,12 @@ "state": "translated", "value": "選填" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "facultatif" + } } } }, @@ -107491,6 +122203,12 @@ "state": "translated", "value": "滿分 100" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sur 100" + } } } }, @@ -107577,6 +122295,12 @@ "state": "translated", "value": "部分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "partiel" + } } } }, @@ -107617,6 +122341,12 @@ "state": "translated", "value": "峰值 %1$@ · %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pic %1$@ · %2$@" + } } } }, @@ -107651,6 +122381,12 @@ "state": "translated", "value": "正向" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "positif" + } } } }, @@ -107685,6 +122421,12 @@ "state": "translated", "value": "預估 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prévu · %@" + } } } }, @@ -107719,6 +122461,12 @@ "state": "translated", "value": "季" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "trimestre" + } } } }, @@ -107805,6 +122553,12 @@ "state": "translated", "value": "讀取中…" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "lecture…" + } } } }, @@ -107839,6 +122593,12 @@ "state": "translated", "value": "就緒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prêt" + } } } }, @@ -107873,6 +122633,12 @@ "state": "translated", "value": "恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "récupération" + } } } }, @@ -107907,6 +122673,12 @@ "state": "translated", "value": "累計結餘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "solde courant" + } } } }, @@ -107941,6 +122713,12 @@ "state": "translated", "value": "秒" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sec" + } } } }, @@ -107975,6 +122753,12 @@ "state": "translated", "value": "並排 · 每次讀數之前的 %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "côte à côte · %@ avant chaque mesure" + } } } }, @@ -108009,6 +122793,12 @@ "state": "translated", "value": "睡眠負債" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "dette de sommeil" + } } } }, @@ -108043,6 +122833,12 @@ "state": "translated", "value": "睡眠時長" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "durée du sommeil" + } } } }, @@ -108077,6 +122873,12 @@ "state": "translated", "value": "輕微" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "léger" + } } } }, @@ -108111,6 +122913,12 @@ "state": "translated", "value": "小" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "petit" + } } } }, @@ -108145,6 +122953,12 @@ "state": "translated", "value": "穩固" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "solide" + } } } }, @@ -108179,6 +122993,12 @@ "state": "translated", "value": "過時 · %@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "obsolète · %@" + } } } }, @@ -108213,6 +123033,12 @@ "state": "translated", "value": "穩定" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "stable" + } } } }, @@ -108247,6 +123073,12 @@ "state": "translated", "value": "步 / 運動單位" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas / unité de mouvement" + } } } }, @@ -108281,6 +123113,12 @@ "state": "translated", "value": "每運動單位的步數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pas par unité de mouvement" + } } } }, @@ -108315,6 +123153,12 @@ "state": "translated", "value": "強度(0–21)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "effort (0–21)" + } } } }, @@ -108349,6 +123193,12 @@ "state": "translated", "value": "強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fort" + } } } }, @@ -108383,6 +123233,12 @@ "state": "translated", "value": "盈餘" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "surplus" + } } } }, @@ -108417,6 +123273,12 @@ "state": "translated", "value": "點擊或拖曳以查看實際數值" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "appuyez ou glissez pour les valeurs réelles" + } } } }, @@ -108451,6 +123313,12 @@ "state": "translated", "value": "目標" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "objectif" + } } } }, @@ -108485,6 +123353,12 @@ "state": "translated", "value": "該訊號" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ce signal" + } } } }, @@ -108519,6 +123393,12 @@ "state": "translated", "value": "過去 %lld 晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "les %lld dernières nuits" + } } } }, @@ -108553,6 +123433,12 @@ "state": "translated", "value": "過去 7 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "les 7 derniers jours" + } } } }, @@ -108587,6 +123473,12 @@ "state": "translated", "value": "昨晚" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "la dernière nuit" + } } } }, @@ -108621,6 +123513,12 @@ "state": "translated", "value": "本%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ce %@" + } } } }, @@ -108655,6 +123553,12 @@ "state": "translated", "value": "這台 Mac" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ce Mac" + } } } }, @@ -108689,6 +123593,12 @@ "state": "translated", "value": "此裝置" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cet appareil" + } } } }, @@ -108775,6 +123685,12 @@ "state": "translated", "value": "至基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "à la référence" + } } } }, @@ -108809,6 +123725,12 @@ "state": "translated", "value": "今天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aujourd'hui" + } } } }, @@ -108843,6 +123765,12 @@ "state": "translated", "value": "一般" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "typique" + } } } }, @@ -108877,6 +123805,12 @@ "state": "translated", "value": "不到一小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moins d'une heure" + } } } }, @@ -108911,6 +123845,12 @@ "state": "translated", "value": "未評分" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "non évalué" + } } } }, @@ -108945,6 +123885,12 @@ "state": "translated", "value": "未動用的天數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "jours intacts" + } } } }, @@ -108979,6 +123925,12 @@ "state": "translated", "value": "上升 %lld 點" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en hausse de %lld points" + } } } }, @@ -109013,6 +123965,12 @@ "state": "translated", "value": "上升 1 點" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "en hausse de 1 point" + } } } }, @@ -109047,6 +124005,12 @@ "state": "translated", "value": "已使用" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "utilisé" + } } } }, @@ -109133,6 +124097,12 @@ "state": "translated", "value": "變異指數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "indice de variation" + } } } }, @@ -109167,6 +124137,12 @@ "state": "translated", "value": "非常強" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "très fort" + } } } }, @@ -109201,6 +124177,12 @@ "state": "translated", "value": "對比 30 天基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs référence sur 30 jours" + } } } }, @@ -109235,6 +124217,12 @@ "state": "translated", "value": "對比 WHOOP" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs WHOOP" + } } } }, @@ -109269,6 +124257,12 @@ "state": "translated", "value": "對比 %lld 歲" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs âge %lld" + } } } }, @@ -109303,6 +124297,12 @@ "state": "translated", "value": "對比無行為基線" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs référence sans comportement" + } } } }, @@ -109337,6 +124337,12 @@ "state": "translated", "value": "對比需求" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs besoin" + } } } }, @@ -109371,6 +124377,12 @@ "state": "translated", "value": "對比前一%@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs %@ préc." + } } } }, @@ -109405,6 +124417,12 @@ "state": "translated", "value": "對比一般水準" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs typique" + } } } }, @@ -109439,6 +124457,12 @@ "state": "translated", "value": "對比一般水準," } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vs typique —" + } } } }, @@ -109473,6 +124497,12 @@ "state": "translated", "value": "高於基線溫度,限制恢復" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "plus chaude que la référence, limite la récupération" + } } } }, @@ -109507,6 +124537,12 @@ "state": "translated", "value": "弱" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "faible" + } } } }, @@ -109541,6 +124577,12 @@ "state": "translated", "value": "週" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "semaine" + } } } }, @@ -109575,6 +124617,12 @@ "state": "translated", "value": "你要記錄什麼?" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "que voulez-vous enregistrer ?" + } } } }, @@ -109609,6 +124657,12 @@ "state": "translated", "value": "年" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "année" + } } } }, @@ -109643,6 +124697,12 @@ "state": "translated", "value": "你的資料" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vos données" + } } } }, @@ -109677,6 +124737,12 @@ "state": "translated", "value": "你的數字、日期和任何備註" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "votre chiffre, la date et une éventuelle note" + } } } }, @@ -109711,6 +124777,12 @@ "state": "translated", "value": "你隨時間變化的讀數" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vos mesures dans le temps" + } } } }, @@ -109751,6 +124823,12 @@ "state": "translated", "value": "區間 %1$lld 百分之 %2$lld" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "zone %1$lld %2$lld pour cent" + } } } }, @@ -109785,6 +124863,12 @@ "state": "translated", "value": "約剩 %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "~%lld jours restants" + } } } }, @@ -109819,6 +124903,12 @@ "state": "translated", "value": "約剩 %lld 小時" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "~%lldh restantes" + } } } }, @@ -109853,6 +124943,12 @@ "state": "translated", "value": "約剩 1 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "~1 jour restant" + } } } }, @@ -109887,6 +124983,12 @@ "state": "translated", "value": "°C" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "°C" + } } } }, @@ -109921,6 +125023,12 @@ "state": "translated", "value": "°F" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "°F" + } } } }, @@ -109955,6 +125063,12 @@ "state": "translated", "value": "± %lld 歲 · 一種體能比較,而非生物學年齡" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "± %lld ans · une comparaison de forme physique, pas un âge biologique" + } } } }, @@ -109995,6 +125109,12 @@ "state": "translated", "value": "± %1$lld · %2$@" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "± %1$lld · %2$@" + } } } }, @@ -110133,6 +125253,12 @@ "state": "translated", "value": "· GPS" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "· GPS" + } } } }, @@ -110167,6 +125293,12 @@ "state": "translated", "value": "· 約第 %lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "· ~jour %lld" + } } } }, @@ -110207,6 +125339,12 @@ "state": "translated", "value": "· 約第 %lld–%lld 天" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "· ~jour %1$lld–%2$lld" + } } } }, @@ -110241,6 +125379,12 @@ "state": "translated", "value": "儲存時 × %@。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "× %@ à l'enregistrement." + } } } }, @@ -110275,6 +125419,12 @@ "state": "translated", "value": "Δ" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Δ" + } } } }, @@ -110361,6 +125511,12 @@ "state": "translated", "value": "–" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "–" + } } } }, @@ -110447,6 +125603,12 @@ "state": "translated", "value": "這裡的「劑量」指的是時間(越晚 = 越強),而非毫克數。" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ici, la « dose » est une question de moment (plus tard dans la journée = plus fort), pas de milligrammes." + } } } }, @@ -110533,6 +125695,12 @@ "state": "translated", "value": "≈ %@ 步(自動)" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "≈ %@ pas (auto)" + } } } }, @@ -110567,6 +125735,12 @@ "state": "translated", "value": "此設定下 ≈ %@ 步" } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "≈ %@ pas avec ce réglage" + } } } }, diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..177f74ff6 --- /dev/null +++ b/android/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,76 @@ + + + + NOOP + Repos, Charge et Effort du jour, avec la fréquence cardiaque en direct et la batterie du bracelet en un coup d\'œil. + Le widget NOOP n\'a pas pu s\'afficher — il se rétablira à la prochaine mise à jour. + + + Aujourd\'hui + Intelligence + Vue couplée + En direct + Intervalles + Sommeil + Respirer + Stress + Entraînements + Tendances + Coach + Ce qui vous fait évoluer + Perspectives + Explorer + Comparer + Santé + Hydratation + Signes vitaux + Carnet de labo + Rythme + Apple Health + Automatisations + Alarmes + Appareils + Sources de données + Sauvegarde et synchro + Vos données, réunies + Notifications + Assistance + Paramètres + Centre de test + Plus + + + Perspectives + Corps + Données + Application + + + FC en direct + Démarrer l\'entraînement + Consigner au journal + Respirer + + + Avant d\'utiliser NOOP + Veuillez lire et accepter les points ci-dessous. + Indépendant : non affilié à WHOOP + NOOP est un projet non officiel : non affilié à, approuvé ou sponsorisé par WHOOP, Inc. « WHOOP » est leur marque déposée, utilisée uniquement pour désigner le matériel avec lequel NOOP fonctionne. + Utiliser NOOP peut enfreindre les conditions d\'utilisation de WHOOP + Utilisez-le uniquement avec un appareil que vous possédez, pour lire vos propres données. La décision de l\'utiliser (et tout effet sur votre compte WHOOP, votre abonnement, votre appareil ou votre garantie) vous appartient, à vos seuls risques. + Expérimental : à vos risques et périls + NOOP communique avec le micrologiciel de votre bracelet via un protocole non officiel, cartographié de manière indépendante. Il subsiste un risque résiduel pour l\'appareil, ses données et sa connexion aux services officiels. Vous assumez ce risque. + Pas un dispositif médical, pas un avis médical + Chaque métrique est une approximation non validée. N\'utilisez pas NOOP pour diagnostiquer, traiter ou prendre une décision de santé. Consultez toujours un professionnel qualifié. + Aucune garantie ; responsabilité limitée + NOOP est gratuit et fourni « tel quel », sans garantie. La responsabilité est limitée dans la mesure maximale permise par la loi qui vous est applicable, et rien ici ne supprime les protections que votre législation locale ne nous permet pas de supprimer. + Les conditions complètes se trouvent dans TERMS.md, fourni avec NOOP. Ceci ne constitue pas un avis juridique. + J\'ai lu et j\'accepte ces conditions, et j\'utilise NOOP avec mon propre appareil et mes propres données, à mes propres risques. + Accepter et continuer + From c39f67f9e1fc11bbf9ae9bc8b8f5752464417979 Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:02:41 +0200 Subject: [PATCH 03/28] Bump to 8.2.1 for fork's first release build Version bump only, to trigger a clean CI build/publish of the update- redirect and French-localization changes for MatB57/noop, and to give the family's devices a distinct version to update to. Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1864db87a..56845013d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 259 - versionName = "8.2.0" + versionCode = 260 + versionName = "8.2.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index 4247a20e9..11ef26c7a 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.0" + MARKETING_VERSION: "8.2.1" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "170" + CURRENT_PROJECT_VERSION: "171" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From 9834144f1bd168afacb7e11582b7013936624270 Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:21:16 +0200 Subject: [PATCH 04/28] Fix bug-report URL tests after retargeting to this fork TestReportLinkTest.kt and TestReportLinkTests.swift still asserted the old ParthJadhav/noop issue URL, which broke Android CI's unit tests after the source was retargeted to MatB57/noop. Co-Authored-By: Claude Sonnet 5 --- StrandTests/TestReportLinkTests.swift | 4 ++-- .../src/test/java/com/noop/testcentre/TestReportLinkTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/StrandTests/TestReportLinkTests.swift b/StrandTests/TestReportLinkTests.swift index 3950f94e2..7b95d72a5 100644 --- a/StrandTests/TestReportLinkTests.swift +++ b/StrandTests/TestReportLinkTests.swift @@ -4,7 +4,7 @@ import StrandAnalytics /// Locks the prefilled new-issue URL (spec section 5.2): it must bind bug_report.yml's existing /// id fields (version/platform/os_version/test_profile/title) and self-apply the "bug,test:" -/// labels, with every component percent-encoded. The repo is ParthJadhav/noop (bug_report.yml line 10). +/// labels, with every component percent-encoded. The repo is MatB57/noop (this fork's own tracker). final class TestReportLinkTests: XCTestCase { func testSleepProfileURLEncodesEveryFieldAndLabel() { @@ -13,7 +13,7 @@ final class TestReportLinkTests: XCTestCase { version: "7.3.0", platform: "iOS", osVersion: "18.5") XCTAssertNotNil(url) let s = url!.absoluteString - XCTAssertTrue(s.hasPrefix("https://github.com/ParthJadhav/noop/issues/new?")) + XCTAssertTrue(s.hasPrefix("https://github.com/MatB57/noop/issues/new?")) XCTAssertTrue(s.contains("template=bug_report.yml")) // Label component: "bug,test:sleep" with the comma percent-encoded. XCTAssertTrue(s.contains("labels=bug%2Ctest:sleep")) diff --git a/android/app/src/test/java/com/noop/testcentre/TestReportLinkTest.kt b/android/app/src/test/java/com/noop/testcentre/TestReportLinkTest.kt index 918bc0072..ca9d31b60 100644 --- a/android/app/src/test/java/com/noop/testcentre/TestReportLinkTest.kt +++ b/android/app/src/test/java/com/noop/testcentre/TestReportLinkTest.kt @@ -18,7 +18,7 @@ class TestReportLinkTest { val s = TestReportLink.reportUrlString( profile = TestDomain.SLEEP, title = "no score last night", version = "7.3.0", platform = "Android", osVersion = "15") - assertTrue(s.startsWith("https://github.com/ParthJadhav/noop/issues/new?")) + assertTrue(s.startsWith("https://github.com/MatB57/noop/issues/new?")) assertTrue(s.contains("template=bug_report.yml")) assertTrue(s.contains("labels=bug%2Ctest:sleep")) assertTrue(s.contains("version=7.3.0")) From 54ff3b327179a4c91a0ed7541d1779dd34d654a7 Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:25:57 +0200 Subject: [PATCH 05/28] Publish v8.2.1 in the AltStore source The first real release built and published from this fork. Prepends the 8.2.1 entry (keeps 8.2.0 for AltStore's version history) and fixes the remaining ParthJadhav source-attribution string. Co-Authored-By: Claude Sonnet 5 --- altstore-source.json | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/altstore-source.json b/altstore-source.json index 10f09caf8..caca3baec 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -11,12 +11,21 @@ "bundleIdentifier": "com.noopapp.noop", "developerName": "NoopApp", "subtitle": "Read your own WHOOP strap, fully offline.", - "localizedDescription": "NOOP talks to a WHOOP 4.0 / 5.0 / MG strap you own directly over Bluetooth — no WHOOP app, no subscription, no cloud. Live heart rate, history sync, and NOOP's own Charge / Effort / Rest scores, all computed on-device and stored only on your iPhone.\n\nIndependent and experimental; not affiliated with WHOOP. Source: https://github.com/ParthJadhav/noop", + "localizedDescription": "NOOP talks to a WHOOP 4.0 / 5.0 / MG strap you own directly over Bluetooth — no WHOOP app, no subscription, no cloud. Live heart rate, history sync, and NOOP's own Charge / Effort / Rest scores, all computed on-device and stored only on your iPhone.\n\nIndependent and experimental; not affiliated with WHOOP. Source: https://github.com/MatB57/noop", "iconURL": "https://github.com/MatB57/noop/raw/main/Strand/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", "tintColor": "44E2B0", "category": "healthcare-fitness", "screenshotURLs": [], "versions": [ + { + "version": "8.2.1", + "buildVersion": "171", + "date": "2026-08-12", + "localizedDescription": "NOOP 8.2.1. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.1/NOOP-v8.2.1-ios.ipa", + "size": 14162393, + "minOSVersion": "17.0" + }, { "version": "8.2.0", "buildVersion": "170", @@ -37,12 +46,12 @@ "NSHealthUpdateUsageDescription": "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." } }, - "version": "8.2.0", - "buildVersion": "170", - "versionDate": "2026-07-05", - "versionDescription": "NOOP 8.2.0. See the release notes for what changed.", - "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.0/NOOP-v8.2.0-ios.ipa", - "size": 14027253, + "version": "8.2.1", + "buildVersion": "171", + "versionDate": "2026-08-12", + "versionDescription": "NOOP 8.2.1. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.1/NOOP-v8.2.1-ios.ipa", + "size": 14162393, "minOSVersion": "17.0" } ], From cd44ac36f2774d40ed5a76b555736e488ec9c9d6 Mon Sep 17 00:00:00 2001 From: Matthias <207672070+noaetmatthias@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:45:51 +0200 Subject: [PATCH 06/28] Bump to 8.2.2 (test release, no other changes) Version-only bump to verify the in-app "Check for updates" flow works end-to-end for both a currently-installed app and a freshly-checking one. Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 56845013d..49d4da898 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 260 - versionName = "8.2.1" + versionCode = 261 + versionName = "8.2.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index 11ef26c7a..a9901fa86 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.1" + MARKETING_VERSION: "8.2.2" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "171" + CURRENT_PROJECT_VERSION: "172" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From 5fe0e9be0c1f45cf94e5ae8a260efbd00de832fd Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:47:57 +0200 Subject: [PATCH 07/28] Android: prompt for the Doze battery-optimisation exemption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AndroidDiagnostics already flags "NOT exempt from battery optimisation" as the #1 cause of missed overnight background BLE, but nothing ever asked the user to fix it. Without the exemption Android freezes the foreground service during Doze: the strap link drops overnight, no R-R/HRV is banked, and the recovery baseline never leaves "calibrating" — on a WHOOP 4 or 5. - Add REQUEST_IGNORE_BATTERY_OPTIMIZATIONS to the manifest (Play permits it for a continuous companion-device link). - New BatteryOptimization helper: isExempt() + the request / settings intents. - Settings → "Keep connected in the background": an amber warning row with an "Allow" button, shown only while background connection is on and the exemption isn't granted. Android-only; no iOS/macOS counterpart. Fixes the likely root cause of "calibrating for weeks" on Android. Co-Authored-By: Claude Sonnet 5 --- android/app/src/main/AndroidManifest.xml | 4 ++ .../java/com/noop/ble/BatteryOptimization.kt | 43 ++++++++++++++++ .../main/java/com/noop/ui/SettingsScreen.kt | 49 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 android/app/src/main/java/com/noop/ble/BatteryOptimization.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a5f21097d..f0038b1ba 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,6 +45,10 @@ + + diff --git a/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt b/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt new file mode 100644 index 000000000..76921bd2d --- /dev/null +++ b/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt @@ -0,0 +1,43 @@ +package com.noop.ble + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.PowerManager +import android.provider.Settings + +/** + * Doze / battery-optimisation exemption. + * + * An app that is NOT whitelisted from battery optimisation gets its foreground service frozen + * during Doze, so NOOP's strap link drops overnight, no R-R/HRV is banked, and the recovery + * baseline never leaves "calibrating" — `AndroidDiagnostics` already calls this out as the #1 cause + * of missed overnight background work. Google Play explicitly permits + * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` for an app maintaining a continuous connection to a + * companion device, which is exactly what NOOP does with the strap. + * + * Nothing here runs on its own: the UI checks [isExempt] and, only if the user taps, launches + * [requestIntent] (falling back to [settingsIntent] on OEMs that disable the direct prompt). + * There is no macOS/iOS counterpart — CoreBluetooth background delivery needs no user-granted + * power exemption. + */ +object BatteryOptimization { + + /** True when NOOP is already exempt from battery optimisation (background work allowed). */ + fun isExempt(context: Context): Boolean = runCatching { + val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager + pm?.isIgnoringBatteryOptimizations(context.packageName) == true + }.getOrDefault(false) + + /** + * The per-app system dialog asking the user to let NOOP ignore battery optimisation + * (`ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`). Some OEMs disable this action, so callers + * should catch `ActivityNotFoundException` and fall back to [settingsIntent]. + */ + fun requestIntent(context: Context): Intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + .setData(Uri.parse("package:${context.packageName}")) + + /** The full battery-optimisation list — the fallback when the direct prompt is unavailable. */ + fun settingsIntent(): Intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) +} diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index f9d2a018b..18e2597b2 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.IosShare import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MenuBook @@ -93,6 +94,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.noop.BuildConfig import com.noop.analytics.Baselines import com.noop.analytics.Zones +import com.noop.ble.BatteryOptimization import com.noop.ble.PuffinExperiment import com.noop.ble.WhoopModel import com.noop.data.DataBackup @@ -1096,6 +1098,53 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } + // Doze / battery-optimisation exemption. Without it Android freezes the foreground + // service overnight — the strap link drops, no overnight R-R/HRV is banked, and Charge + // can sit on "calibrating" forever. Only shown while background connection is on and the + // exemption isn't already granted. No iOS/macOS counterpart. + if (backgroundConnection && !BatteryOptimization.isExempt(context)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Palette.statusWarning.copy(alpha = 0.12f)) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + Icons.Filled.Warning, + contentDescription = null, + tint = Palette.statusWarning, + modifier = Modifier.size(22.dp), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + "Allow background running", + style = NoopType.subhead, + color = Palette.textPrimary, + ) + Text( + "Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected.", + style = NoopType.footnote, + color = Palette.textTertiary, + ) + } + Button( + onClick = { + runCatching { context.startActivity(BatteryOptimization.requestIntent(context)) } + .onFailure { + runCatching { context.startActivity(BatteryOptimization.settingsIntent()) } + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = Palette.accent, + contentColor = Palette.surfaceBase, + ), + ) { Text("Allow") } + } + } + // Continuous HRV capture: keep the dense beat-to-beat (R-R) stream armed even with no Live // screen open, so the strap banks far more data overnight for better HRV/recovery/sleep. // Honest battery framing — continuous HR streaming uses more battery. Needs background From 2f708bcf12d26654fefb6508aa451c36f6cc15e4 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:01:01 +0200 Subject: [PATCH 08/28] Android: in-app APK update with a launch-time prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sideloaded APK had no real update path — "Check for updates" only opened the GitHub release page in a browser, which a non-collaborator (e.g. a parent on a shared build) can't act on easily. - UpdateCheck now also returns the release's .apk asset URL + size. - InAppUpdate: download the APK into cache/updates/, hand it to the system package installer via the existing FileProvider; routes through the per-app "install unknown apps" grant on first use; browser fallback on any failure. - UpdateGate: a throttled (<=1/6h) launch check from NoopRoot that pops a dialog when a newer version is available, with one-tap "Update" (download + install) or "Later" (snoozes that version). Onboarded users only. - Settings "Check for updates" now installs in-app too instead of opening a browser. - Manifest: REQUEST_INSTALL_PACKAGES. file_paths: cache/updates/. Android-only; iOS gets one-tap updates from its AltStore/SideStore source. Co-Authored-By: Claude Sonnet 5 --- android/app/src/main/AndroidManifest.xml | 2 + .../src/main/java/com/noop/ui/MainActivity.kt | 12 ++ .../main/java/com/noop/ui/SettingsScreen.kt | 66 ++++++++- .../src/main/java/com/noop/ui/UpdateGate.kt | 140 ++++++++++++++++++ .../main/java/com/noop/update/InAppUpdate.kt | 123 +++++++++++++++ .../main/java/com/noop/update/UpdateCheck.kt | 43 +++++- android/app/src/main/res/xml/file_paths.xml | 4 +- 7 files changed, 374 insertions(+), 16 deletions(-) create mode 100644 android/app/src/main/java/com/noop/ui/UpdateGate.kt create mode 100644 android/app/src/main/java/com/noop/update/InAppUpdate.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f0038b1ba..4997c2ddb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,6 +49,8 @@ service overnight, the strap link drops, no overnight R-R/HRV is banked, and the recovery baseline never leaves "calibrating". Play permits this for a continuous companion-device link. --> + + diff --git a/android/app/src/main/java/com/noop/ui/MainActivity.kt b/android/app/src/main/java/com/noop/ui/MainActivity.kt index ac47757ed..552996c32 100644 --- a/android/app/src/main/java/com/noop/ui/MainActivity.kt +++ b/android/app/src/main/java/com/noop/ui/MainActivity.kt @@ -199,6 +199,14 @@ object NoopPrefs { const val KEY_ANALYZE_WATERMARK = "noop.analyzeWatermark" + /** Wall-clock ms of the last launch-time update check ([com.noop.ui.UpdateGate]), so the releases + * API is hit at most once per ~6 h across launches. 0 / absent = never checked. */ + const val KEY_LAST_UPDATE_CHECK_MS = "noop.lastUpdateCheckMs" + + /** Version string the user tapped "Later" on in the launch update prompt, so the same version + * isn't re-prompted every launch. The Settings "Check for updates" button ignores this. */ + const val KEY_UPDATE_SNOOZED_VERSION = "noop.updateSnoozedVersion" + fun of(context: Context): SharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE) @@ -756,6 +764,10 @@ fun NoopRoot() { // (stored version behind current), show "What's New" once over the top. AppRoot(viewModel = appViewModel) + // Sideloaded-APK update prompt: throttled launch check against the public GitHub release, with a + // one-tap in-app download + install. Only for onboarded users (past Terms + onboarding gates). + UpdateGate() + if (lastSeenChangelog != AppChangelog.CURRENT_VERSION) { Dialog( onDismissRequest = { diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 18e2597b2..bd5ea2ef4 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -61,6 +61,7 @@ import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField @@ -100,6 +101,7 @@ import com.noop.ble.WhoopModel import com.noop.data.DataBackup import com.noop.ingest.RawSensorExport import com.noop.ingest.WhoopCsvExporter +import com.noop.update.InAppUpdate import com.noop.update.UpdateCheck import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -2069,6 +2071,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // is sent. Android already holds INTERNET (for the opt-in Coach), so this adds nothing. var updChecking by remember { mutableStateOf(false) } var updResult by remember { mutableStateOf(null) } + // Non-null while an in-app APK download is running (0..1), so the card shows progress + // instead of the Download button. + var updDownloadPct by remember { mutableStateOf(null) } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row( verticalAlignment = Alignment.CenterVertically, @@ -2131,13 +2136,60 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), ) - NoopButton( - text = "Download", - leadingIcon = Icons.Filled.Download, - kind = NoopButtonKind.Primary, - onClick = { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(avail.url))) - }, + val pct = updDownloadPct + if (pct != null) { + Text( + "Downloading… ${(pct * 100).toInt()}%", + style = NoopType.footnote, color = Palette.textSecondary, + ) + } else { + NoopButton( + text = "Update", + leadingIcon = Icons.Filled.Download, + kind = NoopButtonKind.Primary, + onClick = { + val apk = avail.apkUrl + when { + apk == null -> + InAppUpdate.openReleasePage(context, avail.url) + !InAppUpdate.canInstall(context) -> { + InAppUpdate.requestInstallPermission(context) + Toast.makeText( + context, + "Allow installing apps, then tap Update again.", + Toast.LENGTH_LONG, + ).show() + } + else -> { + updDownloadPct = 0f + scope.launch { + runCatching { + InAppUpdate.download(context, avail.version, apk) { + updDownloadPct = it + } + }.onSuccess { file -> + updDownloadPct = null + InAppUpdate.install(context, file) + }.onFailure { + updDownloadPct = null + Toast.makeText( + context, + "Download failed — opening the release page.", + Toast.LENGTH_LONG, + ).show() + InAppUpdate.openReleasePage(context, avail.url) + } + } + } + } + }, + ) + } + } + updDownloadPct?.let { p -> + LinearProgressIndicator( + progress = p, + modifier = Modifier.fillMaxWidth(), ) } if (avail.notes.isNotEmpty()) { diff --git a/android/app/src/main/java/com/noop/ui/UpdateGate.kt b/android/app/src/main/java/com/noop/ui/UpdateGate.kt new file mode 100644 index 000000000..297c227b5 --- /dev/null +++ b/android/app/src/main/java/com/noop/ui/UpdateGate.kt @@ -0,0 +1,140 @@ +package com.noop.ui + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.noop.BuildConfig +import com.noop.update.InAppUpdate +import com.noop.update.UpdateCheck +import kotlinx.coroutines.launch + +/** Don't hit the releases API more than once per this window across launches. */ +private const val UPDATE_CHECK_INTERVAL_MS = 6L * 60 * 60 * 1000 // 6 h + +private sealed interface UpdateGateState { + data object Hidden : UpdateGateState + data class Prompt(val info: UpdateCheck.Result.Available) : UpdateGateState + data class Downloading(val info: UpdateCheck.Result.Available, val pct: Float) : UpdateGateState +} + +/** + * Launch-time update gate for the sideloaded Android build. + * + * On a throttled schedule it asks the public GitHub release API for the latest version; if it's + * newer than the running build AND the user hasn't tapped "Later" on that exact version, it pops a + * dialog offering a one-tap update — download the APK into cache, then hand it to the system + * installer. Rendered once, over the live app, from [NoopRoot]. iOS gets equivalent one-tap updates + * from its AltStore/SideStore source, so there is no counterpart there. + */ +@Composable +fun UpdateGate() { + val context = LocalContext.current + val prefs = remember { NoopPrefs.of(context) } + val scope = rememberCoroutineScope() + var state by remember { mutableStateOf(UpdateGateState.Hidden) } + + LaunchedEffect(Unit) { + val last = prefs.getLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, 0L) + if (System.currentTimeMillis() - last < UPDATE_CHECK_INTERVAL_MS) return@LaunchedEffect + val result = UpdateCheck.check(BuildConfig.VERSION_NAME) + prefs.edit().putLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, System.currentTimeMillis()).apply() + if (result is UpdateCheck.Result.Available && result.apkUrl != null) { + val snoozed = prefs.getString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, "") ?: "" + if (result.version != snoozed) state = UpdateGateState.Prompt(result) + } + } + + when (val s = state) { + UpdateGateState.Hidden -> Unit + + is UpdateGateState.Prompt -> AlertDialog( + onDismissRequest = { state = UpdateGateState.Hidden }, + title = { Text("NOOP ${s.info.version} is available") }, + text = { + val body = buildString { + if (s.info.notes.isNotBlank()) append(s.info.notes.take(280).trim()) + if (s.info.apkSize > 0) { + if (isNotEmpty()) append("\n\n") + append("Download ${s.info.apkSize / (1024 * 1024)} MB") + } + } + Text(if (body.isBlank()) "A newer version is ready to install." else body) + }, + confirmButton = { + TextButton(onClick = { + val info = s.info + val apk = info.apkUrl ?: return@TextButton + if (!InAppUpdate.canInstall(context)) { + InAppUpdate.requestInstallPermission(context) + Toast.makeText( + context, + "Allow installing apps, then reopen NOOP to update.", + Toast.LENGTH_LONG, + ).show() + state = UpdateGateState.Hidden + return@TextButton + } + state = UpdateGateState.Downloading(info, 0f) + scope.launch { + runCatching { + InAppUpdate.download(context, info.version, apk) { p -> + state = UpdateGateState.Downloading(info, p) + } + }.onSuccess { file -> + state = UpdateGateState.Hidden + InAppUpdate.install(context, file) + }.onFailure { + state = UpdateGateState.Hidden + Toast.makeText( + context, + "Update download failed — opening the release page.", + Toast.LENGTH_LONG, + ).show() + InAppUpdate.openReleasePage(context, info.url) + } + } + }) { Text("Update") } + }, + dismissButton = { + TextButton(onClick = { + prefs.edit() + .putString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, s.info.version) + .apply() + state = UpdateGateState.Hidden + }) { Text("Later") } + }, + ) + + is UpdateGateState.Downloading -> AlertDialog( + onDismissRequest = { }, + title = { Text("Updating to ${s.info.version}") }, + text = { + Column { + Text("Downloading… ${(s.pct * 100).toInt()}%") + Spacer(Modifier.height(12.dp)) + LinearProgressIndicator( + progress = s.pct, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + confirmButton = {}, + ) + } +} diff --git a/android/app/src/main/java/com/noop/update/InAppUpdate.kt b/android/app/src/main/java/com/noop/update/InAppUpdate.kt new file mode 100644 index 000000000..b5bf32623 --- /dev/null +++ b/android/app/src/main/java/com/noop/update/InAppUpdate.kt @@ -0,0 +1,123 @@ +package com.noop.update + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.core.content.FileProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.net.HttpURLConnection +import java.net.URL + +/** + * In-app APK update for the sideloaded Android build. + * + * NOOP ships as a sideloaded APK (no Play Store), so there is no automatic update path. This does the + * minimum to make one non-technical-user-proof: download the release APK into the app's cache and + * hand it to the system package installer, so the user taps "Update" once instead of hunting a + * GitHub asset in a browser. Nothing is sent — the APK comes straight from the public GitHub + * release. The launch-time check + prompt lives in [com.noop.ui.UpdateGate]; this is just plumbing. + * + * No iOS/macOS counterpart: on iOS the AltStore/SideStore source already delivers one-tap updates, + * and macOS is a drag-to-Applications zip. + */ +object InAppUpdate { + + /** True once the user has allowed NOOP to install packages (the API 26+ per-app gate). */ + fun canInstall(context: Context): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.O || + context.packageManager.canRequestPackageInstalls() + + /** Send the user to the per-app "install unknown apps" screen. They grant once, then retry. */ + fun requestInstallPermission(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + runCatching { + context.startActivity( + Intent( + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:${context.packageName}"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + } + + /** App-private dir the downloaded APKs live in (covered by the FileProvider `updates/` path). */ + private fun updatesDir(context: Context): File = + File(context.cacheDir, "updates").apply { mkdirs() } + + /** + * Download [apkUrl] to `cache/updates/NOOP-v.apk`, reporting 0..1 progress. Any stale + * APKs in the dir are cleared first. Runs on IO; returns the file or throws (the caller catches + * and falls back to the release page). + */ + suspend fun download( + context: Context, + version: String, + apkUrl: String, + onProgress: (Float) -> Unit, + ): File = withContext(Dispatchers.IO) { + val dir = updatesDir(context) + dir.listFiles()?.forEach { it.delete() } + val out = File(dir, "NOOP-v$version.apk") + + var conn = URL(apkUrl).openConnection() as HttpURLConnection + conn.instanceFollowRedirects = true + conn.connectTimeout = 20_000 + conn.readTimeout = 60_000 + // GitHub asset URLs 302 to a signed CDN URL; HttpURLConnection won't cross http<->https on + // its own, so follow one hop by hand if the first response is a redirect. + if (conn.responseCode in 300..399) { + val loc = conn.getHeaderField("Location") + conn.disconnect() + conn = (URL(loc).openConnection() as HttpURLConnection).apply { + instanceFollowRedirects = true + connectTimeout = 20_000 + readTimeout = 60_000 + } + } + try { + if (conn.responseCode !in 200..299) error("HTTP ${conn.responseCode}") + val total = conn.contentLengthLong.takeIf { it > 0 } + conn.inputStream.use { input -> + out.outputStream().use { output -> + val buf = ByteArray(64 * 1024) + var read = 0L + while (true) { + val n = input.read(buf) + if (n < 0) break + output.write(buf, 0, n) + read += n + if (total != null) onProgress((read.toFloat() / total).coerceIn(0f, 1f)) + } + } + } + } finally { + conn.disconnect() + } + onProgress(1f) + out + } + + /** Hand [apk] to the system package installer (the normal "Update NOOP?" screen). */ + fun install(context: Context, apk: File) { + val uri: Uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apk) + context.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "application/vnd.android.package-archive") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) + }, + ) + } + + /** Open the release page in a browser — the fallback when there's no APK asset or install fails. */ + fun openReleasePage(context: Context, url: String) { + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + } +} diff --git a/android/app/src/main/java/com/noop/update/UpdateCheck.kt b/android/app/src/main/java/com/noop/update/UpdateCheck.kt index f2e56af5b..964030434 100644 --- a/android/app/src/main/java/com/noop/update/UpdateCheck.kt +++ b/android/app/src/main/java/com/noop/update/UpdateCheck.kt @@ -7,11 +7,11 @@ import java.net.HttpURLConnection import java.net.URL /** - * User-initiated "Check for updates": a single call to the project's PUBLIC releases API (GitHub) that reads the - * latest version and compares it to the installed one. It runs ONLY when the user taps the button — - * there is no background polling and no auto-update. Nothing about the user is sent; it just reads a - * version number. (Android already holds INTERNET for the opt-in AI Coach, so this adds no new - * capability.) + * Update check against the project's PUBLIC releases API (GitHub): reads the latest version and + * compares it to the installed one. Called both from the Settings "Check for updates" button and, + * throttled, at launch by [com.noop.ui.UpdateGate] to prompt a one-tap in-app APK update. Nothing + * about the user is sent; it just reads a version number and the release's APK asset URL. + * (Android already holds INTERNET for the opt-in AI Coach, so this adds no new capability.) */ object UpdateCheck { @@ -19,7 +19,20 @@ object UpdateCheck { sealed interface Result { data class UpToDate(val version: String) : Result - data class Available(val version: String, val url: String, val notes: String) : Result + + /** + * A newer release is available. [url] is the release page (browser fallback); [apkUrl] is the + * direct `.apk` asset for an in-app download+install, or null when the release has no APK + * attached (then only the browser path is offered). [apkSize] is bytes, 0 if unknown. + */ + data class Available( + val version: String, + val url: String, + val notes: String, + val apkUrl: String? = null, + val apkSize: Long = 0L, + ) : Result + object Failed : Result } @@ -39,8 +52,22 @@ object UpdateCheck { val latest = json.getString("tag_name").removePrefix("v") val url = json.getString("html_url") val notes = cleanNotes(json.optString("body", "")) - if (isNewer(latest, currentVersion)) Result.Available(latest, url, notes) - else Result.UpToDate(latest) + if (!isNewer(latest, currentVersion)) return@runCatching Result.UpToDate(latest) + + // First `.apk` asset on the release → the in-app download target. + var apkUrl: String? = null + var apkSize = 0L + json.optJSONArray("assets")?.let { assets -> + for (i in 0 until assets.length()) { + val a = assets.optJSONObject(i) ?: continue + if (a.optString("name").endsWith(".apk", ignoreCase = true)) { + apkUrl = a.optString("browser_download_url").ifBlank { null } + apkSize = a.optLong("size", 0L) + break + } + } + } + Result.Available(latest, url, notes, apkUrl, apkSize) } finally { conn.disconnect() } diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml index 3c7ac5b02..d79f095ed 100644 --- a/android/app/src/main/res/xml/file_paths.xml +++ b/android/app/src/main/res/xml/file_paths.xml @@ -1,8 +1,10 @@ + - reports/ : the shareable offline trends report PDF (TrendsReport, #436) + - updates/ : the downloaded release APK handed to the system installer (InAppUpdate) --> + From 8c0323c64d0d808f6a8d08ecb93996ce53c74d6e Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:04:51 +0200 Subject: [PATCH 09/28] Revert "Android: in-app APK update with a launch-time prompt" This reverts commit 2f708bcf12d26654fefb6508aa451c36f6cc15e4. --- android/app/src/main/AndroidManifest.xml | 2 - .../src/main/java/com/noop/ui/MainActivity.kt | 12 -- .../main/java/com/noop/ui/SettingsScreen.kt | 66 +-------- .../src/main/java/com/noop/ui/UpdateGate.kt | 140 ------------------ .../main/java/com/noop/update/InAppUpdate.kt | 123 --------------- .../main/java/com/noop/update/UpdateCheck.kt | 43 +----- android/app/src/main/res/xml/file_paths.xml | 4 +- 7 files changed, 16 insertions(+), 374 deletions(-) delete mode 100644 android/app/src/main/java/com/noop/ui/UpdateGate.kt delete mode 100644 android/app/src/main/java/com/noop/update/InAppUpdate.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4997c2ddb..f0038b1ba 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,8 +49,6 @@ service overnight, the strap link drops, no overnight R-R/HRV is banked, and the recovery baseline never leaves "calibrating". Play permits this for a continuous companion-device link. --> - - diff --git a/android/app/src/main/java/com/noop/ui/MainActivity.kt b/android/app/src/main/java/com/noop/ui/MainActivity.kt index 552996c32..ac47757ed 100644 --- a/android/app/src/main/java/com/noop/ui/MainActivity.kt +++ b/android/app/src/main/java/com/noop/ui/MainActivity.kt @@ -199,14 +199,6 @@ object NoopPrefs { const val KEY_ANALYZE_WATERMARK = "noop.analyzeWatermark" - /** Wall-clock ms of the last launch-time update check ([com.noop.ui.UpdateGate]), so the releases - * API is hit at most once per ~6 h across launches. 0 / absent = never checked. */ - const val KEY_LAST_UPDATE_CHECK_MS = "noop.lastUpdateCheckMs" - - /** Version string the user tapped "Later" on in the launch update prompt, so the same version - * isn't re-prompted every launch. The Settings "Check for updates" button ignores this. */ - const val KEY_UPDATE_SNOOZED_VERSION = "noop.updateSnoozedVersion" - fun of(context: Context): SharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE) @@ -764,10 +756,6 @@ fun NoopRoot() { // (stored version behind current), show "What's New" once over the top. AppRoot(viewModel = appViewModel) - // Sideloaded-APK update prompt: throttled launch check against the public GitHub release, with a - // one-tap in-app download + install. Only for onboarded users (past Terms + onboarding gates). - UpdateGate() - if (lastSeenChangelog != AppChangelog.CURRENT_VERSION) { Dialog( onDismissRequest = { diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index bd5ea2ef4..18e2597b2 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -61,7 +61,6 @@ import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField @@ -101,7 +100,6 @@ import com.noop.ble.WhoopModel import com.noop.data.DataBackup import com.noop.ingest.RawSensorExport import com.noop.ingest.WhoopCsvExporter -import com.noop.update.InAppUpdate import com.noop.update.UpdateCheck import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -2071,9 +2069,6 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // is sent. Android already holds INTERNET (for the opt-in Coach), so this adds nothing. var updChecking by remember { mutableStateOf(false) } var updResult by remember { mutableStateOf(null) } - // Non-null while an in-app APK download is running (0..1), so the card shows progress - // instead of the Download button. - var updDownloadPct by remember { mutableStateOf(null) } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row( verticalAlignment = Alignment.CenterVertically, @@ -2136,60 +2131,13 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), ) - val pct = updDownloadPct - if (pct != null) { - Text( - "Downloading… ${(pct * 100).toInt()}%", - style = NoopType.footnote, color = Palette.textSecondary, - ) - } else { - NoopButton( - text = "Update", - leadingIcon = Icons.Filled.Download, - kind = NoopButtonKind.Primary, - onClick = { - val apk = avail.apkUrl - when { - apk == null -> - InAppUpdate.openReleasePage(context, avail.url) - !InAppUpdate.canInstall(context) -> { - InAppUpdate.requestInstallPermission(context) - Toast.makeText( - context, - "Allow installing apps, then tap Update again.", - Toast.LENGTH_LONG, - ).show() - } - else -> { - updDownloadPct = 0f - scope.launch { - runCatching { - InAppUpdate.download(context, avail.version, apk) { - updDownloadPct = it - } - }.onSuccess { file -> - updDownloadPct = null - InAppUpdate.install(context, file) - }.onFailure { - updDownloadPct = null - Toast.makeText( - context, - "Download failed — opening the release page.", - Toast.LENGTH_LONG, - ).show() - InAppUpdate.openReleasePage(context, avail.url) - } - } - } - } - }, - ) - } - } - updDownloadPct?.let { p -> - LinearProgressIndicator( - progress = p, - modifier = Modifier.fillMaxWidth(), + NoopButton( + text = "Download", + leadingIcon = Icons.Filled.Download, + kind = NoopButtonKind.Primary, + onClick = { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(avail.url))) + }, ) } if (avail.notes.isNotEmpty()) { diff --git a/android/app/src/main/java/com/noop/ui/UpdateGate.kt b/android/app/src/main/java/com/noop/ui/UpdateGate.kt deleted file mode 100644 index 297c227b5..000000000 --- a/android/app/src/main/java/com/noop/ui/UpdateGate.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.noop.ui - -import android.widget.Toast -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import com.noop.BuildConfig -import com.noop.update.InAppUpdate -import com.noop.update.UpdateCheck -import kotlinx.coroutines.launch - -/** Don't hit the releases API more than once per this window across launches. */ -private const val UPDATE_CHECK_INTERVAL_MS = 6L * 60 * 60 * 1000 // 6 h - -private sealed interface UpdateGateState { - data object Hidden : UpdateGateState - data class Prompt(val info: UpdateCheck.Result.Available) : UpdateGateState - data class Downloading(val info: UpdateCheck.Result.Available, val pct: Float) : UpdateGateState -} - -/** - * Launch-time update gate for the sideloaded Android build. - * - * On a throttled schedule it asks the public GitHub release API for the latest version; if it's - * newer than the running build AND the user hasn't tapped "Later" on that exact version, it pops a - * dialog offering a one-tap update — download the APK into cache, then hand it to the system - * installer. Rendered once, over the live app, from [NoopRoot]. iOS gets equivalent one-tap updates - * from its AltStore/SideStore source, so there is no counterpart there. - */ -@Composable -fun UpdateGate() { - val context = LocalContext.current - val prefs = remember { NoopPrefs.of(context) } - val scope = rememberCoroutineScope() - var state by remember { mutableStateOf(UpdateGateState.Hidden) } - - LaunchedEffect(Unit) { - val last = prefs.getLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, 0L) - if (System.currentTimeMillis() - last < UPDATE_CHECK_INTERVAL_MS) return@LaunchedEffect - val result = UpdateCheck.check(BuildConfig.VERSION_NAME) - prefs.edit().putLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, System.currentTimeMillis()).apply() - if (result is UpdateCheck.Result.Available && result.apkUrl != null) { - val snoozed = prefs.getString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, "") ?: "" - if (result.version != snoozed) state = UpdateGateState.Prompt(result) - } - } - - when (val s = state) { - UpdateGateState.Hidden -> Unit - - is UpdateGateState.Prompt -> AlertDialog( - onDismissRequest = { state = UpdateGateState.Hidden }, - title = { Text("NOOP ${s.info.version} is available") }, - text = { - val body = buildString { - if (s.info.notes.isNotBlank()) append(s.info.notes.take(280).trim()) - if (s.info.apkSize > 0) { - if (isNotEmpty()) append("\n\n") - append("Download ${s.info.apkSize / (1024 * 1024)} MB") - } - } - Text(if (body.isBlank()) "A newer version is ready to install." else body) - }, - confirmButton = { - TextButton(onClick = { - val info = s.info - val apk = info.apkUrl ?: return@TextButton - if (!InAppUpdate.canInstall(context)) { - InAppUpdate.requestInstallPermission(context) - Toast.makeText( - context, - "Allow installing apps, then reopen NOOP to update.", - Toast.LENGTH_LONG, - ).show() - state = UpdateGateState.Hidden - return@TextButton - } - state = UpdateGateState.Downloading(info, 0f) - scope.launch { - runCatching { - InAppUpdate.download(context, info.version, apk) { p -> - state = UpdateGateState.Downloading(info, p) - } - }.onSuccess { file -> - state = UpdateGateState.Hidden - InAppUpdate.install(context, file) - }.onFailure { - state = UpdateGateState.Hidden - Toast.makeText( - context, - "Update download failed — opening the release page.", - Toast.LENGTH_LONG, - ).show() - InAppUpdate.openReleasePage(context, info.url) - } - } - }) { Text("Update") } - }, - dismissButton = { - TextButton(onClick = { - prefs.edit() - .putString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, s.info.version) - .apply() - state = UpdateGateState.Hidden - }) { Text("Later") } - }, - ) - - is UpdateGateState.Downloading -> AlertDialog( - onDismissRequest = { }, - title = { Text("Updating to ${s.info.version}") }, - text = { - Column { - Text("Downloading… ${(s.pct * 100).toInt()}%") - Spacer(Modifier.height(12.dp)) - LinearProgressIndicator( - progress = s.pct, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - confirmButton = {}, - ) - } -} diff --git a/android/app/src/main/java/com/noop/update/InAppUpdate.kt b/android/app/src/main/java/com/noop/update/InAppUpdate.kt deleted file mode 100644 index b5bf32623..000000000 --- a/android/app/src/main/java/com/noop/update/InAppUpdate.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.noop.update - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.provider.Settings -import androidx.core.content.FileProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.net.HttpURLConnection -import java.net.URL - -/** - * In-app APK update for the sideloaded Android build. - * - * NOOP ships as a sideloaded APK (no Play Store), so there is no automatic update path. This does the - * minimum to make one non-technical-user-proof: download the release APK into the app's cache and - * hand it to the system package installer, so the user taps "Update" once instead of hunting a - * GitHub asset in a browser. Nothing is sent — the APK comes straight from the public GitHub - * release. The launch-time check + prompt lives in [com.noop.ui.UpdateGate]; this is just plumbing. - * - * No iOS/macOS counterpart: on iOS the AltStore/SideStore source already delivers one-tap updates, - * and macOS is a drag-to-Applications zip. - */ -object InAppUpdate { - - /** True once the user has allowed NOOP to install packages (the API 26+ per-app gate). */ - fun canInstall(context: Context): Boolean = - Build.VERSION.SDK_INT < Build.VERSION_CODES.O || - context.packageManager.canRequestPackageInstalls() - - /** Send the user to the per-app "install unknown apps" screen. They grant once, then retry. */ - fun requestInstallPermission(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - runCatching { - context.startActivity( - Intent( - Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - Uri.parse("package:${context.packageName}"), - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) - } - } - - /** App-private dir the downloaded APKs live in (covered by the FileProvider `updates/` path). */ - private fun updatesDir(context: Context): File = - File(context.cacheDir, "updates").apply { mkdirs() } - - /** - * Download [apkUrl] to `cache/updates/NOOP-v.apk`, reporting 0..1 progress. Any stale - * APKs in the dir are cleared first. Runs on IO; returns the file or throws (the caller catches - * and falls back to the release page). - */ - suspend fun download( - context: Context, - version: String, - apkUrl: String, - onProgress: (Float) -> Unit, - ): File = withContext(Dispatchers.IO) { - val dir = updatesDir(context) - dir.listFiles()?.forEach { it.delete() } - val out = File(dir, "NOOP-v$version.apk") - - var conn = URL(apkUrl).openConnection() as HttpURLConnection - conn.instanceFollowRedirects = true - conn.connectTimeout = 20_000 - conn.readTimeout = 60_000 - // GitHub asset URLs 302 to a signed CDN URL; HttpURLConnection won't cross http<->https on - // its own, so follow one hop by hand if the first response is a redirect. - if (conn.responseCode in 300..399) { - val loc = conn.getHeaderField("Location") - conn.disconnect() - conn = (URL(loc).openConnection() as HttpURLConnection).apply { - instanceFollowRedirects = true - connectTimeout = 20_000 - readTimeout = 60_000 - } - } - try { - if (conn.responseCode !in 200..299) error("HTTP ${conn.responseCode}") - val total = conn.contentLengthLong.takeIf { it > 0 } - conn.inputStream.use { input -> - out.outputStream().use { output -> - val buf = ByteArray(64 * 1024) - var read = 0L - while (true) { - val n = input.read(buf) - if (n < 0) break - output.write(buf, 0, n) - read += n - if (total != null) onProgress((read.toFloat() / total).coerceIn(0f, 1f)) - } - } - } - } finally { - conn.disconnect() - } - onProgress(1f) - out - } - - /** Hand [apk] to the system package installer (the normal "Update NOOP?" screen). */ - fun install(context: Context, apk: File) { - val uri: Uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apk) - context.startActivity( - Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, "application/vnd.android.package-archive") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) - }, - ) - } - - /** Open the release page in a browser — the fallback when there's no APK asset or install fails. */ - fun openReleasePage(context: Context, url: String) { - runCatching { - context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) - } - } -} diff --git a/android/app/src/main/java/com/noop/update/UpdateCheck.kt b/android/app/src/main/java/com/noop/update/UpdateCheck.kt index 964030434..f2e56af5b 100644 --- a/android/app/src/main/java/com/noop/update/UpdateCheck.kt +++ b/android/app/src/main/java/com/noop/update/UpdateCheck.kt @@ -7,11 +7,11 @@ import java.net.HttpURLConnection import java.net.URL /** - * Update check against the project's PUBLIC releases API (GitHub): reads the latest version and - * compares it to the installed one. Called both from the Settings "Check for updates" button and, - * throttled, at launch by [com.noop.ui.UpdateGate] to prompt a one-tap in-app APK update. Nothing - * about the user is sent; it just reads a version number and the release's APK asset URL. - * (Android already holds INTERNET for the opt-in AI Coach, so this adds no new capability.) + * User-initiated "Check for updates": a single call to the project's PUBLIC releases API (GitHub) that reads the + * latest version and compares it to the installed one. It runs ONLY when the user taps the button — + * there is no background polling and no auto-update. Nothing about the user is sent; it just reads a + * version number. (Android already holds INTERNET for the opt-in AI Coach, so this adds no new + * capability.) */ object UpdateCheck { @@ -19,20 +19,7 @@ object UpdateCheck { sealed interface Result { data class UpToDate(val version: String) : Result - - /** - * A newer release is available. [url] is the release page (browser fallback); [apkUrl] is the - * direct `.apk` asset for an in-app download+install, or null when the release has no APK - * attached (then only the browser path is offered). [apkSize] is bytes, 0 if unknown. - */ - data class Available( - val version: String, - val url: String, - val notes: String, - val apkUrl: String? = null, - val apkSize: Long = 0L, - ) : Result - + data class Available(val version: String, val url: String, val notes: String) : Result object Failed : Result } @@ -52,22 +39,8 @@ object UpdateCheck { val latest = json.getString("tag_name").removePrefix("v") val url = json.getString("html_url") val notes = cleanNotes(json.optString("body", "")) - if (!isNewer(latest, currentVersion)) return@runCatching Result.UpToDate(latest) - - // First `.apk` asset on the release → the in-app download target. - var apkUrl: String? = null - var apkSize = 0L - json.optJSONArray("assets")?.let { assets -> - for (i in 0 until assets.length()) { - val a = assets.optJSONObject(i) ?: continue - if (a.optString("name").endsWith(".apk", ignoreCase = true)) { - apkUrl = a.optString("browser_download_url").ifBlank { null } - apkSize = a.optLong("size", 0L) - break - } - } - } - Result.Available(latest, url, notes, apkUrl, apkSize) + if (isNewer(latest, currentVersion)) Result.Available(latest, url, notes) + else Result.UpToDate(latest) } finally { conn.disconnect() } diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml index d79f095ed..3c7ac5b02 100644 --- a/android/app/src/main/res/xml/file_paths.xml +++ b/android/app/src/main/res/xml/file_paths.xml @@ -1,10 +1,8 @@ + - reports/ : the shareable offline trends report PDF (TrendsReport, #436) --> - From 69ecdd465461ab1cea250c4d0bfd7719692962d9 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:04:51 +0200 Subject: [PATCH 10/28] Revert "Android: prompt for the Doze battery-optimisation exemption" This reverts commit 5fe0e9be0c1f45cf94e5ae8a260efbd00de832fd. --- android/app/src/main/AndroidManifest.xml | 4 -- .../java/com/noop/ble/BatteryOptimization.kt | 43 ---------------- .../main/java/com/noop/ui/SettingsScreen.kt | 49 ------------------- 3 files changed, 96 deletions(-) delete mode 100644 android/app/src/main/java/com/noop/ble/BatteryOptimization.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f0038b1ba..a5f21097d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,10 +45,6 @@ - - diff --git a/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt b/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt deleted file mode 100644 index 76921bd2d..000000000 --- a/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.noop.ble - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.PowerManager -import android.provider.Settings - -/** - * Doze / battery-optimisation exemption. - * - * An app that is NOT whitelisted from battery optimisation gets its foreground service frozen - * during Doze, so NOOP's strap link drops overnight, no R-R/HRV is banked, and the recovery - * baseline never leaves "calibrating" — `AndroidDiagnostics` already calls this out as the #1 cause - * of missed overnight background work. Google Play explicitly permits - * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` for an app maintaining a continuous connection to a - * companion device, which is exactly what NOOP does with the strap. - * - * Nothing here runs on its own: the UI checks [isExempt] and, only if the user taps, launches - * [requestIntent] (falling back to [settingsIntent] on OEMs that disable the direct prompt). - * There is no macOS/iOS counterpart — CoreBluetooth background delivery needs no user-granted - * power exemption. - */ -object BatteryOptimization { - - /** True when NOOP is already exempt from battery optimisation (background work allowed). */ - fun isExempt(context: Context): Boolean = runCatching { - val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager - pm?.isIgnoringBatteryOptimizations(context.packageName) == true - }.getOrDefault(false) - - /** - * The per-app system dialog asking the user to let NOOP ignore battery optimisation - * (`ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`). Some OEMs disable this action, so callers - * should catch `ActivityNotFoundException` and fall back to [settingsIntent]. - */ - fun requestIntent(context: Context): Intent = - Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) - .setData(Uri.parse("package:${context.packageName}")) - - /** The full battery-optimisation list — the fallback when the direct prompt is unavailable. */ - fun settingsIntent(): Intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) -} diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 18e2597b2..f9d2a018b 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -43,7 +43,6 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.IosShare import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MenuBook @@ -94,7 +93,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.noop.BuildConfig import com.noop.analytics.Baselines import com.noop.analytics.Zones -import com.noop.ble.BatteryOptimization import com.noop.ble.PuffinExperiment import com.noop.ble.WhoopModel import com.noop.data.DataBackup @@ -1098,53 +1096,6 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } - // Doze / battery-optimisation exemption. Without it Android freezes the foreground - // service overnight — the strap link drops, no overnight R-R/HRV is banked, and Charge - // can sit on "calibrating" forever. Only shown while background connection is on and the - // exemption isn't already granted. No iOS/macOS counterpart. - if (backgroundConnection && !BatteryOptimization.isExempt(context)) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(Palette.statusWarning.copy(alpha = 0.12f)) - .padding(14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - Icons.Filled.Warning, - contentDescription = null, - tint = Palette.statusWarning, - modifier = Modifier.size(22.dp), - ) - Column(modifier = Modifier.weight(1f)) { - Text( - "Allow background running", - style = NoopType.subhead, - color = Palette.textPrimary, - ) - Text( - "Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected.", - style = NoopType.footnote, - color = Palette.textTertiary, - ) - } - Button( - onClick = { - runCatching { context.startActivity(BatteryOptimization.requestIntent(context)) } - .onFailure { - runCatching { context.startActivity(BatteryOptimization.settingsIntent()) } - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = Palette.accent, - contentColor = Palette.surfaceBase, - ), - ) { Text("Allow") } - } - } - // Continuous HRV capture: keep the dense beat-to-beat (R-R) stream armed even with no Live // screen open, so the strap banks far more data overnight for better HRV/recovery/sleep. // Honest battery framing — continuous HR streaming uses more battery. Needs background From a55ca5b96d0931d2dbe3edc97d69c34c13bb4317 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:12:18 +0200 Subject: [PATCH 11/28] Android: in-app APK update + Doze battery-optimisation prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes for the sideloaded Android build (redo of the reverted pair — the first attempt used the unimported M3 `Button`; this uses `NoopButton`). In-app update (so a non-collaborator parent doesn't have to hunt a GitHub asset in a browser): - UpdateCheck also returns the release's .apk asset URL + size. - InAppUpdate: download the APK into cache/updates/, hand it to the system package installer via the existing FileProvider; routes through the per-app "install unknown apps" grant on first use; browser fallback on any error. - UpdateGate: a throttled (<=1/6h) launch check from NoopRoot that pops a dialog when a newer version is available — one-tap "Update" (download + install) or "Later" (snoozes that version). Onboarded users only. - Settings "Check for updates" now installs in-app instead of opening a browser. - Manifest: REQUEST_INSTALL_PACKAGES. file_paths: cache/updates/. Battery-optimisation exemption (likely root cause of "calibrating for weeks"): - Manifest: REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. - BatteryOptimization helper: isExempt() + the request / settings intents. - Settings → background connection: an amber warning row with an "Allow" button, shown only while background connection is on and the exemption isn't granted. Android-only; iOS gets updates from its AltStore/SideStore source and needs no power-exemption for CoreBluetooth background delivery. Co-Authored-By: Claude Sonnet 5 --- android/app/src/main/AndroidManifest.xml | 6 + .../java/com/noop/ble/BatteryOptimization.kt | 43 ++++++ .../src/main/java/com/noop/ui/MainActivity.kt | 12 ++ .../main/java/com/noop/ui/SettingsScreen.kt | 113 +++++++++++++- .../src/main/java/com/noop/ui/UpdateGate.kt | 140 ++++++++++++++++++ .../main/java/com/noop/update/InAppUpdate.kt | 123 +++++++++++++++ .../main/java/com/noop/update/UpdateCheck.kt | 33 ++++- android/app/src/main/res/xml/file_paths.xml | 4 +- 8 files changed, 463 insertions(+), 11 deletions(-) create mode 100644 android/app/src/main/java/com/noop/ble/BatteryOptimization.kt create mode 100644 android/app/src/main/java/com/noop/ui/UpdateGate.kt create mode 100644 android/app/src/main/java/com/noop/update/InAppUpdate.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a5f21097d..4997c2ddb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,6 +45,12 @@ + + + + diff --git a/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt b/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt new file mode 100644 index 000000000..76921bd2d --- /dev/null +++ b/android/app/src/main/java/com/noop/ble/BatteryOptimization.kt @@ -0,0 +1,43 @@ +package com.noop.ble + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.PowerManager +import android.provider.Settings + +/** + * Doze / battery-optimisation exemption. + * + * An app that is NOT whitelisted from battery optimisation gets its foreground service frozen + * during Doze, so NOOP's strap link drops overnight, no R-R/HRV is banked, and the recovery + * baseline never leaves "calibrating" — `AndroidDiagnostics` already calls this out as the #1 cause + * of missed overnight background work. Google Play explicitly permits + * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` for an app maintaining a continuous connection to a + * companion device, which is exactly what NOOP does with the strap. + * + * Nothing here runs on its own: the UI checks [isExempt] and, only if the user taps, launches + * [requestIntent] (falling back to [settingsIntent] on OEMs that disable the direct prompt). + * There is no macOS/iOS counterpart — CoreBluetooth background delivery needs no user-granted + * power exemption. + */ +object BatteryOptimization { + + /** True when NOOP is already exempt from battery optimisation (background work allowed). */ + fun isExempt(context: Context): Boolean = runCatching { + val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager + pm?.isIgnoringBatteryOptimizations(context.packageName) == true + }.getOrDefault(false) + + /** + * The per-app system dialog asking the user to let NOOP ignore battery optimisation + * (`ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`). Some OEMs disable this action, so callers + * should catch `ActivityNotFoundException` and fall back to [settingsIntent]. + */ + fun requestIntent(context: Context): Intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + .setData(Uri.parse("package:${context.packageName}")) + + /** The full battery-optimisation list — the fallback when the direct prompt is unavailable. */ + fun settingsIntent(): Intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) +} diff --git a/android/app/src/main/java/com/noop/ui/MainActivity.kt b/android/app/src/main/java/com/noop/ui/MainActivity.kt index ac47757ed..73aae7616 100644 --- a/android/app/src/main/java/com/noop/ui/MainActivity.kt +++ b/android/app/src/main/java/com/noop/ui/MainActivity.kt @@ -199,6 +199,14 @@ object NoopPrefs { const val KEY_ANALYZE_WATERMARK = "noop.analyzeWatermark" + /** Wall-clock ms of the last launch-time update check ([com.noop.ui.UpdateGate]), so the releases + * API is hit at most once per ~6 h across launches. 0 / absent = never checked. */ + const val KEY_LAST_UPDATE_CHECK_MS = "noop.lastUpdateCheckMs" + + /** Version string the user tapped "Later" on in the launch update prompt, so the same version + * isn't re-prompted every launch. The Settings "Check for updates" button ignores this. */ + const val KEY_UPDATE_SNOOZED_VERSION = "noop.updateSnoozedVersion" + fun of(context: Context): SharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE) @@ -756,6 +764,10 @@ fun NoopRoot() { // (stored version behind current), show "What's New" once over the top. AppRoot(viewModel = appViewModel) + // Sideloaded-APK update prompt: a throttled launch check against the public GitHub release, + // with a one-tap in-app download + install. Onboarded users only (past Terms + onboarding). + UpdateGate() + if (lastSeenChangelog != AppChangelog.CURRENT_VERSION) { Dialog( onDismissRequest = { diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index f9d2a018b..514aef089 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.IosShare import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MenuBook @@ -60,6 +61,7 @@ import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField @@ -93,11 +95,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.noop.BuildConfig import com.noop.analytics.Baselines import com.noop.analytics.Zones +import com.noop.ble.BatteryOptimization import com.noop.ble.PuffinExperiment import com.noop.ble.WhoopModel import com.noop.data.DataBackup import com.noop.ingest.RawSensorExport import com.noop.ingest.WhoopCsvExporter +import com.noop.update.InAppUpdate import com.noop.update.UpdateCheck import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -1096,6 +1100,51 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } + // Doze / battery-optimisation exemption. Without it Android freezes the foreground + // service overnight — the strap link drops, no overnight R-R/HRV is banked, and Charge + // can sit on "calibrating" forever. Only shown while background connection is on and the + // exemption isn't already granted. No iOS/macOS counterpart. + if (backgroundConnection && !BatteryOptimization.isExempt(context)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Palette.statusWarning.copy(alpha = 0.12f)) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + Icons.Filled.Warning, + contentDescription = null, + tint = Palette.statusWarning, + modifier = Modifier.size(22.dp), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + "Allow background running", + style = NoopType.subhead, + color = Palette.textPrimary, + ) + Text( + "Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected.", + style = NoopType.footnote, + color = Palette.textTertiary, + ) + } + NoopButton( + text = "Allow", + kind = NoopButtonKind.Primary, + onClick = { + runCatching { context.startActivity(BatteryOptimization.requestIntent(context)) } + .onFailure { + runCatching { context.startActivity(BatteryOptimization.settingsIntent()) } + } + }, + ) + } + } + // Continuous HRV capture: keep the dense beat-to-beat (R-R) stream armed even with no Live // screen open, so the strap banks far more data overnight for better HRV/recovery/sleep. // Honest battery framing — continuous HR streaming uses more battery. Needs background @@ -2020,6 +2069,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // is sent. Android already holds INTERNET (for the opt-in Coach), so this adds nothing. var updChecking by remember { mutableStateOf(false) } var updResult by remember { mutableStateOf(null) } + // Non-null while an in-app APK download is running (0..1), so the card shows progress + // instead of the Update button. + var updDownloadPct by remember { mutableStateOf(null) } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row( verticalAlignment = Alignment.CenterVertically, @@ -2082,13 +2134,60 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), ) - NoopButton( - text = "Download", - leadingIcon = Icons.Filled.Download, - kind = NoopButtonKind.Primary, - onClick = { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(avail.url))) - }, + val pct = updDownloadPct + if (pct != null) { + Text( + "Downloading… ${(pct * 100).toInt()}%", + style = NoopType.footnote, color = Palette.textSecondary, + ) + } else { + NoopButton( + text = "Update", + leadingIcon = Icons.Filled.Download, + kind = NoopButtonKind.Primary, + onClick = { + val apk = avail.apkUrl + when { + apk == null -> + InAppUpdate.openReleasePage(context, avail.url) + !InAppUpdate.canInstall(context) -> { + InAppUpdate.requestInstallPermission(context) + Toast.makeText( + context, + "Allow installing apps, then tap Update again.", + Toast.LENGTH_LONG, + ).show() + } + else -> { + updDownloadPct = 0f + scope.launch { + runCatching { + InAppUpdate.download(context, avail.version, apk) { + updDownloadPct = it + } + }.onSuccess { file -> + updDownloadPct = null + InAppUpdate.install(context, file) + }.onFailure { + updDownloadPct = null + Toast.makeText( + context, + "Download failed — opening the release page.", + Toast.LENGTH_LONG, + ).show() + InAppUpdate.openReleasePage(context, avail.url) + } + } + } + } + }, + ) + } + } + updDownloadPct?.let { p -> + LinearProgressIndicator( + progress = p, + modifier = Modifier.fillMaxWidth(), ) } if (avail.notes.isNotEmpty()) { diff --git a/android/app/src/main/java/com/noop/ui/UpdateGate.kt b/android/app/src/main/java/com/noop/ui/UpdateGate.kt new file mode 100644 index 000000000..297c227b5 --- /dev/null +++ b/android/app/src/main/java/com/noop/ui/UpdateGate.kt @@ -0,0 +1,140 @@ +package com.noop.ui + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.noop.BuildConfig +import com.noop.update.InAppUpdate +import com.noop.update.UpdateCheck +import kotlinx.coroutines.launch + +/** Don't hit the releases API more than once per this window across launches. */ +private const val UPDATE_CHECK_INTERVAL_MS = 6L * 60 * 60 * 1000 // 6 h + +private sealed interface UpdateGateState { + data object Hidden : UpdateGateState + data class Prompt(val info: UpdateCheck.Result.Available) : UpdateGateState + data class Downloading(val info: UpdateCheck.Result.Available, val pct: Float) : UpdateGateState +} + +/** + * Launch-time update gate for the sideloaded Android build. + * + * On a throttled schedule it asks the public GitHub release API for the latest version; if it's + * newer than the running build AND the user hasn't tapped "Later" on that exact version, it pops a + * dialog offering a one-tap update — download the APK into cache, then hand it to the system + * installer. Rendered once, over the live app, from [NoopRoot]. iOS gets equivalent one-tap updates + * from its AltStore/SideStore source, so there is no counterpart there. + */ +@Composable +fun UpdateGate() { + val context = LocalContext.current + val prefs = remember { NoopPrefs.of(context) } + val scope = rememberCoroutineScope() + var state by remember { mutableStateOf(UpdateGateState.Hidden) } + + LaunchedEffect(Unit) { + val last = prefs.getLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, 0L) + if (System.currentTimeMillis() - last < UPDATE_CHECK_INTERVAL_MS) return@LaunchedEffect + val result = UpdateCheck.check(BuildConfig.VERSION_NAME) + prefs.edit().putLong(NoopPrefs.KEY_LAST_UPDATE_CHECK_MS, System.currentTimeMillis()).apply() + if (result is UpdateCheck.Result.Available && result.apkUrl != null) { + val snoozed = prefs.getString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, "") ?: "" + if (result.version != snoozed) state = UpdateGateState.Prompt(result) + } + } + + when (val s = state) { + UpdateGateState.Hidden -> Unit + + is UpdateGateState.Prompt -> AlertDialog( + onDismissRequest = { state = UpdateGateState.Hidden }, + title = { Text("NOOP ${s.info.version} is available") }, + text = { + val body = buildString { + if (s.info.notes.isNotBlank()) append(s.info.notes.take(280).trim()) + if (s.info.apkSize > 0) { + if (isNotEmpty()) append("\n\n") + append("Download ${s.info.apkSize / (1024 * 1024)} MB") + } + } + Text(if (body.isBlank()) "A newer version is ready to install." else body) + }, + confirmButton = { + TextButton(onClick = { + val info = s.info + val apk = info.apkUrl ?: return@TextButton + if (!InAppUpdate.canInstall(context)) { + InAppUpdate.requestInstallPermission(context) + Toast.makeText( + context, + "Allow installing apps, then reopen NOOP to update.", + Toast.LENGTH_LONG, + ).show() + state = UpdateGateState.Hidden + return@TextButton + } + state = UpdateGateState.Downloading(info, 0f) + scope.launch { + runCatching { + InAppUpdate.download(context, info.version, apk) { p -> + state = UpdateGateState.Downloading(info, p) + } + }.onSuccess { file -> + state = UpdateGateState.Hidden + InAppUpdate.install(context, file) + }.onFailure { + state = UpdateGateState.Hidden + Toast.makeText( + context, + "Update download failed — opening the release page.", + Toast.LENGTH_LONG, + ).show() + InAppUpdate.openReleasePage(context, info.url) + } + } + }) { Text("Update") } + }, + dismissButton = { + TextButton(onClick = { + prefs.edit() + .putString(NoopPrefs.KEY_UPDATE_SNOOZED_VERSION, s.info.version) + .apply() + state = UpdateGateState.Hidden + }) { Text("Later") } + }, + ) + + is UpdateGateState.Downloading -> AlertDialog( + onDismissRequest = { }, + title = { Text("Updating to ${s.info.version}") }, + text = { + Column { + Text("Downloading… ${(s.pct * 100).toInt()}%") + Spacer(Modifier.height(12.dp)) + LinearProgressIndicator( + progress = s.pct, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + confirmButton = {}, + ) + } +} diff --git a/android/app/src/main/java/com/noop/update/InAppUpdate.kt b/android/app/src/main/java/com/noop/update/InAppUpdate.kt new file mode 100644 index 000000000..b28a98bdd --- /dev/null +++ b/android/app/src/main/java/com/noop/update/InAppUpdate.kt @@ -0,0 +1,123 @@ +package com.noop.update + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.core.content.FileProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.net.HttpURLConnection +import java.net.URL + +/** + * In-app APK update for the sideloaded Android build. + * + * NOOP ships as a sideloaded APK (no Play Store), so there is no automatic update path. This does the + * minimum to make one non-technical-user-proof: download the release APK into the app's cache and + * hand it to the system package installer, so the user taps "Update" once instead of hunting a + * GitHub asset in a browser. Nothing is sent — the APK comes straight from the public GitHub + * release. The launch-time check + prompt lives in [com.noop.ui.UpdateGate]; this is just plumbing. + * + * No iOS/macOS counterpart: on iOS the AltStore/SideStore source already delivers one-tap updates, + * and macOS is a drag-to-Applications zip. + */ +object InAppUpdate { + + /** True once the user has allowed NOOP to install packages (the API 26+ per-app gate). */ + fun canInstall(context: Context): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.O || + context.packageManager.canRequestPackageInstalls() + + /** Send the user to the per-app "install unknown apps" screen. They grant once, then retry. */ + fun requestInstallPermission(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + runCatching { + context.startActivity( + Intent( + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:${context.packageName}"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + } + + /** App-private dir the downloaded APKs live in (covered by the FileProvider `updates/` path). */ + private fun updatesDir(context: Context): File = + File(context.cacheDir, "updates").apply { mkdirs() } + + /** + * Download [apkUrl] to `cache/updates/NOOP-v.apk`, reporting 0..1 progress. Any stale + * APKs in the dir are cleared first. Runs on IO; returns the file or throws (the caller catches + * and falls back to the release page). + */ + suspend fun download( + context: Context, + version: String, + apkUrl: String, + onProgress: (Float) -> Unit, + ): File = withContext(Dispatchers.IO) { + val dir = updatesDir(context) + dir.listFiles()?.forEach { it.delete() } + val out = File(dir, "NOOP-v$version.apk") + + var conn = URL(apkUrl).openConnection() as HttpURLConnection + conn.instanceFollowRedirects = true + conn.connectTimeout = 20_000 + conn.readTimeout = 60_000 + // GitHub asset URLs 302 to a signed CDN URL; HttpURLConnection won't cross http<->https on + // its own, so follow one hop by hand if the first response is a redirect. + if (conn.responseCode in 300..399) { + val loc = conn.getHeaderField("Location") ?: error("redirect ${conn.responseCode} without Location") + conn.disconnect() + conn = (URL(loc).openConnection() as HttpURLConnection).apply { + instanceFollowRedirects = true + connectTimeout = 20_000 + readTimeout = 60_000 + } + } + try { + if (conn.responseCode !in 200..299) error("HTTP ${conn.responseCode}") + val total = conn.contentLengthLong.takeIf { it > 0 } + conn.inputStream.use { input -> + out.outputStream().use { output -> + val buf = ByteArray(64 * 1024) + var read = 0L + while (true) { + val n = input.read(buf) + if (n < 0) break + output.write(buf, 0, n) + read += n + if (total != null) onProgress((read.toFloat() / total).coerceIn(0f, 1f)) + } + } + } + } finally { + conn.disconnect() + } + onProgress(1f) + out + } + + /** Hand [apk] to the system package installer (the normal "Update NOOP?" screen). */ + fun install(context: Context, apk: File) { + val uri: Uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apk) + context.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "application/vnd.android.package-archive") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) + }, + ) + } + + /** Open the release page in a browser — the fallback when there's no APK asset or install fails. */ + fun openReleasePage(context: Context, url: String) { + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + } +} diff --git a/android/app/src/main/java/com/noop/update/UpdateCheck.kt b/android/app/src/main/java/com/noop/update/UpdateCheck.kt index f2e56af5b..9c03805f3 100644 --- a/android/app/src/main/java/com/noop/update/UpdateCheck.kt +++ b/android/app/src/main/java/com/noop/update/UpdateCheck.kt @@ -19,7 +19,20 @@ object UpdateCheck { sealed interface Result { data class UpToDate(val version: String) : Result - data class Available(val version: String, val url: String, val notes: String) : Result + + /** + * A newer release is available. [url] is the release page (browser fallback); [apkUrl] is the + * direct `.apk` asset for an in-app download+install, or null when the release has no APK + * attached. [apkSize] is bytes, 0 if unknown. + */ + data class Available( + val version: String, + val url: String, + val notes: String, + val apkUrl: String? = null, + val apkSize: Long = 0L, + ) : Result + object Failed : Result } @@ -39,8 +52,22 @@ object UpdateCheck { val latest = json.getString("tag_name").removePrefix("v") val url = json.getString("html_url") val notes = cleanNotes(json.optString("body", "")) - if (isNewer(latest, currentVersion)) Result.Available(latest, url, notes) - else Result.UpToDate(latest) + if (!isNewer(latest, currentVersion)) return@runCatching Result.UpToDate(latest) + + // First `.apk` asset on the release → the in-app download target. + var apkUrl: String? = null + var apkSize = 0L + json.optJSONArray("assets")?.let { assets -> + for (i in 0 until assets.length()) { + val a = assets.optJSONObject(i) ?: continue + if (a.optString("name").endsWith(".apk", ignoreCase = true)) { + apkUrl = a.optString("browser_download_url").ifBlank { null } + apkSize = a.optLong("size", 0L) + break + } + } + } + Result.Available(latest, url, notes, apkUrl, apkSize) } finally { conn.disconnect() } diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml index 3c7ac5b02..d79f095ed 100644 --- a/android/app/src/main/res/xml/file_paths.xml +++ b/android/app/src/main/res/xml/file_paths.xml @@ -1,8 +1,10 @@ + - reports/ : the shareable offline trends report PDF (TrendsReport, #436) + - updates/ : the downloaded release APK handed to the system installer (InAppUpdate) --> + From ba99c447ceb8380d54afe256a29d98ce9a5af948 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:20:30 +0200 Subject: [PATCH 12/28] Bump to 8.2.3 Ships the Android in-app update prompt + the Doze battery-optimisation exemption. Cut as its own release so installs on 8.2.2 detect it. Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 49d4da898..fa3f967b9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 261 - versionName = "8.2.2" + versionCode = 262 + versionName = "8.2.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index a9901fa86..fa243db87 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.2" + MARKETING_VERSION: "8.2.3" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "172" + CURRENT_PROJECT_VERSION: "173" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From 745f5dfb71b52567605c4d4e41e78cdb7746c644 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:38:41 +0200 Subject: [PATCH 13/28] Publish 8.2.3 in the AltStore source Prepend the 8.2.3 entry (real IPA size from the published release) and point the legacy top-level fields at it. Co-Authored-By: Claude Sonnet 5 --- altstore-source.json | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/altstore-source.json b/altstore-source.json index caca3baec..02846b60a 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -17,6 +17,15 @@ "category": "healthcare-fitness", "screenshotURLs": [], "versions": [ + { + "version": "8.2.3", + "buildVersion": "173", + "date": "2026-09-10", + "localizedDescription": "NOOP 8.2.3. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.3/NOOP-v8.2.3-ios.ipa", + "size": 14162400, + "minOSVersion": "17.0" + }, { "version": "8.2.1", "buildVersion": "171", @@ -46,12 +55,12 @@ "NSHealthUpdateUsageDescription": "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." } }, - "version": "8.2.1", - "buildVersion": "171", - "versionDate": "2026-08-12", - "versionDescription": "NOOP 8.2.1. See the release notes for what changed.", - "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.1/NOOP-v8.2.1-ios.ipa", - "size": 14162393, + "version": "8.2.3", + "buildVersion": "173", + "versionDate": "2026-09-10", + "versionDescription": "NOOP 8.2.3. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.3/NOOP-v8.2.3-ios.ipa", + "size": 14162400, "minOSVersion": "17.0" } ], From 29b9711f672ae3a88aff2becece16989ec876177 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:58:55 +0200 Subject: [PATCH 14/28] CI: sign release APKs with a stable keystore from repo secrets Each release run signed with a fresh ephemeral debug key, so Android refused to update in place and forced an uninstall (wiping onboarding + profile + local DB). release.yml now decodes a keystore from the ANDROID_KEYSTORE_B64 secret (+ password/alias secrets) and signs with it, so every release shares one key and in-app updates install over the top. Falls back to the debug key with a loud warning when the secret is unset. Keystore never enters git. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8e3e78d8..c4d562ba3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -127,6 +127,32 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 + # A STABLE signing key is what lets an in-app update install over the previous version + # (same key = "update"; a different key = Android refuses and forces an uninstall → data loss). + # The key lives ONLY in repo secrets (never in git). One-time setup: + # keytool -genkeypair -v -keystore noop-release.jks -alias noop -keyalg RSA -keysize 2048 \ + # -validity 10000 -storepass -keypass -dname "CN=NOOP, O=NoopApp, C=US" + # base64 -w0 noop-release.jks # -> paste as secret ANDROID_KEYSTORE_B64 + # then add secrets ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS (noop) / ANDROID_KEY_PASSWORD. + # Until the secret is set, the APK falls back to an ephemeral debug key (updates need a reinstall). + - name: Prepare release signing keystore + working-directory: android + env: + KS_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} + KS_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASS: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + set -euo pipefail + if [ -n "${KS_B64:-}" ]; then + echo "$KS_B64" | base64 -d > noop-release.jks + printf 'storeFile=noop-release.jks\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \ + "$KS_PASS" "$KEY_ALIAS" "$KEY_PASS" > keystore.properties + echo "Signing with the stable release keystore from repo secrets." + else + echo "::warning::ANDROID_KEYSTORE_B64 secret is not set — this APK is signed with an ephemeral debug key, so installing it needs the previous version uninstalled first (data loss). Add the secret for seamless in-app updates." + fi + - name: Build full release APK run: ./gradlew assembleFullRelease working-directory: android From 6f0c6e1c8eb52d73d7b238b4d5c47fc1ec06e48c Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:02:22 +0200 Subject: [PATCH 15/28] Auto-reload after restore; point Support at this fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android: a backup restore / data import used to leave the app showing stale "calibrating" / "No Data" until the user force-quit and reopened. It now relaunches the process automatically once the DB is swapped (AppRestart). - Support / About: the contact row (macOS/iOS SupportView + Android Settings) now opens this fork's GitHub — code, releases, issues — instead of the upstream project's email. The "Built on" attribution credits are unchanged. Co-Authored-By: Claude Sonnet 5 --- Strand/Screens/SupportView.swift | 12 +++---- Strand/System/ProjectInfo.swift | 6 ++-- .../src/main/java/com/noop/ui/AppRestart.kt | 32 +++++++++++++++++++ .../main/java/com/noop/ui/BackupSyncScreen.kt | 12 ++++--- .../java/com/noop/ui/DataSourcesScreen.kt | 9 ++---- .../main/java/com/noop/ui/SettingsScreen.kt | 18 ++++------- 6 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 android/app/src/main/java/com/noop/ui/AppRestart.kt diff --git a/Strand/Screens/SupportView.swift b/Strand/Screens/SupportView.swift index fee1290ab..487aed8aa 100644 --- a/Strand/Screens/SupportView.swift +++ b/Strand/Screens/SupportView.swift @@ -49,17 +49,17 @@ struct SupportView: View { private var contactCard: some View { NoopCard { Button { - if let url = URL(string: "mailto:\(ProjectInfo.contactEmail)") { PlatformOpen.url(url) } + if let url = URL(string: ProjectInfo.projectURL) { PlatformOpen.url(url) } } label: { - groupedRow(icon: "envelope.fill", tint: StrandPalette.accent, - title: "Get in touch", - detail: String(localized: "Questions, feedback, bugs: \(ProjectInfo.contactEmail)"), + groupedRow(icon: "chevron.left.forwardslash.chevron.right", tint: StrandPalette.accent, + title: "Project home", + detail: String(localized: "Code, releases, and where to report an issue: \(ProjectInfo.projectURL)"), showsChevron: true) .contentShape(Rectangle()) } .buttonStyle(.plain) - .accessibilityLabel("Email \(ProjectInfo.contactEmail)") - .help("Email \(ProjectInfo.contactEmail)") + .accessibilityLabel("Open \(ProjectInfo.projectURL)") + .help("Open \(ProjectInfo.projectURL)") } } diff --git a/Strand/System/ProjectInfo.swift b/Strand/System/ProjectInfo.swift index 388f141d5..180985436 100644 --- a/Strand/System/ProjectInfo.swift +++ b/Strand/System/ProjectInfo.swift @@ -5,10 +5,10 @@ enum ProjectInfo { static let appName = "NOOP" static let tagline = "Your strap. Your data. Your machine. Local-first, no cloud." static let version = "0.1.0" - /// Public contact for questions, feedback, bug reports. Baked into every platform. - static let contactEmail = "thenoopapp@gmail.com" + /// This fork's home — code, releases, and where to file an issue. Baked into every platform. + static let projectURL = "https://github.com/MatB57/noop" - /// Open-source reverse-engineering this is built on. + /// Open-source reverse-engineering this is built on — kept intact regardless of who maintains the fork. static let attributions: [(repo: String, note: String)] = [ ("johnmiddleton12/my-whoop", "WHOOP 4.0 BLE protocol"), ("b-nnett/goose", "WHOOP 5.0 BLE protocol"), diff --git a/android/app/src/main/java/com/noop/ui/AppRestart.kt b/android/app/src/main/java/com/noop/ui/AppRestart.kt new file mode 100644 index 000000000..5ebe01cae --- /dev/null +++ b/android/app/src/main/java/com/noop/ui/AppRestart.kt @@ -0,0 +1,32 @@ +package com.noop.ui + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import kotlin.system.exitProcess + +/** + * Relaunch NOOP from scratch. + * + * A backup restore / data import swaps the on-disk SQLite file, but the running process still holds + * open Room + GRDB connections, cached repositories, view-models and in-memory LiveState built from + * the OLD database — which is why a restore used to show stale "calibrating" / "No Data" until the + * user force-quit and reopened the app by hand. This does that for them: start a fresh task and + * hard-exit the current process so every handle and cache is gone and the new DB is read clean. + */ +object AppRestart { + + /** Toast-then-relaunch after [delayMs] so the user sees the confirmation before the screen blinks. */ + fun relaunch(context: Context, delayMs: Long = 700L) { + val appContext = context.applicationContext + Handler(Looper.getMainLooper()).postDelayed({ + val intent = appLaunchIntent(appContext).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + appContext.startActivity(intent) + // Hard-kill: a graceful finish would leave the old DB handles / analytics caches alive. + exitProcess(0) + }, delayMs) + } +} diff --git a/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt b/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt index 06968862c..66226eb18 100644 --- a/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt +++ b/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt @@ -73,12 +73,14 @@ fun BackupSyncScreen() { scope.launch { val r = withContext(Dispatchers.IO) { DataBackup.importFrom(context, uri) } busy = false - val msg = when (r) { - is DataBackup.ImportResult.NeedsRestart -> - "Restored. Fully close and reopen NOOP to load it." - is DataBackup.ImportResult.Failed -> r.message + when (r) { + is DataBackup.ImportResult.NeedsRestart -> { + Toast.makeText(context, "Restored — reloading NOOP…", Toast.LENGTH_SHORT).show() + AppRestart.relaunch(context) + } + is DataBackup.ImportResult.Failed -> + Toast.makeText(context, r.message, Toast.LENGTH_LONG).show() } - Toast.makeText(context, msg, Toast.LENGTH_LONG).show() } } diff --git a/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt b/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt index dfc993310..84403c782 100644 --- a/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt +++ b/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt @@ -186,11 +186,8 @@ fun DataSourcesScreen(vm: AppViewModel) { when (result) { is DataBackup.ImportResult.NeedsRestart -> { restartNeeded = true - Toast.makeText( - context, - "Imported. Fully close and reopen NOOP to load it.", - Toast.LENGTH_LONG, - ).show() + Toast.makeText(context, "Imported — reloading NOOP…", Toast.LENGTH_SHORT).show() + AppRestart.relaunch(context) } is DataBackup.ImportResult.Failed -> Toast.makeText(context, result.message, Toast.LENGTH_LONG).show() @@ -851,7 +848,7 @@ fun DataSourcesScreen(vm: AppViewModel) { } if (restartNeeded) { Text( - "Import staged. Fully close and reopen NOOP to load the new data.", + "Import staged — NOOP is reloading. If it doesn't, fully close and reopen it.", style = NoopType.subhead, color = Palette.statusWarning, ) diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 514aef089..f6eeddbcb 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -2373,8 +2373,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { RowDivider() - // Support link — opens the project's contact email (same address the - // Support screen lists). NOOP is anonymous, so email is the support channel. + // Support link — opens this fork's GitHub (code, releases, and where to file an issue). val supportInteraction = remember { MutableInteractionSource() } Box( modifier = Modifier @@ -2387,27 +2386,24 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { interactionSource = supportInteraction, indication = null, ) { - val intent = Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse("mailto:$SUPPORT_EMAIL") - putExtra(Intent.EXTRA_SUBJECT, "NOOP support") - } + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(PROJECT_URL)) try { context.startActivity(intent) } catch (_: ActivityNotFoundException) { - Toast.makeText(context, "Email us at $SUPPORT_EMAIL", Toast.LENGTH_LONG).show() + Toast.makeText(context, PROJECT_URL, Toast.LENGTH_LONG).show() } } .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "Contact support at $SUPPORT_EMAIL" }, + .semantics { contentDescription = "Open the project home, $PROJECT_URL" }, ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { - Text("Support & contact", style = NoopType.headline, color = Palette.textPrimary) + Text("Support & project home", style = NoopType.headline, color = Palette.textPrimary) Text( - "Questions, feedback, bugs: $SUPPORT_EMAIL", + "Code, releases, and where to report an issue: $PROJECT_URL", style = NoopType.footnote, color = Palette.textSecondary, ) @@ -2488,7 +2484,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { } } -private const val SUPPORT_EMAIL = "thenoopapp@gmail.com" +private const val PROJECT_URL = "https://github.com/MatB57/noop" // MARK: - App icon swap (v3 "Titanium & Gold") From 5c5a8551be7bf5635d41c2163a10a21f8da61845 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:05:14 +0200 Subject: [PATCH 16/28] Android: runtime French dictionary scaffold (Fr.tr / res/raw) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for translating the Android UI without a mass strings.xml extraction: Fr.load() parses the bundled EN->FR dictionary (the same pairs the iOS String Catalog ships) once at startup when the device language is French, and tr("English literal") swaps in the French rendering at the call site — falling through to English for anything unknown or format-bearing, so it is always safe. No call sites yet; wrapping the screens follows. Co-Authored-By: Claude Sonnet 5 --- .../src/main/java/com/noop/NoopApplication.kt | 2 + android/app/src/main/java/com/noop/i18n/Fr.kt | 44 + android/app/src/main/java/com/noop/ui/Tr.kt | 8 + android/app/src/main/res/raw/fr_strings.json | 2871 +++++++++++++++++ 4 files changed, 2925 insertions(+) create mode 100644 android/app/src/main/java/com/noop/i18n/Fr.kt create mode 100644 android/app/src/main/java/com/noop/ui/Tr.kt create mode 100644 android/app/src/main/res/raw/fr_strings.json diff --git a/android/app/src/main/java/com/noop/NoopApplication.kt b/android/app/src/main/java/com/noop/NoopApplication.kt index a52bd781b..0badcc017 100644 --- a/android/app/src/main/java/com/noop/NoopApplication.kt +++ b/android/app/src/main/java/com/noop/NoopApplication.kt @@ -31,6 +31,8 @@ class NoopApplication : Application() { // Record any uncaught crash to a file so it rides along in the shareable strap log — a // device-specific crash (e.g. Insights #224/#267) is otherwise lost to an unreachable logcat. CrashCapture.install(this) + // Load the runtime French dictionary (no-op unless the device language is French). + com.noop.i18n.Fr.load(this) } /** Process-wide Room-backed store. One instance shared by the UI and the background service. */ diff --git a/android/app/src/main/java/com/noop/i18n/Fr.kt b/android/app/src/main/java/com/noop/i18n/Fr.kt new file mode 100644 index 000000000..e4ba5e601 --- /dev/null +++ b/android/app/src/main/java/com/noop/i18n/Fr.kt @@ -0,0 +1,44 @@ +package com.noop.i18n + +import android.content.Context +import com.noop.R +import org.json.JSONObject +import java.util.Locale + +/** + * Lightweight runtime French localisation for the Android UI. + * + * The Android Compose screens carry their copy as hardcoded English string literals — they never + * went through `strings.xml`. Rather than a mass extraction, [tr] wraps a literal at its call site + * and swaps in a French rendering when the device language is French. The translations are the same + * English→French pairs the iOS String Catalog already ships (bundled as `res/raw/fr_strings.json`). + * + * Anything not in the map — an unknown phrase, or a string that carries `$`/`%` format pieces — + * falls straight through to the English original, so a call to [tr] is always safe and never throws. + */ +object Fr { + + @Volatile private var map: Map = emptyMap() + @Volatile private var active: Boolean = false + + /** Parse the bundled dictionary once. Call from `Application.onCreate`. No-op when not French. */ + fun load(context: Context) { + active = Locale.getDefault().language.lowercase() == "fr" + if (!active) return + map = runCatching { + val text = context.resources.openRawResource(R.raw.fr_strings) + .bufferedReader().use { it.readText() } + val obj = JSONObject(text) + val out = HashMap(obj.length() * 2) + val keys = obj.keys() + while (keys.hasNext()) { + val k = keys.next() + out[k] = obj.getString(k) + } + out + }.getOrDefault(emptyMap()) + } + + /** English literal in, French out when available and the device language is French. */ + fun tr(en: String): String = if (active) map[en] ?: en else en +} diff --git a/android/app/src/main/java/com/noop/ui/Tr.kt b/android/app/src/main/java/com/noop/ui/Tr.kt new file mode 100644 index 000000000..a2ee066e5 --- /dev/null +++ b/android/app/src/main/java/com/noop/ui/Tr.kt @@ -0,0 +1,8 @@ +package com.noop.ui + +/** + * Call-site shorthand for [com.noop.i18n.Fr.tr] so a UI file can wrap an English string literal as + * `tr("Recovery")` with no import. Returns the French rendering on a French device when one exists, + * otherwise the string unchanged. Safe in and out of `@Composable` scope (it is a plain function). + */ +fun tr(en: String): String = com.noop.i18n.Fr.tr(en) diff --git a/android/app/src/main/res/raw/fr_strings.json b/android/app/src/main/res/raw/fr_strings.json new file mode 100644 index 000000000..389907da1 --- /dev/null +++ b/android/app/src/main/res/raw/fr_strings.json @@ -0,0 +1,2871 @@ +{ +"%@ %@ relationship (r = %@, n = %lld).": "Relation %1$@ %2$@ (r = %3$@, n = %4$lld).", +"%@ (Beta)": "%@ (bêta)", +"%@ (experimental)": "%@ (expérimental)", +"%@ Cohen's d %@. %@": "%1$@ d de Cohen %2$@. %3$@", +"%@ Cohen's d %@. Exploratory, not yet significant.": "%1$@ d de Cohen %2$@. Exploratoire, pas encore significatif.", +"%@ Cohen's d %@. Statistically significant.": "%1$@ d de Cohen %2$@. Statistiquement significatif.", +"%@ Effort": "Effort %@", +"%@ No clear relationship. They move largely independently.": "%@ Aucune relation claire. Ils évoluent en grande partie de façon indépendante.", +"%@ When %@ rises, %@ tends to fall, a %@ %@ link.": "%1$@ Quand %2$@ augmente, %3$@ a tendance à baisser, un lien %4$@ %5$@.", +"%@ When %@ rises, %@ tends to rise, a %@ %@ link.": "%1$@ Quand %2$@ augmente, %3$@ a tendance à augmenter, un lien %4$@ %5$@.", +"%@ agrees: %@": "%1$@ est d'accord : %2$@", +"%@ br/min": "%@ resp/min", +"%@ breaths per minute: %@": "%1$@ respirations par minute : %2$@", +"%@ calibrating": "%@ en calibration", +"%@ h avg": "%@ h moy.", +"%@ in bed": "%@ au lit", +"%@ in bed · %@ efficiency": "%1$@ au lit · %2$@ d'efficacité", +"%@ in bed · %@ efficiency · stages approximate (on-device)": "%1$@ au lit · %2$@ d'efficacité · phases approximatives (sur l'appareil)", +"%@ in bed · %lld%% efficiency": "%1$@ au lit · %2$lld%% d'efficacité", +"%@ is %@ better than baseline on days you logged this behaviour.": "%1$@ est %2$@ meilleur que la référence les jours où vous avez consigné ce comportement.", +"%@ is %@ worse than baseline on days you logged this behaviour.": "%1$@ est %2$@ moins bon que la référence les jours où vous avez consigné ce comportement.", +"%@ is flat against baseline on logged intervention days.": "%@ est stable par rapport à la référence les jours d'intervention consignés.", +"%@ is your active band. Pair or switch bands in Devices.": "%@ est votre bracelet actif. Associez ou changez de bracelet dans Appareils.", +"%@ kg total volume": "%@ kg de volume total", +"%@ link · r = %@ · n = %lld days": "Lien %1$@ · r = %2$@ · n = %3$lld jours", +"%@ of %@ litres": "%1$@ sur %2$@ litres", +"%@ pace": "Allure %@", +"%@ says %@. Tap to compare": "%1$@ dit %2$@. Touchez pour comparer", +"%@ steps per motion unit": "%@ pas par unité de mouvement", +"%@ test mode": "Mode test %@", +"%@ trend": "Tendance %@", +"%@ unavailable": "%@ indisponible", +"%@ versus %@, r equals %@, %lld days": "%@ versus %@, r = %@, %lld jours", +"%@ vs start · peak %@ ms": "%1$@ vs départ · pic %2$@ ms", +"%@ wake time": "Heure de réveil %@", +"%@ will ask for Bluetooth in a moment.": "%@ va demander l'accès au Bluetooth dans un instant.", +"%@ wrist alerts": "%@ alertes au poignet", +"%@ · %@s in / %@s out": "%1$@ · %2$@s en / %3$@s hors", +"%@ · %lld mg": "%1$@ · %2$lld mg", +"%@ · Beta": "%@ · Bêta", +"%@ · amount not logged": "%@ · quantité non consignée", +"%@ · best signal per metric, from %lld sources. Everything stays on %@.": "%1$@ · meilleur signal par métrique, à partir de %2$lld sources. Tout reste sur %3$@.", +"%@ · some sparse series widened": "%@ · certaines séries clairsemées élargies", +"%@ · your record, on %@.": "%1$@ · votre record, le %2$@.", +"%@ – %@ · %lld days": "%1$@ – %2$@ · %3$lld jours", +"%@ – %@ · 1 day": "%1$@ – %2$@ · 1 jour", +"%@ ↔ %@: r = %@ (%@ %@) over %lld shared days.": "%1$@ ↔ %2$@ : r = %3$@ (%4$@ %5$@) sur %6$lld jours communs.", +"%@%@ C vs your normal": "%1$@%2$@ C vs votre normale", +"%@%@ vs typ": "%1$@%2$@ vs typ.", +"%@%@%@ vs typical": "%1$@%2$@%3$@ vs typique", +"%@%lld vs base": "%1$@%2$lld vs base", +"%@, %@": "%1$@, %2$@", +"%@, %@, in use": "%1$@, %2$@, en cours d'utilisation", +"%@, %@, no data": "%1$@, %2$@, aucune donnée", +"%@, %@. %@": "%1$@, %2$@. %3$@", +"%@, %@: %@": "%1$@, %2$@ : %3$@", +"%@, a good sign.": "%@, bon signe.", +"%@, correlation %@, %lld days": "%@, corrélation %@, %lld jours", +"%@, latest %@, %lld readings": "%1$@, dernière %2$@, %3$lld mesures", +"%@, mood %lld of 5": "%1$@, humeur %2$lld sur 5", +"%@, range %@ to %@": "%@, plage %@ à %@", +"%@, signal %lld of 4": "%1$@, signal %2$lld sur 4", +"%@, worth a look.": "%@, à surveiller.", +"%@. %@": "%1$@. %2$@", +"%@. %@. %@": "%1$@. %2$@. %3$@", +"%@. Swipe or tap to change day": "%@. Balayez ou touchez pour changer de jour", +"%@: %@": "%1$@ : %2$@", +"%@: %@ %@": "%1$@ : %2$@ %3$@", +"%@: %@ last night%@": "%@ : %@ la nuit dernière%@", +"%@: %@ last night, %lld percent of the night": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit", +"%@: %@ last night, %lld percent of the night%@": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit%4$@", +"%@: %@ last night, %lld percent of the night, typical %@": "%1$@ : %2$@ cette nuit, %3$lld pour cent de la nuit, typique %4$@", +"%@: %@ litres": "%1$@ : %2$@ litres", +"%@: %@ of Charge": "%1$@ : %2$@ de Charge", +"%@: %@ this week, down %@ week over week": "%1$@ : %2$@ cette semaine, en baisse de %3$@ par rapport à la semaine dernière", +"%@: %@ this week, no comparison.": "%1$@ : %2$@ cette semaine, aucune comparaison.", +"%@: %@ this week, unchanged %@ week over week": "%1$@ : %2$@ cette semaine, stable à %3$@ par rapport à la semaine dernière", +"%@: %@ this week, up %@ week over week": "%1$@ : %2$@ cette semaine, en hausse de %3$@ par rapport à la semaine dernière", +"%@: %@, %@": "%1$@ : %2$@, %3$@", +"%@: %@, %lld percent of the night": "%1$@ : %2$@, %3$lld pour cent de la nuit", +"%@: %@. %@": "%1$@ : %2$@. %3$@", +"%@: %@. %@, %@. %@.": "%1$@ : %2$@. %3$@, %4$@. %5$@.", +"%@: %@. %@. %@.": "%1$@ : %2$@. %3$@. %4$@.", +"%@: %@. Fix in Settings.": "%1$@ : %2$@. À corriger dans Paramètres.", +"%@: estimated %lld steps, phone %lld steps, %lld percent difference": "%1$@ : %2$lld pas estimés, %3$lld pas sur le téléphone, %4$lld pour cent de différence", +"%@: no data": "%@ : aucune donnée", +"%lld %@": "%1$lld %2$@", +"%lld %@ reading your heart rate": "%1$lld %2$@ lisent votre fréquence cardiaque", +"%lld app%@ on": "%lld app%@ sur", +"%lld apps on": "%lld applications actives", +"%lld avg bpm": "%lld bpm en moyenne", +"%lld beats per minute, zone %lld": "%1$lld battements par minute, zone %2$lld", +"%lld breaths": "%lld respirations", +"%lld breaths · %@": "%1$lld respirations · %2$@", +"%lld chunks": "%lld blocs", +"%lld chunks pulled": "%lld blocs récupérés", +"%lld d ago": "il y a %lld j", +"%lld days": "%lld jours", +"%lld days ago": "il y a %lld jours", +"%lld days without it": "%lld jours sans", +"%lld days your phone also counted": "%lld jours également comptés par votre téléphone", +"%lld days · %@": "%1$lld jours · %2$@", +"%lld days · %@ · some sparse series widened": "%1$lld jours · %2$@ · certaines séries clairsemées élargies", +"%lld days · %lld sleeps": "%1$lld jours · %2$lld sommeils", +"%lld days · %lld sleeps stored": "%lld jours · %lld nuits stockées", +"%lld days · %lld workouts": "%1$lld jours · %2$lld entraînements", +"%lld decoded, %lld console": "%1$lld décodés, %2$lld console", +"%lld devices reading your heart rate": "%lld appareils lisent votre fréquence cardiaque", +"%lld drinks": "%lld boissons", +"%lld frame%@ captured this session.": "%lld trame%@ capturée cette séance.", +"%lld frames captured this session.": "%lld images capturées lors de cette séance.", +"%lld h ago": "il y a %lld h", +"%lld hours": "%lld heures", +"%lld intakes still in the estimate. Rough guide only, based on what you logged.": "%lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez consigné.", +"%lld logged caffeines are past your bedtime cutoff. They may still be on board and keep you up. Just a timing heads-up.": "%lld prises de caféine consignées dépassent votre limite avant le coucher. Elles peuvent encore agir et vous empêcher de dormir. Juste un repère de timing.", +"%lld logged days": "%lld jours consignés", +"%lld markers": "%lld marqueurs", +"%lld markers tracked · %lld readings": "%1$lld marqueurs suivis · %2$lld mesures", +"%lld markers tracked · 1 reading": "%lld marqueurs suivis · 1 mesure", +"%lld min ago": "il y a %lld min", +"%lld new days of history landed. Open Trends to see them.": "%lld nouveaux jours d'historique sont arrivés. Ouvrez Tendances pour les voir.", +"%lld nights ago": "il y a %lld nuits", +"%lld nights to go": "encore %lld nuits", +"%lld of %lld": "%1$lld sur %2$lld", +"%lld of %lld days": "%1$lld sur %2$lld jours", +"%lld of %lld nights": "%lld de %lld nuits", +"%lld of %lld sessions": "%1$lld sur %2$lld séances", +"%lld of 1 session": "%lld sur 1 séance", +"%lld of 7 days had data this week": "%lld jours sur 7 avaient des données cette semaine", +"%lld overlapping days · %@ %@ correlation": "%lld jours chevauchants · corrélation %@ %@", +"%lld pairs": "%lld paires", +"%lld percent": "%lld pour cent", +"%lld points, mean %@, range %@ to %@": "%lld points, moyenne %@, plage %@ à %@", +"%lld reading%@ line up so far, not enough to read a trend yet (NOOP waits for %lld).": "%1$lld mesure%2$@ concordent jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %3$lld).", +"%lld reading%@ · your own entries": "%1$lld mesure%2$@ · vos propres entrées", +"%lld readings": "%lld mesures", +"%lld readings across %lld · %@": "%1$lld mesures sur %2$lld · %3$@", +"%lld readings across %lld · %@ · sparse widened": "%1$lld mesures sur %2$lld · %3$@ · clairsemé élargi", +"%lld readings line up so far, not enough to read a trend yet (NOOP waits for %lld).": "%1$lld mesures concordent jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %2$lld).", +"%lld readings line up, but there isn't enough variation to compute a relationship.": "%lld mesures concordent, mais il n'y a pas assez de variation pour calculer une relation.", +"%lld readings used · %@ %@ association. This is your own data sitting side by side. It's not a medical finding, and it shows association, not cause.": "%1$lld mesures utilisées · association %2$@ %3$@. Ce sont vos propres données mises côte à côte. Ce n'est pas un constat médical, et cela montre une association, pas une cause.", +"%lld readings · %@": "%1$lld mesures · %2$@", +"%lld readings · sparse, widened to %@": "%1$lld mesures · clairsemé, élargi à %2$@", +"%lld readings · your own entries": "%lld mesures · vos propres entrées", +"%lld recent": "%lld récentes", +"%lld rows skipped": "%lld lignes ignorées", +"%lld seconds remaining in %@": "%1$lld secondes restantes dans %2$@", +"%lld series": "%lld séries", +"%lld sessions": "%lld séances", +"%lld sessions · %@": "%1$lld séances · %2$@", +"%lld sessions · sparse, widened to %@": "%1$lld séances · clairsemé, élargi à %2$@", +"%lld skipped": "%lld ignorés", +"%lld total": "%lld au total", +"%lld type-0x2F historical-offload frame%@ seen outside our sync. These are history (e.g. another app pulling the strap's backlog), not a live R22 stream (#494).": "%1$lld trame%2$@ de déchargement historique type-0x2F vue(s) en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494).", +"%lld type-0x2F historical-offload frames seen outside our sync. These are history (e.g. another app pulling the strap's backlog), not a live R22 stream (#494).": "%lld trames de déchargement historique type-0x2F vues en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494).", +"%lld unread": "%lld non lus", +"%lld weeks ago": "il y a %lld semaines", +"%lld workouts": "%lld entraînements", +"%lld year%@ %@ than your age": "%1$lld an%2$@ %3$@ que votre âge", +"%lld years older than your age": "%lld ans de plus que votre âge", +"%lld years younger than your age": "%lld ans de moins que votre âge", +"%lld yr%@ %@": "%1$lld an%2$@ %3$@", +"%lld yrs": "%lld ans", +"%lld yrs older": "%lld ans de plus", +"%lld yrs younger": "%lld ans de moins", +"%lld%% Charge": "%lld%% de Charge", +"%lld%% of today's goal": "%lld%% de l'objectif du jour", +"%lld+ drinks": "%lld+ boissons", +"%lld-minute average": "Moyenne sur %lld minutes", +"%lld-second average": "Moyenne sur %lld secondes", +"%lld/7 days": "%lld/7 jours", +"%lldd ago": "il y a %lldj", +"%lldh %lldm": "%1$lldh %2$lldm", +"%lldh ago": "il y a %lldh", +"%lldh elevated": "%lldh élevée", +"%lldm ago": "il y a %lldmin", +"%lldm in zone": "%lldm en zone", +"%llds left · %lld beats": "%1$llds restantes · %2$lld battements", +"%lld–%lld%@ · step %lld": "%lld–%lld%@ · étape %lld", +"%lld′ %lld″": "%1$lld′ %2$lld″", +"(nothing to share yet)": "(rien à partager pour l'instant)", +"* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); no steps over BLE on a 4.0. No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (selon le firmware) ; pas de pas via BLE sur un 4.0. Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", +"* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw motion count (#78). No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut de mouvement (#78). Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", +"0 pts": "0 pt", +"0–%@ scale": "Échelle 0–%@", +"1 app on": "1 application active", +"1 day": "1 jour", +"1 day your phone also counted": "1 jour également compté par votre téléphone", +"1 day · %@": "1 jour · %@", +"1 day · %@ · some sparse series widened": "1 jour · %@ · certaines séries clairsemées élargies", +"1 device reading your heart rate": "1 appareil lit votre fréquence cardiaque", +"1 frame captured this session.": "1 image capturée lors de cette séance.", +"1 hour": "1 heure", +"1 marker": "1 marqueur", +"1 marker tracked · %lld readings": "1 marqueur suivi · %lld mesures", +"1 marker tracked · 1 reading": "1 marqueur suivi · 1 mesure", +"1 new day of history landed. Open Trends to see it.": "1 nouveau jour d'historique est arrivé. Ouvrez Tendances pour le voir.", +"1 night ago": "il y a 1 nuit", +"1 night to go": "encore 1 nuit", +"1 pair": "1 paire", +"1 reading across %lld · %@": "1 mesure sur %1$lld · %2$@", +"1 reading across %lld · %@ · sparse widened": "1 mesure sur %1$lld · %2$@ · clairsemé élargi", +"1 reading lines up so far, not enough to read a trend yet (NOOP waits for %lld).": "1 mesure concorde jusqu'ici, pas encore assez pour lire une tendance (NOOP attend %lld).", +"1 reading · %@": "1 mesure · %@", +"1 reading · sparse, widened to %@": "1 mesure · clairsemé, élargi à %@", +"1 reading · your own entries": "1 mesure · votre propre entrée", +"1 row skipped": "1 ligne ignorée", +"1 session": "1 séance", +"1 session · %@": "1 séance · %@", +"1 session · sparse, widened to %@": "1 séance · clairsemé, élargi à %@", +"1 type-0x2F historical-offload frame seen outside our sync. These are history (e.g. another app pulling the strap's backlog), not a live R22 stream (#494).": "1 trame de déchargement historique type-0x2F vue en dehors de notre synchronisation. Il s'agit d'historique (par ex. une autre app qui récupère l'arriéré du bracelet), pas d'un flux R22 en direct (#494).", +"1 workout": "1 entraînement", +"1 year": "1 an", +"1 year older than your age": "1 an de plus que votre âge", +"1 year younger than your age": "1 an de moins que votre âge", +"1 yr older": "1 an de plus", +"1 yr younger": "1 an de moins", +"14 days": "14 jours", +"14-day trend": "Tendance sur 14 jours", +"14d": "14j", +"180 DAYS": "180 JOURS", +"1Y": "1A", +"28d": "28j", +"2pm+": "à partir de 14h", +"3 months": "3 mois", +"30 DAYS": "30 JOURS", +"30 days": "30 jours", +"30D": "30J", +"30d": "30j", +"365 DAYS": "365 JOURS", +"5-minute average · selected day": "Moyenne sur 5 minutes · jour sélectionné", +"5-minute average · since midnight": "Moyenne sur 5 minutes · depuis minuit", +"6 months": "6 mois", +"7 DAYS": "7 JOURS", +"7 days": "7 jours", +"7D": "7J", +"7d": "7j", +"90 DAYS": "90 JOURS", +"90D": "90J", +"90d": "90j", +"A %@ tonight tends to line up with about %@ higher on tomorrow’s %@ for you, based on %lld of your %@ days.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de plus sur votre %3$@ de demain, sur la base de %4$lld de vos %5$@ jours.", +"A %@ tonight tends to line up with about %@ higher on tomorrow’s %@ for you, based on typical patterns.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de plus sur votre %3$@ de demain, sur la base de schémas typiques.", +"A %@ tonight tends to line up with about %@ lower on tomorrow’s %@ for you, based on %lld of your %@ days.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de %4$lld de vos %5$@ jours.", +"A %@ tonight tends to line up with about %@ lower on tomorrow’s %@ for you, based on typical patterns.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de schémas typiques.", +"A 60-second snapshot of your beat-to-beat (R-R) intervals from the strap, cleaned (range and ectopic-beat filtering) before computing RMSSD the same way your overnight HRV is computed.": "Un instantané de 60 secondes de vos intervalles battement à battement (R-R) provenant du bracelet, nettoyés (filtrage de plage et des battements ectopiques) avant de calculer le RMSSD de la même façon que votre VFC nocturne.", +"A WHOOP export backfills recovery, strain, sleep and workouts. Apple Health can add HR, HRV, sleep, SpO₂, steps, workouts and weight.": "Un export WHOOP complète rétroactivement récupération, effort, sommeil et entraînements. Apple Santé peut ajouter FC, VFC, sommeil, SpO₂, pas, entraînements et poids.", +"A WHOOP membership runs $300–480 a year, for life. NOOP does this for free: one person, no servers, no subscription.": "Un abonnement WHOOP coûte 300 à 480 $ par an, à vie. NOOP fait cela gratuitement : une personne, aucun serveur, aucun abonnement.", +"A badge shows whether a number was scored on-device by NOOP, or imported from Whoop or Apple Health.": "Un badge indique si un chiffre a été calculé sur l'appareil par NOOP, ou importé depuis Whoop ou Apple Santé.", +"A baseline-normalized recovery score: your resting heart rate, sleep quality and night-to-night consistency, weighted against your own baseline, with heart-rate variability (rMSSD) leading wherever the strap gives us a clean reading.": "Un score de récupération normalisé par rapport à une référence : votre fréquence cardiaque au repos, la qualité de votre sommeil et la régularité nuit après nuit, pondérées par rapport à votre propre référence, la variabilité de la fréquence cardiaque (rMSSD) prenant le dessus partout où le bracelet fournit une mesure propre.", +"A blend of how long you slept versus your personal need (the biggest factor), how efficiently (asleep versus in bed), how much was restorative (deep + REM sleep), and how consistent your sleep and wake timing is.": "Un mélange de la durée de votre sommeil par rapport à votre besoin personnel (le facteur le plus important), de son efficacité (endormi par rapport au temps au lit), de sa part réparatrice (sommeil profond + REM), et de la régularité de vos horaires de sommeil et de réveil.", +"A bulk markers CSV import (date, marker, value, unit) lands with the file importers in Data Sources, same as nutrition and lifting. For now, add readings one at a time above. Everything you import stays on %@.": "Un import CSV groupé de marqueurs (date, marqueur, valeur, unité) arrive avec les importateurs de fichiers dans Sources de données, comme pour la nutrition et la musculation. Pour l'instant, ajoutez les mesures une par une ci-dessus. Tout ce que vous importez reste sur %@.", +"A buzz, not a banner": "Une vibration, pas une bannière", +"A calm evening reminder, timed from your wake time and usual sleep need. It's a suggestion, not an alarm.": "Un rappel calme le soir, calé sur votre heure de réveil et votre besoin de sommeil habituel. C'est une suggestion, pas une alarme.", +"A calm hour now helps you hit your wake time well-rested.": "Une heure calme maintenant vous aide à atteindre votre heure de réveil bien reposé.", +"A calm nudge %lldh %lldm before your wake time.": "Un rappel discret %1$lldh %2$lldm avant votre heure de réveil.", +"A cardiovascular load in the Banister TRIMP family: time spent in each heart-rate zone, weighted so harder zones count for more, summed into one daily figure.": "Une charge cardiovasculaire de la famille Banister TRIMP : le temps passé dans chaque zone de fréquence cardiaque, pondéré pour que les zones plus intenses comptent davantage, additionné en un seul chiffre quotidien.", +"A clean, shareable one-page PDF of your recovery, sleep, HRV, resting heart rate and strain over a date range. Saved on your %@. Nothing leaves the device.": "Un PDF d'une page, clair et partageable, de votre récupération, sommeil, VFC, fréquence cardiaque au repos et effort sur une période donnée. Enregistré sur votre %@. Rien ne quitte l'appareil.", +"A couple of metrics depend on which Apple Watch you wear. Wrist temperature, which feeds skin temp, arrived with Series 8, so older watches don't report it. Blood oxygen is the bigger one: Apple removed the SpO₂ sensor from the newest US units over a patent dispute, so those simply don't measure it.": "Quelques métriques dépendent de l'Apple Watch que vous portez. La température du poignet, qui alimente la température cutanée, est arrivée avec la Series 8 ; les montres plus anciennes ne la fournissent donc pas. L'oxygène sanguin est le cas le plus notable : Apple a retiré le capteur SpO₂ des derniers modèles américains à la suite d'un litige de brevet, ces derniers ne le mesurent donc tout simplement pas.", +"A couple of metrics depend on your model": "Quelques métriques dépendent de votre modèle", +"A few honest words, so nothing's a surprise.": "Quelques mots honnêtes, pour qu'il n'y ait pas de surprise.", +"A few more days and we can show your Fitness Age.": "Encore quelques jours et nous pourrons afficher votre Âge de forme physique.", +"A few more days and we can show your Vitality.": "Encore quelques jours et nous pourrons afficher votre Vitalité.", +"A few more days of wear, plus the basics below, and we can show your Fitness Age.": "Encore quelques jours de port, plus les informations de base ci-dessous, et nous pourrons afficher votre Âge de forme physique.", +"A few signals are up": "Quelques signaux sont en hausse", +"A gentle buzz when your HRV drops while your heart rate is calm, a cue to take a paced breath. Rate-limited to once every 15 minutes; off by default.": "Une vibration douce quand votre VFC chute alors que votre fréquence cardiaque est calme, une invitation à respirer à un rythme régulier. Limité à une fois toutes les 15 minutes ; désactivé par défaut.", +"A gentle wrist buzz when you've been sitting too long, a nudge to get up and move. Inferred from the strap's motion on each history sync, so it lags real time by a sync or two.": "Une légère vibration au poignet quand vous êtes assis depuis trop longtemps, une invitation à vous lever et à bouger. Déduite du mouvement du bracelet à chaque synchronisation d'historique, elle accuse donc un retard d'une ou deux synchronisations par rapport au temps réel.", +"A logged caffeine is past your bedtime cutoff. It may still be on board and keep you up. Just a timing heads-up.": "Une prise de caféine consignée dépasse votre limite avant le coucher. Elle peut encore agir et vous empêcher de dormir. Juste un repère de timing.", +"A lower resting heart rate usually means a higher charge.": "Une fréquence cardiaque au repos plus basse signifie généralement une charge plus élevée.", +"A note on blood oxygen and your model": "Une note sur l'oxygène sanguin et votre modèle", +"A period is likely between %@ and %@ (a window, not a fixed date).": "Un cycle est probable entre le %1$@ et le %2$@ (une fenêtre, pas une date fixe).", +"A private notebook, not a medical service.": "Un carnet privé, pas un service médical.", +"A private window into your recovery, sleep and strain, read straight from your strap, kept only on this Mac.": "Une fenêtre privée sur votre récupération, sommeil et effort, lu directement depuis votre bracelet, conservé uniquement sur ce Mac.", +"A private window into your recovery, sleep and strain. Read straight from your strap, kept only on %@.": "Une fenêtre privée sur votre récupération, votre sommeil et votre effort. Lue directement depuis votre bracelet, conservée uniquement sur %@.", +"A quick word before we connect": "Un mot rapide avant de nous connecter", +"A read-only export of the decoded sensor streams NOOP already stores. Works on any strap. Nothing is written to your device, and nothing is uploaded.": "Un export en lecture seule des flux de capteurs décodés que NOOP stocke déjà. Fonctionne avec n'importe quel bracelet. Rien n'est écrit sur votre appareil, rien n'est envoyé en ligne.", +"A relaxation rhythm, not cardiac control. It never paces below a safe rate and you can stop anytime. If your heart rate doesn't settle, we'll say so plainly.": "Un rythme de relaxation, pas un contrôle cardiaque. Il ne descend jamais sous une fréquence sûre et vous pouvez arrêter à tout moment. Si votre fréquence cardiaque ne se stabilise pas, nous vous le dirons clairement.", +"A reminder to recharge before bed when the strap drops to 15%, and a heads-up when it reaches 100%, each at most once per charge cycle.": "Un rappel de recharger avant le coucher quand le bracelet descend à 15 %, et un avis quand il atteint 100 %, chacun au plus une fois par cycle de charge.", +"A ring talks to one owner at a time.": "Une bague ne communique qu'avec un seul propriétaire à la fois.", +"A shareable one-page PDF of recovery, sleep, HRV, resting HR and strain over a range, saved on your %@.": "Un PDF d'une page partageable de récupération, sommeil, VFC, FC au repos et effort sur une période, enregistré sur votre %@.", +"A signature ring distils HRV, resting heart rate and sleep into one calm read on whether to push or rest.": "Une bague signature condense VFC, fréquence cardiaque au repos et sommeil en une lecture claire pour savoir s'il faut pousser ou se reposer.", +"A simple goal that adjusts to your effort. General wellness guidance, not medical advice.": "Un objectif simple qui s'adapte à votre effort. Orientation générale de bien-être, pas un avis médical.", +"A standalone companion for your WHOOP. Everything stays on this device, your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil, votre historique, votre flux en direct, vos chiffres. Rien n'est téléchargé. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", +"A standalone companion for your WHOOP. Everything stays on this device: your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil : votre historique, votre flux en direct, vos chiffres. Rien n'est envoyé en ligne. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", +"A still, seated snapshot of your heart-rate variability": "Un instantané, assis et immobile, de votre variabilité de fréquence cardiaque", +"A sync is already in progress.": "Une synchronisation est déjà en cours.", +"A transparent cardiorespiratory recipe that recovers deep and REM better than the default staging. Opt-in and experimental: it only changes how already-detected nights are split into stages (detection and scores are unchanged), and the default staging stays in place if you leave this off. Takes effect on the next nights staged.": "Une recette cardiorespiratoire transparente qui restitue mieux le sommeil profond et REM que la répartition par défaut. Facultatif et expérimental : cela change seulement la manière dont les nuits déjà détectées sont réparties en phases (la détection et les scores restent inchangés), et la répartition par défaut reste en place si vous laissez cette option désactivée. Prend effet sur les prochaines nuits traitées.", +"A typical recent day": "Une journée récente typique", +"A watch samples your heart-rate variability rather than streaming it all night, so your Charge score needs roughly seven nights to calibrate. Until then NOOP shows “needs more data”, never a guessed number.": "Une montre échantillonne votre variabilité de fréquence cardiaque au lieu de la diffuser toute la nuit ; votre score de Charge a donc besoin d'environ sept nuits pour se calibrer. En attendant, NOOP affiche « données insuffisantes », jamais un chiffre deviné.", +"A weekly digest needs a few days of history. Wear your strap or import your WHOOP export in Data Sources.": "Un résumé hebdomadaire nécessite quelques jours d'historique. Portez votre bracelet ou importez votre export WHOOP dans Sources de données.", +"A wellness estimate from your habits, not a clinical biological age.": "Une estimation de bien-être basée sur vos habitudes, pas un âge biologique clinique.", +"ADVANCED": "AVANCÉ", +"ALL": "TOUT", +"ALL TIME": "DEPUIS TOUJOURS", +"ALL YOUR DATA · NONE OF THE CLOUD": "TOUTES VOS DONNÉES · SANS LE CLOUD", +"ALL-OUT": "MAXIMAL", +"API key": "Clé API", +"API key (optional)": "Clé API (facultatif)", +"AVG": "MOY", +"AVG HR": "FC MOY", +"AVG/SESS": "MOY/SÉANCE", +"About": "À propos", +"About %lld mg may still be active": "Environ %lld mg pourraient encore être actifs", +"About %lld min earlier than your schedule": "Environ %lld min plus tôt que prévu", +"About %lld min later than your schedule": "Environ %lld min plus tard que prévu", +"About Apple Watch data": "À propos des données Apple Watch", +"About Lab Book": "À propos de Lab Book", +"About in sync with your schedule": "À peu près en phase avec votre programme", +"About the same as your age": "À peu près comme votre âge", +"About this nap": "À propos de cette sieste", +"About you": "À propos de vous", +"About your main sleep": "À propos de votre sommeil principal", +"Accept & Continue": "Accepter et continuer", +"Across the quiet windows we could read, your beat-to-beat timing held a tight, even shape.": "Sur les fenêtres calmes que nous avons pu lire, le rythme battement à battement est resté serré et régulier.", +"Active": "Actif", +"Active Energy": "Énergie active", +"Active energy": "Énergie active", +"Active hours end": "Fin des heures actives", +"Active hours start": "Début des heures actives", +"Active · Live": "Actif · En direct", +"Activity": "Activité", +"Activity & Energy": "Activité et énergie", +"Activity Breakdown": "Répartition de l'activité", +"Activity Cost": "Coût de l'activité", +"Acts as a standard Bluetooth heart-rate strap. Pair NOOP from your treadmill, bike or app to see your strap's heart rate there.": "Fonctionne comme un ceinture cardio Bluetooth standard. Appairez NOOP depuis votre tapis, vélo ou application pour y voir la fréquence cardiaque de votre bracelet.", +"Acute (7-day) vs chronic (28-day) training load. 0.8–1.3 is the sweet spot.": "Charge d'entraînement aiguë (7 jours) vs chronique (28 jours). 0,8–1,3 est la zone idéale.", +"Add": "Ajouter", +"Add Workout": "Ajouter un entraînement", +"Add a custom marker": "Ajouter un marqueur personnalisé", +"Add a custom question…": "Ajouter une question personnalisée…", +"Add a device": "Ajouter un appareil", +"Add a marker reading": "Ajouter une mesure de marqueur", +"Add a metric to compare": "Ajouter une métrique à comparer", +"Add a nap": "Ajouter une sieste", +"Add a reading": "Ajouter une mesure", +"Add a workout": "Ajouter un entraînement", +"Add metric": "Ajouter une métrique", +"Add nap": "Ajouter une sieste", +"Add workout": "Ajouter un entraînement", +"Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On %@ only. Nothing is synced.": "Ajoute un journal d'hydratation simple avec un objectif quotidien qui s'ajuste à votre effort. Touchez pour ajouter une gorgée, une tasse ou une bouteille et regardez l'anneau de progression se remplir. Sur %@ uniquement. Rien n'est synchronisé.", +"Adds a water-log card to your dashboard": "Ajoute une carte de suivi de l'eau à votre tableau de bord", +"Adjust amount": "Ajuster la quantité", +"Adjust manually": "Ajuster manuellement", +"Adjust this session's details.": "Ajustez les détails de cette séance.", +"Advanced": "Avancé", +"Advanced HRV": "VFC avancée", +"Advanced. I already have my ring's key.": "Avancé. J'ai déjà la clé de ma bague.", +"Advanced: I already have my ring's key": "Avancé : j'ai déjà la clé de ma bague", +"Advanced: use your own key": "Avancé : utiliser votre propre clé", +"Aerobic": "Aérobie", +"After a sync, NOOP looks over your recent heart rate for a sustained, raised stretch that looks like exercise and offers to save it. It only ever suggests. Nothing is saved until you tap Save, and you can dismiss any suggestion. Deliberately conservative, so the odd workout may be missed. On %@ only.": "Après une synchronisation, NOOP examine votre fréquence cardiaque récente à la recherche d'une période soutenue et élevée qui ressemble à de l'exercice, et propose de l'enregistrer. Il ne fait que suggérer. Rien n'est enregistré tant que vous n'appuyez pas sur Enregistrer, et vous pouvez ignorer toute suggestion. Volontairement prudent, il se peut donc qu'un entraînement occasionnel passe inaperçu. Sur %@ uniquement.", +"After your iPhone reboots, unlock it once. Until you do, iOS keeps NOOP's files locked (Data Protection), so new history can't be written or synced.": "Après le redémarrage de votre iPhone, déverrouillez-le une fois. Jusque-là, iOS garde les fichiers de NOOP verrouillés (protection des données), donc aucun nouvel historique ne peut être écrit ni synchronisé.", +"Age": "Âge", +"Age, %lld years": "Âge, %lld ans", +"Alarms": "Alarmes", +"Alcohol": "Alcool", +"Alert": "Alerte", +"Alerts": "Alertes", +"All": "Tout", +"All Sessions": "Toutes les séances", +"All caught up": "Tout est à jour", +"All history": "Tout l'historique", +"All of this is computed on your own device from data you already have. No heartbeat data leaves it.": "Tout ceci est calculé sur votre propre appareil à partir de données que vous avez déjà. Aucune donnée de battement cardiaque n'en sort.", +"All stays on %@. Nothing is sent anywhere.": "Tout reste sur %@. Rien n'est envoyé nulle part.", +"Allow Apple Health access": "Autoriser l'accès à Apple Santé", +"Allow access": "Autoriser l'accès", +"Already clean.": "Déjà propre.", +"Also share my patterns & Lab Book": "Partager aussi mes tendances et mon carnet de laboratoire", +"Also share my patterns and Lab Book with the coach": "Partager aussi mes tendances et mon carnet de laboratoire avec le Coach", +"Always rely on your doctor, pharmacist, or a qualified professional to interpret results and make decisions. If a number worries you, talk to them, not to an app.": "Fiez-vous toujours à votre médecin, pharmacien ou un professionnel qualifié pour interpréter les résultats et prendre des décisions. Si un chiffre vous inquiète, parlez-en à eux, pas à une application.", +"Amount": "Quantité", +"Amount in mg (optional)": "Quantité en mg (facultatif)", +"Amount in millilitres": "Quantité en millilitres", +"An HRV reading needs the live R-R stream. Open the Live screen and connect your strap, then come back.": "Une mesure de VFC nécessite le flux R-R en direct. Ouvrez l'écran En direct et connectez votre bracelet, puis revenez.", +"An estimated VO2max from the Nes / HUNT Fitness Study model (resting heart rate, age and activity), read against population norms to express it as a fitness age.": "Un VO2max estimé selon le modèle de l'étude Nes / HUNT Fitness (fréquence cardiaque au repos, âge et activité), comparé aux normes de population pour l'exprimer sous forme d'âge physique.", +"An experimental picture of your beat-to-beat heart timing. Not an ECG and not a diagnosis. You'll read and accept an experimental note before it shows anything.": "Un aperçu expérimental du rythme battement par battement de votre cœur. Ce n'est ni un ECG ni un diagnostic. Vous lirez et accepterez une note expérimentale avant qu'il n'affiche quoi que ce soit.", +"An experimental picture of your beat-to-beat timing": "Un aperçu expérimental de votre rythme battement par battement", +"An experimental picture of your beat-to-beat timing. Please read these first.": "Un aperçu expérimental de votre rythme battement par battement. Veuillez d'abord lire ceci.", +"Analyse my sleep": "Analyser mon sommeil", +"And make sure it's charged.": "Et assurez-vous qu'il est chargé.", +"Answers are about the night and day leading into this morning, the same attribution a WHOOP export uses, so logged and imported days line up.": "Les réponses portent sur la nuit et la journée précédant ce matin, la même attribution qu'utilise un export WHOOP, afin que les jours enregistrés et importés correspondent.", +"Any OpenAI-compatible server: Ollama, LM Studio, llama.cpp, or your own gateway. Stays on your network; nothing leaves %@.": "Tout serveur compatible OpenAI : Ollama, LM Studio, llama.cpp, ou votre propre passerelle. Reste sur votre réseau ; rien ne quitte %@.", +"Any amount helps. Thank you, genuinely.": "Tout montant est utile. Merci, sincèrement.", +"Anything that needs Oura's cloud (web dashboard, Oura's coaching, shared circles).": "Tout ce qui nécessite le cloud d'Oura (tableau de bord web, coaching Oura, cercles partagés).", +"Anything you see here (including any side-by-side trend) is your own information shown back to you. It's an association, never a cause, and never a medical finding.": "Tout ce que vous voyez ici (y compris toute tendance comparée) est votre propre information qui vous est restituée. C'est une association, jamais une cause, et jamais un constat médical.", +"App icon": "Icône de l'application", +"Appearance": "Apparence", +"Apple Health": "Apple Santé", +"Apple Health (Live)": "Apple Santé (En direct)", +"Apple Health isn't available on %@.": "Apple Santé n'est pas disponible sur %@.", +"Apple Health isn't available on this device, so there's nothing to connect here.": "Apple Santé n'est pas disponible sur cet appareil, il n'y a donc rien à connecter ici.", +"Apple Health lives on the iPhone, not the Mac, so connecting your Apple Watch happens there. Open NOOP on your iPhone, head to Settings, and run this same Apple Watch setup. Your scores then show up across your devices.": "Apple Santé se trouve sur l'iPhone, pas sur le Mac, donc la connexion de votre Apple Watch se fait là-bas. Ouvrez NOOP sur votre iPhone, allez dans Paramètres et effectuez cette même configuration Apple Watch. Vos scores apparaîtront ensuite sur tous vos appareils.", +"Apple Health sync failed: %@": "Échec de la synchronisation avec Apple Santé : %@", +"Apple Watch. Needs more data to score this yet.": "Apple Watch. A besoin de plus de données pour évaluer cela.", +"Apple's own sleep stages drive Rest directly. This is one of the watch's strengths.": "Les phases de sommeil propres à Apple alimentent directement le Repos. C'est l'un des points forts de la montre.", +"Apple's sleep stages are strong, and they drive your Rest score directly.": "Les phases de sommeil d'Apple sont fiables et alimentent directement votre score de Repos.", +"Apple-logged": "Enregistré par Apple", +"Apr": "Avr", +"Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. We send the same alarm command the official app sends, but a strap-driven wake-up hasn't been confirmed on our side yet, so please keep a backup alarm for now.": "Armée sur le bracelet lui-même, elle peut donc vibrer à votre heure de réveil même si votre téléphone est en veille ou NOOP fermé. Nous envoyons la même commande d'alarme que l'application officielle, mais un réveil piloté par le bracelet n'a pas encore été confirmé de notre côté, alors gardez pour l'instant une alarme de secours.", +"Arms the strap to buzz at your wake time, even if NOOP is closed. Still experimental on WHOOP 4.0, so keep a backup alarm until you've confirmed it wakes you.": "Arme le bracelet pour vibrer à votre heure de réveil, même si NOOP est fermé. Encore expérimental sur WHOOP 4.0, gardez donc une alarme de secours jusqu'à ce que vous ayez confirmé qu'il vous réveille.", +"Ask Coach about your data…": "Demandez à Coach à propos de vos données…", +"Ask about your charge, effort, rest and workouts, grounded in your own numbers.": "Posez des questions sur votre charge, votre effort, votre repos et vos entraînements, ancrées dans vos propres chiffres.", +"Ask about your recovery, strain, sleep and workouts, grounded in your own numbers.": "Posez des questions sur votre récupération, effort, sommeil et entraînements, basées sur vos propres chiffres.", +"Ask your first question": "Posez votre première question", +"Asleep": "Endormi", +"Asleep Time": "Temps de sommeil", +"Asleep avg": "Moy. endormi", +"Asleep duration": "Durée de sommeil", +"At a glance": "En un coup d'œil", +"Attach this to a bug report.": "Joignez ceci à un rapport de bug.", +"Audio cues": "Signaux sonores", +"Aug": "Août", +"Auto · %@ confidence": "Auto · confiance %@", +"Auto-detect workouts": "Détecter automatiquement les entraînements", +"Auto-nudge": "Rappel automatique", +"Automatic": "Automatique", +"Automation": "Automatisation", +"Automations": "Automatisations", +"Autonomic balance (LF/HF)": "Équilibre autonome (LF/HF)", +"Autonomic load": "Charge autonome", +"Autonomic load from HRV and resting heart rate": "Charge autonome à partir de la VFC et de la FC au repos", +"Autonomic load through the day": "Charge autonome au fil de la journée", +"Autonomic load today: %@": "Charge autonome aujourd'hui : %@", +"Autonomic rigidity from your heart-rate rhythm. Higher means a more rigid, stressed rhythm.": "Rigidité autonome déduite du rythme de votre fréquence cardiaque. Plus c'est élevé, plus le rythme est rigide et stressé.", +"Average": "Moyenne", +"Average HR must be 25–250 bpm.": "La FC moyenne doit être comprise entre 25 et 250 bpm.", +"Average Heart Rate": "Fréquence cardiaque moyenne", +"Average heart rate": "Fréquence cardiaque moyenne", +"Average heart rate in beats per minute, optional": "Fréquence cardiaque moyenne en battements par minute, facultatif", +"Avg": "Moy.", +"Avg HR": "FC moy.", +"Avg HR is shown as typed. The HR graph, zones and Effort stay from the recorded session.": "La FC moy. est affichée telle que saisie. Le graphique de FC, les zones et l'Effort restent ceux de la séance enregistrée.", +"Avg pace": "Allure moy.", +"Awaiting strap": "En attente du bracelet", +"Awake": "Éveillé", +"BANISTER TRIMP / HR ZONES": "BANISTER TRIMP / ZONES DE FC", +"BEAT-TO-BEAT": "BATTEMENT PAR BATTEMENT", +"BEAT-TO-BEAT SCATTER": "NUAGE BATTEMENT PAR BATTEMENT", +"BEATS READ": "BATTEMENTS LUS", +"BMI": "IMC", +"Back": "Retour", +"Back to standard setup": "Retour à la configuration standard", +"Back to the marker list": "Retour à la liste des marqueurs", +"Back up now": "Sauvegarder maintenant", +"Backed up": "Sauvegardé", +"Background Bluetooth has OS limits: iOS may pause NOOP when it's not in the foreground, so keep it open while syncing a fresh strap.": "Le Bluetooth en arrière-plan a des limites au niveau du système : iOS peut mettre NOOP en pause lorsqu'il n'est pas au premier plan, alors gardez-le ouvert pendant la synchronisation d'un nouveau bracelet.", +"Backs up to your folder about once a day and keeps the latest %lld. On this platform it runs when you next open NOOP.": "Sauvegarde dans votre dossier environ une fois par jour et conserve les %lld plus récentes. Sur cette plateforme, cela s'exécute à la prochaine ouverture de NOOP.", +"Backup & Sync": "Sauvegarde et synchronisation", +"Backup & Sync to a folder…": "Sauvegarde et synchronisation vers un dossier…", +"Backup & restore": "Sauvegarde et restauration", +"Backup exported": "Sauvegarde exportée", +"Backup failed - re-pick the folder and try again.": "Échec de la sauvegarde, resélectionnez le dossier et réessayez.", +"Backup failed: %@": "Échec de la sauvegarde : %@", +"Backup folder": "Dossier de sauvegarde", +"Backup imported": "Sauvegarde importée", +"Backup problem": "Problème de sauvegarde", +"Backup wake: your smart alarm time is here.": "Réveil de secours : votre heure d'alarme intelligente est arrivée.", +"Baevsky Stress Index": "Indice de stress de Baevsky", +"Balance": "Équilibre", +"Balanced": "Équilibré", +"Based mostly on typical patterns, not yet yours. Log a few more %@ days and this becomes yours.": "Basé principalement sur des tendances types, pas encore les vôtres. Enregistrez encore quelques jours de %@ et cela deviendra le vôtre.", +"Baseline": "Référence", +"Baselines are learned on-device over your first 14 days. Until then, typical ranges apply.": "Les références sont apprises sur l'appareil au cours de vos 14 premiers jours. D'ici là, des plages types s'appliquent.", +"Battery": "Batterie", +"Battery %lld percent": "Batterie à %lld pour cent", +"Battery %lld%%": "Batterie %lld%%", +"Battery %lld%% · Charging": "Batterie %lld%% · En charge", +"Battery alerts": "Alertes de batterie", +"Beat-to-beat timing varies for many ordinary reasons: breathing, movement, an imperfect optical reading, or the occasional extra or skipped beat that most healthy people have.": "Le rythme battement par battement varie pour de nombreuses raisons ordinaires : la respiration, le mouvement, une lecture optique imparfaite, ou le battement occasionnel en trop ou manquant que la plupart des personnes en bonne santé ont.", +"Beats": "Battements", +"Beats per minute": "Battements par minute", +"Beats per minute across the session": "Battements par minute au cours de la séance", +"Bedtime": "Heure du coucher", +"Before you turn on Rhythm": "Avant d'activer Rhythm", +"Before you use NOOP": "Avant d'utiliser NOOP", +"Behaviour": "Comportement", +"Behaviour Effects": "Effets comportementaux", +"Below Zone 1": "Sous la zone 1", +"Below need": "Sous les besoins", +"Beta": "Bêta", +"Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, and HRV needs you to be still. No Oura Readiness or SpO₂ percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO₂ ne provient de la bague (importez un fichier Oura pour cela).", +"Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, les pas sont un simple comptage de mouvement, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO2 ne provient de la bague (importez un fichier Oura pour cela).", +"Beta. Read this first.": "Bêta. Lisez ceci d'abord.", +"Blood Oxygen": "Oxygène sanguin", +"Blood O₂": "O₂ sanguin", +"Blood oxygen": "Oxygène sanguin", +"Blood oxygen (SpO2 %)": "Oxygène sanguin (SpO2 %)", +"Blood oxygen (SpO₂)": "Oxygène sanguin (SpO₂)", +"Blood panel": "Bilan sanguin", +"Blood pressure": "Tension artérielle", +"Body": "Corps", +"Body Age": "Âge corporel", +"Body Age %lld": "Âge corporel %lld", +"Body Composition": "Composition corporelle", +"Body Fat": "Graisse corporelle", +"Body clock": "Horloge biologique", +"Body fat": "Graisse corporelle", +"Bond your strap on the Live screen to feel the transitions hands-free.": "Associez votre bracelet sur l'écran En direct pour ressentir les transitions sans les mains.", +"Bonded · streaming": "Associé · flux actif", +"Bonded, streaming.": "Associé, flux actif.", +"Bonded. You're set.": "Associé. Vous êtes prêt.", +"Both NOOP and the Oura app can use a ring you own by key, but only one can hold the Bluetooth link at a time.": "NOOP et l'application Oura peuvent tous deux utiliser une bague que vous possédez via une clé, mais un seul peut détenir la liaison Bluetooth à la fois.", +"Bottle": "Bouteille", +"Bounce back": "Récupération", +"Box": "Carré", +"Box 4-4": "Carré 4-4", +"Breathe": "Respirer", +"Breathe at the pace your last “find my pace” sweep locked in, if you have one. Otherwise a calm 5.5 breaths/min.": "Respirez au rythme fixé par votre dernier balayage « trouver mon rythme », si vous en avez un. Sinon, un rythme calme de 5,5 respirations/min.", +"Breathe in": "Inspirez", +"Breathe in for %lld seconds": "Inspirez pendant %lld secondes", +"Breathe in…": "Inspirez…", +"Breathe mode": "Mode respiration", +"Breathe now": "Respirez maintenant", +"Breathe out": "Expirez", +"Breathe out for %lld seconds": "Expirez pendant %lld secondes", +"Breathe out…": "Expirez…", +"Breaths per minute": "Respirations par minute", +"Bring your history": "Apportez votre historique", +"Broadcast HR is ON. Your strap is advertising its heart rate continuously, which keeps its radio hot and drains the battery faster. Turn it off when you're not using it with another device.": "La diffusion de la FC est ACTIVÉE. Votre bracelet diffuse en continu sa fréquence cardiaque, ce qui maintient sa radio active et vide la batterie plus vite. Désactivez-la lorsque vous ne l'utilisez pas avec un autre appareil.", +"Broadcast heart rate": "Diffuser la fréquence cardiaque", +"Broadcast heart rate (Garmin/ANT)": "Diffuser la fréquence cardiaque (Garmin/ANT)", +"Broadcast heart rate as a Bluetooth sensor": "Diffuser la fréquence cardiaque comme capteur Bluetooth", +"Broadcasting": "Diffusion en cours", +"Broadcasts the live heart rate NOOP derives from the strap once connected.": "Diffuse la fréquence cardiaque en direct que NOOP déduit du bracelet une fois connecté.", +"Broadcasts the strap's own live heart rate over Bluetooth.": "Diffuse via Bluetooth la fréquence cardiaque en direct propre au bracelet.", +"Bug report": "Rapport de bug", +"Build your own": "Créez le vôtre", +"Building": "Construction en cours", +"Building (1 window)": "Construction en cours, 1 fenêtre", +"Building from your strap": "Construction depuis votre bracelet", +"Building your baseline": "Construction de votre référence", +"Building your baseline. About %lld more %@ until your scores are personal.": "Construction de votre référence. Encore environ %1$lld %2$@ avant que vos scores ne soient personnalisés.", +"Building your baseline. About %lld more nights until your scores are personal.": "Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés.", +"Building your baseline. About 1 more night until your scores are personal.": "Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés.", +"Building your best-sourced record…": "Construction de votre relevé le mieux documenté…", +"Building, moves as you do": "Construction, évolue avec vous", +"Building, wear it tonight": "Construction, portez-le ce soir", +"Building. Keep logging.": "En construction. Continuez à enregistrer.", +"Builds a redacted .zip, shows you exactly what it contains, then opens a prefilled GitHub issue. You attach the file on the next screen.": "Crée un .zip anonymisé, vous montre exactement son contenu, puis ouvre une issue GitHub prérempli. Vous joignez le fichier à l'écran suivant.", +"Built from Apple's cardio-fitness VO₂ max estimate, the same number the Fitness app shows.": "Calculé à partir de l'estimation du VO₂ max cardio d'Apple, le même chiffre qu'affiche l'application Fitness.", +"Built from published methods (Nes/HUNT) on %@. It's a fitness comparison against an average peer your age, not a biological or medical age.": "Calculé selon des méthodes publiées (Nes/HUNT) sur %@. C'est une comparaison de forme physique avec une personne moyenne de votre âge, pas un âge biologique ou médical.", +"Built from the watch's cardio-fitness VO₂ max, the same number the Fitness app shows.": "Calculé à partir du VO₂ max cardio de la montre, le même chiffre qu'affiche l'application Fitness.", +"Built on": "Construit le", +"Buzz Strap": "Faire vibrer le bracelet", +"Buzz back (confirm)": "Vibration en retour (confirmer)", +"Buzz cues on": "Signaux haptiques activés", +"Buzz strap": "Vibrer le bracelet", +"Buzz strength": "Intensité de la vibration", +"Buzz the time": "Vibrer l'heure", +"Buzz when you hit your top zone (ease off) and again when you recover. Uses your max HR from Settings.": "Vibre lorsque vous atteignez votre zone maximale (ralentissez) et à nouveau lorsque vous récupérez. Utilise votre FC max des Paramètres.", +"Buzz your strap or mark a moment from Siri, Spotlight, the Shortcuts app, or a Back-Tap / automation. No setup needed.": "Faites vibrer votre bracelet ou marquez un instant depuis Siri, Spotlight, l'application Raccourcis, ou un Tapback / une automatisation. Aucune configuration nécessaire.", +"Buzz your strap when these apps notify you. Everything runs on %@.": "Faites vibrer votre bracelet lorsque ces applications vous notifient. Tout s'exécute sur %@.", +"Buzz your strap when these apps notify you. Everything runs on this Mac.": "Vibrez votre bracelet quand ces apps vous notifient. Tout fonctionne sur ce Mac.", +"Buzzes after you've been sitting past your threshold.": "Vibre après que vous êtes resté assis au-delà de votre seuil.", +"Buzzes your wrist": "Vibre à votre poignet", +"By Day": "Par jour", +"By sport": "Par sport", +"By the numbers": "En chiffres", +"CALIBRATING": "CALIBRAGE", +"CLOUD SHAPE": "FORME DU NUAGE", +"COMPLETE": "TERMINÉ", +"CONNECTED": "CONNECTÉ", +"CONNECTING": "CONNEXION EN COURS", +"CONTROL CENTER": "CENTRE DE CONTRÔLE", +"CSV exported": "CSV exporté", +"CUSTOMISE": "PERSONNALISER", +"Caffeine": "Caféine", +"Caffeine may still be active": "La caféine pourrait encore être active", +"Calendar": "Calendrier", +"Calibrate steps estimate": "Calibrer l'estimation des pas", +"Calibrate the steps estimate": "Calibrer l'estimation des pas", +"Calibrate your steps": "Calibrer vos pas", +"Calibrating": "Calibration en cours", +"Calibrating (%lld of %lld)": "Calibrage, %1$lld sur %2$lld", +"Calibrating, %lld of %lld": "Calibration, %lld de %lld", +"Calibrating, %lld of %lld nights": "Calibrage, %lld nuits sur %lld", +"Calibrating, no data yet": "Calibrage, aucune donnée pour le moment", +"Calibrating, no heart rate banked yet today": "Calibrage, aucune fréquence cardiaque enregistrée aujourd'hui", +"Calibrating. Building your baseline. About %lld more nights until your scores are personal.": "Calibrage. Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés.", +"Calibrating. Building your baseline. About 1 more night until your scores are personal.": "Calibrage. Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés.", +"Calibrating. Too thin to read yet.": "Calibrage. Encore trop peu de données pour une lecture.", +"Calibration": "Calibrage", +"Calm": "Calme", +"Calm me": "Calmez-moi", +"Calm me · 3 min": "Calmez-moi · 3 min", +"Calm time": "Temps calme", +"Calories In": "Calories consommées", +"Calories in kilocalories, optional": "Calories en kilocalories, facultatif", +"Calories must be 0–20,000.": "Les calories doivent être comprises entre 0 et 20 000.", +"Can't connect: your strap's pairing was reset": "Connexion impossible : l'appairage de votre bracelet a été réinitialisé", +"Cancel": "Annuler", +"Capturing": "Capture en cours", +"Capturing. %lld seconds remaining, %lld beats collected.": "Capture en cours. %1$lld secondes restantes, %2$lld battements collectés.", +"Carbs": "Glucides", +"Cardiac": "Cardiaque", +"Cardiovascular load for the day, on a 0–100 scale (was 0–21).": "Charge cardiovasculaire de la journée, sur une échelle de 0 à 100 (auparavant 0 à 21).", +"Category": "Catégorie", +"Change": "Modifier", +"Change folder": "Changer de dossier", +"Change marker": "Changer de marqueur", +"Change photo": "Changer la photo", +"Changes the Bluetooth name your WHOOP 4.0 advertises (what you see when pairing). The strap reboots to apply, so the new name appears the next time it connects. WHOOP 4.0 only.": "Modifie le nom Bluetooth que diffuse votre WHOOP 4.0 (celui que vous voyez lors de l'appairage). Le bracelet redémarre pour appliquer le changement, le nouveau nom apparaît donc à la prochaine connexion. WHOOP 4.0 uniquement.", +"Charge %lld out of 100": "Charge %lld sur 100", +"Charge (all history)": "Charge (tout l'historique)", +"Charge (past year)": "Charge (année écoulée)", +"Charge baseline calibrating. %@, %@. %@.": "Calibrage de la référence de charge. %1$@, %2$@. %3$@.", +"Charge baseline recalibrating": "Recalibrage de la référence de charge", +"Charge calibrating": "Charge en calibration", +"Charge calibrating, needs more data": "Charge en calibration, besoin de plus de données", +"Charge is low and sleep was consistent.": "La charge est basse et le sommeil a été régulier.", +"Charge is low but sleep ran short.": "La charge est basse et le sommeil a été insuffisant.", +"Charge is low.": "La charge est basse.", +"Charge is steady and sleep was consistent.": "La charge est stable et le sommeil a été régulier.", +"Charge is steady but sleep ran short.": "La charge est stable mais le sommeil a été insuffisant.", +"Charge is steady.": "La charge est stable.", +"Charge is strong and sleep was consistent.": "La charge est élevée et le sommeil a été régulier.", +"Charge is strong but sleep ran short.": "La charge est élevée mais le sommeil a été insuffisant.", +"Charge is strong.": "La charge est élevée.", +"Charge model": "Modèle de charge", +"Charge out of date, last synced %@. Open NOOP on iPhone.": "Charge non à jour, dernière synchro %@. Ouvrez NOOP sur iPhone.", +"Charge stale · %@": "Charge obsolète · %@", +"Charge trend": "Tendance de la charge", +"Charge unavailable": "Charge indisponible", +"Charge weighs your HRV against your personal baseline (~55%%), resting heart rate (~20%%), rest quality (~15%%), respiration (~5%%) and skin-temperature deviation (~5%%). Effort is a 0–%@ cardiovascular load from time in heart-rate zones. Rest is staged from movement and heart rate. Everything is computed here from the strap's raw data. It works for any day NOOP collected raw streams.": "La charge pondère votre VFC par rapport à votre référence personnelle (~55 %%), votre fréquence cardiaque au repos (~20 %%), la qualité du repos (~15 %%), la respiration (~5 %%) et l'écart de température cutanée (~5 %%). L'effort est une charge cardiovasculaire de 0 à %@ calculée à partir du temps passé dans les zones de fréquence cardiaque. Le repos est décomposé en phases à partir du mouvement et de la fréquence cardiaque. Tout est calculé ici à partir des données brutes du bracelet. Cela fonctionne pour tout jour où NOOP a collecté des flux bruts.", +"Charge · Effort · Rest": "Charge · Effort · Repos", +"Charge → Next-day charge": "Charge → Charge du lendemain", +"Charge, Effort and Rest (and how they differ from WHOOP).": "Charge, Effort et Repos (et en quoi ils diffèrent de WHOOP).", +"Charge, Effort and Rest are scored on your own device from your strap data. Charge needs about four nights of sleep to learn your baseline (that's \"Calibrating\", counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. On a WHOOP 5 or MG the strap banks little history, so that count can sit at 0 of 4 until you have worn it across a few nights. That's the strap's sync limit, not a fault. Before there's a number, NOOP shows what it can without faking one.": "Charge, Effort et Repos sont calculés sur votre propre appareil à partir des données de votre bracelet. La charge nécessite environ quatre nuits de sommeil pour apprendre votre référence (c'est le « Calibrage », compté en nuits sur 4 sur la bague), et continue de s'affiner au cours de vos premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, ce compteur peut donc rester à 0 sur 4 jusqu'à ce que vous l'ayez porté quelques nuits. C'est la limite de synchronisation du bracelet, pas un défaut. Tant qu'il n'y a pas de chiffre, NOOP montre ce qu'il peut sans en inventer un.", +"Charge, Effort and Rest become personal after a few nights of wear.": "Charge, Effort et Repos deviennent personnalisés après quelques nuits de port.", +"Charge, Effort and Rest build over your next few nights of wear.": "Charge, Effort et Repos se construisent au cours de vos prochaines nuits de port.", +"Charge, Effort, Rest, HRV, resting and live heart rate, and strap battery at a glance.": "Charge, Effort, Repos, VFC, fréquence cardiaque au repos et en direct, et batterie du bracelet en un coup d'œil.", +"Charge: how recovered are you?": "Charge : à quel point êtes-vous récupéré ?", +"Charging": "En charge", +"Chart colours": "Couleurs des graphiques", +"Check for updates": "Rechercher des mises à jour", +"Check the values and try again.": "Vérifiez les valeurs et réessayez.", +"Checking…": "Vérification…", +"Checks the project's home (GitHub) for the latest version when you tap. Nothing else is sent.": "Vérifie la page du projet (GitHub) pour la dernière version lorsque vous touchez. Rien d'autre n'est envoyé.", +"Choose": "Choisir", +"Choose .csv…": "Choisir .csv…", +"Choose .gpx / .tcx / .fit…": "Choisir .gpx / .tcx / .fit…", +"Choose Light, Dark, or follow your system. Dark is the signature near-black; Light keeps the same clean look on a bright canvas.": "Choisissez Clair, Sombre, ou suivez votre système. Sombre est le noir profond caractéristique ; Clair conserve le même look épuré sur un fond lumineux.", +"Choose Mi Fitness export…": "Choisir l'export Mi Fitness…", +"Choose a backup": "Choisir une sauvegarde", +"Choose a folder for NOOP backups (for example a Google Drive or iCloud folder).": "Choisissez un dossier pour les sauvegardes NOOP (par exemple un dossier Google Drive ou iCloud).", +"Choose a wearable signal to compare": "Choisissez un signal d'appareil connecté à comparer", +"Choose export.zip…": "Choisir export.zip…", +"Choose export…": "Choisir l'export…", +"Choose folder": "Choisir un dossier", +"Choose how NOOP looks. The whole app updates as you tap. You can change this any time in Settings → Appearance.": "Choisissez l'apparence de NOOP. Toute l'application se met à jour au fur et à mesure. Vous pouvez changer cela à tout moment dans Paramètres → Apparence.", +"Choose how distances, weights, heights, temperatures and Effort are shown. Your data is always stored the same way. This only changes the display.": "Choisissez comment les distances, poids, tailles, températures et l'Effort sont affichés. Vos données sont toujours stockées de la même manière. Cela ne change que l'affichage.", +"Choose photo": "Choisir une photo", +"Choose the buzz pattern for %@": "Choisir le schéma de vibration pour %@", +"Choose which Key Metrics show and reorder them": "Choisissez les indicateurs clés à afficher et réorganisez-les", +"Choose which cards show and reorder them": "Choisissez les cartes à afficher et réorganisez-les", +"Choose which tiles show on your Control Center and reorder them with the arrows.": "Choisissez les tuiles à afficher dans votre Centre de contrôle et réorganisez-les avec les flèches.", +"Classic": "Classique", +"Clean up": "Nettoyer", +"Clean up leftover import files": "Nettoyer les fichiers d'import restants", +"Clean up now": "Nettoyer maintenant", +"Cleaning up…": "Nettoyage en cours…", +"Clear": "Effacer", +"Clear %@ override, use the default wake time": "Effacer la substitution %@, utiliser l'heure de réveil par défaut", +"Clear all": "Tout effacer", +"Close": "Fermer", +"Close HRV reading": "Fermer la mesure de VFC", +"Close Rhythm": "Fermer Rhythm", +"Close Support": "Fermer l'assistance", +"Coach instructions": "Instructions du Coach", +"Coach instructions editor": "Éditeur des instructions du Coach", +"Coach is thinking": "Coach réfléchit", +"Coach is thinking…": "Coach réfléchit…", +"Coach reads a summary of your last two weeks plus 30-day averages and recent workouts, then answers in plain language. Try a suggestion below.": "Coach lit un résumé de vos deux dernières semaines, les moyennes sur 30 jours et vos récents entraînements, puis répond en langage clair. Essayez une suggestion ci-dessous.", +"Coach said: %@": "Coach a dit : %@", +"Coach talks only to the server URL you set. Point it at a local model (Ollama, LM Studio, llama.cpp) to keep everything on your own machine. Nothing is sent until you ask.": "Coach ne parle qu'à l'URL de serveur que vous définissez. Pointez-le vers un modèle local (Ollama, LM Studio, llama.cpp) pour tout garder sur votre propre machine. Rien n'est envoyé tant que vous ne le demandez pas.", +"Coach uses your own API key. Pick a provider, paste a key, and choose a model. Your key is stored securely in the Keychain and never leaves %@ except as the request you make.": "Coach utilise votre propre clé API. Choisissez un fournisseur, collez une clé, et choisissez un modèle. Votre clé est stockée en sécurité dans le Trousseau et ne quitte jamais %@ sauf sous la forme de la requête que vous effectuez.", +"Coach uses your own API key. Pick a provider, paste a key, and choose a model. Your key is stored securely in the macOS Keychain and never leaves your Mac except as the request you make.": "Coach utilise votre propre clé API. Choisissez un fournisseur, collez une clé et sélectionnez un modèle. Votre clé est stockée de façon sécurisée dans le trousseau macOS et ne quitte jamais votre Mac sauf pour la requête que vous effectuez.", +"Coherence": "Cohérence", +"Coherence 5.5": "Cohérence 5,5", +"Coherence estimate": "Estimation de cohérence", +"Coherent": "Cohérent", +"Collapse": "Réduire", +"Collapse coach instructions": "Réduire les instructions du Coach", +"Collapsed": "Réduit", +"Collect a few logged intervention days before reading the effect. Baseline and imported metrics stay in place.": "Enregistrez quelques jours d'intervention avant de lire l'effet. La référence et les métriques importées restent en place.", +"Coming soon": "Bientôt disponible", +"Coming together": "En cours d'assemblage", +"Compare": "Comparer", +"Compare needs at least two metrics with history. Import your WHOOP export in Data Sources first.": "Comparer nécessite au moins deux métriques avec un historique. Importez d'abord votre export WHOOP dans Sources de données.", +"Compare sources": "Comparer les sources", +"Compare with a signal": "Comparer avec un signal", +"Complete": "Terminé", +"Compliance": "Conformité", +"Computed live from your Apple Watch via Health. Recovery needs about a week of nights to calibrate, and every watch-derived score is labelled with its confidence. Only the metrics your watch actually records are listed above.": "Calculé en direct depuis votre Apple Watch via Santé. La récupération nécessite environ une semaine de nuits pour se calibrer, et chaque score dérivé de la montre est étiqueté avec son niveau de confiance. Seules les métriques réellement enregistrées par votre montre sont listées ci-dessus.", +"Confidence": "Confiance", +"Confidence: %@ (distance %@)": "Confiance : %1$@ (distance %2$@)", +"Confidence: calibrating": "Confiance : calibrage", +"Confidence: estimate": "Confiance : estimation", +"Confidence: reliable": "Confiance : fiable", +"Configure": "Configurer", +"Connect": "Connecter", +"Connect Apple Health": "Connecter Apple Santé", +"Connect a WHOOP, a heart-rate strap or a gym machine and watch each beat in real time: heart rate, variability and zones as they happen. Already have history elsewhere? Import it from WHOOP, Apple Health, Oura, Fitbit or Garmin.": "Connectez un WHOOP, un bracelet cardio ou un appareil de salle de sport et regardez chaque battement en temps réel : fréquence cardiaque, variabilité et zones au fur et à mesure. Vous avez déjà un historique ailleurs ? Importez-le depuis WHOOP, Apple Santé, Oura, Fitbit ou Garmin.", +"Connect a provider": "Connecter un fournisseur", +"Connect strap for buzz cues": "Connecter le bracelet pour les signaux haptiques", +"Connect the strap first, then mark a workout from the live stream.": "Connectez d'abord le bracelet, puis marquez un entraînement depuis le flux en direct.", +"Connect to read wear state": "Connectez-vous pour lire l'état de port", +"Connect to this ring": "Se connecter à cette bague", +"Connect your WHOOP strap in NOOP, then try again.": "Connectez votre bracelet WHOOP dans NOOP, puis réessayez.", +"Connect your strap first.": "Connectez d'abord votre bracelet.", +"Connect your strap first. The reading needs the live R-R stream.": "Connectez d'abord votre bracelet. La mesure nécessite le flux R-R en direct.", +"Connect your strap for haptic guidance, you'll feel one pulse on the inhale, two on the exhale, so you can breathe with your eyes closed.": "Connectez votre bracelet pour un guidage haptique, vous sentirez une impulsion à l'inspiration, deux à l'expiration, pour respirer les yeux fermés.", +"Connect your strap for haptic guidance. You'll feel one pulse on the inhale, two on the exhale, so you can breathe with your eyes closed.": "Connectez votre bracelet pour un guidage haptique. Vous sentirez une pulsation à l'inspiration, deux à l'expiration, afin de pouvoir respirer les yeux fermés.", +"Connect your strap for the felt cue. The sweep paces you with one buzz on the inhale, two on the exhale.": "Connectez votre bracelet pour le signal ressenti. Le balayage vous guide avec une vibration à l'inspiration, deux à l'expiration.", +"Connect your strap on the Live screen to take a reading.": "Connectez votre bracelet dans l'écran En direct pour effectuer une mesure.", +"Connect your strap to enable": "Connectez votre bracelet pour activer", +"Connect your strap to sync its stored history. Until then, only imported data shows here.": "Connectez votre bracelet pour synchroniser son historique enregistré. En attendant, seules les données importées s'affichent ici.", +"Connect your strap to test": "Connectez votre bracelet pour tester", +"Connect your strap. Calm me is a felt rhythm on the wrist, so it needs a bonded connection.": "Connectez votre bracelet. « Calmez-moi » est un rythme ressenti au poignet, il nécessite donc une connexion appairée.", +"Connected": "Connecté", +"Connected now": "Connecté maintenant", +"Connected to %@. Pair or switch bands in Devices.": "Connecté à %@. Appairez ou changez de bracelet dans Appareils.", +"Connected, waiting for a streaming state.": "Connecté, en attente d'un état de diffusion.", +"Connected.": "Connecté.", +"Connected. Finishing the secure pairing handshake…": "Connecté. Finalisation de la poignée de main d'appairage sécurisée…", +"Connected. History sync is experimental on 5.0.": "Connecté. La synchronisation de l'historique est expérimentale sur 5.0.", +"Connected. Reading on launch and when you return to NOOP.": "Connecté. Lecture au lancement et lorsque vous revenez sur NOOP.", +"Connecting…": "Connexion en cours…", +"Connection": "Connexion", +"Connection uptime": "Durée de connexion", +"Consistency": "Régularité", +"Continue": "Continuer", +"Continue to get your ring ready": "Continuez pour préparer votre bague", +"Continuous HRV capture": "Capture continue de la VFC", +"Contributors": "Contributeurs", +"Control Center": "Centre de contrôle", +"Controls unlocked": "Commandes déverrouillées", +"Cooler than your baseline": "Plus fraîche que votre référence", +"Copied!": "Copié !", +"Copy": "Copier", +"Copy %@ address": "Copier l'adresse %@", +"Copy address": "Copier l'adresse", +"Copy environment dump": "Copier le rapport d'environnement", +"Copy report.txt": "Copier report.txt", +"Copy the redacted report to the clipboard": "Copier le rapport caviardé dans le presse-papiers", +"Correct when you went to bed and woke. Stages are re-derived from your data; the edit is kept through the next strap sync.": "Corrigez l'heure de coucher et de réveil. Les phases sont recalculées à partir de vos données ; la modification est conservée jusqu'à la prochaine synchronisation du bracelet.", +"Could not take over": "Impossible de prendre le relais", +"Couldn't change the app icon": "Impossible de changer l'icône de l'application", +"Couldn't check. Try again.": "Vérification impossible. Réessayez.", +"Couldn't locate the NOOP database. %@": "Base de données NOOP introuvable. %@", +"Couldn't lock today": "Impossible de verrouiller aujourd'hui", +"Couldn't open the backup archive: %@": "Impossible d'ouvrir l'archive de sauvegarde : %@", +"Couldn't open the local store.": "Impossible d'ouvrir le stockage local.", +"Couldn't read clearly": "Lecture peu claire", +"Couldn't read the local store right now. Try again in a moment.": "Impossible de lire le stockage local pour le moment. Réessayez dans un instant.", +"Couldn't remove the data: %@": "Impossible de supprimer les données : %@", +"Couldn't restore that backup.": "Impossible de restaurer cette sauvegarde.", +"Couldn't safely back up right now. Recent changes are still in the write-ahead log.": "Sauvegarde impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées.", +"Couldn't safely export right now. Recent changes are still in the database's write-ahead log. Close any in-flight sync, then try again.": "Export impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées de la base de données. Fermez toute synchronisation en cours, puis réessayez.", +"Couldn't start. Needs a connected strap and a resting heart rate.": "Démarrage impossible. Un bracelet connecté et une fréquence cardiaque au repos sont nécessaires.", +"Couldn't write the strap log right now.": "Impossible d'écrire le journal du bracelet pour le moment.", +"Counter ticks per step. Leave at 1.0 unless your steps run high. On a WHOOP 5/MG they can run very high (10× or more), so this goes up to 30. Walk a known 1,000 steps and divide NOOP's count by the real count to get your value.": "Impulsions du compteur par pas. Laissez à 1,0 sauf si votre nombre de pas est trop élevé. Sur un WHOOP 5/MG, il peut être très élevé (10× ou plus), d'où une plage allant jusqu'à 30. Marchez 1 000 pas connus et divisez le comptage de NOOP par le nombre réel pour obtenir votre valeur.", +"Crunching your raw streams…": "Traitement de vos flux bruts…", +"Cup": "Tasse", +"Current calibration": "Calibration actuelle", +"Current charge": "Charge actuelle", +"Current physiology, strap trust, and session controls in one working view.": "Physiologie actuelle, fiabilité du bracelet et commandes de séance dans une seule vue de travail.", +"Current: %@": "Actuel : %@", +"Custom": "Personnalisé", +"Custom %lld ml": "Personnalisé %lld ml", +"Custom marker name": "Nom du marqueur personnalisé", +"Custom marker unit": "Unité du marqueur personnalisé", +"Custom model id": "ID de modèle personnalisé", +"Custom size": "Taille personnalisée", +"Customise your cards": "Personnalisez vos cartes", +"Customised. Your edited instructions frame every reply.": "Personnalisé. Vos instructions modifiées encadrent chaque réponse.", +"Custom…": "Personnalisé…", +"Cutoff before bed": "Limite avant le coucher", +"Cycle awareness": "Suivi du cycle", +"Cycle phase: %@.": "Phase du cycle : %@.", +"Cycle phase: %@. About day %lld to %lld.": "Phase du cycle : %1$@. Environ du jour %2$lld au %3$lld.", +"Cycle phase: %@. About day %lld.": "Phase du cycle : %1$@. Environ jour %2$lld.", +"DAY %lld/%lld": "JOUR %1$lld/%2$lld", +"DEPLETED": "ÉPUISÉ", +"DESCRIPTIVE STATS": "STATISTIQUES DESCRIPTIVES", +"DIAGNOSTIC TOOLS": "OUTILS DE DIAGNOSTIC", +"DONE": "TERMINÉ", +"DURATION + EFFICIENCY + STAGES": "DURÉE + EFFICACITÉ + PHASES", +"Daily 0–3 proxy": "Proxy quotidien 0–3", +"Daily auto-backup": "Sauvegarde automatique quotidienne", +"Daily auto-export of the strap log": "Export automatique quotidien du journal du bracelet", +"Daily auto-export time": "Heure d'export automatique quotidien", +"Daily signals": "Signaux quotidiens", +"Dark": "Sombre", +"Data & App": "Données et application", +"Data Sources": "Sources de données", +"Data sources": "Sources de données", +"Date and time taken": "Date et heure de la mesure", +"Date taken": "Date de la mesure", +"Day": "Jour", +"Day %lld": "Jour %lld", +"Day 1: bright light %@–%@, lights-out around %@.": "Jour 1 : lumière vive %1$@–%2$@, extinction des lumières vers %3$@.", +"Day Strain": "Effort du jour", +"Day Stress": "Stress du jour", +"Day strain": "Effort du jour", +"Day-cycle background": "Arrière-plan du cycle journalier", +"Days": "Jours", +"Days with higher %@ tend to be your better-mood days.": "Les jours avec un %@ plus élevé ont tendance à être vos jours de meilleure humeur.", +"Days with higher %@ tend to be your lower-mood days.": "Les jours avec un %@ plus élevé ont tendance à être vos jours de moins bonne humeur.", +"Daytime sleep": "Sommeil diurne", +"Dec": "Déc", +"Deep": "Profond", +"Deep (SWS) Sleep": "Sommeil profond (SWS)", +"Deep Timeline": "Chronologie détaillée", +"Deep Timeline, every second of your day, zoomable": "Chronologie détaillée, chaque seconde de votre journée, avec zoom", +"Deep calm": "Calme profond", +"Deep data (R22) needs an iPhone or Android. A Mac can't form the encrypted bond a 5/MG requires.": "Les données approfondies (R22) nécessitent un iPhone ou un Android. Un Mac ne peut pas établir l'appairage chiffré requis par un 5/MG.", +"Deep sleep": "Sommeil profond", +"Default": "Par défaut", +"Delete": "Supprimer", +"Delete %@": "Supprimer %@", +"Delete all of this device's data?": "Supprimer toutes les données de cet appareil ?", +"Delete data": "Supprimer les données", +"Delete the %lld millilitre drink logged at %@": "Supprimer la boisson de %1$lld millilitres enregistrée à %2$@", +"Delete this custom question": "Supprimer cette question personnalisée", +"Delete this device's data…": "Supprimer les données de cet appareil…", +"Delete this reading": "Supprimer cette mesure", +"Delete this sleep": "Supprimer ce sommeil", +"Delete this sleep session?": "Supprimer cette séance de sommeil ?", +"Depleted": "Épuisé", +"Detected": "Détecté", +"Device": "Appareil", +"Device actions for %@": "Actions sur l'appareil pour %@", +"Device metrics": "Métriques de l'appareil", +"Device name": "Nom de l'appareil", +"Device, iOS build, Data Protection and sideload status, for bug reports.": "Appareil, version d'iOS, statut de Data Protection et de sideload, pour les rapports de bug.", +"Devices": "Appareils", +"Diagnostics": "Diagnostic", +"Diastolic": "Diastolique", +"Diastolic blood pressure in mmHg": "Pression artérielle diastolique en mmHg", +"Different wake time per day": "Heure de réveil différente chaque jour", +"Differs slightly": "Diffère légèrement", +"Disconnect": "Déconnecter", +"Disconnect provider": "Déconnecter le fournisseur", +"Disconnected": "Déconnecté", +"Dismiss": "Ignorer", +"Dismiss (not a workout)": "Ignorer (pas un entraînement)", +"Dismiss this workout suggestion": "Ignorer cette suggestion d'entraînement", +"Dismiss to Updates": "Ignorer et classer dans Mises à jour", +"Don't ask again": "Ne plus demander", +"Don't buzz when the strap is off your wrist.": "Ne pas vibrer quand le bracelet n'est pas au poignet.", +"Don't see it?": "Vous ne le voyez pas ?", +"Don't see it? That's normal.": "Vous ne le voyez pas ? C'est normal.", +"Done": "Terminé", +"Done customising dashboard": "Personnalisation du tableau de bord terminée", +"Done editing Key Metrics": "Modification des indicateurs clés terminée", +"Done. Save this reading to keep it in your trends.": "Terminé. Enregistrez cette mesure pour la conserver dans vos tendances.", +"Dose-response": "Dose-réponse", +"Dose-response curve. Each extra %@ lines up with about %@ on %@, typical patterns.": "Courbe dose-réponse. Chaque %@ supplémentaire correspond à environ %@ sur %@, schémas typiques.", +"Dose-response curve. Each extra %@ lines up with about %@ on %@, your own data.": "Courbe dose-réponse. Chaque %@ supplémentaire correspond à environ %@ sur %@, vos propres données.", +"Double tap to collapse": "Double-tapez pour réduire", +"Double tap to expand": "Double-tapez pour développer", +"Double-tap the strap to trigger an action on %@. (The strap exposes a single double-tap gesture.)": "Double-tapez sur le bracelet pour déclencher une action sur %@. (Le bracelet ne propose qu'un seul geste de double-tap.)", +"Download": "Télécharger", +"Drag to pan · double-tap to reset": "Glissez pour déplacer · double-tap pour réinitialiser", +"Drag to reorder. Toggle a card on or off. Cards with no value yet show a dash.": "Glissez pour réorganiser. Activez ou désactivez une carte. Les cartes sans valeur affichent un tiret.", +"Drives your Fitness Age": "Détermine votre âge de forme physique", +"Dumps the last 24 hours of decoded per-sample sensor streams (heart rate, R-R, motion, steps, SpO₂, skin temperature, respiration, events) to a single CSV. All on %@, nothing uploaded. Share it to help prototype and test sleep, activity and strength algorithms.": "Exporte les 24 dernières heures des flux de capteurs décodés échantillon par échantillon (fréquence cardiaque, R-R, mouvement, pas, SpO₂, température cutanée, respiration, événements) dans un seul fichier CSV. Tout sur %@, rien n'est envoyé. Partagez-le pour aider à prototyper et tester les algorithmes de sommeil, d'activité et de force.", +"Duplicate as manual…": "Dupliquer en manuel…", +"Duration": "Durée", +"Duration in minutes": "Durée en minutes", +"During quiet, still, resting windows, NOOP looks at the timing between your heartbeats (R-R intervals) and draws their Poincaré scatter. From the cloud it computes its short and long axes (SD1, SD2) and a few plain regularity numbers. Movement and noisy windows are skipped, not shown. These are transparent, published descriptive statistics: a picture of your timing, never a clinical measurement.": "Pendant les périodes calmes, immobiles et au repos, NOOP examine le rythme entre vos battements cardiaques (intervalles R-R) et trace leur nuage de Poincaré. À partir du nuage, il calcule ses axes court et long (SD1, SD2) et quelques chiffres de régularité simples. Les périodes avec mouvement ou bruit sont ignorées, non affichées. Ce sont des statistiques descriptives transparentes et publiées : une image de votre rythme, jamais une mesure clinique.", +"EARLIER": "PLUS TÔT", +"EARLY SIGNAL": "SIGNAL PRÉCOCE", +"EFFORT BUILDING": "EFFORT EN CONSTRUCTION", +"EXPLORATORY": "EXPLORATOIRE", +"EXPORT": "EXPORTER", +"EXTRA / SKIPPED": "EXTRA / IGNORÉ", +"Each dot pairs one heartbeat interval with the next. A tight line along the diagonal means a steady beat; a rounder, more spread-out cloud means the timing varied more.": "Chaque point associe un intervalle de battement cardiaque au suivant. Une ligne serrée le long de la diagonale indique un battement régulier ; un nuage plus rond et plus étalé indique une variation plus importante du rythme.", +"Each line min–max normalized within %@ · %@": "Chaque ligne normalisée min–max dans %1$@ · %2$@", +"Each metric, where your Apple Watch sits on it, and why.": "Chaque métrique, où se situe votre Apple Watch, et pourquoi.", +"Each score follows a published method, computed on your device. We name the method family so you can read up on it, and we never claim to reproduce another company's number exactly.": "Chaque score suit une méthode publiée, calculée sur votre appareil. Nous indiquons la famille de méthode pour que vous puissiez vous documenter, et nous ne prétendons jamais reproduire exactement le chiffre d'une autre entreprise.", +"Each test logs extra detail for one part of the app while you wear the strap, then bundles it for a bug report.": "Chaque test consigne des détails supplémentaires pour une partie de l'application pendant que vous portez le bracelet, puis les regroupe pour un rapport de bug.", +"Early warning: take it easy": "Alerte précoce : allez-y doucement", +"Edit": "Modifier", +"Edit Key Metrics": "Modifier les indicateurs clés", +"Edit Workout": "Modifier l'entraînement", +"Edit coach instructions": "Modifier les instructions du Coach", +"Edit details…": "Modifier les détails…", +"Edit drink": "Modifier la boisson", +"Edit how the coach thinks and talks. Takes effect on your next message.": "Modifiez la façon dont le Coach raisonne et s'exprime. Prend effet à votre prochain message.", +"Edit nap times": "Modifier les horaires de sieste", +"Edit nap times (edited)": "Modifier les horaires de sieste (modifié)", +"Edit sleep times": "Modifier les horaires de sommeil", +"Edit sleep times (edited)": "Modifier les horaires de sommeil (modifié)", +"Edit today's mood": "Modifier l'humeur d'aujourd'hui", +"Edit…": "Modifier…", +"Effect size": "Taille d'effet", +"Efficiency": "Efficacité", +"Effort scale": "Échelle d'effort", +"Effort this %@": "Effort sur ce %@", +"Effort: how hard did your heart work?": "Effort : à quel point votre cœur a-t-il travaillé ?", +"Eligible for live physiology": "Éligible à la physiologie en direct", +"Email": "E-mail", +"Email %@": "Envoyer un e-mail à %@", +"Enable Apple Health": "Activer Apple Santé", +"Enable inactivity reminder": "Activer le rappel d'inactivité", +"Enable wrist alerts": "Activer les alertes au poignet", +"Encrypted": "Chiffré", +"Encrypted stream: deep controls and history sync available.": "Flux chiffré : commandes avancées et synchronisation de l'historique disponibles.", +"End": "Terminer", +"End the experiment plan. Journal and metric history stay untouched.": "Terminer le plan d'expérience. Le journal et l'historique des métriques restent intacts.", +"End workout": "Terminer l'entraînement", +"Enjoying NOOP?": "Vous appréciez NOOP ?", +"Enter NOOP": "Entrer dans NOOP", +"Enter a model id": "Entrez un ID de modèle", +"Enter a sport.": "Saisissez un sport.", +"Entered together; stored as two markers so each lines up cleanly against your signals.": "Saisis ensemble ; enregistrés comme deux marqueurs afin que chacun s'aligne précisément avec vos signaux.", +"Equal breath · ~5.5 br/min coherence": "Respiration égale · cohérence ~5,5 resp/min", +"Error: %@": "Erreur : %@", +"Estimate": "Estimation", +"Estimate (partial data)": "Estimation (données partielles)", +"Estimate from PPG-derived R-R: relaxation guidance, not a clinical reading. Your pace drifts, so we date it and you can re-measure anytime.": "Estimation à partir des R-R dérivés du PPG : guide de relaxation, pas une mesure clinique. Votre rythme dérive, nous la datons donc et vous pouvez remesurer à tout moment.", +"Estimate from today's effort, your typical sleep and your %lld-night recovery baseline, not a measurement. Your real Charge is scored from tomorrow's HRV when you wake.": "Estimation à partir de l'effort d'aujourd'hui, de votre sommeil habituel et de votre référence de récupération sur %lld nuits, pas une mesure. Votre véritable Charge est calculée à partir de la VFC de demain à votre réveil.", +"Estimate only, a higher RMSSD while paced usually means your parasympathetic \"rest\" branch is engaging. It is not a clinical reading; trends over a session matter more than any single number.": "Estimation uniquement, un RMSSD plus élevé lors d'un rythme imposé signifie généralement que votre branche parasympathique de « repos » s'engage. Ce n'est pas une lecture clinique ; les tendances sur une séance comptent plus que chaque chiffre seul.", +"Estimate only: a higher RMSSD while paced usually means your parasympathetic \"rest\" branch is engaging. It is not a clinical reading; trends over a session matter more than any single number.": "Estimation seulement : un RMSSD plus élevé pendant le rythme guidé signifie généralement que votre branche parasympathique de « repos » s'active. Ce n'est pas une mesure clinique ; les tendances sur une séance comptent plus qu'un chiffre isolé.", +"Estimated body-clock low around %@": "Creux estimé de l'horloge biologique vers %@", +"Estimated from R-R interval": "Estimé à partir de l'intervalle R-R", +"Estimated from your WHOOP's motion, calibrated to your phone. Not a measured step count.": "Estimé à partir du mouvement de votre WHOOP, calibré sur votre téléphone. Ce n'est pas un nombre de pas mesuré.", +"Estimated max heart rate · %lld bpm": "FC max estimée · %lld bpm", +"Estimated mostly cleared. Nothing logged is likely still active.": "Estimé en grande partie éliminé. Rien de ce qui est enregistré n'est probablement encore actif.", +"Estimated runtime left": "Autonomie restante estimée", +"Estimated vs your phone": "Estimé par rapport à votre téléphone", +"Eve": "Soir", +"Evening": "Soir", +"Evening forecast": "Prévision du soir", +"Event": "Événement", +"Every beat, every night, every day, woven into one quiet picture of you. Welcome to NOOP.": "Chaque battement, chaque nuit, chaque jour, tissé en un portrait silencieux de vous. Bienvenue dans NOOP.", +"Every day": "Chaque jour", +"Every metric is an unvalidated approximation. Don't use NOOP to diagnose, treat, or make any health decision. Always consult a qualified professional.": "Chaque métrique est une approximation non validée. N'utilisez pas NOOP pour diagnostiquer, traiter ou prendre une décision de santé. Consultez toujours un professionnel qualifié.", +"Every release, mirrored. A fallback if GitHub is ever down.": "Chaque version, mise en miroir. Une solution de repli si GitHub venait à être indisponible.", +"Every score carries a small honesty label. Calibrating means NOOP is still learning your baseline, or doesn't have enough data yet. Building means there's enough to show, but it's thin. Solid means full inputs are present. When NOOP can't compute a score honestly, it shows nothing rather than a fake number.": "Chaque score porte une petite étiquette de transparence. « Calibration » signifie que NOOP apprend encore votre référence ou n'a pas encore assez de données. « En construction » signifie qu'il y en a assez pour afficher quelque chose, mais c'est mince. « Solide » signifie que toutes les données sont présentes. Quand NOOP ne peut pas calculer un score honnêtement, il n'affiche rien plutôt qu'un chiffre inventé.", +"Every second of your day, zoomable.": "Chaque seconde de votre journée, avec zoom.", +"Every session, threaded together.": "Chaque séance, reliée ensemble.", +"Every signal, one tap deep.": "Chaque signal, à un tap.", +"Everyone has a breathing pace (usually between 4.5 and 7 breaths a minute) where the heart's rhythm swings the most with each breath. We pace you through a few candidate paces, measure how your HRV responds, and lock the one that resonates best for you.": "Chacun a un rythme respiratoire (généralement entre 4,5 et 7 respirations par minute) où le rythme cardiaque oscille le plus à chaque respiration. Nous vous guidons à travers quelques rythmes candidats, mesurons la réponse de votre VFC, et fixons celui qui résonne le mieux pour vous.", +"Everything else, one tap away": "Tout le reste, à portée d'un tap", +"Everything here is a pattern in your own logged days: an association with an effect size and confidence, never a cause or a diagnosis. Population patterns are shown as “typical” and are always overridden by your own data once you have enough of it. Approximations, not WHOOP’s scores; not a medical device.": "Tout ici est un schéma issu de vos propres jours enregistrés : une association avec une taille d'effet et un niveau de confiance, jamais une cause ou un diagnostic. Les schémas de population sont indiqués comme « typiques » et sont toujours remplacés par vos propres données dès que vous en avez suffisamment. Des approximations, pas les scores de WHOOP ; ce n'est pas un dispositif médical.", +"Everything lives on %@. No account, no sync, no cloud. Your thread is yours alone.": "Tout réside sur %@. Aucun compte, aucune synchronisation, aucun cloud. Votre conversation n'appartient qu'à vous.", +"Everything stays on %@. Bring your history in once, then it's yours.": "Tout reste sur %@. Importez votre historique une seule fois, il est ensuite à vous.", +"Everything stays on this Mac. Bring your history in once, then it's yours.": "Tout reste sur ce Mac. Importez votre historique une fois, il est à vous.", +"Everything stays on your device": "Tout reste sur votre appareil", +"Exact model unknown. Shows what every WHOOP can do. * on-device estimate · no SpO₂ % off the strap (import a WHOOP CSV for that).": "Modèle exact inconnu. Montre ce que chaque WHOOP peut faire. * estimation sur l'appareil · pas de % SpO₂ depuis le bracelet (importez un CSV WHOOP pour cela).", +"Expand for the full read": "Développer pour la lecture complète", +"Expanded": "Développé", +"Experiment behaviour": "Comportement de l'expérience", +"Experiment outcome metric": "Métrique de résultat de l'expérience", +"Experiment progress": "Progression de l'expérience", +"Experiment window length": "Durée de la fenêtre de l'expérience", +"Experimental": "Expérimental", +"Experimental sleep staging (V2)": "Détection des phases de sommeil expérimentale (V2)", +"Experimental wellness visualization: not a diagnosis, not an ECG, and not a medical device. It cannot detect any heart condition. Beat-to-beat variation has many ordinary, benign causes. If you feel unwell or are worried, contact a qualified professional; in an emergency, your local emergency service. Everything is computed on your device.": "Visualisation expérimentale du bien-être : ce n'est pas un diagnostic, pas un ECG, et pas un dispositif médical. Elle ne peut détecter aucune affection cardiaque. La variation battement à battement a de nombreuses causes courantes et bénignes. Si vous ne vous sentez pas bien ou êtes inquiet, contactez un professionnel qualifié ; en cas d'urgence, votre service d'urgence local. Tout est calculé sur votre appareil.", +"Experimental · Sleep staging": "Expérimental · Phases de sommeil", +"Experimental · WHOOP 5 / MG": "Expérimental · WHOOP 5 / MG", +"Experimental, and not a medical device": "Expérimental, et ce n'est pas un dispositif médical", +"Experimental, best-effort support. We're still testing these, so they might not connect on every device. They never make up data, and they'll tell you honestly when live isn't possible.": "Prise en charge expérimentale, au mieux des capacités. Nous testons encore ces appareils, ils pourraient donc ne pas se connecter sur tous les modèles. Ils n'inventent jamais de données et vous diront honnêtement quand le direct n'est pas possible.", +"Experimental: at your own risk": "Expérimental : à vos risques et périls", +"Experimental: if your band needs pairing, we'll tell you honestly rather than show a fake reading.": "Expérimental : si votre bracelet nécessite un appairage, nous vous le dirons honnêtement plutôt que d'afficher une mesure fictive.", +"Experimental: live heart rate where the band exposes it. Some bands need a pairing we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, recovery, skin temp, SpO₂ or steps.": "Expérimental : fréquence cardiaque en direct là où le bracelet l'expose. Certains bracelets nécessitent un appairage que nous ne pouvons pas encore effectuer. NOOP le dira honnêtement et n'affichera jamais de chiffre inventé. Pas de sommeil, récupération, température cutanée, SpO₂ ni pas.", +"Experimental: this is best-effort. If live doesn't work, you can export from Zepp and import the file.": "Expérimental : ceci est au mieux des capacités. Si le direct ne fonctionne pas, vous pouvez exporter depuis Zepp et importer le fichier.", +"Explained by": "Expliqué par", +"Explore": "Explorer", +"Export": "Exporter", +"Export CSV…": "Exporter en CSV…", +"Export NOOP backup": "Exporter la sauvegarde NOOP", +"Export NOOP data as CSV": "Exporter les données NOOP en CSV", +"Export PDF": "Exporter en PDF", +"Export failed": "Échec de l'export", +"Export failed: %@": "Échec de l'export : %@", +"Export for Shortcuts (Apple Health)": "Exporter pour Raccourcis (Apple Santé)", +"Export frames…": "Exporter les trames…", +"Export problem": "Problème d'export", +"Export raw + log": "Exporter les données brutes + le journal", +"Export raw sensor data (CSV)": "Exporter les données brutes du capteur (CSV)", +"Export trends report": "Exporter le rapport de tendances", +"Exporting…": "Export en cours…", +"Export…": "Exporter…", +"FULL BOND": "APPAIRAGE COMPLET", +"Fair": "Correct", +"Fallback": "Solution de repli", +"Fat": "Graisses", +"Fat burn": "Combustion des graisses", +"Features": "Fonctionnalités", +"Feb": "Fév", +"Female": "Féminin", +"Fetch the available models from %@ using your saved key": "Récupérer les modèles disponibles depuis %@ avec votre clé enregistrée", +"File": "Fichier", +"Find it in the sidebar under Data Sources.": "Retrouvez-le dans la barre latérale sous Sources de données.", +"Find your resonance pace": "Trouvez votre rythme de résonance", +"Find your strap": "Trouvez votre bracelet", +"Fine-tune when alerts reach your wrist.": "Réglez précisément quand les alertes atteignent votre poignet.", +"Finish": "Terminer", +"Finishing the pairing handshake. Sync now becomes available once the strap is paired.": "Finalisation de la poignée de main d'appairage. La synchronisation immédiate devient disponible une fois le bracelet appairé.", +"Fire a single haptic pulse on the strap (requires a bonded connection)": "Envoyer une impulsion haptique sur le bracelet (nécessite une connexion associée)", +"Fire a test buzz now": "Envoyer une vibration test maintenant", +"Fire a test haptic buzz on the strap (requires an active strap connection)": "Envoyer une vibration haptique test sur le bracelet (nécessite une connexion active)", +"Fires a test buzz on your strap": "Envoie une vibration test sur votre bracelet", +"Fires the moment the strap leaves your wrist.": "Se déclenche dès que le bracelet quitte votre poignet.", +"Firmware version %@": "Version du firmware %@", +"Fitness Age": "Âge fitness", +"Fitness Age %lld, %@. Tap to see the trend.": "Âge de forme physique %1$lld, %2$@. Tapez pour voir la tendance.", +"Fitness Age %lld, %lld year%@ %@ than your age. Tap to see the trend.": "Âge de forme physique %1$lld, %2$lld an%3$@ %4$@ que votre âge. Tapez pour voir la tendance.", +"Fitted from": "Ajusté à partir de", +"Fix in Settings": "Corriger dans les Paramètres", +"Flags accepted, but the enable sequence doesn't start a separate live stream. The deep records arrive as part of the normal history sync (#494).": "Indicateurs acceptés, mais la séquence d'activation ne démarre pas de flux en direct séparé. Les enregistrements détaillés arrivent dans le cadre de la synchronisation normale de l'historique (#494).", +"Follicular": "Folliculaire", +"For a WHOOP 4.0, which sends no step count: NOOP estimates steps from motion, calibrated to your phone. Tap to see how close it is and adjust it.": "Pour un WHOOP 4.0, qui n'envoie aucun nombre de pas : NOOP estime les pas à partir du mouvement, calibré sur votre téléphone. Tapez pour voir sa précision et l'ajuster.", +"Force-quit the official WHOOP app first, or pairing may fail.": "Forcez d'abord la fermeture de l'application officielle WHOOP, sinon l'appairage pourrait échouer.", +"Forget the saved key and disconnect": "Oublier la clé enregistrée et déconnecter", +"Frame": "Trame", +"Frame summary": "Résumé de la trame", +"Fri": "Ven", +"Friday": "Vendredi", +"From": "De", +"From strap HR": "Depuis la FC du bracelet", +"From the watch's wrist-temperature sensor during sleep, on Series 8 and later. Older models don't have the sensor, so it reads “not available” rather than zero.": "Depuis le capteur de température au poignet de la montre pendant le sommeil, sur Series 8 et ultérieures. Les modèles plus anciens n'ont pas le capteur, il affiche donc « non disponible » plutôt que zéro.", +"From your nightly sensor": "Depuis votre capteur nocturne", +"From your nightly temperature": "Depuis votre température nocturne", +"Full strap stream is active.": "Le flux complet du bracelet est actif.", +"Full sweep · ~13 min": "Balayage complet · ~13 min", +"Fully quit and reopen NOOP to load it.": "Fermez complètement puis rouvrez NOOP pour le charger.", +"Functional strength": "Force fonctionnelle", +"Fused on %@. Nothing leaves it: no account, no cloud.": "Fusionné sur %@. Rien n'en sort : aucun compte, aucun cloud.", +"Garmin watch": "Montre Garmin", +"Generated by NOOP on %@ · all on-device, no account, no cloud.": "Généré par NOOP sur %@ · tout sur l'appareil, aucun compte, aucun cloud.", +"Get Started": "Commencer", +"Get a notification when the strap battery runs low (15%) so you can top it up before tonight, and when it finishes charging.": "Recevez une notification quand la batterie du bracelet est faible (15 %) afin de la recharger avant ce soir, et quand elle a fini de charger.", +"Get in touch": "Nous contacter", +"Get it ready, then scan.": "Préparez-le, puis scannez.", +"Get the latest release": "Obtenir la dernière version", +"Get your ring ready": "Préparez votre bague", +"Getting your devices ready": "Préparation de vos appareils", +"GitHub: code, releases, issues and the wiki.": "GitHub : code, versions, tickets et le wiki.", +"Give %@ %@ a name you'll recognise.": "Donnez à %1$@ %2$@ un nom que vous reconnaîtrez.", +"Give it a few minutes of charge if the battery is low.": "Laissez-le charger quelques minutes si la batterie est faible.", +"Goes to the Apple Health permission step": "Accède à l'étape d'autorisation Apple Santé", +"Going to sleep": "Endormissement", +"Good": "Bon", +"Good afternoon": "Bon après-midi", +"Good evening": "Bonsoir", +"Good morning": "Bonjour", +"Good morning. Your smart alarm just woke you.": "Bonjour. Votre alarme intelligente vient de vous réveiller.", +"Got it": "Compris", +"Grab this when you report a bug. It tells me what the app saw.": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé.", +"Grab this when you report a bug. It tells me what the app saw. (The full live log is also on the Live screen.)": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé. (Le journal complet en direct est aussi disponible dans l'écran En direct.)", +"Grant Health access": "Autoriser l'accès à Santé", +"Gravity coverage": "Couverture de gravité", +"Great": "Excellent", +"Gym equipment": "Équipement de salle", +"Gym equipment (FTMS)": "Équipement de salle (FTMS)", +"HEART RATE": "FRÉQUENCE CARDIAQUE", +"HF power": "Puissance HF", +"HIGH": "ÉLEVÉ", +"HR %@ · Effort %@": "FC %@ · Effort %@", +"HR ZONE": "ZONE FC", +"HR Zones": "Zones FC", +"HR Zones (All)": "Zones FC (toutes)", +"HR Zones 1–3": "Zones FC 1–3", +"HR Zones 4–5": "Zones FC 4–5", +"HR density (per min)": "Densité FC (par min)", +"HR eased %lld → %lld over %@.": "FC réduite de %1$lld à %2$lld sur %3$@.", +"HR held steady (%lld → %lld). Try a paced breath instead.": "FC restée stable (%1$lld → %2$lld). Essayez plutôt une respiration guidée.", +"HR settled %lld → %lld over %@.": "FC stabilisée de %1$lld à %2$lld sur %3$@.", +"HR settled over %@.": "FC stabilisée sur %@.", +"HR unavailable": "FC indisponible", +"HR-zone coaching": "Coaching par zone de FC", +"HRV": "VFC", +"HRV %@ · RHR %@": "VFC %@ · FCR %@", +"HRV %lld%% over baseline": "VFC %lld %% au-dessus de la référence", +"HRV %lld%% under baseline": "VFC %lld %% en dessous de la référence", +"HRV Reading": "Mesure de la VFC", +"HRV has dropped well below your baseline, pointing to elevated stress or fatigue. Ease off and give your body time to recover.": "La VFC est tombée bien en dessous de votre référence, signe de stress ou de fatigue élevés. Levez le pied et laissez à votre corps le temps de récupérer.", +"HRV is above baseline, a sign of a relaxed, well-recovered nervous system. Stress is low.": "La VFC est au-dessus de la référence, signe d'un système nerveux détendu et bien récupéré. Le stress est faible.", +"HRV is below your baseline, so ease into the day": "La VFC est en dessous de votre référence, alors abordez la journée en douceur", +"HRV reading": "Mesure de la VFC", +"HRV reading not started": "Mesure de la VFC non démarrée", +"HRV ↔ Charge": "VFC ↔ Charge", +"Had it": "Fait", +"Haptic coaching": "Coaching haptique", +"Haptic-paced breathing · find your pace · calm down": "Respiration rythmée par vibrations · trouvez votre rythme · calmez-vous", +"Haptic-paced breathing · watch your HRV respond": "Respiration rythmée haptiquement · observez votre VFC réagir", +"Haptics on": "Haptiques activés", +"Hard to read right now": "Difficile à lire pour le moment", +"Heads-up": "Attention", +"Heads-up. Your WHOOP only talks to one phone at a time. Force-quit the official WHOOP app first, or pairing may fail.": "Attention. Votre WHOOP ne communique qu'avec un seul téléphone à la fois. Forcez d'abord la fermeture de l'application WHOOP officielle, sinon le jumelage risque d'échouer.", +"Health": "Santé", +"Health Monitor": "Moniteur de santé", +"Health database": "Base de données Santé", +"Health insights": "Perspectives sur la santé", +"Heart & Vitals": "Cœur et constantes", +"Heart Rate": "Fréquence cardiaque", +"Heart Rate Variability": "Variabilité de la fréquence cardiaque", +"Heart rate": "Fréquence cardiaque", +"Heart rate %lld beats per minute": "Fréquence cardiaque %lld battements par minute", +"Heart rate (live, best-effort)": "Fréquence cardiaque (en direct, sous réserve)", +"Heart rate during %@": "Fréquence cardiaque pendant %@", +"Heart rate not available": "Fréquence cardiaque non disponible", +"Heart rate plus active energy give a solid daily cardiovascular load. An on-watch workout sharpens it further.": "La fréquence cardiaque et l'énergie active donnent une charge cardiovasculaire quotidienne solide. Un entraînement lancé depuis la montre l'affine encore plus.", +"Heart rate stream is active.": "Le flux de fréquence cardiaque est actif.", +"Heart rate variability": "Variabilité de la fréquence cardiaque", +"Heart rate · HRV (live)* · Strain": "Fréquence cardiaque · VFC (en direct)* · Effort", +"Heart rate · HRV · Skin temp* · Resp rate* · Sleep · Strain · Battery": "Fréquence cardiaque · VFC · Temp. cutanée* · Fréq. respiratoire* · Sommeil · Effort · Batterie", +"Heart rate · HRV · Skin temp* · Resp rate* · Steps* · Sleep · Strain · Battery": "Fréquence cardiaque · VFC · Temp. cutanée* · Fréq. respiratoire* · Pas* · Sommeil · Effort · Batterie", +"Heart rate · HRV* · Sleep · Resting HR · Skin temp* · Battery": "Fréquence cardiaque · VFC* · Sommeil · FC au repos · Temp. cutanée* · Batterie", +"Heart rate* · HRV* · Sleep* · Resting HR* · Skin temp* · Battery*": "Fréquence cardiaque* · VFC* · Sommeil* · FC au repos* · Temp. cutanée* · Batterie*", +"Heart-rate strap": "Bracelet de fréquence cardiaque", +"Heart-rate variability": "Variabilité de la fréquence cardiaque", +"Heart-rate variability as the engine behind your charge score.": "La variabilité de la fréquence cardiaque comme moteur de votre score de charge.", +"Heart-rate zone split: %@": "Répartition des zones de fréquence cardiaque : %@", +"Height": "Taille", +"Height in centimetres": "Taille en centimètres", +"Height in inches": "Taille en pouces", +"Height, %lld feet %lld inches": "Taille, %1$lld pieds %2$lld pouces", +"Help & Contact": "Aide et contact", +"Helping most: %@": "Aide le plus : %@", +"Hidden": "Masqué", +"Hide %@": "Masquer %@", +"Hide data source detail": "Masquer le détail de la source de données", +"Hide this question": "Masquer cette question", +"High": "Élevé", +"History": "Historique", +"History fills the dashboard immediately": "L'historique remplit immédiatement le tableau de bord", +"History sync": "Synchronisation de l'historique", +"History sync is experimental on 5.0.": "La synchronisation de l'historique est expérimentale sur 5.0.", +"History synced": "Historique synchronisé", +"History synced %@": "Historique synchronisé %@", +"Holding you back: %@": "Vous freine : %@", +"Holds the screen awake while you're recording a workout, so your live heart rate stays visible without the device dimming. Only applies during a recording. The screen sleeps normally the rest of the time. Leaving it on does use a bit more battery, and means your unlocked screen stays visible for the whole workout, so flip it off if that's a concern.": "Garde l'écran allumé pendant l'enregistrement d'un entraînement, pour que votre fréquence cardiaque en direct reste visible sans que l'appareil s'assombrisse. Ne s'applique que pendant un enregistrement. Le reste du temps, l'écran se met en veille normalement. Le laisser activé consomme un peu plus de batterie et signifie que votre écran déverrouillé reste visible pendant tout l'entraînement, alors désactivez-le si cela vous préoccupe.", +"Hours asleep": "Heures de sommeil", +"Hours asleep trend": "Tendance des heures de sommeil", +"Hours vs Needed": "Heures vs nécessaires", +"How %@ is calculated": "Comment %@ est calculé", +"How Charge is calculated": "Comment la charge est calculée", +"How Charge is calculated. The method behind the score.": "Comment la Charge est calculée. La méthode derrière le score.", +"How Charge, Effort and Rest are calculated, and how they differ from WHOOP.": "Comment Charge, Effort et Repos sont calculés, et en quoi ils diffèrent de WHOOP.", +"How Effort is calculated": "Comment l'effort est calculé", +"How NOOP splits a night into light / deep / REM. This is a separate, opt-in recipe. Your default staging is unchanged unless you turn it on.": "Comment NOOP répartit une nuit en léger / profond / paradoxal. C'est une méthode distincte et facultative. Votre répartition par défaut reste inchangée tant que vous ne l'activez pas.", +"How NOOP works": "Comment fonctionne NOOP", +"How Rest is calculated": "Comment le repos est calculé", +"How Stress is calculated": "Comment le stress est calculé", +"How They Move Together": "Comment ils évoluent ensemble", +"How accurate is this?": "Quelle est la précision de ces données ?", +"How accurate is this? %@ the data behind your Fitness Age": "Quelle est la précision de ces données ? %@ les données derrière votre âge fitness", +"How accurate is this? Hide the data behind your Fitness Age": "Quelle est la précision de ces données ? Masquer les données derrière votre âge fitness", +"How accurate is this? Show the data behind your Fitness Age": "Quelle est la précision de ces données ? Afficher les données derrière votre âge fitness", +"How closely a good night tracks next-morning charge.": "À quel point une bonne nuit reflète la charge du lendemain matin.", +"How much one day's charge carries into the next.": "Dans quelle mesure la charge d'un jour se répercute sur le suivant.", +"How recovered you are, led by HRV versus your personal baseline.": "Votre niveau de récupération, déterminé par la VFC par rapport à votre référence personnelle.", +"How restorative your sleep was: duration, efficiency, deep+REM, timing.": "Le caractère réparateur de votre sommeil : durée, efficacité, profond+paradoxal, horaires.", +"How strong the buzz is.": "L'intensité de la vibration.", +"How sure is NOOP? · Solid · Building · Calibrating": "Quel est le niveau de confiance de NOOP ? · Solide · En construction · Calibrage", +"How this is computed": "Comment c'est calculé", +"How this is measured": "Comment cela est mesuré", +"How this score is calculated": "Comment ce score est calculé", +"How this works": "Comment ça fonctionne", +"How to read this": "Comment lire ceci", +"How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin temperature are measured from the strap (skin temp is shown as the deviation from your own baseline). Workouts is the count of activities you logged or that were detected. Recovery, Strain and Stress are NOOP's own on-device scores, not clinical measures: Recovery is a daily readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load derived from heart rate, and Stress is a 0–3 autonomic-load index from resting HR and HRV.": "Comment lire ceci : la VFC, la FC au repos, la durée du sommeil, la fréquence respiratoire et la température cutanée sont mesurées par le bracelet (la température cutanée est affichée comme un écart par rapport à votre propre référence). Entraînements correspond au nombre d'activités que vous avez enregistrées ou qui ont été détectées. Récupération, Effort et Stress sont des scores propres à NOOP calculés sur l'appareil, pas des mesures cliniques : la Récupération est un indice composite de forme quotidienne (VFC, FC au repos, sommeil et tendance de température cutanée), l'Effort est la charge cardiovasculaire dérivée de la fréquence cardiaque, et le Stress est un indice de charge autonome de 0 à 3 basé sur la FC au repos et la VFC.", +"How your scores are computed": "Comment vos scores sont calculés", +"How your scores work": "Comment fonctionnent vos scores", +"How your sleep is sorted": "Comment votre sommeil est classé", +"How's my charge trending?": "Comment évolue ma charge ?", +"How's your mood today?": "Comment est votre humeur aujourd'hui ?", +"Hydration": "Hydratation", +"Hydration today": "Hydratation aujourd'hui", +"Hydration tracking": "Suivi de l'hydratation", +"Hypnogram": "Hypnogramme", +"I have read and accept these terms, and I'm using NOOP with my own device and my own data, at my own risk.": "J'ai lu et j'accepte ces conditions, et j'utilise NOOP avec mon propre appareil et mes propres données, à mes risques et périls.", +"I understand": "J'ai compris", +"I understand this disconnects the ring from Oura and that NOOP cannot undo it for me.": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi.", +"I understand this disconnects the ring from Oura and that NOOP cannot undo it for me. To go back to Oura I would factory-reset the ring again and set it up in the Oura app.": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi. Pour revenir à Oura, je devrai réinitialiser la bague aux paramètres d'usine et la configurer à nouveau dans l'application Oura.", +"I understand this is an experimental wellness feature, not a medical device or a diagnosis.": "Je comprends qu'il s'agit d'une fonctionnalité de bien-être expérimentale, et non d'un dispositif médical ni d'un diagnostic.", +"I'm awake": "Je suis réveillé", +"I'm wearing it": "Je le porte", +"IDLE": "INACTIF", +"INBOX": "BOÎTE DE RÉCEPTION", +"Idle": "Inactif", +"If the Oura app is still running it will hold the ring and adoption will fail. Force-quit Oura, then scan.": "Si l'application Oura est encore en cours d'exécution, elle retiendra la bague et l'adoption échouera. Forcez la fermeture d'Oura, puis lancez le scan.", +"If you don't see the prompt, enable NOOP under Settings › Health › Data Access & Devices.": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils.", +"If you don't see the prompt, turn NOOP on under Settings › Health › Data Access & Devices.": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils.", +"If you extracted your ring's 16-byte key from a previous Oura setup, NOOP can talk to the ring with that key without resetting it, so the Oura app keeps working too. NOOP does not extract keys for you and cannot help you find one. If you do not know what this means, go back and use the standard setup or file import.": "Si vous avez extrait la clé de 16 octets de votre bague depuis une configuration Oura précédente, NOOP peut communiquer avec la bague grâce à cette clé sans la réinitialiser, si bien que l'application Oura continue elle aussi de fonctionner. NOOP n'extrait pas de clé pour vous et ne peut pas vous aider à en trouver une. Si vous ne savez pas ce que cela signifie, revenez en arrière et utilisez la configuration standard ou l'import de fichier.", +"If you feel unwell or are worried about your heart, contact a qualified professional; in an emergency, your local emergency service. Do not rely on NOOP.": "Si vous vous sentez mal ou si vous êtes inquiet pour votre cœur, contactez un professionnel qualifié ; en cas d'urgence, appelez les services d'urgence locaux. Ne comptez pas sur NOOP.", +"If you're still seated, buzz again this often.": "Si vous êtes toujours assis, vibrer à nouveau à cette fréquence.", +"Illness early-warning": "Alerte précoce de maladie", +"Imaging": "Imagerie", +"Imperial": "Impérial", +"Import": "Importer", +"Import Apple Health export": "Importer un export Apple Santé", +"Import NOOP backup": "Importer une sauvegarde NOOP", +"Import WHOOP export": "Importer un export WHOOP", +"Import a WHOOP export, Apple Health or a second band and your best-sourced record builds here, on this device.": "Importez un export WHOOP, Apple Santé ou un second bracelet, et votre historique le plus complet se construit ici, sur cet appareil.", +"Import a single exported workout file from any brand (Garmin, Coros, Suunto, Wahoo, Polar, Strava, Apple) straight off your device. GPS route, distance, heart rate and calories come in where the file has them. Fully offline; nothing leaves %@.": "Importez un seul fichier d'entraînement exporté depuis n'importe quelle marque (Garmin, Coros, Suunto, Wahoo, Polar, Strava, Apple) directement depuis votre appareil. Le tracé GPS, la distance, la fréquence cardiaque et les calories sont importés lorsque le fichier les contient. Entièrement hors ligne ; rien ne quitte %@.", +"Import an Apple Health export (Health app → profile → Export All Health Data → export.zip). 7 years of HR, HRV, sleep, SpO₂, steps and more, streamed locally. Large exports take a minute or two.": "Importez un export Apple Santé (application Santé → profil → Exporter toutes les données de santé → export.zip). 7 ans de FC, VFC, sommeil, SpO₂, pas et plus, traités localement. Les exports volumineux prennent une minute ou deux.", +"Import daily nutrition totals from a Cronometer or MacroFactor CSV export: calories in, protein, carbs, fat (and weight if present). Other trackers work too if the file has a date column and daily totals.": "Importez les totaux nutritionnels quotidiens depuis un export CSV Cronometer ou MacroFactor : calories consommées, protéines, glucides, lipides (et poids si présent). D'autres applications de suivi fonctionnent aussi si le fichier contient une colonne date et des totaux quotidiens.", +"Import failed. Your existing data was kept. %@": "Échec de l'import. Vos données existantes ont été conservées. %@", +"Import failed: %@": "Échec de l'import : %@", +"Import readings": "Importer des mesures", +"Import temp files": "Importer des fichiers temporaires", +"Import your Mi Band history (steps, heart rate, resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness app. On your iPhone: Files → On My iPhone → Mi Fitness, long-press the folder → Compress, then choose the .zip here. Fully offline; no Xiaomi account or Bluetooth needed. Smart Band 8/9/10.": "Importez l'historique de votre Mi Band (pas, fréquence cardiaque, FC au repos, phases de sommeil, SpO₂, stress et score de sommeil) directement depuis l'application Mi Fitness. Sur votre iPhone : Fichiers → Sur mon iPhone → Mi Fitness, appui long sur le dossier → Compresser, puis choisissez le fichier .zip ici. Entièrement hors ligne ; aucun compte Xiaomi ni Bluetooth requis. Smart Band 8/9/10.", +"Import your full WHOOP history (recovery, strain, sleep, workouts) from a data export (.zip). Works for WHOOP 4.0, 5.0 and MG. Get one at app.whoop.com → Data Management.": "Importez tout votre historique WHOOP (récupération, effort, sommeil, entraînements) depuis un export de données (.zip). Fonctionne pour WHOOP 4.0, 5.0 et MG. Obtenez-en un sur app.whoop.com → Data Management.", +"Import your history first. A WHOOP export in Data Sources fills every metric you can explore here in about a minute.": "Importez d'abord votre historique. Un export WHOOP dans Sources de données remplit toutes les métriques que vous pouvez explorer ici en environ une minute.", +"Import your own data export from Oura, Fitbit or Garmin: sleep, resting heart rate, HRV, steps and more, where the export has them. Download it from the brand's app (Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then choose the file here. Fully offline; nothing leaves %@. Each brand's own readiness or sleep score is kept for reference only. Your scores stay yours.": "Importez votre propre export de données depuis Oura, Fitbit ou Garmin : sommeil, fréquence cardiaque au repos, VFC, pas et plus, lorsque l'export les contient. Téléchargez-le depuis l'application de la marque (Oura : Compte → Export Data ; Fitbit : Google Takeout ; Garmin : Export Your Data), puis choisissez le fichier ici. Entièrement hors ligne ; rien ne quitte %@. Le score de forme ou de sommeil propre à chaque marque est conservé à titre de référence uniquement. Vos scores restent les vôtres.", +"Import your strength-training history from a Hevy CSV export or a Liftosaur JSON export. Each workout becomes a Strength session with a training-volume estimate (weight × reps). It's a volume figure, not a measured strain. It never changes your Effort.": "Importez votre historique de musculation depuis un export CSV Hevy ou un export JSON Liftosaur. Chaque entraînement devient une séance de Force avec une estimation du volume d'entraînement (poids × répétitions). C'est un chiffre de volume, pas un effort mesuré. Cela ne modifie jamais votre Effort.", +"Imported": "Importé", +"Imported %lld days (%lld values)": "%1$lld jours importés (%2$lld valeurs)", +"Imported %lld workouts": "%lld entraînements importés", +"Imported 1 workout": "1 entraînement importé", +"Importing overwrites everything currently on %@. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect. Export CSV writes a WHOOP-format zip of your days, sleeps, workouts and journal that re-imports into NOOP on Mac, iPhone, or Android. On-device computed rows are marked APPROXIMATE in its Source column; the full backup stays the lossless restore path.": "L'import écrase tout ce qui se trouve actuellement sur %@. Vos anciennes données sont conservées dans un fichier annexe par précaution. NOOP doit être relancé pour que l'import prenne effet. « Exporter en CSV » écrit un zip au format WHOOP de vos jours, sommeils, entraînements et journal, qui peut être réimporté dans NOOP sur Mac, iPhone ou Android. Les lignes calculées sur l'appareil sont marquées APPROXIMATIVE dans la colonne Source ; la sauvegarde complète reste le moyen de restauration sans perte.", +"Importing overwrites everything currently on this Mac. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect.": "L'importation écrase tout ce qui se trouve actuellement sur ce Mac. Vos anciennes données sont conservées dans un fichier annexe au cas où. NOOP doit être relancé pour qu'une importation prenne effet.", +"Importing…": "Importation…", +"Import…": "Importer…", +"In your data so far, this doesn’t move your %@ the way it typically does.": "Dans vos données actuelles, cela ne fait pas varier votre %@ comme d'habitude.", +"In your range": "Dans votre plage", +"Inactivity reminder": "Rappel d'inactivité", +"Incl. Helio. Live heart rate where the band exposes it. Help us test.": "Incl. Helio. Fréquence cardiaque en direct lorsque le bracelet la fournit. Aidez-nous à tester.", +"Independent, and experimental": "Indépendant, et expérimental", +"Independent: not affiliated with WHOOP": "Indépendant : non affilié à WHOOP", +"Informational only, not medical advice.": "À titre informatif uniquement, pas un avis médical.", +"Insights": "Perspectives", +"Insights read your journal and outcomes. Import your WHOOP export, which includes your journal, in Data Sources to unlock them.": "Les perspectives lisent votre journal et vos résultats. Importez votre export WHOOP, qui inclut votre journal, dans Sources de données pour les débloquer.", +"Installed outside the App Store": "Installé en dehors de l'App Store", +"Installing NOOP's key and confirming the ring answers only to NOOP. Keep the ring close and do not open the Oura app.": "Installation de la clé de NOOP et vérification que la bague ne répond qu'à NOOP. Gardez la bague à proximité et n'ouvrez pas l'application Oura.", +"Intensity Minutes": "Minutes d'intensité", +"Intensity minutes": "Minutes d'intensité", +"Interrogate what affects what.": "Interrogez ce qui affecte quoi.", +"Interval Timer": "Minuteur d'intervalle", +"Intervals": "Intervalles", +"Intraday": "Intrajournalier", +"It all stays on your strap and %@: no account, no cloud.": "Tout reste sur votre bracelet et %@ : pas de compte, pas de cloud.", +"It is a picture, not a verdict": "C'est un instantané, pas un verdict", +"It is not a substitute for a professional": "Cela ne remplace pas un professionnel", +"It isn't held by the WHOOP phone app. Only one host at a time: close the app or turn off its Bluetooth.": "Il n'est pas retenu par l'application WHOOP du téléphone. Un seul hôte à la fois : fermez l'application ou désactivez son Bluetooth.", +"It shows the shape of your heartbeat timing and a plain-language description of how steady it looked. It does not tell you whether anything is right or wrong.": "Cela montre la forme du rythme de vos battements et une description en langage simple de sa régularité apparente. Cela ne vous dit pas si quelque chose est normal ou anormal.", +"It's a notebook, not a lab. NOOP lines up the numbers you enter. It doesn't test, read, or judge them. Not medical advice.": "C'est un carnet, pas un laboratoire. NOOP aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse ni ne les juge. Ce n'est pas un avis médical.", +"It's charged and worn. The sensor needs skin contact to wake.": "Il est chargé et porté. Le capteur a besoin d'un contact avec la peau pour se réveiller.", +"It's within about a metre of %@.": "Il se trouve à environ un mètre de %@.", +"Jan": "Janv.", +"Jul": "Juil.", +"Jun": "Juin", +"Keep it within about a metre of %@.": "Gardez-le à environ un mètre de %@.", +"Keep screen on during a workout": "Garder l'écran allumé pendant un entraînement", +"Keep the Oura app instead (import a file)": "Conserver l'application Oura à la place (importer un fichier)", +"Keep the Oura app instead, import a file": "Conserver l'application Oura à la place, importer un fichier", +"Keep the ring on the charger or on your finger so it stays awake.": "Gardez la bague sur le chargeur ou à votre doigt pour qu'elle reste éveillée.", +"Keep your bloods, BP and body numbers private, on %@.": "Gardez vos résultats sanguins, votre tension et vos données corporelles privés, sur %@.", +"Keep your own numbers here": "Conservez vos propres données ici", +"Keeps the detailed beat-to-beat heart-rate stream running all day and night, not just while a live screen is open, so NOOP captures much more for overnight HRV, recovery and sleep. Uses more battery: your strap streams heart rate continuously while connected.": "Maintient le flux détaillé battement par battement de la fréquence cardiaque actif jour et nuit, pas seulement lorsqu'un écran en direct est ouvert, afin que NOOP capture bien plus de données pour la VFC nocturne, la récupération et le sommeil. Utilise plus de batterie : votre bracelet transmet en continu la fréquence cardiaque tant qu'il est connecté.", +"Key Metrics": "Métriques clés", +"LAST NIGHT": "NUIT DERNIÈRE", +"LIGHT": "LÉGER", +"LIVE HR ONLY": "FC EN DIRECT UNIQUEMENT", +"LIVE PHYSIOLOGY": "PHYSIOLOGIE EN DIRECT", +"LOCAL ONLY": "LOCAL UNIQUEMENT", +"LONG AXIS": "GRAND AXE", +"LOW": "FAIBLE", +"LOW SIGNAL": "SIGNAL FAIBLE", +"Lab Book": "Carnet de labo", +"Lab Book is a private notebook, not a medical service. NOOP stores and lines up the numbers you enter. It doesn't test, read, diagnose, or advise. Your records never leave %@; there's no account or cloud, so it isn't \"HIPAA-covered.\" Always rely on your doctor or pharmacist to interpret results.": "Le Carnet de labo est un carnet privé, pas un service médical. NOOP stocke et aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse, ne les diagnostique ni ne les commente. Vos données ne quittent jamais %@ ; il n'y a ni compte ni cloud, donc ce n'est pas « couvert par HIPAA ». Fiez-vous toujours à votre médecin ou pharmacien pour interpréter les résultats.", +"Lab Book keeps your own numbers. It doesn't test, read, or judge them, and it's not medical advice. Everything stays on %@.": "Le Carnet de labo conserve vos propres données. Il ne les teste, ne les analyse ni ne les juge, et ce n'est pas un avis médical. Tout reste sur %@.", +"Last %lld beats": "%lld derniers battements", +"Last 14 nights": "14 dernières nuits", +"Last 30 days": "30 derniers jours", +"Last 6 months": "6 derniers mois", +"Last 7 days": "7 derniers jours", +"Last 90 days": "90 derniers jours", +"Last Charge breakdown": "Dernier détail de la Charge", +"Last HRV reading": "Dernière mesure de VFC", +"Last Workouts": "Derniers entraînements", +"Last backup: %@": "Dernière sauvegarde : %@", +"Last gate fired": "Dernier déclencheur activé", +"Last import": "Dernier import", +"Last night": "La nuit dernière", +"Last night · %@": "Nuit dernière · %@", +"Last night, %@. Tonight's lands after you sleep with the strap on.": "Nuit dernière, %@. Le résultat de ce soir arrive une fois que vous aurez dormi avec le bracelet.", +"Last night, read in two seconds.": "La nuit dernière, lu en deux secondes.", +"Last offload completed": "Dernier déchargement terminé", +"Last offload result": "Résultat du dernier déchargement", +"Last reported by strap": "Dernière donnée signalée par le bracelet", +"Last seen %@": "Vu pour la dernière fois %@", +"Last session": "Dernière séance", +"Last session: %@": "Dernière séance : %@", +"Last sleep": "Dernier sommeil", +"Last sync": "Dernière synchronisation", +"Last synced %@.": "Dernière synchronisation %@.", +"Last synced %lld minutes ago. Reconnect to pull the latest.": "Dernière synchronisation il y a %lld minutes. Reconnectez-vous pour récupérer les dernières données.", +"Last synced %lldm ago": "Dernière synchronisation il y a %lldmin", +"Last week": "La semaine dernière", +"Last year": "L'année dernière", +"Later": "Plus tard", +"Latest": "Dernier", +"Latest entry: %@.": "Dernière entrée : %@.", +"Latest reading": "Dernière lecture", +"Latest sleep · %@": "Dernier sommeil · %@", +"Latest sleep, %@. This is your last scored session. Wear the strap overnight for a fresh score.": "Dernier sommeil, %@. C'est votre dernière séance notée. Portez le bracelet pendant la nuit pour obtenir un nouveau score.", +"Lean Body Mass": "Masse maigre", +"Lean Mass": "Masse maigre", +"Lean body mass": "Masse corporelle maigre", +"Learning": "Apprentissage", +"Learning your baseline, %lld of %lld nights.": "Apprentissage de votre référence, %lld de %lld nuits.", +"Learning your pattern": "Apprentissage de votre schéma", +"Leave none active": "Ne laisser aucune active", +"Led by your heart-rate variability (HRV) measured against your own personal baseline, plus resting heart rate, last night's Rest, breathing rate, and a skin-temperature signal (an early illness or overreach flag). Higher HRV versus your baseline means more Charge. NOOP needs a few nights to learn your baseline first. Until then you'll see “Calibrating”.": "Déterminée par votre variabilité de fréquence cardiaque (VFC) mesurée par rapport à votre référence personnelle, ainsi que par la fréquence cardiaque au repos, le Repos de la nuit dernière, la fréquence respiratoire et un signal de température cutanée (un indicateur précoce de maladie ou de surcharge). Une VFC plus élevée par rapport à votre référence signifie plus de Charge. NOOP a d'abord besoin de quelques nuits pour apprendre votre référence. Jusque-là, vous verrez « Calibrage ».", +"Led by your heart-rate variability versus your own baseline. The watch samples HRV rather than streaming it, so this needs about a week of nights to calibrate. Until then NOOP shows “needs more data”, never a guessed number.": "Déterminée par votre variabilité de fréquence cardiaque par rapport à votre propre référence. La montre échantillonne la VFC au lieu de la transmettre en continu, ce qui nécessite environ une semaine de nuits pour se calibrer. D'ici là, NOOP affiche « besoin de plus de données », jamais un chiffre estimé.", +"Leftover import copies": "Copies d'import restantes", +"Let NOOP tap your wrist for the things you turn on below, so you can leave your phone and still feel what matters.": "Laissez NOOP tapoter votre poignet pour les fonctions que vous activez ci-dessous, afin que vous puissiez laisser votre téléphone tout en ressentant ce qui compte.", +"Let the check-in fire on its own. Off keeps it manual: you start a breath from Breathe yourself.": "Laisser le check-in se déclencher tout seul. Désactivé, cela reste manuel : vous démarrez vous-même une respiration depuis Respirer.", +"Let the coach use my data": "Laisser le coach utiliser mes données", +"Lifting": "Musculation", +"Lifting log (Hevy / Liftosaur)": "Journal de musculation (Hevy / Liftosaur)", +"Light": "Léger", +"Light + sleep timing only": "Lumière + horaires de sommeil uniquement", +"Light Sleep": "Sommeil léger", +"Likely your Oura warranty and support, because the ring is no longer paired to Oura. Treat this as permanent.": "Probablement votre garantie et le support Oura, car la bague n'est plus jumelée à Oura. Considérez ceci comme permanent.", +"Lining them up…": "Alignement en cours…", +"Live": "En direct", +"Live Body Console": "Console corporelle en direct", +"Live HR": "FC en direct", +"Live HR (not fully paired)": "FC en direct (jumelage incomplet)", +"Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery (those are WHOOP-only).": "FC en direct + R-R uniquement · pas de sommeil, récupération, température cutanée, SpO₂, pas ou batterie (réservés à WHOOP).", +"Live HR works. Free the strap to unlock buzz, alarms & sync": "La FC en direct fonctionne. Libérez le bracelet pour débloquer vibrations, alarmes et synchro", +"Live heart rate": "Fréquence cardiaque en direct", +"Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap, WHOOP 4.0 is never affected.": "La fréquence cardiaque en direct fonctionne déjà sur un bracelet WHOOP 5/MG. Ces sondes vont plus loin et essaient d'en extraire davantage. Ce sont des estimations, désactivées par défaut, et elles ne touchent jamais qu'un bracelet 5/MG, le WHOOP 4.0 n'est jamais affecté.", +"Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap. WHOOP 4.0 is never affected.": "La fréquence cardiaque en direct fonctionne déjà sur un bracelet WHOOP 5/MG. Ces sondes vont plus loin et tentent d'en tirer davantage. Ce sont des suppositions, désactivées par défaut, qui ne touchent jamais qu'un bracelet 5/MG. WHOOP 4.0 n'est jamais concerné.", +"Live heart rate and energy, recorded on your wrist. Stays on device.": "Fréquence cardiaque et énergie en direct, enregistrées au poignet. Reste sur l'appareil.", +"Live heart rate appears here once a strap is connected.": "La fréquence cardiaque en direct apparaît ici une fois qu'un bracelet est connecté.", +"Live heart rate in Dynamic Island": "Fréquence cardiaque en direct dans la Dynamic Island", +"Live heart rate is flowing; full strap controls need an encrypted bond.": "La fréquence cardiaque en direct circule ; les contrôles complets du bracelet nécessitent une liaison chiffrée.", +"Live heart rate is instant. Recovery, strain and sleep sharpen as NOOP learns your baseline over your first nights of wear. Want your history now? Import your WHOOP export in Data Sources and it backfills in about a minute.": "La fréquence cardiaque en direct est instantanée. Récupération, effort et sommeil s'affinent à mesure que NOOP apprend votre référence pendant vos premières nuits de port. Vous voulez votre historique tout de suite ? Importez votre export WHOOP dans Sources de données, il se recharge en environ une minute.", +"Live heart rate on bands that don't need pairing. Help us test.": "Fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de jumelage. Aidez-nous à tester.", +"Live heart rate over time": "Fréquence cardiaque en direct dans le temps", +"Live heart rate, and HRV when the ring can measure it.": "Fréquence cardiaque en direct, et VFC quand la bague peut la mesurer.", +"Live machine data over Bluetooth FTMS. No sleep, recovery, skin temp or SpO₂. Effort needs the machine's heart rate; without it the session logs the machine metrics only.": "Données de machine en direct via Bluetooth FTMS. Pas de sommeil, récupération, température cutanée ou SpO₂. L'Effort nécessite la fréquence cardiaque de la machine ; sans elle, la séance n'enregistre que les métriques de la machine.", +"Live now. Your scores are building.": "En direct maintenant. Vos scores se construisent.", +"Live vitals, streamed from the strap.": "Constantes en direct, diffusées depuis le bracelet.", +"Loading the day…": "Chargement de la journée…", +"Loading your history…": "Chargement de votre historique…", +"Loading your sessions…": "Chargement de vos séances…", +"Loading your sleep history…": "Chargement de votre historique de sommeil…", +"Local Oura support is new and we cannot test every ring here. It may not connect on your ring, and it can change between updates. NOOP never makes up a number. If something does not work, it will tell you plainly.": "Le support Oura local est récent et nous ne pouvons pas tester chaque bague ici. Il se peut qu'il ne se connecte pas à votre bague, et cela peut changer d'une mise à jour à l'autre. NOOP n'invente jamais de chiffre. Si quelque chose ne fonctionne pas, il vous le dira clairement.", +"Lock %@": "Verrouiller %@", +"Lock the Mac when I take the strap off": "Verrouiller le Mac quand je retire le bracelet", +"Locked": "Verrouillé", +"Locked %@ · paces drift, re-measure anytime.": "Verrouillé %@ · les rythmes évoluent, remesurez à tout moment.", +"Locked %@. Switch to Breathe to use it, or re-measure above.": "Verrouillé %@. Passez à Respirer pour l'utiliser, ou remesurez ci-dessus.", +"Log": "Journal", +"Log %@": "Enregistrer %@", +"Log a coffee, tea, or energy drink and NOOP shows a rough estimate of how much may still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement.": "Enregistrez un café, un thé ou une boisson énergisante, et NOOP affiche une estimation approximative de ce qui pourrait encore être actif. C'est un repère basé sur une demi-vie type de 5 à 6 heures, pas une mesure.", +"Log a session you tracked elsewhere.": "Enregistrez une séance suivie ailleurs.", +"Log a sleep mark": "Enregistrer un repère de sommeil", +"Log alcohol or late caffeine with an amount and NOOP fits a personal dose curve: how much each extra unit tends to move your numbers. Until then it shows typical patterns, clearly labelled as not yet yours.": "Enregistrez de l'alcool ou de la caféine tardive avec une quantité, et NOOP ajuste une courbe de dose personnelle : dans quelle mesure chaque unité supplémentaire a tendance à faire varier vos chiffres. En attendant, il affiche des schémas types, clairement indiqués comme n'étant pas encore les vôtres.", +"Log at least one behaviour above before starting an experiment.": "Enregistrez au moins un comportement ci-dessus avant de lancer une expérience.", +"Log behaviours above. After a few days of answers, NOOP ranks how each one moves your charge, HRV and rest. Importing a WHOOP export (which includes its journal) backfills history instantly.": "Enregistrez des comportements ci-dessus. Après quelques jours de réponses, NOOP classe l'influence de chacun sur votre charge, votre VFC et votre repos. Importer un export WHOOP (qui inclut son journal) recharge instantanément l'historique.", +"Log custom %lld millilitres": "Enregistrer %lld millilitres personnalisés", +"Log going to sleep": "Enregistrer l'endormissement", +"Log journal": "Enregistrer le journal", +"Log period start": "Enregistrer le début des règles", +"Log waking up": "Enregistrer le réveil", +"Logged %lld millilitres at %@": "%1$lld millilitres enregistrés à %2$@", +"Logged as a nap. Wrong? Tap Edit to adjust your sleep and wake times.": "Enregistré comme sieste. Une erreur ? Touchez Modifier pour ajuster vos heures de coucher et de réveil.", +"Logged bedtime at %@.": "Heure de coucher enregistrée à %@.", +"Logged sessions across %@.": "Séances enregistrées sur %@.", +"Logged today": "Enregistré aujourd'hui", +"Logged wake-up at %@.": "Réveil enregistré à %@.", +"Logging ahead for tomorrow: today's activities inform tomorrow's recovery, just as yesterday's are reflected in today's. Tomorrow's answers line up with tomorrow's morning.": "Enregistrement anticipé pour demain : les activités d'aujourd'hui influencent la récupération de demain, tout comme celles d'hier se reflètent dans celle d'aujourd'hui. Les réponses de demain correspondront au matin de demain.", +"Long exhale · downshift to rest": "Expiration longue · bascule vers le repos", +"Looks like a workout": "Ressemble à un entraînement", +"Looks like a workout around %@–%@ (avg HR %lld, %lld min). Save it?": "Ressemble à un entraînement vers %@–%@ (FC moy. %lld, %lld min). L'enregistrer ?", +"Looks like a workout on %@ around %@–%@ (avg HR %lld, %lld min). Save it?": "Ressemble à un entraînement le %1$@ vers %2$@–%3$@ (FC moy. %4$lld, %5$lld min). L'enregistrer ?", +"Looks like a workout yesterday around %@–%@ (avg HR %lld, %lld min). Save it?": "Ressemble à un entraînement hier vers %@–%@ (FC moy. %lld, %lld min). L'enregistrer ?", +"Low": "Faible", +"Low battery": "Batterie faible", +"Low confidence": "Faible confiance", +"Low confidence staging. This night scored high efficiency but very little deep or REM, more likely an estimate miss than a real restorative shortfall.": "Répartition à faible confiance. Cette nuit affiche une efficacité élevée mais très peu de sommeil profond ou paradoxal, plus probablement une erreur d'estimation qu'un véritable déficit réparateur.", +"Luteal": "Lutéale", +"M": "L", +"MEDIUM": "MOYEN", +"METHOD": "MÉTHODE", +"MODERATE": "MODÉRÉ", +"Main sleep": "Sommeil principal", +"Maintain": "Maintenir", +"Make %@ your active device now? It will provide your live data. You can change this any time.": "Faire de %@ votre appareil actif maintenant ? Il fournira vos données en direct. Vous pouvez changer cela à tout moment.", +"Make %@ your active strap? From now on it provides your live data. %@'s history stays exactly as it is. Only new days come from %@.": "Faire de %1$@ votre bracelet actif ? Dès maintenant, il fournira vos données en direct. L'historique de %2$@ reste exactement tel quel. Seuls les nouveaux jours proviendront de %3$@.", +"Make active": "Rendre actif", +"Make it yours": "Faites-en le vôtre", +"Make sure it isn't already connected to another app (Zwift, the gym's app, a bike computer…).": "Assurez-vous qu'il n'est pas déjà connecté à une autre application (Zwift, l'application de la salle, un compteur vélo…).", +"Make sure it isn't connected to another app (a bike computer, the brand's own app…).": "Assurez-vous qu'il n'est pas connecté à une autre application (un compteur vélo, l'application propre à la marque…).", +"Make sure it's NOT connected to the official WHOOP app right now.": "Assurez-vous qu'il n'est PAS connecté à l'application WHOOP officielle en ce moment.", +"Make sure it's awake and not connected elsewhere.": "Assurez-vous qu'il est éveillé et non connecté ailleurs.", +"Make sure the Oura app is fully closed. A ring answers one owner at a time.": "Assurez-vous que l'application Oura est complètement fermée. Une bague ne répond qu'à un seul propriétaire à la fois.", +"Make the strap do things, tap to act, walk away to lock, train by feel.": "Faites faire des choses au bracelet, tapez pour agir, éloignez-vous pour verrouiller, entraînez-vous au ressenti.", +"Make the strap do things: tap to act, walk away to lock, train by feel.": "Faites faire des choses à votre bracelet : touchez pour agir, éloignez-vous pour verrouiller, entraînez-vous au feeling.", +"Make this your active device?": "Faire de cet appareil votre appareil actif ?", +"Make this your active strap?": "Faire de ce bracelet votre bracelet actif ?", +"Makes your WHOOP 5.0/MG advertise its heart rate as a standard Bluetooth HR sensor, so a Garmin (Edge/watch), Zwift or gym equipment can use it during a workout. Applied on the next connection (and immediately if connected); writes the strap's whoop_live_hr_in_adv_ind_pkt flag. Reversible. iPhone-side only. A Mac can't write to a 5/MG.": "Fait en sorte que votre WHOOP 5.0/MG diffuse sa fréquence cardiaque comme un capteur FC Bluetooth standard, afin qu'un Garmin (Edge/montre), Zwift ou un équipement de salle puisse l'utiliser pendant un entraînement. Appliqué à la prochaine connexion (et immédiatement si déjà connecté) ; modifie l'indicateur whoop_live_hr_in_adv_ind_pkt du bracelet. Réversible. Côté iPhone uniquement. Un Mac ne peut pas écrire sur un 5/MG.", +"Male": "Masculin", +"Manage devices": "Gérer les appareils", +"Manual": "Manuel", +"Manual (you set this by hand)": "Manuel (défini manuellement)", +"Manual override": "Remplacement manuel", +"Manual steps coefficient": "Coefficient manuel des pas", +"Map of your %@ route, %@.": "Carte de votre parcours de %1$@, %2$@.", +"Mar": "Mars", +"Mark a Moment": "Marquer un moment", +"Mark a moment": "Marquer un moment", +"Mark all read": "Tout marquer comme lu", +"Mark done today": "Marquer comme fait aujourd'hui", +"Marker": "Repère", +"Markers": "Marqueurs", +"Match": "Correspondance", +"Max HR": "FC max", +"Max Heart Rate": "Fréquence cardiaque maximale", +"Max heart rate": "Fréquence cardiaque maximale", +"Max heart rate override": "Remplacement FC max", +"Max heart rate override, %@": "Remplacement FC max, %@", +"May": "Mai", +"Mean": "Moyenne", +"Mean %@": "Moyenne %@", +"Mean HR": "FC moyenne", +"Measurement system": "Système de mesure", +"Measuring…": "Mesure en cours…", +"Medium": "Moyen", +"Menu and settings": "Menu et paramètres", +"Metric": "Métrique", +"Metric Relationships": "Relations entre métriques", +"Metrics": "Métriques", +"Mid-cycle shift": "Décalage en milieu de cycle", +"Mind": "Esprit", +"Minutes seated before the first nudge.": "Minutes assis avant la première relance.", +"Min–max normalized · sparse series widened past %@ · %@": "Normalisé min-max · séries clairsemées élargies au-delà de %1$@ · %2$@", +"Mirror at noop dot fans, a fallback if GitHub is down": "Miroir sur noop point fans, une solution de secours si GitHub est indisponible", +"Mirror: noop.fans": "Miroir : noop.fans", +"Missing": "Manquant", +"Model": "Modèle", +"Moderate": "Modéré", +"Moment marked.": "Moment marqué.", +"Mon": "Lun", +"Monday": "Lundi", +"Mood": "Humeur", +"Mood, alongside your body's signals": "Humeur, aux côtés des signaux de votre corps", +"More": "Plus", +"Morning": "Matin", +"Most Active": "Le plus actif", +"Mostly %@ (%@).": "Principalement %1$@ (%2$@).", +"Mostly steady, with a few isolated extra or skipped beats. Very common and usually nothing.": "Globalement régulier, avec quelques battements supplémentaires ou manqués isolés. Très courant et généralement sans gravité.", +"Motion": "Mouvement", +"Move": "Bouger", +"Move %@ down": "Déplacer %@ vers le bas", +"Move %@ up": "Déplacer %@ vers le haut", +"Move all your NOOP data to another machine. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces %@'s data with a backup.": "Déplacez toutes vos données NOOP vers un autre appareil. L'export enregistre tout (historique, sommeils, entraînements, paramètres) dans un seul fichier que vous pouvez copier ; l'import remplace les données de %@ par une sauvegarde.", +"Move all your NOOP data to another machine. Export saves everything, history, sleeps, workouts, settings, to a single file you can copy across; import replaces this Mac's data with a backup.": "Transférez toutes vos données NOOP vers une autre machine. L'export sauvegarde tout, historique, nuits, entraînements, paramètres, dans un seul fichier à copier ; l'import remplace les données de ce Mac par une sauvegarde.", +"Move reminder": "Rappel de mouvement", +"Movement": "Mouvement", +"Mute wrist alerts overnight.": "Couper les alertes au poignet pendant la nuit.", +"My Dashboard": "Mon tableau de bord", +"My WHOOP": "Mon WHOOP", +"N-of-1 protocol": "Protocole N-de-1", +"NEW": "NOUVEAU", +"NO DATA": "PAS DE DONNÉES", +"NOOP %@ is here. Tap to read what's new.": "NOOP %@ est arrivé. Appuyez pour découvrir les nouveautés.", +"NOOP Charge": "Charge NOOP", +"NOOP can read a coarse menstrual-cycle phase from your nightly skin temperature, entirely on your device. It is awareness only: not contraception, not a fertility predictor, not a medical service.": "NOOP peut estimer une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur votre appareil. C'est uniquement informatif : ni contraception, ni prédicteur de fertilité, ni service médical.", +"NOOP can run off only an Apple Watch, no chest strap needed. The watch is the sensor; NOOP does the thinking on your phone, computing Charge, Rest, Effort and your Fitness Age from your Health data, all on-device.": "NOOP peut fonctionner avec une simple Apple Watch, sans ceinture pectorale. La montre est le capteur ; NOOP fait les calculs sur votre téléphone, en calculant Charge, Repos, Effort et votre âge de forme physique à partir de vos données Santé, le tout sur l'appareil.", +"NOOP can tap your wrist when your %@ needs you. No glance at the screen required.": "NOOP peut tapoter votre poignet quand votre %@ a besoin de vous. Pas besoin de regarder l'écran.", +"NOOP computed": "Calculé par NOOP", +"NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.": "NOOP estime vos pas à partir du mouvement de votre WHOOP, calibré sur le nombre de pas de votre téléphone. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas.", +"NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0–100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": "NOOP vous donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de votre bracelet en utilisant une science du sport publiée et évaluée par des pairs, et calculés entièrement sur votre appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les mêmes trois questions avec une science ouverte, donc ils suivront généralement la direction de ceux de WHOOP, mais ne correspondront pas chiffre pour chiffre. Et c'est bien le but.", +"NOOP is a personal, open project: not the WHOOP app, and not affiliated with WHOOP. It reads a strap you own, on your own device. Treat it as a capable work-in-progress rather than a finished product.": "NOOP est un projet personnel et ouvert : ce n'est pas l'application WHOOP et il n'est pas affilié à WHOOP. Il lit un bracelet que vous possédez, sur votre propre appareil. Considérez-le comme un travail en cours capable plutôt qu'un produit fini.", +"NOOP is an unofficial project: not affiliated with, endorsed by, or sponsored by WHOOP, Inc. \"WHOOP\" is their trademark, used only to name the hardware NOOP works with.": "NOOP est un projet non officiel : il n'est ni affilié, ni approuvé, ni sponsorisé par WHOOP, Inc. « WHOOP » est leur marque déposée, utilisée uniquement pour nommer le matériel avec lequel NOOP fonctionne.", +"NOOP is free and provided \"as is\", with no warranty. Liability is limited to the maximum extent the law that applies to you allows, and nothing here removes protections your local law won't let us remove.": "NOOP est gratuit et fourni « tel quel », sans garantie. La responsabilité est limitée dans la mesure maximale permise par la loi qui vous est applicable, et rien ici ne supprime les protections que votre législation locale ne nous permet pas de supprimer.", +"NOOP is not a medical device. It is for informational and personal-insight purposes only and is not intended to diagnose, treat, cure or prevent any condition. Talk to a clinician for medical advice.": "NOOP n'est pas un dispositif médical. Il est destiné uniquement à des fins d'information et d'introspection personnelle et n'est pas conçu pour diagnostiquer, traiter, guérir ou prévenir une condition. Consultez un clinicien pour des conseils médicaux.", +"NOOP is opening your on-device data. Your paired bands will appear here in a moment.": "NOOP ouvre vos données sur l'appareil. Vos bracelets couplés apparaîtront ici dans un instant.", +"NOOP looks for known notification apps on %@: Mail, Outlook, WhatsApp, Teams, Messages, Slack and similar. Install one and it'll appear here automatically.": "NOOP recherche les applications de notification connues sur %@ : Mail, Outlook, WhatsApp, Teams, Messages, Slack et similaires. Installez-en une et elle apparaîtra ici automatiquement.", +"NOOP looks for machines that broadcast the standard Bluetooth Fitness Machine service.": "NOOP recherche les appareils qui diffusent le service Bluetooth Fitness Machine standard.", +"NOOP never decides whether a value is \"normal,\" \"high,\" or \"low.\" Any reference range shown is exactly what you typed from your own report.": "NOOP ne décide jamais si une valeur est « normale », « élevée » ou « basse ». Toute plage de référence affichée est exactement ce que vous avez saisi depuis votre propre rapport.", +"NOOP never fills this in. It only shows back exactly what you type from your own report.": "NOOP ne remplit jamais ce champ lui-même. Il affiche uniquement ce que vous saisissez depuis votre propre rapport.", +"NOOP never makes up a number. When it can't compute one honestly it tells you what's missing and what to do, rather than showing a fake value.": "NOOP n'invente jamais de chiffre. Quand il ne peut pas en calculer un honnêtement, il vous dit ce qui manque et quoi faire, plutôt que d'afficher une fausse valeur.", +"NOOP never shows you a number it had to make up. If a score isn't ready, it tells you why and what to do next. Everything here runs on your device, from your strap.": "NOOP ne vous montre jamais un chiffre qu'il a dû inventer. Si un score n'est pas prêt, il vous explique pourquoi et quoi faire ensuite. Tout ici fonctionne sur votre appareil, à partir de votre bracelet.", +"NOOP pairs directly with your WHOOP over Bluetooth, no WHOOP app, no cloud.": "NOOP se connecte directement à votre WHOOP via Bluetooth, sans app WHOOP, sans cloud.", +"NOOP pairs directly with your WHOOP over Bluetooth: no WHOOP app, no cloud.": "NOOP se couple directement à votre WHOOP via Bluetooth : pas d'application WHOOP, pas de cloud.", +"NOOP picks the best-sourced number and shows you where each came from. It's for wellness and curiosity. It doesn't diagnose or replace medical advice.": "NOOP choisit le chiffre le mieux sourcé et vous montre d'où vient chacun. C'est pour le bien-être et la curiosité. Il ne diagnostique pas et ne remplace pas un avis médical.", +"NOOP picks your main sleep as your longest real block, and (once it has learned your usual hours) the one nearest your normal sleep time. Everything else that day is a nap. You can always edit bed and wake times.": "NOOP choisit comme sommeil principal votre plus long bloc réel, et (une fois qu'il a appris vos horaires habituels) celui le plus proche de votre heure de sommeil normale. Tout le reste ce jour-là est une sieste. Vous pouvez toujours modifier les heures de coucher et de réveil.", +"NOOP reads live heart rate on bands that don't require pairing. Newer bands need an auth handshake we can't do yet.": "NOOP lit la fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de couplage. Les bracelets plus récents nécessitent une authentification que nous ne pouvons pas encore effectuer.", +"NOOP reads live heart rate when the band exposes it. Some bands need a pairing we can't do yet. If so, we'll say so honestly.": "NOOP lit la fréquence cardiaque en direct quand le bracelet l'expose. Certains bracelets nécessitent un couplage que nous ne pouvons pas encore effectuer. Si c'est le cas, nous vous le dirons honnêtement.", +"NOOP reads your Apple Watch data through Apple Health, on your phone, nothing leaves the device. You choose exactly what to share.": "NOOP lit les données de votre Apple Watch via Apple Santé, sur votre téléphone, rien ne quitte l'appareil. Vous choisissez exactement ce que vous partagez.", +"NOOP reads your heart rate, HRV, resting heart rate, sleep, steps, energy and VO₂ max from Apple Health to compute your scores. It all stays on this iPhone, and you pick exactly what to share on the next screen.": "NOOP lit votre fréquence cardiaque, VFC, fréquence cardiaque au repos, sommeil, pas, énergie et VO₂ max depuis Apple Santé pour calculer vos scores. Tout reste sur cet iPhone, et vous choisissez exactement quoi partager à l'écran suivant.", +"NOOP scores your charge, effort and rest itself: on-device, no cloud.": "NOOP calcule lui-même votre charge, votre effort et votre repos : sur l'appareil, sans cloud.", +"NOOP scores your recovery, strain and sleep itself, on-device, no cloud.": "NOOP calcule lui-même votre récupération, effort et sommeil, sur l'appareil, sans cloud.", +"NOOP shows the %@ reading because it %@ for this metric: a higher-trust source here, not a verdict that the others are wrong.": "NOOP affiche la mesure de %1$@ car il %2$@ pour cette métrique : une source plus fiable ici, pas un verdict que les autres se trompent.", +"NOOP stores and lines up the numbers you enter yourself. It does not test you, read your results, give medical advice, or diagnose anything.": "NOOP stocke et aligne les chiffres que vous saisissez vous-même. Il ne vous teste pas, ne lit pas vos résultats, ne donne pas d'avis médical et ne diagnostique rien.", +"NOOP stores this key only on this device, in the same place it stores your paired bands.": "NOOP stocke cette clé uniquement sur cet appareil, au même endroit que vos bracelets couplés.", +"NOOP talks to your strap directly over Bluetooth Low Energy. There's no server in the middle. The connection is local, and so is every reading it pulls in.": "NOOP communique directement avec votre bracelet via Bluetooth Low Energy. Il n'y a aucun serveur intermédiaire. La connexion est locale, tout comme chaque mesure qu'il récupère.", +"NOOP talks to your strap's firmware over an unofficial, independently-mapped protocol. There is a residual risk to the device, its data, and its connection to official services. You assume that risk.": "NOOP communique avec le micrologiciel de votre bracelet via un protocole non officiel, cartographié de manière indépendante. Il subsiste un risque résiduel pour l'appareil, ses données et sa connexion aux services officiels. Vous assumez ce risque.", +"NOOP taps your strap so an alert lands on your wrist instead of your screen. No need to reach for it. Everything stays on %@.": "NOOP tapote votre bracelet pour qu'une alerte arrive sur votre poignet plutôt que sur votre écran. Pas besoin de le prendre en main. Tout reste sur %@.", +"NOOP will install its own key on the ring and become its owner. The Oura app will no longer control this ring. This is intended and it cannot be undone from NOOP.": "NOOP installera sa propre clé sur la bague et en deviendra le propriétaire. L'application Oura ne contrôlera plus cette bague. C'est voulu et cela ne peut pas être annulé depuis NOOP.", +"NOOP will look for it nearby.": "NOOP le recherchera à proximité.", +"NOOP will re-learn your baseline from tonight's data onward. Your history is kept, and it takes a few nights to settle.": "NOOP réapprendra votre référence à partir des données de cette nuit. Votre historique est conservé, et il faut quelques nuits pour se stabiliser.", +"NOOP · open on iPhone": "NOOP · ouvrir sur iPhone", +"NOOP's own Charge, Effort and Rest, computed on your device from published methods.": "La Charge, l'Effort et le Repos propres à NOOP, calculés sur votre appareil selon des méthodes publiées.", +"NOOP's primary, fully-supported band": "Le bracelet principal de NOOP, entièrement pris en charge", +"NOOP, all your data, none of the cloud.": "NOOP, toutes vos données, sans le cloud.", +"NOOP-computed": "Calculé par NOOP", +"NOOP. No data yet, open NOOP on your iPhone to sync.": "NOOP. Pas encore de données, ouvrez NOOP sur votre iPhone pour synchroniser.", +"NOOP. Scores out of date, last synced %@. Open NOOP on iPhone to refresh.": "NOOP. Scores non à jour, dernière synchro %@. Ouvrez NOOP sur iPhone pour actualiser.", +"NOOP: all your data, none of the cloud.": "NOOP : toutes vos données, aucun cloud.", +"Name": "Nom", +"Name & confirm": "Nommer et confirmer", +"Nap ended": "Sieste terminée", +"Nap started": "Sieste commencée", +"Nap(s)": "Sieste(s)", +"Naps": "Siestes", +"Navy": "Marine", +"Needs at least 14 days of history. When two or more signals drift together you get a banner on the dashboard and a notification, at most once a day.": "Nécessite au moins 14 jours d'historique. Quand deux signaux ou plus dérivent ensemble, vous recevez une bannière sur le tableau de bord et une notification, au maximum une fois par jour.", +"Needs interval frames": "Nécessite des trames d'intervalle", +"Needs more data": "Nécessite plus de données", +"Needs the full encrypted bond: close the official WHOOP app and pair the strap to NOOP first (a live-HR-only link can't carry the unlock).": "Nécessite le couplage chiffré complet : fermez l'application WHOOP officielle et couplez d'abord le bracelet à NOOP (une liaison FC en direct seule ne peut pas transporter le déverrouillage).", +"Needs the strap": "Nécessite le bracelet", +"Needs the strap. No data for today. Was your strap worn and connected overnight?": "Nécessite le bracelet. Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?", +"Neutral": "Neutre", +"Never": "Jamais", +"New data added": "Nouvelles données ajoutées", +"New here?": "Nouveau ici ?", +"New release notes and fresh data will land here.": "Les nouvelles notes de version et les données récentes arriveront ici.", +"New strap name": "Nouveau nom du bracelet", +"Newer WHOOP band, experimental in NOOP": "Bracelet WHOOP plus récent, expérimental dans NOOP", +"Next day": "Jour suivant", +"Next morning": "Matin suivant", +"Next night": "Nuit suivante", +"Next week": "Semaine suivante", +"Night detail": "Détail de nuit", +"Nights": "Nuits", +"No": "Non", +"No %@ here": "Aucun %@ ici", +"No Charge breakdown yet": "Aucun détail de Charge pour l'instant", +"No Data": "Aucune donnée", +"No HRV value": "Aucune valeur de VFC", +"No RMSSD · not enough R-R data": "Aucun RMSSD · données R-R insuffisantes", +"No SpO₂ import or Health value": "Aucun import SpO₂ ni valeur Santé", +"No WHOOP? You can still continue. Pair a heart-rate strap (Polar, Wahoo, Coospo, Garmin HRM…) or a gym machine under Devices, or import from WHOOP, Apple Health, Oura, Fitbit, Garmin and more under Data Sources. You can do either any time.": "Pas de WHOOP ? Vous pouvez quand même continuer. Couplez une ceinture cardio (Polar, Wahoo, Coospo, Garmin HRM…) ou une machine de salle dans Appareils, ou importez depuis WHOOP, Apple Santé, Oura, Fitbit, Garmin et plus dans Sources de données. Vous pouvez faire l'un ou l'autre à tout moment.", +"No account, no cloud, no sync. NOOP talks only to your strap and keeps everything local. Your data is yours alone.": "Aucun compte, aucun cloud, aucune synchronisation. NOOP ne communique qu'avec votre bracelet et garde tout en local. Vos données n'appartiennent qu'à vous.", +"No active stream": "Aucun flux actif", +"No backup yet.": "Aucune sauvegarde pour l'instant.", +"No backups found": "Aucune sauvegarde trouvée", +"No biometrics yet. Import your WHOOP export (and Apple Health if you have it) in Data Sources to fill this in.": "Pas encore de données biométriques. Importez votre export WHOOP (et Apple Santé si vous l'avez) dans Sources de données pour les compléter.", +"No caffeine logged. Log an intake to see an estimate.": "Aucune caféine enregistrée. Enregistrez une prise pour voir une estimation.", +"No cardio load yet. Effort builds once your heart rate climbs into your effort zone (around 50% of your heart-rate reserve). A calm day honestly reads near zero.": "Aucune charge cardio pour l'instant. L'Effort augmente quand votre fréquence cardiaque atteint votre zone d'effort (environ 50 % de votre réserve de fréquence cardiaque). Une journée calme affiche honnêtement une valeur proche de zéro.", +"No chest strap? No problem. NOOP can run off only your Apple Watch. It reads your watch's data through Apple Health and works out your Charge, Rest, Effort and Fitness Age right here on your phone. Everything stays on the device.": "Pas de ceinture pectorale ? Aucun problème. NOOP peut fonctionner avec votre seule Apple Watch. Il lit les données de votre montre via Apple Santé et calcule votre Charge, Repos, Effort et âge de forme physique directement sur votre téléphone. Tout reste sur l'appareil.", +"No clear pattern": "Aucun schéma clair", +"No clear reading yet": "Aucune mesure claire pour l'instant", +"No completed offload yet": "Aucun déchargement terminé pour l'instant", +"No data": "Pas de données", +"No data for these metrics in %@. Widen the range or pick metrics you've logged.": "Pas de données pour ces métriques dans %@. Élargissez la plage ou choisissez des métriques que vous avez enregistrées.", +"No data for today. Was your strap worn and connected overnight?": "Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?", +"No data, open NOOP on iPhone": "Pas de données, ouvrez NOOP sur iPhone", +"No days yet where both NOOP and your phone counted steps. Once your phone logs a few days alongside the strap, they'll appear here so you can see how close the estimate is.": "Aucun jour pour l'instant où NOOP et votre téléphone ont tous deux compté les pas. Une fois que votre téléphone enregistre quelques jours en parallèle du bracelet, ils apparaîtront ici pour voir la précision de l'estimation.", +"No drinks logged yet. Tap Sip, Cup or Bottle to start.": "Aucune boisson enregistrée pour l'instant. Appuyez sur Gorgée, Tasse ou Bouteille pour commencer.", +"No earlier night stored yet. Earlier nights sync in the morning.": "Aucune nuit antérieure stockée pour l'instant. Les nuits précédentes se synchronisent le matin.", +"No extra tonight. Your %@ forecast stays where it is.": "Rien d'extra ce soir. Votre prévision de %@ reste inchangée.", +"No folder chosen yet. Pick one your cloud app already syncs, or any local folder.": "Aucun dossier choisi pour l'instant. Choisissez-en un que votre application cloud synchronise déjà, ou n'importe quel dossier local.", +"No heart rate for this day": "Aucune fréquence cardiaque pour ce jour", +"No heart-rate data": "Aucune donnée de fréquence cardiaque", +"No history yet.": "Aucun historique pour l'instant.", +"No iOS diagnostics available.": "Aucun diagnostic iOS disponible.", +"No intraday stress data yet today.": "Aucune donnée de stress intrajournalière aujourd'hui pour l'instant.", +"No live heart rate yet. Open Live to pair your strap.": "Aucune fréquence cardiaque en direct pour l'instant. Ouvrez En direct pour coupler votre bracelet.", +"No live stream.": "Aucun flux en direct.", +"No match. Add it as a custom marker below.": "Aucune correspondance. Ajoutez-le comme marqueur personnalisé ci-dessous.", +"No metrics yet. Import your Whoop export or wear the strap to begin.": "Aucune métrique pour l'instant. Importez votre export Whoop ou portez le bracelet pour commencer.", +"No motion synced yet": "Aucun mouvement synchronisé pour l'instant", +"No movement detail for this night": "Aucun détail de mouvement pour cette nuit", +"No movement detail recorded for this night": "Aucun détail de mouvement enregistré pour cette nuit", +"No naps recorded for this day.": "Aucune sieste enregistrée pour ce jour.", +"No nightly skin-temp value": "Aucune valeur de température cutanée nocturne", +"No nights here yet. Import your WHOOP export in Data Sources to see every night, your sleep stages and trends straight away. Or open Intelligence to see last night computed from the strap after you wear it to bed.": "Pas encore de nuits ici. Importez votre export WHOOP dans Sources de données pour voir chaque nuit, vos stades de sommeil et tendances immédiatement. Ou ouvrez Intelligence pour voir la nuit dernière calculée depuis le bracelet après l'avoir porté au lit.", +"No nights with sleep data yet. Your ledger fills in as you wear the strap to bed.": "Aucune nuit avec données de sommeil pour l'instant. Votre registre se remplit à mesure que vous portez le bracelet pour dormir.", +"No numeric readings yet.": "Aucune mesure numérique pour l'instant.", +"No on-device store yet.": "Aucun stockage sur l'appareil pour l'instant.", +"No overlap yet between this marker and %@. Log a few more readings (and keep wearing your strap).": "Aucun recoupement pour l'instant entre ce marqueur et %@. Enregistrez quelques mesures supplémentaires (et continuez à porter votre bracelet).", +"No profile photo set": "Aucune photo de profil définie", +"No raw capture has been recorded yet this session.": "Aucune capture brute n'a été enregistrée pendant cette session pour l'instant.", +"No readings recorded.": "Aucune lecture enregistrée.", +"No readings this week": "Aucune mesure cette semaine", +"No readings this week yet": "Aucune mesure cette semaine pour l'instant", +"No respiratory-rate value": "Aucune valeur de fréquence respiratoire", +"No resting HR value": "Aucune valeur de fréquence cardiaque au repos", +"No scored days in this window. Widen the range or import more history.": "Aucun jour avec score dans cette période. Élargissez la plage ou importez plus d'historique.", +"No sources yet": "Aucune source pour l'instant", +"No stage data recorded for this night.": "Aucune donnée de phase enregistrée pour cette nuit.", +"No strap connected": "Aucun bracelet connecté", +"No strap connected. Put your WHOOP nearby and tap Re-scan to pair.": "Aucun bracelet connecté. Placez votre WHOOP à proximité et appuyez sur Rescanner pour le coupler.", +"No stress history yet. Import your WHOOP export in Data Sources to see it.": "Pas encore d'historique de stress. Importez votre export WHOOP dans Sources de données pour le voir.", +"No supported apps found": "Aucune application prise en charge trouvée", +"No usable activity found. Point at a .gpx, .tcx or .fit workout file.": "Aucune activité utilisable trouvée. Indiquez un fichier d'entraînement .gpx, .tcx ou .fit.", +"No usable rows found. Check the file has a date column (yyyy-MM-dd) and daily totals.": "Aucune ligne utilisable trouvée. Vérifiez que le fichier a une colonne de date (yyyy-MM-dd) et des totaux journaliers.", +"No warranty; liability limited": "Aucune garantie ; responsabilité limitée", +"No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or skin-temp readings fell inside %@. Wear your strap a few more days, or pick a wider range, then export again.": "Aucune mesure d'entraînement, de stress, de récupération, de sommeil, de VFC, de fréquence cardiaque au repos, d'effort, de fréquence respiratoire ou de température cutanée ne se situait dans %@. Portez votre bracelet quelques jours de plus, ou choisissez une plage plus large, puis exportez à nouveau.", +"No workouts found. Point at a Hevy CSV export or a Liftosaur JSON export.": "Aucun entraînement trouvé. Indiquez un export CSV Hevy ou un export JSON Liftosaur.", +"No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in, or add one you tracked elsewhere.": "Aucun entraînement pour l'instant. Ils proviennent de l'historique de votre WHOOP et d'Apple Santé. Importez dans Sources de données pour les récupérer, ou ajoutez-en un que vous avez suivi ailleurs.", +"No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in.": "Pas encore d'entraînements. Ils proviennent de votre historique WHOOP et Apple Santé. Importez dans Sources de données pour les intégrer.", +"Non-binary": "Non-binaire", +"Noon": "Midi", +"Normalized": "Normalisé", +"Normalized overlay": "Superposition normalisée", +"Not a medical device, not medical advice": "Pas un dispositif médical, pas un avis médical", +"Not a workout": "Pas un entraînement", +"Not affiliated with, endorsed by, or connected to WHOOP. Interoperability software for hardware you own and your own data. Use it only with a device you own, and not in breach of any agreement that applies to you. Not a medical device.": "Non affilié à, approuvé par ou connecté à WHOOP. Logiciel d'interopérabilité pour le matériel que vous possédez et vos propres données. Utilisez-le uniquement avec un appareil que vous possédez et sans enfreindre aucun accord qui vous est applicable. Pas un dispositif médical.", +"Not available": "Non disponible", +"Not calibrated": "Non calibré", +"Not calibrated yet": "Pas encore calibré", +"Not connected": "Non connecté", +"Not connected. Open Live to pair.": "Non connecté. Ouvrez En direct pour coupler.", +"Not enough clean beat data to lock a pace today. Try again rested, sitting still with the strap snug. For now we'll pace you at 5.5 br/min (coherence).": "Pas assez de données de battements propres pour fixer un rythme aujourd'hui. Réessayez reposé, assis immobile avec le bracelet bien ajusté. Pour l'instant, nous vous guiderons à 5,5 resp/min (cohérence).", +"Not enough clean beats. Sit still and try again.": "Pas assez de battements propres. Restez immobile et réessayez.", +"Not enough clean beats. Sit still and try again. %lld of %lld beats survived filtering (need %lld).": "Pas assez de battements propres. Restez immobile et réessayez. %1$lld battements sur %2$lld ont survécu au filtrage (il en faut %3$lld).", +"Not enough data": "Données insuffisantes", +"Not enough data for this window.": "Pas assez de données pour cette fenêtre.", +"Not enough data in this range yet": "Données insuffisantes dans cette plage pour l'instant", +"Not enough data yet": "Données insuffisantes pour l'instant", +"Not enough nights yet.": "Pas encore assez de nuits.", +"Not enough overlap between your journal answers and %@ to measure an effect yet. Keep logging. Effects need days both with and without each behaviour.": "Pas assez de recoupement entre vos réponses au journal et %@ pour mesurer un effet pour l'instant. Continuez à enregistrer. Les effets nécessitent des jours avec et sans chaque comportement.", +"Not enough overlap between your journal answers and %@ yet. Keep logging. Each behaviour needs days both with and without it before NOOP can read its effect.": "Pas encore assez de recoupement entre vos réponses au journal et %@. Continuez à enregistrer. Chaque comportement nécessite des jours avec et sans avant que NOOP puisse en lire l'effet.", +"Not enough overlapping days between these metrics in %@. Widen the range.": "Pas assez de jours chevauchants entre ces métriques dans %@. Élargissez la plage.", +"Not enough overlapping history to correlate your metrics yet.": "Pas encore assez d'historique chevauchant pour corréler vos métriques.", +"Not enough recent days to chart a trend yet. Import a history or keep wearing your strap.": "Pas encore assez de jours récents pour tracer une tendance. Importez un historique ou continuez à porter votre bracelet.", +"Not now": "Pas maintenant", +"Not recording": "Aucun enregistrement", +"Not recording. Strap not connected. Tap to connect.": "Aucun enregistrement. Bracelet non connecté. Appuyez pour connecter.", +"Not set": "Non défini", +"Not showing up? Make sure you reset the ring in the Oura app and force-quit it, then tap Rescan. A ring still owned by Oura will not list here.": "Elle n'apparaît pas ? Assurez-vous d'avoir réinitialisé la bague dans l'application Oura et de l'avoir forcée à quitter, puis appuyez sur Rescanner. Une bague encore possédée par Oura n'apparaîtra pas ici.", +"Not showing up? The official WHOOP app may still be holding it. Force-quit that app, then tap Rescan.": "Il n'apparaît pas ? L'application WHOOP officielle le retient peut-être encore. Forcez la fermeture de cette application, puis appuyez sur Rescanner.", +"Not synced yet": "Pas encore synchronisé", +"Note (optional)": "Note (facultatif)", +"Nothing": "Rien", +"Nothing here yet. Tap Enable Apple Health above to read your data live, or import a Health export .zip in Data Sources.": "Rien pour l'instant. Appuyez sur Activer Apple Santé ci-dessus pour lire vos données en direct, ou importez un fichier .zip d'export Santé dans Sources de données.", +"Nothing here yet. This sideloaded install can't read Apple Health directly. Import a Health export .zip in Data Sources, or turn on Shortcuts Export to bring your strap data into Health.": "Rien pour l'instant. Cette installation sideloadée ne peut pas lire Apple Santé directement. Importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export via Raccourcis pour faire entrer les données de votre bracelet dans Santé.", +"Nothing imported": "Rien d'importé", +"Nothing imported yet. In Data Sources, choose your Mi Fitness export (a .zip of the Mi Fitness app folder from the Files app) to bring in your steps, heart rate, sleep stages, SpO₂ and stress.": "Rien d'importé pour l'instant. Dans Sources de données, choisissez votre export Mi Fitness (un .zip du dossier de l'application Mi Fitness depuis l'application Fichiers) pour récupérer vos pas, votre fréquence cardiaque, vos phases de sommeil, votre SpO₂ et votre stress.", +"Nothing imported yet. On an iPhone: Health app, tap your photo, Export All Health Data, then import the .zip here in Data Sources.": "Rien d'importé encore. Sur iPhone : app Santé, tapez votre photo, Exporter toutes les données de santé, puis importez le .zip ici dans Sources de données.", +"Nothing in the catalog moves clearly with %@ over this window. Widen the range to surface relationships.": "Rien dans le catalogue n'évolue clairement avec %@ sur cette fenêtre. Élargissez la plage pour faire apparaître des relations.", +"Nothing leaves your %@": "Rien ne quitte votre %@", +"Nothing notable": "Rien de notable", +"Nothing offloaded for this window yet.": "Rien de déchargé pour cette période pour l'instant.", +"Nothing selected yet.": "Rien de sélectionné encore.", +"Nothing to export": "Rien à exporter", +"Nothing to fuse yet": "Rien à fusionner pour l'instant", +"Nothing to reclaim right now. NOOP already cleans up import scratch space automatically.": "Rien à récupérer pour l'instant. NOOP nettoie déjà automatiquement l'espace temporaire d'import.", +"Notifications are off, so this can't buzz yet. Turn on the master switch in Notifications to let it through.": "Les notifications sont désactivées, donc cela ne peut pas encore vibrer. Activez l'interrupteur principal dans Notifications pour le laisser passer.", +"Notify on low and full battery": "Notifier en cas de batterie faible ou pleine", +"Now": "Maintenant", +"OFFLINE": "HORS LIGNE", +"Off": "Désactivé", +"Off baseline": "Hors référence", +"Off wrist": "Hors du poignet", +"Off, the coach answers generally and sends none of your metrics.": "Désactivé, le coach répond de manière générale et n'envoie aucune de vos métriques.", +"Off: only your core metrics are shared, not your patterns or Lab Book.": "Désactivé : seules vos métriques principales sont partagées, pas vos tendances ni votre Carnet de labo.", +"Off: the coach answers generally and sends none of your metrics.": "Désactivé : le Coach répond de manière générale et n'envoie aucune de vos métriques.", +"Offers to save a workout when it spots sustained elevated heart rate": "Propose d'enregistrer un entraînement quand il détecte une fréquence cardiaque élevée soutenue", +"Offline": "Hors ligne", +"Okay": "OK", +"On %@": "Sur %@", +"On a 5/MG connection NOOP will send a puffin realtime-stream request after the handshake, and log what comes back. If you have a 5/MG strap, turning this on and sharing your strap log helps map the protocol. No effect on WHOOP 4.0.": "Sur une connexion 5/MG, NOOP enverra une requête de flux temps réel puffin après la poignée de main et journalisera ce qui revient. Si vous avez un bracelet 5/MG, activer ceci et partager votre journal de bracelet aide à cartographier le protocole. Aucun effet sur le WHOOP 4.0.", +"On a beta version of iOS, things can break that work on the release build.": "Sur une version bêta d'iOS, des choses qui fonctionnent sur la version finale peuvent casser.", +"On iPhone this is a sideloaded build. Re-sign it about every 7 days on a free Apple ID (longer on a paid account). After your phone reboots, unlock it once so NOOP can read and sync its data.": "Sur iPhone, il s'agit d'une version sideloadée. Resignez-la environ tous les 7 jours avec un identifiant Apple gratuit (plus longtemps avec un compte payant). Après le redémarrage de votre téléphone, déverrouillez-le une fois pour que NOOP puisse lire et synchroniser ses données.", +"On iPhone this is best-effort (iOS decides when background tasks run). Everything stays on %@.": "Sur iPhone, cela se fait au mieux (c'est iOS qui décide quand les tâches en arrière-plan s'exécutent). Tout reste sur %@.", +"On target": "Dans l'objectif", +"On the days your phone also counted steps, NOOP learns how much your motion maps to steps, then applies that to the strap-only days. The more matching days it has, the more it trusts the estimate.": "Les jours où votre téléphone comptait aussi les pas, NOOP apprend comment votre mouvement se traduit en pas, puis applique cela aux jours avec seulement le bracelet. Plus il a de jours correspondants, plus il fait confiance à l'estimation.", +"On wrist": "Au poignet", +"On, your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching.": "Activé, votre récupération, sommeil, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", +"On-device": "Sur l'appareil", +"On-device estimate (approximate), not a diagnosis.": "Estimation sur l'appareil (approximative), pas un diagnostic.", +"On-device footprint": "Empreinte sur l'appareil", +"On: a short summary of your strongest patterns and logged health numbers is added. Summaries only, never raw readings.": "Activé : un court résumé de vos tendances les plus marquées et de vos données de santé enregistrées est ajouté. Uniquement des résumés, jamais de mesures brutes.", +"On: your charge, rest, HRV and workouts are shared with the provider for tailored coaching.": "Activé : votre charge, repos, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", +"Once NOOP has 14 nights of history, in-range compares each vital to your own baseline (approximate, not medical advice); until then, typical adult ranges apply.": "Une fois que NOOP a 14 nuits d'historique, « dans la norme » compare chaque signe vital à votre propre référence (approximatif, pas un avis médical) ; jusque-là, les plages typiques pour un adulte s'appliquent.", +"Once this week has a day or two of data, your week-in-review appears here.": "Une fois que cette semaine a un ou deux jours de données, votre bilan de la semaine apparaît ici.", +"One reading so far: %@ %@. Log a few more to see a trend.": "Une seule mesure pour l'instant : %@ %@. Enregistrez-en quelques-unes de plus pour voir une tendance.", +"Only buzz when worn": "Vibrer uniquement quand porté", +"Only during active hours": "Uniquement pendant les heures actives", +"Only if your server requires one": "Uniquement si votre serveur en exige une", +"Only nudge during your active hours.": "Ne relancer que pendant vos heures actives.", +"Only when worn": "Uniquement quand porté", +"Open": "Ouvrir", +"Open Backup and Sync to a folder": "Ouvrir Sauvegarde et synchronisation vers un dossier", +"Open NOOP": "Ouvrir NOOP", +"Open NOOP first so it can reach your strap.": "Ouvrez d'abord NOOP pour qu'il puisse atteindre votre bracelet.", +"Open NOOP near your strap and let it catch up (a full history sync can take a while on first run). Once a day or two of motion lands, your step estimate and the calibration below will start to fill in.": "Ouvrez NOOP près de votre bracelet et laissez-le se mettre à jour (une synchronisation complète de l'historique peut prendre du temps au premier lancement). Une fois qu'un ou deux jours de mouvement sont arrivés, votre estimation de pas et la calibration ci-dessous commenceront à se remplir.", +"Open NOOP on your iPhone to sync": "Ouvrez NOOP sur votre iPhone pour synchroniser", +"Open Rhythm": "Ouvrir Rhythm", +"Open Test Centre": "Ouvrir le Centre de test", +"Open Test Centre to report a bug": "Ouvrez le Centre de test pour signaler un bug", +"Open live view": "Ouvrir la vue en direct", +"Open the official Oura app and remove this ring (Oura calls it \"factory reset\" or \"unpair and reset\"). This wipes the ring's owner so NOOP can take it over.": "Ouvrez l'application Oura officielle et retirez cette bague (Oura appelle cela « réinitialisation d'usine » ou « désassocier et réinitialiser »). Cela efface le propriétaire de la bague pour que NOOP puisse en prendre le contrôle.", +"Open-source BLE reverse-engineering work. Thank you.": "Travail de rétro-ingénierie BLE open-source. Merci.", +"Opens the Apple Watch setup and Health permission": "Ouvre la configuration de l'Apple Watch et l'autorisation Santé", +"Opens the Devices screen, where you pair and switch bands.": "Ouvre l'écran Appareils, où vous couplez et changez de bracelets.", +"Opens the GitHub releases page in your browser. NOOP itself makes no network connections, the macOS sandbox has no network entitlement.": "Ouvre la page des versions GitHub dans votre navigateur. NOOP lui-même n'effectue aucune connexion réseau, le bac à sable macOS n'a pas de droit réseau.", +"Opens workout detail": "Ouvre le détail de l'entraînement", +"Optional": "Facultatif", +"Optional note": "Note facultative", +"Optional trackers, off by default. Turn them on to add their cards. Everything stays on %@.": "Trackers facultatifs, désactivés par défaut. Activez-les pour ajouter leurs cartes. Tout reste sur %@.", +"Optional, on-device reads from your nightly signals. Each is off by default: for awareness only, never a diagnosis.": "Lectures facultatives, sur l'appareil, à partir de vos signaux nocturnes. Chacune est désactivée par défaut : uniquement à titre informatif, jamais un diagnostic.", +"Optional. Add a photo for the avatar in the top-left. Stored only on %@. NOOP is offline, so it's never uploaded.": "Facultatif. Ajoutez une photo pour l'avatar en haut à gauche. Stockée uniquement sur %@. NOOP est hors ligne, elle n'est donc jamais téléversée.", +"Optional. Stored only on this device.": "Facultatif. Stocké uniquement sur cet appareil.", +"Optional: adds your VO₂max estimate. The Fitness Age itself doesn't need it. Measure around your middle, at the navel.": "Facultatif : ajoute votre estimation de VO₂max. L'âge de forme physique lui-même n'en a pas besoin. Mesurez autour de votre taille, au niveau du nombril.", +"Optional: import now, or continue and return to Data Sources later.": "Facultatif : importez maintenant, ou continuez et revenez à Sources de données plus tard.", +"Other": "Autre", +"Other metrics (R-R, frames, battery, history) need a full sync.": "Les autres métriques (R-R, trames, batterie, historique) nécessitent une synchronisation complète.", +"Other sources don’t offload raw per-second data on-device.": "Les autres sources ne déchargent pas de données brutes à la seconde sur l'appareil.", +"Oura / Fitbit / Garmin export": "Export Oura / Fitbit / Garmin", +"Oura Readiness / Sleep score": "Score Préparation / Sommeil d'Oura", +"Oura ring": "Bague Oura", +"Oura's own Readiness and Sleep scores. NOOP does not copy them. It computes its own.": "Les scores Préparation et Sommeil propres à Oura. NOOP ne les copie pas. Il calcule les siens.", +"Outcome": "Résultat", +"Outcome metric": "Métrique de résultat", +"Outside range": "Hors plage", +"Over your readings, %@ and %@ move largely independently. No clear relationship.": "Sur vos mesures, %1$@ et %2$@ évoluent largement de manière indépendante. Aucune relation claire.", +"Overlay": "Superposition", +"Overlay 2–4 signals": "Superposer 2–4 signaux", +"Overlay signals, draw conclusions.": "Superposez les signaux, tirez des conclusions.", +"Overnight computed": "Calculé pendant la nuit", +"Overnight only": "Nuit uniquement", +"Overnight sleep staging, resting heart rate, skin-temperature trend, motion and battery, read straight off the ring.": "Phases de sommeil nocturne, fréquence cardiaque au repos, tendance de température cutanée, mouvement et batterie, lus directement depuis la bague.", +"Override the automatic fit with your own steps-per-motion value. Useful if your phone has no step history to learn from, or the estimate runs consistently high or low. Set it back to auto by dragging to the far left.": "Remplacez l'ajustement automatique par votre propre valeur de pas par mouvement. Utile si votre téléphone n'a pas d'historique de pas pour apprendre, ou si l'estimation est systématiquement trop haute ou trop basse. Remettez sur auto en faisant glisser complètement à gauche.", +"Own your data, offline": "Possédez vos données, hors ligne", +"Owned": "Possédé", +"PAIRED": "COUPLÉ", +"PAUSED": "EN PAUSE", +"PEAK": "PIC", +"POWER": "PUISSANCE", +"PRIMED": "PRÊT", +"Pace": "Rythme", +"Pair and manage the bands NOOP reads from.": "Couplez et gérez les bracelets que NOOP lit.", +"Paired": "Couplé", +"Paired locally. NOOP owns this ring while it holds the key. If you reset it again or set it up in the Oura app, NOOP no longer owns it and you would re-add it to take it over.": "Couplée localement. NOOP possède cette bague tant qu'il détient la clé. Si vous la réinitialisez à nouveau ou la configurez dans l'application Oura, NOOP ne la possède plus et vous devrez la rajouter pour en reprendre le contrôle.", +"Paired · idle": "Couplé · inactif", +"Pairing help: %@": "Aide au couplage : %@", +"Pairing…": "Couplage en cours…", +"Pairs directly with your strap over Bluetooth: no WHOOP app, no cloud.": "Se couple directement à votre bracelet via Bluetooth : pas d'application WHOOP, pas de cloud.", +"Parasympathetic (rest) band of your HRV.": "Bande parasympathique (repos) de votre VFC.", +"Partial": "Partiel", +"Passive": "Passif", +"Paste your %@ API key": "Collez votre clé API %@", +"Patterns in your own data: association, not cause.": "Tendances dans vos propres données : association, pas cause.", +"Paused": "En pause", +"Peak": "Pic", +"Peak heart rate": "Fréquence cardiaque de pointe", +"Peaked": "Au sommet", +"Pearson r over the visible window · |r| ≥ 0.30, n ≥ 10": "Pearson r sur la fenêtre visible · |r| ≥ 0,30, n ≥ 10", +"Per extra %@": "Par %@ supplémentaire", +"Per night, trailing 30 days": "Par nuit, sur les 30 derniers jours", +"Per-night need": "Besoin par nuit", +"Per-night sleep balance: %lld nights, net %@": "Bilan de sommeil par nuit : %1$lld nuits, net %2$@", +"Personal Experiment": "Expérience personnelle", +"Personal curve · prior-shrunk": "Courbe personnelle · réduite par prior", +"Phone": "Téléphone", +"Pick %@": "Choisir %@", +"Pick a new active strap": "Choisir un nouveau bracelet actif", +"Pick a sport. NOOP records HR, peak, average and effort from the live feed.": "Choisissez un sport. NOOP enregistre la FC, le pic, la moyenne et l'effort à partir du flux en direct.", +"Pick a wearable signal (resting HR, HRV, sleep, Charge, weight…) to line it up against this marker. NOOP averages the signal over the %@ before each reading.": "Choisissez un signal du dispositif porté (FC au repos, VFC, sommeil, Charge, poids…) à comparer avec ce marqueur. NOOP fait la moyenne du signal sur les %@ précédant chaque mesure.", +"Pick one behaviour you log, one outcome, and a short window. NOOP compares the days you log the behaviour against your behaviour-free days before the start.": "Choisissez un comportement que vous enregistrez, un résultat et une courte période. NOOP compare les jours où vous enregistrez le comportement aux jours sans ce comportement avant le début.", +"Pick the ring": "Choisir la bague", +"Pick when the nap started and ended. NOOP stages it from your data as its own session, separate from the night's sleep.": "Choisissez quand la sieste a commencé et fini. NOOP la traite à partir de vos données comme une session à part, séparée du sommeil nocturne.", +"Pick which apps reach your wrist in Settings → Notifications.": "Choisissez quelles applications atteignent votre poignet dans Paramètres → Notifications.", +"Pick your device": "Choisissez votre appareil", +"Pick your strap below, then tap Scan. NOOP will find it.": "Choisissez votre bracelet ci-dessous, puis appuyez sur Scanner. NOOP le trouvera.", +"Picked as your main sleep because it started near your usual sleep time.": "Choisi comme sommeil principal car il a commencé près de votre heure de sommeil habituelle.", +"Picked as your main sleep because it was your longest block (%@), near your usual bedtime.": "Choisi comme sommeil principal car c'était votre plus long bloc (%@), près de votre heure de coucher habituelle.", +"Picked as your main sleep because it was your longest block (%@).": "Choisi comme sommeil principal car c'était votre plus long bloc (%@).", +"Pinch to zoom · drag to pan": "Pincer pour zoomer · glisser pour déplacer", +"Plan a trip or shift": "Planifier un voyage ou un décalage", +"Plan · %lld-day shift": "Plan · décalage de %lld jours", +"Please read and accept the points below.": "Veuillez lire et accepter les points ci-dessous.", +"Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio broadcast": "Diffusion Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio", +"Poor": "Faible", +"Power users only.": "Réservé aux utilisateurs avancés.", +"Powers Charge, Effort, Rest and Sleep": "Alimente Charge, Effort, Repos et Sommeil", +"Powers Charge, Effort, Rest, Sleep + Health Monitor": "Alimente Charge, Effort, Repos, Sommeil + Moniteur de santé", +"Powers Effort now; Charge and Rest once enough nights and decode are confirmed": "Alimente l'Effort dès maintenant ; Charge et Repos une fois assez de nuits et le décodage confirmés", +"Powers Rest, Effort, Fitness Age and steps, plus Charge once recovery calibrates": "Alimente Repos, Effort, âge de forme physique et pas, plus Charge une fois la récupération calibrée", +"Powers the live console + Effort. No Charge, Rest or Sleep": "Alimente la console en direct + Effort. Aucune Charge, Repos ou Sommeil", +"Preparing…": "Préparation en cours…", +"Presence automation: set a Focus, pause media, set away…": "Automatisation de présence : définir un mode Focus, mettre en pause les contenus, se marquer absent…", +"Preview": "Aperçu", +"Preview dose": "Aperçu de la dose", +"Previous day": "Jour précédent", +"Previous night": "Nuit précédente", +"Previous week": "Semaine précédente", +"Previously paired but not currently connected. Re-scan to reconnect.": "Précédemment couplé mais non connecté actuellement. Rescannez pour reconnecter.", +"Primed": "Prêt", +"Probably not illness": "Probablement pas une maladie", +"Profile": "Profil", +"Profile photo": "Photo de profil", +"Project home & source": "Page et code source du projet", +"Project home and source code on GitHub": "Page et code source du projet sur GitHub", +"Proof that the console is current": "Preuve que la console est à jour", +"Protein": "Protéines", +"Provider": "Fournisseur", +"Pulling your strap's stored history. This drains oldest-first; a deep backlog now continues automatically across passes instead of waiting between syncs.": "Récupération de l'historique stocké sur votre bracelet. Il se vide en commençant par le plus ancien ; un arriéré important se poursuit désormais automatiquement sur plusieurs passages au lieu d'attendre entre les synchronisations.", +"Pulls your strap's stored history immediately, without waiting for the next automatic sync.": "Récupère immédiatement l'historique stocké sur votre bracelet, sans attendre la prochaine synchronisation automatique.", +"Put the band into pairing mode, on your wrist and awake.": "Mettez le bracelet en mode couplage, au poignet et actif.", +"Put the strap on first. The deep stream is on-wrist only.": "Mettez d'abord le bracelet. Le flux approfondi fonctionne uniquement au poignet.", +"Put your WHOOP 4.0 on your wrist and make sure it's awake.": "Mettez votre WHOOP 4.0 au poignet et assurez-vous qu'il est actif.", +"Put your strap on": "Mettez votre bracelet", +"QUICK ACTIONS": "ACTIONS RAPIDES", +"Questions, feedback, bugs, %@": "Questions, retours, bugs, %@", +"Questions, feedback, bugs: %@": "Questions, retours, bugs : %@", +"Quick actions": "Actions rapides", +"Quick sweep · ~7 min": "Balayage rapide · ~7 min", +"Quiet hours": "Heures de silence", +"Quiet hours end": "Fin des heures silencieuses", +"Quiet hours start": "Début des heures silencieuses", +"R-R intervals": "Intervalles R-R", +"RECORDING": "ENREGISTREMENT", +"RECORDING WORKOUT": "ENREGISTREMENT DE L'ENTRAÎNEMENT", +"REM Sleep": "Sommeil paradoxal", +"REM sleep": "Sommeil paradoxal", +"REST": "REPOS", +"RESTING HR + SLEEP + HRV": "FC AU REPOS + SOMMEIL + VFC", +"RMSSD %.0f ms now vs your ~%.0f ms baseline (estimate from PPG-derived R-R).": "RMSSD %.0f ms maintenant contre votre référence de ~%.0f ms (estimation à partir des R-R dérivés du PPG).", +"RMSSD %lld milliseconds": "RMSSD %lld millisecondes", +"ROUND": "CYCLE", +"RSA RESPONSE BY PACE": "RÉPONSE RSA PAR ALLURE", +"RSA response by pace": "Réponse RSA par allure", +"Radio connected, stream not yet trusted.": "Radio connectée, flux pas encore fiable.", +"Range": "Plage", +"Ranked · your data": "Classé · vos données", +"Ranked, lag-aware: which of your habits actually move your Charge, plus your personal alcohol/caffeine dose-response.": "Classé, avec prise en compte du délai : quelles habitudes influencent réellement votre Charge, plus votre réponse personnelle à la dose d'alcool/caféine.", +"Raw · per second": "Brut · par seconde", +"Re-anchors every baseline that feeds Charge to your recent nights. No stored day is deleted.": "Réancre chaque référence qui alimente la Charge sur vos nuits récentes. Aucun jour enregistré n'est supprimé.", +"Re-label as": "Réétiqueter comme", +"Re-nudge every": "Relancer toutes les", +"Re-scan": "Nouveau scan", +"Re-share your live strap heart rate over Bluetooth as a standard heart-rate sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read it. Local Bluetooth only. Nothing leaves %@. Off by default.": "Repartagez la fréquence cardiaque en direct de votre bracelet via Bluetooth comme un capteur de fréquence cardiaque standard, afin qu'un tapis de course, un vélo, Zwift, Peloton ou toute application fitness à proximité puisse la lire. Bluetooth local uniquement. Rien ne quitte %@. Désactivé par défaut.", +"React when the strap comes off or goes on. Note: macOS reserves true auto-UNLOCK for Apple Watch, so this can lock, not unlock.": "Réagissez lorsque le bracelet est retiré ou remis. Remarque : macOS réserve le vrai déverrouillage automatique à l'Apple Watch, ceci peut donc verrouiller, pas déverrouiller.", +"React when the strap comes off or goes on. Run a Shortcut to set a Focus, pause media, mark yourself away.": "Réagissez lorsque le bracelet est retiré ou remis. Exécutez un raccourci pour activer un mode Focus, mettre en pause les médias ou vous marquer absent.", +"Read the full Lab Book note": "Lire la note complète du Cahier de labo", +"Read the full note": "Lire la note complète", +"Read your heart rate, HRV, blood oxygen, respiratory rate, sleep, steps and energy straight from Apple Health, and write NOOP's strap-derived metrics back. Everything stays on %@.": "Lisez votre fréquence cardiaque, VFC, oxygène sanguin, fréquence respiratoire, sommeil, pas et énergie directement depuis Apple Santé, et réécrivez les métriques de NOOP issues du bracelet. Tout reste sur %@.", +"Readiness": "Préparation", +"Readiness: %@": "Préparation : %@", +"Readiness: %@. %@": "Préparation : %1$@. %2$@", +"Reading": "Mesure", +"Reading complete": "Mesure terminée", +"Reading incomplete": "Mesure incomplète", +"Reading your %@…": "Lecture de votre %@…", +"Reading your Apple Health history…": "Lecture de votre historique Apple Santé…", +"Reading your Fitness Age…": "Lecture de votre âge de forme…", +"Reading your Mi Band history…": "Lecture de l'historique de votre Mi Band…", +"Reading your Vitality…": "Lecture de votre Vitality…", +"Reading your heart-rate variability and resting heart rate…": "Lecture de votre variabilité de la fréquence cardiaque et FC au repos…", +"Reading your history…": "Lecture de votre historique…", +"Reading your journal and outcomes…": "Lecture de votre journal et résultats…", +"Reading your logbook…": "Lecture de votre journal…", +"Reading your sources…": "Lecture de vos sources…", +"Reads a coarse menstrual-cycle phase from your nightly skin temperature, entirely on %@. Awareness only: not contraception, not a fertility predictor, not a medical service. The card appears in Health.": "Lit une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur %@. À titre informatif uniquement : ni contraception, ni prédicteur de fertilité, ni service médical. La carte apparaît dans Santé.", +"Ready": "Prêt", +"Ready for a marked effort.": "Prêt pour un effort marqué.", +"Ready to breathe": "Prêt à respirer", +"Ready to connect your watch?": "Prêt à connecter votre montre ?", +"Ready to scan": "Prêt à scanner", +"Ready to sync": "Prêt à synchroniser", +"Ready-made actions": "Actions prêtes à l'emploi", +"Recalibrate": "Recalibrer", +"Recalibrate Charge baseline": "Recalibrer la référence de Charge", +"Recalibrate your Charge baseline?": "Recalibrer votre référence de Charge ?", +"Recent": "Récent", +"Recent intervals: %@ ms": "Intervalles récents : %@ ms", +"Recent moments": "Moments récents", +"Recharge your WHOOP before tonight.": "Rechargez votre WHOOP avant ce soir.", +"Reclaimable": "Récupérable", +"Reclaimed %@.": "%@ récupérés.", +"Recompute": "Recalculer", +"Reconnect help: %@": "Aide à la reconnexion : %@", +"Reconnect to pull the latest.": "Reconnectez-vous pour récupérer les dernières données.", +"Reconnects this run": "Reconnexions de cette session", +"Record a timestamped moment (and buzz the strap if it's connected).": "Enregistrez un moment horodaté (et faites vibrer le bracelet s'il est connecté).", +"Record a timestamped moment in NOOP.": "Enregistrez un moment horodaté dans NOOP.", +"Record or inspect the current stream": "Enregistrer ou inspecter le flux actuel", +"Record puffin frames to a file": "Enregistrer les trames puffin dans un fichier", +"Recorded on device": "Enregistré sur l'appareil", +"Recording": "Enregistrement", +"Recording status, not shown for a past day": "État d'enregistrement, non affiché pour un jour passé", +"Recording. Your strap is connected and saving data.": "Enregistrement en cours. Votre bracelet est connecté et enregistre les données.", +"Records & sources": "Enregistrements et sources", +"Records a live machine workout, Effort-scored from HR when the machine reports it": "Enregistre un entraînement sur machine en direct, l'Effort étant calculé à partir de la FC lorsque la machine la transmet", +"Recovery": "Récupération", +"Recovery / Charge": "Récupération / Charge", +"Recovery calendar, %lld days, average %lld, low %lld, high %lld": "Calendrier de récupération, %lld jours, moyenne %lld, minimum %lld, maximum %lld", +"Recovery calendar, no data": "Calendrier de récupération, aucune donnée", +"Recovery takes about a week": "La récupération prend environ une semaine", +"Recovery weighs your HRV against your personal baseline (~60%), resting heart rate (~20%), sleep performance (~15%) and respiration (~5%). Day strain is a 0–21 cardiovascular load from time in heart-rate zones. Sleep is staged from movement and heart rate. Everything is computed here from the strap's raw data, it works for any day NOOP collected raw streams.": "La récupération pondère votre VFC par rapport à votre référence personnelle (~60%), fréquence cardiaque au repos (~20%), performance du sommeil (~15%) et respiration (~5%). L'effort du jour est une charge cardiovasculaire de 0 à 21 basée sur le temps dans les zones de fréquence cardiaque. Le sommeil est stagé à partir du mouvement et de la fréquence cardiaque. Tout est calculé ici à partir des données brutes du bracelet, cela fonctionne pour chaque jour où NOOP a collecté des flux bruts.", +"Recovery, NOOP's Charge score, is led by your heart-rate variability measured against your own personal baseline. A chest strap streams beat-to-beat data densely all night, so it can learn that baseline fast. An Apple Watch instead samples HRV, a handful of readings through the day plus overnight, so the signal is real but sparser.": "La Récupération, le score de Charge de NOOP, est principalement déterminée par votre variabilité de fréquence cardiaque mesurée par rapport à votre référence personnelle. Un bracelet pectoral transmet des données battement par battement de manière dense toute la nuit, il peut donc apprendre cette référence rapidement. Une Apple Watch échantillonne quant à elle la VFC, avec quelques mesures dans la journée et pendant la nuit, le signal est donc réel mais plus épars.", +"Recovery, Test Centre, experimental probes, and backup. Tucked away to keep the everyday screen tidy.": "Récupération, Centre de test, sondes expérimentales et sauvegarde. Rangés pour garder l'écran du quotidien épuré.", +"Reference range from my report (optional)": "Plage de référence de mon bilan (facultatif)", +"Reference range from your own report, optional": "Plage de référence de votre propre bilan, facultatif", +"Refresh": "Actualiser", +"Refresh models": "Actualiser les modèles", +"Refresh models from provider": "Actualiser les modèles depuis le fournisseur", +"Refresh strap battery and connection state.": "Actualiser la batterie et l'état de connexion du bracelet.", +"Relax": "Détente", +"Relax 4-6": "Détente 4-6", +"Relaxation guidance from your own numbers: not a health alert, and not a diagnosis. Trends matter more than any single number.": "Conseils de détente basés sur vos propres chiffres : ni alerte santé, ni diagnostic. Les tendances comptent plus qu'un chiffre isolé.", +"Release notes": "Notes de version", +"Remaining": "Restant", +"Remind me to wind down": "Me rappeler de me détendre", +"Remove": "Supprimer", +"Remove %@": "Supprimer %@", +"Remove %@? NOOP will stop connecting to it. Its recorded data is kept and you can re-add it any time.": "Supprimer %@ ? NOOP cessera de s'y connecter. Ses données enregistrées sont conservées et vous pouvez le rajouter à tout moment.", +"Remove Apple Health imported data": "Supprimer les données importées d'Apple Santé", +"Remove Apple Health imported data?": "Supprimer les données importées d'Apple Santé ?", +"Remove a question to tidy your list. Custom questions are deleted; the built-in ones are hidden and can be restored below.": "Supprimez une question pour épurer votre liste. Les questions personnalisées sont supprimées ; les questions intégrées sont masquées et peuvent être restaurées ci-dessous.", +"Remove caffeine intake at %@": "Supprimer la prise de caféine à %@", +"Remove imported data": "Supprimer les données importées", +"Remove photo": "Supprimer la photo", +"Remove this device?": "Supprimer cet appareil ?", +"Removed": "Supprimé", +"Removed all Apple Health imported data.": "Toutes les données importées d'Apple Santé ont été supprimées.", +"Removed · data kept": "Supprimé · données conservées", +"Removes this recorded sleep and recomputes the day without it. This can't be undone.": "Supprime ce sommeil enregistré et recalcule le jour sans lui. Cette action est irréversible.", +"Removing…": "Suppression…", +"Rename": "Renommer", +"Rename device": "Renommer l'appareil", +"Replace all current data with the backup %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde %@ ? Cette action est irréversible.", +"Replace all current data with the backup from %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde du %@ ? Cette action est irréversible.", +"Replace all data": "Remplacer toutes les données", +"Replace this device's data with one of the backups in your folder. This overwrites current data, so back up first if you're unsure.": "Remplacez les données de cet appareil par l'une des sauvegardes de votre dossier. Cela écrase les données actuelles, faites donc d'abord une sauvegarde en cas de doute.", +"Report": "Signaler", +"Report a %@ bug": "Signaler un bug de %@", +"Report a bug with my log": "Signaler un bug avec mon journal", +"Rescan": "Rescanner", +"Reset": "Réinitialiser", +"Reset API key": "Réinitialiser la clé API", +"Reset Key Metrics to default": "Réinitialiser les métriques clés par défaut", +"Reset coach instructions to default": "Réinitialiser les instructions du Coach par défaut", +"Reset dashboard cards to default": "Réinitialiser les cartes du tableau de bord par défaut", +"Reset it in the Oura app first, then scan.": "Réinitialisez-le d'abord dans l'application Oura, puis scannez.", +"Reset it in the Oura app first.": "Réinitialisez-le d'abord dans l'application Oura.", +"Reset key": "Réinitialiser la clé", +"Reset to default": "Réinitialiser par défaut", +"Resonance": "Résonance", +"Resp Rate": "Fréq. resp.", +"Respect quiet hours": "Respecter les heures calmes", +"Respiratory": "Respiratoire", +"Respiratory Rate": "Fréquence respiratoire", +"Respiratory rate": "Fréquence respiratoire", +"Rest": "Repos", +"Rest baseline": "Référence de repos", +"Rest quality": "Qualité du repos", +"Rest up": "Reposez-vous", +"Rest ↔ Charge": "Repos ↔ Charge", +"Rest: how restorative was your sleep?": "Repos : à quel point votre sommeil a-t-il été réparateur ?", +"Restart": "Redémarrer", +"Restarts the roughly 4-night build-up for Charge and your HRV baseline from tonight. Use it if a bad first week set your baseline off. Your history stays.": "Relance la montée en charge d'environ 4 nuits pour la Charge et votre référence de VFC à partir de ce soir. Utilisez-le si une mauvaise première semaine a faussé votre référence. Votre historique reste intact.", +"Resting HR": "FC au repos", +"Resting HR is elevated and HRV is below your baseline, both classic signs of high activation. Prioritise rest, hydration and an easy day.": "La FC au repos est élevée et la VFC est en dessous de votre référence, deux signes classiques d'une forte activation. Privilégiez le repos, l'hydratation et une journée légère.", +"Resting HR ↔ Charge": "FC au repos ↔ Charge", +"Resting Heart Rate": "Fréquence cardiaque au repos", +"Resting heart rate": "Fréquence cardiaque au repos", +"Resting heart rate and HRV are sitting at or below baseline: low physiological stress. You're in a calm, recovered state.": "La fréquence cardiaque au repos et la VFC sont au niveau ou en dessous de la référence : faible stress physiologique. Vous êtes dans un état calme et récupéré.", +"Resting heart rate is low and HRV is up. Your nervous system looks well-recovered and calm. A great day to push if you want to.": "La fréquence cardiaque au repos est basse et la VFC est en hausse. Votre système nerveux semble bien récupéré et calme. Une excellente journée pour pousser si vous le souhaitez.", +"Resting heart rate is running high versus your norm. Your body is under load today. Keep effort light.": "La fréquence cardiaque au repos est élevée par rapport à votre norme. Votre corps est sous charge aujourd'hui. Gardez un effort léger.", +"Resting stress nudge (experimental)": "Rappel de stress au repos (expérimental)", +"Restorative": "Réparateur", +"Restorative Sleep": "Sommeil réparateur", +"Restore": "Restaurer", +"Restore backup %@": "Restaurer la sauvegarde %@", +"Restore backup from %@": "Restaurer la sauvegarde du %@", +"Restore from a backup…": "Restaurer à partir d'une sauvegarde…", +"Restore problem": "Problème de restauration", +"Restore this backup?": "Restaurer cette sauvegarde ?", +"Restore to Today": "Restaurer dans Aujourd'hui", +"Restored": "Restauré", +"Resume": "Reprendre", +"Return to workout": "Retour à l'entraînement", +"Reveal in Finder": "Afficher dans le Finder", +"Reverse the above when you return.": "Inversez ce qui précède à votre retour.", +"Reverts to the default profile icon": "Revient à l'icône de profil par défaut", +"Review before sharing": "Vérifier avant de partager", +"Rhythm": "Rythme", +"Rhythm only looks during quiet, still, resting windows, so it needs a calm night's worth of steady beats. Once there's a clean window, the scatter and its description show here.": "Rythme n'observe que pendant les fenêtres calmes, immobiles et de repos, il a donc besoin d'une nuit calme aux battements réguliers. Une fois qu'une fenêtre propre existe, le nuage de points et sa description s'affichent ici.", +"Rhythm visualization (experimental)": "Visualisation du rythme (expérimental)", +"Ring key (32 hex characters)": "Clé de la bague (32 caractères hexadécimaux)", +"Ring key, 32 hexadecimal characters": "Clé de la bague, 32 caractères hexadécimaux", +"Rolling RMSSD %lld milliseconds": "RMSSD glissant %lld millisecondes", +"Rough": "Difficile", +"Rough guide only, based on what you logged.": "Simple indication approximative, basée sur ce que vous avez consigné.", +"Round %lld of %lld": "Round %1$lld sur %2$lld", +"Route": "Itinéraire", +"Run NOOP actions hands-free.": "Exécutez des actions NOOP en mains libres.", +"Run a Shortcut when put back on": "Exécuter un raccourci lorsqu'il est remis", +"Run a Shortcut when taken off": "Exécuter un raccourci lorsqu'il est retiré", +"Run a Shortcut…": "Exécuter un raccourci…", +"Run a clean personal test": "Réaliser un test personnel propre", +"Run down": "Épuisé", +"Run now": "Exécuter maintenant", +"Running": "En cours", +"Runs the stream only during your quiet hours window (22:00 to 07:00 by default), roughly halving the battery cost. Daytime Stress readings will be sparser, since Stress reads this live stream.": "N'exécute le flux que pendant votre fenêtre d'heures calmes (de 22h00 à 07h00 par défaut), ce qui réduit environ de moitié le coût en batterie. Les mesures de Stress en journée seront plus rares, car Stress lit ce flux en direct.", +"SD1:SD2 ratio": "Rapport SD1:SD2", +"SECONDS": "SECONDES", +"SENSOR": "CAPTEUR", +"SESSION DONE": "SÉANCE TERMINÉE", +"SESSIONS": "SÉANCES", +"SHORT AXIS": "AXE COURT", +"SIGNIFICANT": "SIGNIFICATIF", +"SLEEP": "SOMMEIL", +"SPEED": "VITESSE", +"STEPS ESTIMATE": "ESTIMATION DES PAS", +"STRAP LOG": "JOURNAL DU BRACELET", +"STREAMING": "DIFFUSION", +"STRENUOUS": "INTENSE", +"STRONGER SIGNAL": "SIGNAL PLUS FORT", +"SYNCING %lld": "SYNCHRONISATION %lld", +"Same cardiovascular-load idea as WHOOP's Day Strain (0–21). We rescaled the top of the ladder from 21 to 100 so all three scores share one scale. The rungs didn't move, so a 100 is as rare as a 21.0 was.": "Même principe de charge cardiovasculaire que l'Effort du jour de WHOOP (0-21). Nous avons redimensionné le haut de l'échelle de 21 à 100 pour que les trois scores partagent une seule échelle. Les échelons n'ont pas bougé, un 100 est donc aussi rare que l'était un 21,0.", +"Same core idea as WHOOP's Recovery % (HRV-led recovery), but our weighting and baseline maths are our own, and openly documented.": "Même principe de base que le % de Récupération de WHOOP (récupération pilotée par la VFC), mais notre pondération et nos calculs de référence sont les nôtres, et documentés ouvertement.", +"Sat": "Sam", +"Saturday": "Samedi", +"Save": "Enregistrer", +"Save & Continue": "Enregistrer et continuer", +"Save a full backup to a folder you choose - point it at Google Drive, iCloud or Dropbox for off-device sync.": "Enregistrez une sauvegarde complète dans un dossier de votre choix - pointez-le vers Google Drive, iCloud ou Dropbox pour une synchronisation hors appareil.", +"Save it": "Enregistrer", +"Save key": "Enregistrer la clé", +"Save reading": "Enregistrer la mesure", +"Save workout": "Enregistrer l'entraînement", +"Saved": "Enregistré", +"Saved %@ to NOOP's folder in the Files app.": "%@ enregistré dans le dossier de NOOP dans l'application Fichiers.", +"Saved %@ to your Documents folder.": "%@ enregistré dans votre dossier Documents.", +"Saved a backup to your folder.": "Sauvegarde enregistrée dans votre dossier.", +"Saved to %@. Copy this file to your other %@ and use Import there to restore everything.": "Enregistré dans %1$@. Copiez ce fichier sur votre autre %2$@ et utilisez Importer là-bas pour tout restaurer.", +"Saved to %@. The zip re-imports into NOOP (Data Sources → WHOOP Export) on any Mac, iPhone, or Android device.": "Enregistré dans %@. Le zip se réimporte dans NOOP (Sources de données → Export WHOOP) sur n'importe quel Mac, iPhone ou appareil Android.", +"Saves every raw 5/MG frame (with a timestamp and the live heart rate) to a JSON file you can share to help map the biometric layout. This only records frames the strap already sent (it never writes to your strap), so it is safe to leave on. Export the file and attach it to a protocol-mapping issue.": "Enregistre chaque trame brute 5/MG (avec un horodatage et la fréquence cardiaque en direct) dans un fichier JSON que vous pouvez partager pour aider à cartographier la structure biométrique. Cela n'enregistre que les trames déjà envoyées par le bracelet (rien n'est jamais écrit sur votre bracelet), il est donc sans risque de le laisser activé. Exportez le fichier et joignez-le à un signalement de cartographie de protocole.", +"Saves every raw 5/MG frame (with a timestamp and the live heart rate) to a JSON file you can share to help map the biometric layout. This only records frames the strap already sent, it never writes to your strap, so it is safe to leave on. Export the file and attach it to a protocol-mapping issue.": "Enregistre chaque trame brute 5/MG (avec un horodatage et la fréquence cardiaque en direct) dans un fichier JSON que vous pouvez partager pour aider à cartographier la disposition biométrique. Cela n'enregistre que les trames que le bracelet a déjà envoyées, il n'écrit jamais sur votre bracelet, il est donc sans danger de le laisser activé. Exportez le fichier et joignez-le à une issue de cartographie de protocole.", +"Saves the raw capture and the strap log together as a matched pair. Attach both to a protocol-mapping issue.": "Enregistre la capture brute et le journal du bracelet ensemble, comme une paire assortie. Joignez les deux à un signalement de cartographie de protocole.", +"Save…": "Enregistrer…", +"Saving to: %@": "Enregistrement dans : %@", +"Saving…": "Enregistrement…", +"Scan": "Scanner", +"Scan & Connect": "Scanner et connecter", +"Scan & connect": "Scanner et connecter", +"Scan connects to %@. To pair or switch bands, open Devices.": "« Scanner » se connecte à %@. Pour appairer ou changer de bracelet, ouvrez Appareils.", +"Scan for %@": "Rechercher %@", +"Scan for your Oura ring": "Rechercher votre bague Oura", +"Scan for your ring": "Rechercher votre bague", +"Scanning your data…": "Analyse de vos données…", +"Scanning…": "Scan en cours…", +"Scores still calibrating, no single day-owner yet": "Scores encore en calibration, pas encore de propriétaire unique du jour", +"Scroll to zoom · drag to pan": "Défiler pour zoomer · glisser pour déplacer", +"Search": "Rechercher", +"Search markers": "Rechercher des marqueurs", +"Search markers (e.g. LDL, ferritin)": "Rechercher des marqueurs (ex. LDL, ferritine)", +"Search sport": "Rechercher un sport", +"Searching": "Recherche en cours", +"Searching…": "Recherche…", +"See how Charge, Effort and Rest are calculated, and how they differ from WHOOP.": "Découvrez comment la Charge, l'Effort et le Repos sont calculés, et en quoi ils diffèrent de WHOOP.", +"See recovery, beautifully": "Visualisez la récupération, avec élégance", +"See what shaped your Charge": "Découvrez ce qui a façonné votre Charge", +"See your full readiness": "Voir votre préparation complète", +"Self-tracking, not a clinical assessment. If low mood persists, talk to a professional. You deserve support.": "Auto-suivi, pas une évaluation clinique. Si une humeur basse persiste, parlez-en à un professionnel. Vous méritez du soutien.", +"Send": "Envoyer", +"Send a haptic buzz to your WHOOP strap.": "Envoyez une vibration haptique à votre bracelet WHOOP.", +"Send enable sequence to strap": "Envoyer la séquence d'activation au bracelet", +"Sep": "Sept", +"Server URL": "URL du serveur", +"Session": "Séance", +"Session done": "Séance terminée", +"Session ended. Try a paced breath instead.": "Séance terminée. Essayez plutôt une respiration cadencée.", +"Session live": "Séance en direct", +"Session progress": "Progression de la séance", +"Sessions": "Séances", +"Set a wake time for specific days (a lie-in at the weekend, say). Days you leave alone use the time above.": "Définissez une heure de réveil pour des jours précis (une grasse matinée le week-end, par exemple). Les jours non modifiés utilisent l'heure ci-dessus.", +"Set custom container size": "Définir une taille de récipient personnalisée", +"Set this up on your iPhone": "Configurez ceci sur votre iPhone", +"Set up Apple Watch": "Configurer l'Apple Watch", +"Settings": "Paramètres", +"Settling": "Stabilisation", +"Setup": "Configuration", +"Sex": "Sexe", +"Share": "Partager", +"Share of imported zone time, duration-weighted across sessions (approximate).": "Part du temps en zone importé, pondérée par la durée sur l'ensemble des séances (approximative).", +"Share of imported zone time, duration-weighted across sessions, approximate.": "Part du temps de zone importé, pondéré par la durée entre les séances, approximatif.", +"Sharing %lld bpm. Waiting for a device to pair.": "Partage de %lld bpm. En attente d'un appareil à appairer.", +"Shortcut name": "Nom du raccourci", +"Shortcuts Export": "Export Raccourcis", +"Shortcuts file export": "Export de fichier Raccourcis", +"Should you push today?": "Devriez-vous forcer aujourd'hui ?", +"Show %@": "Afficher %@", +"Show & reorder": "Afficher et réorganiser", +"Show all metrics": "Afficher toutes les métriques", +"Show all metrics, %lld more": "Afficher toutes les métriques, %lld de plus", +"Show fewer": "Afficher moins", +"Show fewer metrics": "Afficher moins de métriques", +"Show what NOOP is synced from": "Afficher les sources de synchronisation de NOOP", +"Shows a soft sunrise, day, dusk and night scene behind the Today screen. Turn it off for a plain dark canvas. Your cards stay exactly as readable.": "Affiche une douce scène de lever de soleil, jour, crépuscule et nuit derrière l'écran Aujourd'hui. Désactivez-le pour un fond sombre uni. Vos cartes restent tout aussi lisibles.", +"Shows the Apple Health permission sheet": "Affiche la fenêtre d'autorisation Apple Santé", +"Shows the advanced settings sections": "Affiche les sections des réglages avancés", +"Shows your live heart rate on the Lock Screen and in the Dynamic Island while the strap is connected. Turn it off to keep your live HR out of the Dynamic Island. (Any one already showing clears within a moment.)": "Affiche votre fréquence cardiaque en direct sur l'écran verrouillé et dans la Dynamic Island tant que le bracelet est connecté. Désactivez-le pour garder votre FC en direct hors de la Dynamic Island. (Une FC déjà affichée disparaît sous peu.)", +"Signal Trust": "Fiabilité du signal", +"Signals up": "Signaux en hausse", +"Silent haptic HIIT, the strap buzzes the transitions": "HIIT haptique silencieux, le bracelet vibre aux transitions", +"Silent haptic HIIT: the strap buzzes the transitions": "HIIT haptique silencieux : le bracelet vibre aux transitions", +"Similar in spirit to WHOOP's Sleep Performance %; our composite is our own.": "Similaire dans l'esprit au % de Performance de sommeil de WHOOP ; notre composite nous est propre.", +"Single": "Unique", +"Single reading in range": "Mesure unique dans la plage", +"Sip": "Gorgée", +"Sip %lld ml · Cup %lld ml · Bottle %lld ml": "Gorgée %1$lld ml · Tasse %2$lld ml · Bouteille %3$lld ml", +"Siri & Shortcuts": "Siri et Raccourcis", +"Sit still and breathe normally. Tap below to take a 60-second reading.": "Restez immobile et respirez normalement. Touchez ci-dessous pour effectuer une mesure de 60 secondes.", +"Sit still and breathe with the buzz. You can stop anytime; a stopped sweep won't lock a pace.": "Restez immobile et respirez au rythme de la vibration. Vous pouvez arrêter à tout moment ; un balayage interrompu ne fixera pas d'allure.", +"Sit still, breathe normally. Keep your wrist relaxed and steady.": "Restez immobile, respirez normalement. Gardez votre poignet détendu et stable.", +"Sitting for": "Assis depuis", +"Skin Temp": "Temp. cutanée", +"Skin Temperature": "Température cutanée", +"Skin temperature": "Température cutanée", +"Skin temperature %@. %@. Reliable.": "Température cutanée %1$@. %2$@. Fiable.", +"Skin-temperature deviation": "Écart de température cutanée", +"Skin-temperature trend": "Tendance de la température cutanée", +"Skip alerts when the strap is off your wrist.": "Ignorer les alertes lorsque le bracelet n'est pas au poignet.", +"Skip for now": "Passer pour l'instant", +"Skip today": "Ignorer aujourd'hui", +"Sleep": "Sommeil", +"Sleep & Rest": "Sommeil et Repos", +"Sleep / Rest": "Sommeil / Repos", +"Sleep Consistency": "Régularité du sommeil", +"Sleep Debt": "Dette de sommeil", +"Sleep Efficiency": "Efficacité du sommeil", +"Sleep Need": "Besoin de sommeil", +"Sleep Performance": "Performance du sommeil", +"Sleep Score": "Score de sommeil", +"Sleep avg": "Sommeil moy.", +"Sleep marks": "Marqueurs de sommeil", +"Sleep performance": "Performance de sommeil", +"Sleep performance %lld of 100": "Performance de sommeil %lld sur 100", +"Sleep quality": "Qualité du sommeil", +"Sleep score": "Score de sommeil", +"Sleep scored from how long you slept versus how much you needed, how efficient the night was, and the restorative (deep and REM) share of it.": "Sommeil évalué à partir de la durée dormie par rapport à votre besoin, de l'efficacité de la nuit et de la part réparatrice (sommeil profond et paradoxal).", +"Sleep sorting, scores, recording, and where your numbers come from.": "Tri du sommeil, scores, enregistrement et provenance de vos chiffres.", +"Sleep stage breakdown: deep %lld percent, light %lld percent, REM %lld percent, awake %lld percent": "Répartition des stades de sommeil : profond %lld %, léger %lld %, REM %lld %, éveillé %lld %", +"Sleep stages, %@": "Stades de sommeil, %@", +"Sleep stages, no data": "Stades de sommeil, aucune donnée", +"Sleep staging": "Répartition des phases de sommeil", +"Sleep steadiness: Rest varied ±%@ pts night to night.": "Stabilité du sommeil : le Repos a varié de ±%@ pts d'une nuit à l'autre.", +"Sleep · scores · recording · where your numbers come from": "Sommeil · scores · enregistrement · provenance de vos chiffres", +"Sleep-debt ledger": "Registre de dette de sommeil", +"Slightly off baseline (HRV is a little low), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la VFC est un peu basse), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", +"Slightly off baseline (resting HR is a touch high), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la FC au repos est un peu élevée), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", +"Slope source": "Source de la pente", +"Slow threads": "Fils lents", +"Smart alarm": "Réveil intelligent", +"So your zones, calories and baselines are accurate.": "Pour que vos zones, calories et références soient exactes.", +"Soft tone on each phase · respects silent mode": "Tonalité douce à chaque phase · respecte le mode silencieux", +"Solid": "Solide", +"Solid signal.": "Signal solide.", +"Some occasional extra or skipped beats": "Quelques battements supplémentaires ou manqués occasionnels", +"Source Apple Health": "Source Apple Santé", +"Source imported activity file": "Source : fichier d'activité importé", +"Source imported lifting log": "Source : journal de musculation importé", +"Source manual entry": "Source : saisie manuelle", +"Source on-device detected": "Source : détecté sur l'appareil", +"Source: %@": "Source : %@", +"Source: Apple Watch": "Source : Apple Watch", +"Sources differ": "Les sources diffèrent", +"Sparse, widened to %@ · %lld readings": "Épars, élargi à %1$@ · %2$lld mesures", +"Speed · Cadence · Power · Distance · Energy · Heart rate (if the machine sends it)": "Vitesse · Cadence · Puissance · Distance · Énergie · Fréquence cardiaque (si la machine l'envoie)", +"Square breath · steady focus": "Respiration carrée · concentration stable", +"Stage breakdown": "Répartition des stades", +"Stages vs typical": "Stades vs typique", +"Standard HR is not a full bond": "La FC standard n'est pas un appairage complet", +"Standard HR mode (low bandwidth)": "Mode FC standard (faible bande passante)", +"Standard HR mode, low bandwidth. %@": "Mode FC standard, faible bande passante. %@", +"Start": "Démarrer", +"Start %@": "Démarrer %@", +"Start a Breathe session": "Démarrer une séance de Respiration", +"Start a live stream": "Démarrer un flux en direct", +"Start a local experiment using today's date as day one.": "Démarrez une expérience locale en utilisant la date d'aujourd'hui comme jour un.", +"Start a workout": "Démarrer un entraînement", +"Start a workout when the stream matters. NOOP records the interval, HR, peak, average and effort from the same live feed.": "Démarrez un entraînement quand le flux compte. NOOP enregistre l'intervalle, la FC, le pic, la moyenne et l'effort à partir du même flux en direct.", +"Start a workout, log your journal, or breathe": "Démarrez un entraînement, remplissez votre journal ou respirez", +"Start can't be in the future.": "Le début ne peut pas être dans le futur.", +"Start date and time": "Date et heure de début", +"Start experiment": "Démarrer l'expérience", +"Start session": "Démarrer la séance", +"Start workout": "Démarrer l'entraînement", +"Started %@ · testing %@": "Commencé %@ · test %@", +"Started at %lld bpm · the rhythm trails your heart down.": "Commencé à %lld bpm · le rythme accompagne votre cœur vers le bas.", +"Starting…": "Démarrage…", +"State": "État", +"Stay in the loop": "Restez informé", +"Steady": "Stable", +"Step back to a day the strap was worn.": "Revenez à un jour où le bracelet était porté.", +"Step calibration": "Étalonnage des pas", +"Step calibration, %@ counter ticks per step": "Étalonnage des pas, %@ impulsions du compteur par pas", +"Step to another week with the arrows above to see its review.": "Passez à une autre semaine avec les flèches ci-dessus pour voir son bilan.", +"Steps": "Pas", +"Steps & workouts": "Pas et entraînements", +"Steps (estimated)": "Pas (estimés)", +"Steps / motion": "Pas / mouvement", +"Steps estimate": "Estimation des pas", +"Steps estimate calibration. %@. Opens the calibration screen.": "Étalonnage de l'estimation des pas. %@. Ouvre l'écran d'étalonnage.", +"Steps today": "Pas aujourd'hui", +"Steps, active energy and logged workouts feed Effort. Dense and reliable.": "Les pas, l'énergie active et les entraînements enregistrés alimentent l'Effort. Dense et fiable.", +"Steps, active energy and logged workouts feed your Effort. Dense and reliable.": "Les pas, l'énergie active et les entraînements enregistrés alimentent votre Effort. Dense et fiable.", +"Steps, heart rate, sleep, SpO₂ and stress, imported from Mi Fitness, read locally on %@.": "Pas, fréquence cardiaque, sommeil, SpO₂ et stress, importés depuis Mi Fitness, lus localement sur %@.", +"Steps, heart, sleep, body composition and VO₂ max, read locally on %@.": "Pas, cœur, sommeil, composition corporelle et VO₂ max, lus localement sur %@.", +"Still": "Immobile", +"Stop": "Arrêter", +"Stop caffeine after about %@ to keep most of it cleared by %@.": "Arrêtez la caféine vers %1$@ pour qu'elle soit en grande partie éliminée d'ici %2$@.", +"Stop session": "Arrêter la séance", +"Stop sweep": "Arrêter le balayage", +"Stops reading a cycle phase from your nightly temperature. You can turn it back on here any time.": "Arrête de déduire une phase du cycle à partir de votre température nocturne. Vous pouvez le réactiver ici à tout moment.", +"Stops the screen dimming while a workout is recording": "Empêche l'écran de s'assombrir pendant l'enregistrement d'un entraînement", +"Storage": "Stockage", +"Storage unavailable": "Stockage indisponible", +"Stored as %@. %@": "Enregistré sous %1$@. %2$@", +"Strain nudges and your smart alarm tap your wrist the moment they fire.": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet dès qu'ils se déclenchent.", +"Strain nudges and your smart alarm tap your wrist the same way.": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet de la même manière.", +"Strained": "Sous tension", +"Strap": "Bracelet", +"Strap accepted %lld/15 R22 flags…": "Le bracelet a accepté %lld/15 indicateurs R22…", +"Strap accepted all 15 R22 flags": "Le bracelet a accepté les 15 indicateurs R22", +"Strap battery": "Batterie du bracelet", +"Strap battery %lld percent%@%@": "Batterie du bracelet %1$lld pour cent%2$@%3$@", +"Strap bonded": "Bracelet associé", +"Strap connected": "Bracelet connecté", +"Strap data into Apple Health without HealthKit, for sideloaded installs.": "Données du bracelet vers Apple Santé sans HealthKit, pour les installations en sideload.", +"Strap fully charged": "Bracelet complètement chargé", +"Strap history": "Historique du bracelet", +"Strap idle": "Bracelet inactif", +"Strap live": "Bracelet en direct", +"Strap log exported": "Journal du bracelet exporté", +"Strap name": "Nom du bracelet", +"Strap needed": "Bracelet requis", +"Strap not connected": "Bracelet non connecté", +"Strap not connected. Tap to connect.": "Bracelet non connecté. Touchez pour connecter.", +"Strap not worn": "Bracelet non porté", +"Strap sync": "Sync du bracelet", +"Strap wake-alarm": "Réveil du bracelet", +"Streaming live": "Diffusion en direct", +"Streaming now": "En cours de diffusion", +"Strength Activity Time": "Temps d'activité de force", +"Stress %@ of 3, %@": "Stress %@ de 3, %@", +"Stress Trend": "Tendance du stress", +"Stress avg": "Stress moy.", +"Stress check-in": "Bilan de stress", +"Stress check-ins (haptic)": "Bilans de stress (haptiques)", +"Stress is derived from two autonomic signals.": "Le stress est dérivé de deux signaux autonomes.", +"Stress monitor": "Moniteur de stress", +"Stress trend": "Tendance du stress", +"Strong": "Fort", +"Suggested prompt: %@": "Suggestion : %@", +"Summary": "Résumé", +"Sun": "Dim", +"Sunday": "Dimanche", +"Support": "Assistance", +"Support NOOP": "Soutenir NOOP", +"Support the build": "Soutenir le développement", +"Suppress auto-nudges overnight (10pm–7am).": "Désactiver les alertes automatiques la nuit (22h–7h).", +"Sustained high stress": "Stress élevé prolongé", +"Sweep progress": "Progression du balayage", +"Sweeping…": "Balayage en cours…", +"Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward).": "Tonus sympathique contre parasympathique d'après la VFC dans le domaine fréquentiel. Plus élevé penche vers le sympathique (côté stress).", +"Sync": "Synchroniser", +"Sync now": "Synchroniser maintenant", +"Synced from": "Synchronisé depuis", +"Synced from: %@": "Synchronisé depuis : %@", +"Syncing": "Synchronisation", +"Syncing strap history…": "Synchronisation de l'historique du bracelet…", +"Syncing…": "Synchronisation…", +"Syncs your strap's stored history right away, instead of waiting for the next automatic sync.": "Synchronise immédiatement l'historique enregistré de votre bracelet, au lieu d'attendre la prochaine synchronisation automatique.", +"Synthesis": "Synthèse", +"Synthesis. %@": "Synthèse. %@", +"System": "Système", +"System follows your %@'s light or dark setting.": "Le système suit le réglage clair ou sombre de votre %@.", +"Systolic": "Systolique", +"Systolic blood pressure in mmHg": "Pression artérielle systolique en mmHg", +"TEST MODES": "MODES DE TEST", +"THE BASICS": "LES BASES", +"THE ONE RULE": "LA RÈGLE UNIQUE", +"THE THREE SCORES": "LES TROIS SCORES", +"TIME": "TEMPS", +"TYPICAL EFFORT": "EFFORT TYPIQUE", +"Tag a few sessions of the same activity and NOOP will learn its personal recovery cost.": "Étiquetez quelques séances de la même activité et NOOP apprendra son coût de récupération personnel.", +"Take a 60-second seated HRV reading from the live R-R stream.": "Faites une mesure de VFC assise de 60 secondes à partir du flux R-R en direct.", +"Take an HRV reading": "Faire une mesure de VFC", +"Take another reading": "Faire une nouvelle mesure", +"Take it over locally. Beta.": "Prenez-en le contrôle en local. Bêta.", +"Take over": "Prendre le contrôle", +"Take over this ring": "Prendre le contrôle de cet anneau", +"Take over this ring?": "Prendre le contrôle de cet anneau ?", +"Take over your ring locally. Beta. This replaces the Oura app.": "Prenez le contrôle de votre anneau en local. Bêta. Cela remplace l'application Oura.", +"Takes effect on the next analytics pass (after the next sync).": "Prend effet lors de la prochaine analyse (après la prochaine synchronisation).", +"Taking over your ring": "Prise de contrôle de votre anneau", +"Tap a drink to edit it, or use the trash to delete.": "Touchez une boisson pour la modifier, ou utilisez la corbeille pour la supprimer.", +"Tap a workout for its detail · tap ••• to re-label, edit or delete it.": "Touchez un entraînement pour ses détails · touchez ••• pour le renommer, le modifier ou le supprimer.", +"Tap the one that's yours.": "Touchez celui qui vous appartient.", +"Tap to compare": "Touchez pour comparer", +"Tap to edit the amount": "Touchez pour modifier la quantité", +"Tap to log": "Touchez pour enregistrer", +"Tap when you're heading to bed or when you wake. Each tap is logged with the time. It doesn't change tonight's detected sleep.": "Touchez quand vous allez vous coucher ou quand vous vous réveillez. Chaque appui est enregistré avec l'heure. Cela ne modifie pas le sommeil détecté cette nuit.", +"Temperature": "Température", +"Temperature unit": "Unité de température", +"Test %@ buzz": "Test de vibration %@", +"Test Centre": "Centre de test", +"Test action": "Action test", +"Test buzz": "Vibration test", +"Testing %@ br/min…": "Test à %@ resp/min…", +"That file is too large to import.": "Ce fichier est trop volumineux pour être importé.", +"That file isn't a NOOP backup. It doesn't look like a SQLite database.": "Ce fichier n'est pas une sauvegarde NOOP. Il ne ressemble pas à une base de données SQLite.", +"That is not a 32-character hex key.": "Ce n'est pas une clé hexadécimale de 32 caractères.", +"That's normal. A paced breath often settles things when a metronome alone doesn't.": "C'est normal. Une respiration cadencée apaise souvent les choses là où un métronome seul n'y parvient pas.", +"That's why a watch-only Charge starts out “Calibrating”. NOOP needs about seven nights of your HRV to learn what normal looks like for you. Until it has them it withholds the score rather than guess. Once the baseline is set, your Charge appears with its confidence, on the same 0–100 scale as a strap's.": "C'est pourquoi une Charge basée uniquement sur la montre commence par « Étalonnage ». NOOP a besoin d'environ sept nuits de votre VFC pour apprendre ce qui est normal pour vous. Tant qu'il ne les a pas, il préfère ne pas afficher de score plutôt que de deviner. Une fois la référence établie, votre Charge apparaît avec son niveau de confiance, sur la même échelle de 0 à 100 qu'un bracelet.", +"The Oura app and your Oura account stop working with this ring. This is the point. You are replacing Oura.": "L'application Oura et votre compte Oura cessent de fonctionner avec cet anneau. C'est le but recherché. Vous remplacez Oura.", +"The average above was edited. The graph, zones and Effort stay from the recorded session.": "La moyenne ci-dessus a été modifiée. Le graphique, les zones et l'Effort restent ceux de la séance enregistrée.", +"The backup archive doesn't contain a database file.": "L'archive de sauvegarde ne contient pas de fichier de base de données.", +"The best-sourced number per metric across every band you use.": "Le chiffre le mieux documenté pour chaque métrique, parmi tous les bracelets que vous utilisez.", +"The full terms are in TERMS.md, shipped with NOOP. This is not legal advice.": "Les conditions complètes se trouvent dans TERMS.md, fourni avec NOOP. Ceci n'est pas un avis juridique.", +"The honest catch: a watch isn't a chest strap. It's brilliant at sleep, steps, workouts and fitness, and lighter on the dense heart-rate-variability a strap measures all night. So recovery takes about a week to calibrate, and a couple of metrics depend on your watch model. NOOP is upfront about all of it. Every watch-derived number carries a confidence, and where the watch can't be honest, NOOP shows nothing instead of a made-up figure.": "Le hic honnête : une montre n'est pas un bracelet pectoral. Elle excelle pour le sommeil, les pas, les entraînements et la forme physique, et est plus légère sur la VFC dense qu'un bracelet mesure toute la nuit. La récupération met donc environ une semaine à s'étalonner, et quelques métriques dépendent du modèle de votre montre. NOOP est transparent sur tout cela. Chaque chiffre dérivé de la montre porte un niveau de confiance, et là où la montre ne peut pas être honnête, NOOP n'affiche rien plutôt qu'un chiffre inventé.", +"The line is each waking hour's 0–3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": "La ligne représente l'indicateur 0 à 3 de chaque heure éveillée, évalué par rapport à vos propres heures calmes du jour. La barre ci-dessous répartit votre journée en temps de stress calme, modéré et élevé.", +"The master switch for every wrist buzz (inactivity, stress, alerts). Off keeps the strap quiet no matter what else is on.": "L'interrupteur principal de chaque vibration au poignet (inactivité, stress, alertes). Désactivé, il garde le bracelet silencieux quoi qu'il arrive d'autre.", +"The method behind the score, not today's values.": "La méthode derrière le score, pas les valeurs du jour.", +"The nudge fires %lldh %lldm before this.": "L'alerte se déclenche %1$lldh %2$lldm avant cela.", +"The numbers": "Les chiffres", +"The scatter looked rounder and more spread out than a tight, steady beat. This has many ordinary causes and is not a diagnosis.": "La dispersion semblait plus ronde et plus étalée qu'un battement compact et régulier. Cela a de nombreuses causes ordinaires et n'est pas un diagnostic.", +"The strap alarm is a silent buzz, not a sound": "L'alarme du bracelet est une vibration silencieuse, pas un son", +"The strap buzzes a gentle rhythm just below your current heart rate, a felt metronome to relax toward. It trails your heart down rather than yanking it, and stops on its own.": "Le bracelet vibre selon un rythme doux juste en dessous de votre fréquence cardiaque actuelle, un métronome ressenti vers lequel vous détendre. Il accompagne votre cœur vers le bas plutôt que de le tirer brusquement, et s'arrête de lui-même.", +"The thread of you over time.": "Le fil de vous au fil du temps.", +"The wake-alarm above buzzes your wrist from the strap's own firmware. It can't sound a loud alarm, and a sideloaded app can't sound a reliable background wake on this device either (that needs a critical-alert permission this build doesn't have). Keep your phone's built-in Clock alarm as a backup. NOOP's phone-based smart wake (light-sleep detection) is available on the Android app.": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore, et une application en sideload ne peut pas non plus déclencher un réveil en arrière-plan fiable sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas). Gardez le réveil intégré de l'application Horloge de votre téléphone en secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android.", +"The wake-alarm above buzzes your wrist from the strap's own firmware. It can't sound a loud alarm. We also schedule a backup notification at your wake time, but a sideloaded app can't sound a guaranteed wake on this device (that needs a critical-alert permission this build doesn't have), so Focus or silent mode can still mute it. Keep your phone's built-in Clock alarm as your real backup. NOOP's phone-based smart wake (light-sleep detection) is available on the Android app.": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore. Nous programmons aussi une notification de secours à l'heure de votre réveil, mais une application en sideload ne peut pas garantir un réveil sonore sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas), donc le mode Ne pas déranger ou le mode silencieux peuvent quand même la couper. Gardez le réveil intégré de l'application Horloge de votre téléphone comme véritable secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android.", +"Theme": "Thème", +"There are no NOOP backups in your folder yet. Use Back up now first.": "Il n'y a pas encore de sauvegarde NOOP dans votre dossier. Utilisez d'abord Sauvegarder maintenant.", +"There wasn't a calm, still window clean enough to describe. Try again after a settled night.": "Il n'y a pas eu de fenêtre calme et immobile assez nette à décrire. Réessayez après une nuit paisible.", +"There's about %@ of leftover import scratch space to reclaim. This never removes your imported data.": "Il reste environ %@ d'espace temporaire d'importation à récupérer. Cela ne supprime jamais vos données importées.", +"There's no NOOP data to export yet.": "Il n'y a pas encore de données NOOP à exporter.", +"There's no NOOP data to export yet. Import or record some first.": "Il n'y a pas encore de données NOOP à exporter. Importez ou enregistrez-en d'abord.", +"These are experimental probes, off by default. The fuller WHOOP 5/MG controls and the raw-sensor CSV export still live in Settings under Diagnostics.": "Ce sont des sondes expérimentales, désactivées par défaut. Les commandes WHOOP 5/MG plus complètes et l'export CSV des capteurs bruts se trouvent toujours dans Paramètres, sous Diagnostics.", +"These are extra, on-demand HRV lenses computed from today's R-R intervals. They are informational and do not change the stress score above.": "Ce sont des perspectives VFC supplémentaires, à la demande, calculées à partir des intervalles R-R du jour. Elles sont informatives et ne modifient pas le score de stress ci-dessus.", +"These are independent approximations from a consumer strap, built on open science: not medical advice, and not WHOOP's official scores.": "Ce sont des approximations indépendantes issues d'un bracelet grand public, basées sur la science ouverte : ni un avis médical, ni les scores officiels de WHOOP.", +"These are independent estimates computed on your device from your Apple Health data, not medical advice. Confidence labels are honest about how much NOOP knows so far.": "Ce sont des estimations indépendantes calculées sur votre appareil à partir de vos données Apple Santé, pas un avis médical. Les niveaux de confiance indiquent honnêtement ce que NOOP sait pour l'instant.", +"These are the days where your phone also counted steps, so NOOP can learn how your motion maps to steps. Or set the coefficient manually below.": "Ce sont les jours où votre téléphone a aussi compté les pas, afin que NOOP puisse apprendre comment votre mouvement se traduit en pas. Ou réglez le coefficient manuellement ci-dessous.", +"These are your own numbers shown back to you. NOOP doesn't decide whether any value is normal, high or low.": "Ce sont vos propres chiffres qui vous sont montrés. NOOP ne décide pas si une valeur est normale, élevée ou basse.", +"These days are excluded from the estimate (your phone's real count is shown instead). They're here only so you can judge the estimate's accuracy.": "Ces jours sont exclus de l'estimation (le comptage réel de votre téléphone est affiché à la place). Ils sont là uniquement pour que vous puissiez juger de la précision de l'estimation.", +"These power your heart-rate zones, calorie estimates and recovery baselines. Keep them accurate.": "Ces éléments alimentent vos zones de fréquence cardiaque, estimations de calories et références de récupération. Gardez-les précis.", +"These vitals are from %@": "Ces signes vitaux proviennent de %@", +"Thinking": "Réflexion en cours", +"This builds from the strap as it syncs. Effort and rest appear after you have worn it and slept a night. Charge needs about four nights of sleep to learn your baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first couple of weeks. On a WHOOP 5 or MG the strap banks little history, so the night count can climb slowly or sit at 0 of 4 until you have worn it across a few nights. That's its sync limit, not a fault. Import your WHOOP export to skip the wait.": "Cela se construit à partir du bracelet au fil de sa synchronisation. L'Effort et le Repos apparaissent après l'avoir porté et dormi une nuit. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre votre référence (vous verrez \"Étalonnage\" jusque-là), et continue de s'affiner pendant vos deux premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, donc le compteur de nuits peut progresser lentement ou rester à 0 sur 4 tant que vous ne l'avez pas porté quelques nuits. C'est sa limite de synchronisation, pas un défaut. Importez votre export WHOOP pour éviter l'attente.", +"This builds from the strap as it syncs. Strain and sleep appear after you have worn it and slept a night. Recovery needs about a week of nights to learn your baseline, or import your WHOOP export to skip the wait.": "Ceci se construit depuis le bracelet au fil de la synchronisation. L'effort et le sommeil apparaissent après que vous l'ayez porté et dormi une nuit. La récupération nécessite environ une semaine de nuits pour apprendre votre référence, ou importez votre export WHOOP pour éviter l'attente.", +"This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", +"This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to grant here.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici.", +"This install can't connect to Apple Health directly. It was signed with a profile that doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", +"This install can't connect to Apple Health directly. It was signed with a profile that doesn't include Apple's Health permission, so there's nothing to grant here.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici.", +"This is a sideloaded build, installed outside the App Store. It needs re-signing periodically: roughly every 7 days on a free Apple ID, about a year on a paid developer account.": "Ceci est une version en sideload, installée en dehors de l'App Store. Elle doit être resignée périodiquement : environ tous les 7 jours avec un identifiant Apple gratuit, environ un an avec un compte développeur payant.", +"This is a wellness visualization, not a screening test. It does not tell you to see a clinician and it names no condition. This is not legal or medical advice.": "Ceci est une visualisation de bien-être, pas un test de dépistage. Elle ne vous dit pas de consulter un médecin et ne nomme aucune affection. Ceci n'est pas un avis juridique ou médical.", +"This is an experimental wellness visualization of your beat-to-beat timing. It is NOT an ECG, and it cannot diagnose, detect, or rule out any heart condition.": "Ceci est une visualisation expérimentale de bien-être de la cadence battement par battement. Ce n'est PAS un ECG, et cela ne peut ni diagnostiquer, ni détecter, ni écarter une affection cardiaque.", +"This is exactly what your report will contain. Nothing leaves %@ until you tap Share.": "C'est exactement ce que contiendra votre rapport. Rien ne quitte %@ tant que vous ne touchez pas Partager.", +"This is the only feature that leaves %@. It sends a summary of your metrics to %@ using your own key. Nothing is sent until you ask.": "C'est la seule fonctionnalité qui quitte %1$@. Elle envoie un résumé de vos métriques à %2$@ en utilisant votre propre clé. Rien n'est envoyé tant que vous ne le demandez pas.", +"This is the only feature that leaves your Mac, it sends a summary of your metrics to %@ using your own key. Nothing is sent until you ask.": "C'est la seule fonctionnalité qui quitte votre Mac, elle envoie un résumé de vos métriques à %@ en utilisant votre propre clé. Rien n'est envoyé jusqu'à ce que vous le demandiez.", +"This is your last scored session. Wear the strap overnight for a fresh score.": "Voici votre dernière séance notée. Portez le bracelet une nuit pour obtenir un nouveau score.", +"This is your only sleep block today.": "C'est votre seul bloc de sommeil aujourd'hui.", +"This isn't a NOOP backup from this app. It's missing the migration bookkeeping a NOOP backup carries (it looks like an Android backup or another app's database), and restoring it would strand your store. To move your history across platforms, export the WHOOP-format CSV on the other device (Settings → Export data) and import that here, or import your original WHOOP / Apple Health export.": "Ce n'est pas une sauvegarde NOOP provenant de cette application. Il lui manque les informations de migration qu'une sauvegarde NOOP contient (elle ressemble à une sauvegarde Android ou à la base de données d'une autre application), et la restaurer bloquerait votre stockage. Pour transférer votre historique entre plateformes, exportez le CSV au format WHOOP sur l'autre appareil (Paramètres → Exporter les données) et importez-le ici, ou importez votre export WHOOP / Apple Santé d'origine.", +"This night scored high efficiency but very little deep or REM, more likely a staging estimate miss than a real restorative shortfall. The totals are kept as-is; read the split with care.": "Cette nuit a obtenu une efficacité élevée mais très peu de sommeil profond ou paradoxal, ce qui ressemble plus à une erreur d'estimation des phases qu'à un réel déficit de récupération. Les totaux sont conservés tels quels ; interprétez la répartition avec prudence.", +"This permanently deletes all data recorded from %@. This can't be undone.": "Cela supprime définitivement toutes les données enregistrées depuis %@. Cette action est irréversible.", +"This permanently deletes everything imported from Apple Health: heart rate, HRV, sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "Cela supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, sommeil, pas, entraînements et plus encore. Les données en direct de votre bracelet ne sont pas concernées. Cette action est irréversible.", +"This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays.": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé.", +"This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays. Use it if a bad first week, like wearing it while sick, set your baseline off.": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé. Utilisez cette option si une mauvaise première semaine, par exemple portée pendant une maladie, a faussé votre référence.", +"This session": "Cette séance", +"This session's contribution to the day's Effort, as captured during the workout.": "La contribution de cette séance à l'Effort du jour, telle qu'enregistrée pendant l'entraînement.", +"This sideloaded build expired %lld days ago. Re-sign it to keep it running.": "Cette version en sideload a expiré il y a %lld jours. Resignez-la pour continuer à l'utiliser.", +"This sideloaded build expired 1 day ago. Re-sign it to keep it running.": "Cette version en sideload a expiré il y a 1 jour. Resignez-la pour continuer à l'utiliser.", +"This sideloaded build expires in %lld days. Re-sign to keep it running.": "Cette version en sideload expire dans %lld jours. Resignez-la pour continuer à l'utiliser.", +"This sideloaded build expires in 1 day. Re-sign to keep it running.": "Cette version en sideload expire dans 1 jour. Resignez-la pour continuer à l'utiliser.", +"This stands on community reverse-engineering. Huge thanks:": "Ceci repose sur la rétro-ingénierie communautaire. Un immense merci :", +"This stays on your device. It is never uploaded, never synced, never shared.": "Cela reste sur votre appareil. Ce n'est jamais téléversé, jamais synchronisé, jamais partagé.", +"This week": "Cette semaine", +"Three quiet promises.": "Trois promesses discrètes.", +"Threshold": "Seuil", +"Thu": "Jeu", +"Thursday": "Jeudi", +"Time": "Heure", +"Time asleep": "Temps de sommeil", +"Time in Bed": "Temps au lit", +"Time in each %HRmax zone, derived from the strap's heart rate over this window (approximate).": "Temps dans chaque zone %HRmax, calculé à partir de la fréquence cardiaque du bracelet sur cette période (approximatif).", +"Time of day": "Moment de la journée", +"Time range": "Plage de temps", +"Time to move. You've been seated a while.": "Il est temps de bouger. Vous êtes assis depuis un moment.", +"Time to wind down": "Il est temps de se détendre", +"Timing forecast": "Prévision des horaires", +"Tip: choose a folder in iCloud Drive and your backups sync to all your Apple devices automatically, no account setup needed.": "Astuce : choisissez un dossier dans iCloud Drive et vos sauvegardes se synchroniseront automatiquement sur tous vos appareils Apple, sans configuration de compte.", +"Tip: the share sheet can save the PDF to Files, AirDrop it, or send it on.": "Astuce : le menu de partage permet d'enregistrer le PDF dans Fichiers, de l'envoyer par AirDrop ou de le transférer.", +"To get your Apple Health data in anyway: import a Health export .zip in Data Sources, or turn on Shortcuts Export to feed your strap data into Health without the entitlement. (A build installed from the App Store or signed with a paid Apple Developer account connects directly.)": "Pour intégrer quand même vos données Apple Santé : importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export Raccourcis pour envoyer les données de votre bracelet vers Santé sans l'entitlement. (Une version installée depuis l'App Store ou signée avec un compte Apple Developer payant se connecte directement.)", +"Today": "Aujourd'hui", +"Today's Timeline": "Chronologie du jour", +"Today's drinks": "Boissons du jour", +"Today's mood": "Humeur du jour", +"Today's mood: %@, %lld of 5": "Humeur du jour : %1$@, %2$lld sur 5", +"Today's scores owned by %@": "Scores du jour gérés par %@", +"Today's stress split: calm %@, moderate %@, high %@.": "Répartition du stress du jour : calme %1$@, modéré %2$@, élevé %3$@.", +"Today’s Synthesis": "Synthèse du jour", +"Tomorrow": "Demain", +"Tomorrow's Charge": "Charge de demain", +"Tomorrow's Charge estimate %lld plus or minus %lld": "Estimation de la Charge de demain %1$lld plus ou moins %2$lld", +"Tomorrow’s %@": "%@ de demain", +"Tonight": "Cette nuit", +"Tonight's lands after you sleep with the strap on.": "Le score de cette nuit arrivera après avoir dormi avec le bracelet.", +"Tonight’s forecast": "Prévision pour cette nuit", +"Top sport": "Sport principal", +"Total Calories": "Calories totales", +"Total Distance": "Distance totale", +"Total Time": "Temps total", +"Total Workouts": "Total des entraînements", +"Track a workout manually. Records heart rate and effort until you end it.": "Suivez un entraînement manuellement. Enregistre la fréquence cardiaque et l'effort jusqu'à ce que vous le terminiez.", +"Trailing %lld days": "%lld derniers jours", +"Trailing window": "Fenêtre glissante", +"Train by feel. The strap buzzes so you don't have to watch a screen.": "Entraînez-vous au ressenti. Le bracelet vibre pour que vous n'ayez pas à regarder un écran.", +"Treadmill, indoor bike, rower or cross-trainer (Bluetooth FTMS)": "Tapis de course, vélo d'intérieur, rameur ou vélo elliptique (Bluetooth FTMS)", +"Trend": "Tendance", +"Trend only where supported, and Apple removed the SpO₂ sensor from the newest US units, so on those it simply isn't there. NOOP shows nothing rather than a fake reading.": "Tendance uniquement là où c'est pris en charge, et Apple a retiré le capteur SpO₂ des tout derniers modèles américains, donc il est tout simplement absent sur ceux-ci. NOOP n'affiche rien plutôt qu'une mesure factice.", +"Trend, %lld points, latest %@, low %@, high %@": "Tendance, %lld points, dernier %@, minimum %@, maximum %@", +"Trends": "Tendances", +"Trends need history to draw. Import your WHOOP export in Data Sources to see weeks, months and years instantly.": "Les tendances ont besoin d'historique pour s'afficher. Importez votre export WHOOP dans Sources de données pour voir semaines, mois et années instantanément.", +"Trends report": "Rapport de tendances", +"Try WHOOP 5/MG protocol probes": "Essayer les sondes de protocole WHOOP 5/MG", +"Try again": "Réessayer", +"Tue": "Mar", +"Tuesday": "Mardi", +"Turn off": "Désactiver", +"Turn off cycle awareness": "Désactiver le suivi du cycle", +"Turn on Rhythm": "Activer Rythme", +"Turn on a test for the thing that's wrong, wear the strap, then tap Report. All on %@.": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Tout sur %@.", +"Turn on a test for the thing that's wrong, wear the strap, then tap Report. Your strap log, recalibrate, scheduled export and experimental probes all live here too.": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Le journal du bracelet, le réétalonnage, l'export planifié et les sondes expérimentales se trouvent aussi ici.", +"Turn on cycle awareness": "Activer le suivi du cycle", +"Turn on the wind-down reminder below to land at your wake time rested.": "Activez le rappel de détente ci-dessous pour arriver reposé à l'heure de votre réveil.", +"Type in a blood-pressure reading or a cholesterol value from your last appointment. It stays on %@, and over time you'll see how it lines up with your sleep, heart rate and recovery.": "Saisissez une mesure de tension artérielle ou une valeur de cholestérol issue de votre dernière consultation. Elle reste sur %@, et avec le temps vous verrez comment elle s'accorde avec votre sommeil, votre fréquence cardiaque et votre récupération.", +"Type in a number from your own report. It stays on %@.": "Saisissez un chiffre issu de votre propre rapport. Il reste sur %@.", +"Typical for you": "Typique pour vous", +"Typical range": "Plage typique", +"Unit": "Unité", +"Units": "Unités", +"Unknown": "Inconnu", +"Unlock WHOOP 5/MG deep data (R22)": "Débloquer les données approfondies WHOOP 5/MG (R22)", +"Unlocks your VO₂max": "Débloque votre VO₂max", +"Unread. %@. %@": "Non lu. %1$@. %2$@", +"Updated weekly": "Mis à jour hebdomadairement", +"Updates": "Mises à jour", +"Updates, %lld unread": "Mises à jour, %lld non lues", +"Use": "Utiliser", +"Use NOOP with just an Apple Watch. What it's great at, and where it's lighter than a strap.": "Utilisez NOOP avec une simple Apple Watch. Ce en quoi elle excelle, et où elle est plus limitée qu'un bracelet.", +"Use NOOP with your watch": "Utiliser NOOP avec votre montre", +"Use custom model": "Utiliser un modèle personnalisé", +"Use file import": "Utiliser l'import de fichier", +"Use file import for Oura": "Utiliser l'import de fichier pour Oura", +"Use it only with a device you own, to read your own data. Whether to use it (and any effect on your WHOOP account, subscription, device, or warranty) is your decision, and your risk alone.": "Utilisez-le uniquement avec un appareil qui vous appartient, pour lire vos propres données. L'utiliser (et tout effet sur votre compte WHOOP, votre abonnement, votre appareil ou votre garantie) relève de votre seule décision et de votre seul risque.", +"Use my resonance pace": "Utiliser mon rythme de résonance", +"Uses the watch's Broadcast Heart Rate. We'll show you how.": "Utilise la diffusion de la fréquence cardiaque de la montre. Nous vous montrerons comment faire.", +"Using": "Utilisation", +"Using NOOP may breach WHOOP's Terms of Service": "L'utilisation de NOOP peut enfreindre les conditions d'utilisation de WHOOP", +"Using NOOP on iPhone": "Utiliser NOOP sur iPhone", +"VO₂ Max (estimated)": "VO₂ Max (estimé)", +"Value": "Valeur", +"Variation is normal and often benign": "La variation est normale et souvent bénigne", +"Version %@ is available": "La version %@ est disponible", +"Very strong": "Très fort", +"Vibrate your connected WHOOP strap.": "Faites vibrer votre bracelet WHOOP connecté.", +"View active workout": "Voir l'entraînement actif", +"View detail": "Voir le détail", +"View the active workout": "Voir l'entraînement actif", +"View the full plan": "Voir le plan complet", +"Visual only": "Visuel uniquement", +"Vital Signs": "Signes vitaux", +"Vitality": "Vitalité", +"WHAT IT'S GREAT AT": "CE EN QUOI ELLE EXCELLE", +"WHAT MOVES YOU ›": "CE QUI VOUS FAIT BOUGER ›", +"WHAT THE WATCH CAN DO": "CE QUE LA MONTRE PEUT FAIRE", +"WHAT TO EXPECT": "À QUOI S'ATTENDRE", +"WHAT'S NEW": "NOUVEAUTÉS", +"WHERE IT'S LIGHTER THAN A STRAP": "OÙ ELLE EST PLUS LIMITÉE QU'UN BRACELET", +"WHOOP 4.0 is tested and works end to end. WHOOP 5.0/MG is newer: live heart rate works today, but deeper metrics (recovery, strain, sleep) for 5/MG are still being figured out. NOOP always tells you what's live versus still building.": "WHOOP 4.0 est testé et fonctionne de bout en bout. WHOOP 5.0/MG est plus récent : la fréquence cardiaque en direct fonctionne déjà, mais les métriques plus approfondies (récupération, effort, sommeil) pour 5/MG sont encore en cours d'étude. NOOP vous indique toujours ce qui est actif et ce qui est encore en construction.", +"WHOOP 4.0 is the supported path": "WHOOP 4.0 est la voie prise en charge", +"WHOOP 4.0 protocol": "Protocole WHOOP 4.0", +"WHOOP 4.0 · motion → steps": "WHOOP 4.0 · mouvement → pas", +"WHOOP 5.0 / MG bonds to one device at a time. Unpair it from the official WHOOP app first.": "WHOOP 5.0 / MG se couple à un seul appareil à la fois. Dissociez-le d'abord de l'application officielle WHOOP.", +"WHOOP 5.0 / MG support is newer and still experimental in NOOP.": "La prise en charge de WHOOP 5.0 / MG est plus récente et encore expérimentale dans NOOP.", +"WHOOP 5.0 protocol": "Protocole WHOOP 5.0", +"WHOOP 5.0/MG pairs with one app at a time. If a scan finds nothing, unpair it in the official WHOOP app and fully close that app, then Scan again.": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si un scan ne trouve rien, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis relancez le scan.", +"WHOOP 5.0/MG pairs with one app at a time. If nothing's found, unpair it in the official WHOOP app and fully close that app, then Scan.": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si rien n'est trouvé, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis lancez le scan.", +"WHOOP 5/MG straps hand a fresh app only live heart rate. The official app switches on the deeper streams (high-rate HR + motion + history) by writing a set of feature flags, a sequence two independent projects have documented. With this on, the button below sends that exact sequence to your strap. Unlike everything else here it does write to the strap, but it's reversible (it only changes which data the strap chooses to emit) and is the same thing the official app does. Experimental: it may do nothing on your firmware. iPhone/Android only. A Mac can't write to a 5/MG.": "Les bracelets WHOOP 5/MG ne fournissent à une nouvelle application que la fréquence cardiaque en direct. L'application officielle active les flux plus approfondis (FC haute fréquence + mouvement + historique) en écrivant un ensemble d'indicateurs de fonctionnalités, une séquence documentée par deux projets indépendants. Une fois cette option activée, le bouton ci-dessous envoie exactement cette séquence à votre bracelet. Contrairement à tout le reste ici, cela écrit bien sur le bracelet, mais c'est réversible (cela ne change que les données que le bracelet choisit d'émettre) et c'est exactement ce que fait l'application officielle. Expérimental : cela peut n'avoir aucun effet sur votre micrologiciel. iPhone/Android uniquement. Un Mac ne peut pas écrire sur un 5/MG.", +"WHOOP Export": "Export WHOOP", +"WHOOP Strap (Live BLE)": "Bracelet WHOOP (BLE en direct)", +"WHOOP import": "Import WHOOP", +"WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps are an early, in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper sleep and recovery data.": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque sont un ajout récent, encore en développement : ils diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP.", +"WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps stream live heart rate and HRV, but not WHOOP's deeper sleep and recovery data.": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP.", +"WHOOP straps don't appear in your %@'s Bluetooth settings. They advertise on a custom profile that only apps like NOOP can find, so there's nothing to pair there, and you shouldn't try.": "Les bracelets WHOOP n'apparaissent pas dans les réglages Bluetooth de votre %@. Ils s'annoncent sur un profil personnalisé que seules des applications comme NOOP peuvent détecter : il n'y a donc rien à associer là-bas, et vous ne devriez pas essayer.", +"WHOOP's imported per-zone split for this session.": "Répartition par zone importée de WHOOP pour cette séance.", +"WORK": "TRAVAIL", +"WORKOUT IN PROGRESS": "ENTRAÎNEMENT EN COURS", +"Waist (optional)": "Tour de taille (facultatif)", +"Waist in centimetres": "Tour de taille en centimètres", +"Waist in inches": "Tour de taille en pouces", +"Waist not set": "Tour de taille non défini", +"Waist, %lld centimetres": "Tour de taille, %lld centimètres", +"Waist, %lld inches": "Tour de taille, %lld pouces", +"Waiting for R-R": "En attente de R-R", +"Waiting for R-R intervals.": "En attente des intervalles R-R.", +"Waiting for a resting heart rate. Start a live reading first, or come back when you're still.": "En attente d'une fréquence cardiaque au repos. Démarrez d'abord une mesure en direct, ou revenez lorsque vous êtes immobile.", +"Wake": "Réveil", +"Wake at": "Réveil à", +"Wake me with a strap buzz": "Me réveiller avec une vibration du bracelet", +"Wake the machine. Start pedalling, walking or rowing so it powers on its Bluetooth.": "Réveillez l'appareil. Commencez à pédaler, marcher ou ramer pour qu'il active son Bluetooth.", +"Wake time": "Heure de réveil", +"Wake your Amazfit / Zepp band and make sure it isn't connected to the Zepp app right now.": "Réveillez votre bracelet Amazfit / Zepp et assurez-vous qu'il n'est pas connecté à l'application Zepp en ce moment.", +"Wake your Mi Band and make sure it isn't connected to the Mi Fitness / Zepp Life app right now.": "Réveillez votre Mi Band et assurez-vous qu'elle n'est pas connectée à l'application Mi Fitness / Zepp Life en ce moment.", +"Wake your strap. Put it on, or dampen the contacts.": "Réveillez votre bracelet. Portez-le, ou humidifiez les contacts.", +"Walking": "Marche", +"Want the full breakdown of every metric and how sure NOOP is about each one? The “About Apple Watch data” page in Settings has the honest table.": "Vous voulez le détail complet de chaque métrique et le degré de certitude de NOOP pour chacune ? La page « À propos des données Apple Watch » dans Paramètres contient le tableau honnête.", +"Warmer than your baseline": "Plus chaude que votre référence", +"Warming up. Keep moving to climb into Zone 1.": "Échauffement. Continuez à bouger pour atteindre la Zone 1.", +"Warn me about caffeine close to bedtime": "M'avertir en cas de caféine proche du coucher", +"Warn me when I log caffeine too close to bedtime. A timing guide from your own bedtime, not a measurement.": "M'avertir lorsque j'enregistre de la caféine trop près de l'heure du coucher. Un repère basé sur votre propre heure de coucher, pas une mesure.", +"Watch": "Montre", +"Watch for early-illness signs": "Surveiller les premiers signes de maladie", +"Watch your heart, live": "Observez votre cœur, en direct", +"Watches your resting HR, HRV, skin temperature and respiration against your own 28-day baseline. On-device and approximate: informational only, not a diagnosis.": "Surveille votre FC au repos, votre VFC, votre température cutanée et votre respiration par rapport à votre propre référence de 28 jours. Sur l'appareil et approximatif : à titre informatif uniquement, pas un diagnostic.", +"We could not take over this ring.": "Nous n'avons pas pu prendre le contrôle de cet anneau.", +"We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's banked motion history, so your strap needs to sync that history before NOOP has anything to count.": "Nous ne voyons encore aucun mouvement provenant de votre bracelet. Les pas sont estimés à partir de l'historique de mouvement stocké par votre WHOOP, donc votre bracelet doit d'abord synchroniser cet historique avant que NOOP ait quoi que ce soit à compter.", +"Wear & presence": "Port et présence", +"Wear it snug on your wrist or bicep, sensor against skin.": "Portez-le ajusté au poignet ou au biceps, capteur contre la peau.", +"Wear state": "État de port", +"Wear the strap for scoring": "Portez le bracelet pour obtenir un score", +"Wear the strap overnight and these read from your nightly skin temperature.": "Portez le bracelet pendant la nuit et ces valeurs seront lues à partir de votre température cutanée nocturne.", +"Wear the strap, tap once, then let it sync and share your strap log.": "Portez le bracelet, touchez une fois, laissez-le se synchroniser, puis partagez le journal du bracelet.", +"Wed": "Mer", +"Wednesday": "Mercredi", +"Week in review": "Bilan de la semaine", +"Weekdays": "Jours de semaine", +"Weekends": "Week-ends", +"Weekly": "Hebdomadaire", +"Weight": "Poids", +"Weight in kilograms": "Poids en kilogrammes", +"Weight in pounds": "Poids en livres", +"Weight, %lld pounds": "Poids, %lld livres", +"Wellbeing": "Bien-être", +"Wellness score": "Score de bien-être", +"What \"recording\" means": "Ce que signifie « enregistrement »", +"What Lab Book is (and isn't)": "Ce qu'est le Lab Book (et ce qu'il n'est pas)", +"What Moves You": "Ce qui vous fait bouger", +"What NOOP does": "Ce que fait NOOP", +"What are you adding?": "Qu'ajoutez-vous ?", +"What changed": "Ce qui a changé", +"What correlates": "Ce qui corrèle", +"What each activity costs your recovery": "Ce que chaque activité coûte à votre récupération", +"What moves you. Ranked patterns in your own data, and your dose-response.": "Ce qui vous fait bouger. Des tendances classées dans vos propres données, et votre réponse à la dose.", +"What moves your %@": "Ce qui fait évoluer votre %@", +"What shaped it": "Ce qui l'a façonné", +"What shaped your Charge": "Ce qui a façonné votre Charge", +"What should today's training look like?": "À quoi devrait ressembler l'entraînement du jour ?", +"What to expect": "À quoi s'attendre", +"What tracks your mood (%lld check-ins)": "Ce qui suit votre humeur (%lld check-ins)", +"What you get": "Ce que vous obtenez", +"What you lose": "Ce que vous perdez", +"What your watch is great at, where it's lighter than a chest strap, and how sure NOOP is.": "Ce en quoi votre montre excelle, où elle est plus légère qu'une ceinture pectorale, et le niveau de confiance de NOOP.", +"What's new in NOOP %@": "Nouveautés de NOOP %@", +"What's new in the app and your data": "Nouveautés dans l'application et vos données", +"When %@ is higher, %@ tends to be higher.": "Quand %1$@ est plus élevé, %2$@ a tendance à l'être aussi.", +"When %@ is higher, %@ tends to be lower.": "Quand %1$@ est plus élevé, %2$@ a tendance à être plus bas.", +"When I double-tap": "Quand je double-tape", +"When a fresh, non-exercise HRV dip is detected while you're still, NOOP offers a one-minute guided breath: a single confirming buzz and a dismissible card. Never an alarm, never a diagnosis.": "Quand une chute récente de la VFC hors exercice est détectée pendant que vous êtes immobile, NOOP propose une respiration guidée d'une minute : une seule vibration de confirmation et une carte que vous pouvez ignorer. Jamais une alarme, jamais un diagnostic.", +"When on, NOOP taps your wrist for the apps you pick below, so you can leave the %@ and still feel what matters.": "Une fois activé, NOOP fait vibrer votre poignet pour les applications que vous choisissez ci-dessous, afin que vous puissiez laisser %@ de côté tout en ressentant ce qui compte.", +"When the %@ apps you choose send a notification, NOOP taps your strap: Slack, Calendar, Messages, whatever matters. Everything stays on %@.": "Quand les applications de %1$@ que vous choisissez envoient une notification, NOOP fait vibrer votre bracelet : Slack, Calendrier, Messages, tout ce qui compte. Tout reste sur %2$@.", +"When the ring is reset and waking, tap Scan below.": "Quand l'anneau est réinitialisé et se réveille, appuyez sur « Scanner » ci-dessous.", +"When the system prompt appears, choose Allow so NOOP can find your strap.": "Quand la demande système apparaît, choisissez Autoriser pour que NOOP puisse trouver votre bracelet.", +"When this is on, NOOP rewrites a plain-text file (On My iPhone › NOOP › noop_sync.txt) each time you leave the app: one line per 15 minutes of heart rate, HRV and steps, read straight from your strap. Pair it with the Siri Shortcut that reads the file and logs everything into Apple Health (no HealthKit entitlement needed), so it works on sideloaded installs. The setup guide and the pre-built Shortcut live in the project wiki on GitHub.": "Quand c'est activé, NOOP réécrit un fichier texte brut (Sur mon iPhone › NOOP › noop_sync.txt) chaque fois que vous quittez l'application : une ligne par tranche de 15 minutes de fréquence cardiaque, VFC et pas, lue directement depuis votre bracelet. Associez-le au raccourci Siri qui lit le fichier et enregistre tout dans Apple Santé (aucune autorisation HealthKit requise), pour que cela fonctionne aussi sur les installations en sideload. Le guide de configuration et le raccourci prêt à l'emploi se trouvent dans le wiki du projet sur GitHub.", +"When you import an Apple Health or WHOOP export, iOS hands NOOP a private copy of the file. NOOP reads it, saves your data into the health database, then deletes the copy. Older builds didn't delete every copy. This screen reclaims any that were left behind.": "Quand vous importez un export Apple Santé ou WHOOP, iOS remet à NOOP une copie privée du fichier. NOOP la lit, enregistre vos données dans la base de santé, puis supprime la copie. Les anciennes versions ne supprimaient pas toutes les copies. Cet écran récupère celles qui sont restées en place.", +"When your strap is connected NOOP is saving data live. \"Last synced\" tells you how fresh it is. If it says \"Not recording\", reconnect.": "Quand votre bracelet est connecté, NOOP enregistre les données en direct. « Dernière synchro » indique leur fraîcheur. S'il indique « Aucun enregistrement », reconnectez-vous.", +"Where NOOP's on-device space is going, and a one-tap clean-up.": "Où va l'espace de NOOP sur l'appareil, et un nettoyage en un geste.", +"Where a sensor isn't on your watch, NOOP reads “not available” for that metric, never a zero, never an invented number. Everything else keeps working.": "Là où un capteur est absent de votre montre, NOOP affiche « non disponible » pour cette mesure, jamais un zéro, jamais un chiffre inventé. Tout le reste continue de fonctionner.", +"Where your numbers come from": "D'où viennent vos chiffres", +"Which strap are you pairing?": "Quel bracelet associez-vous ?", +"Which strap?": "Quel bracelet ?", +"Whoop import": "Importation Whoop", +"Why am I run down?": "Pourquoi suis-je épuisé ?", +"Why does this grow?": "Pourquoi cela augmente-t-il ?", +"Why recovery calibrates over about a week": "Pourquoi la récupération se calibre en une semaine environ", +"Why this is logged as a nap": "Pourquoi c'est enregistré comme une sieste", +"Why this is your main sleep": "Pourquoi c'est votre sommeil principal", +"Why this sleep?": "Pourquoi ce sommeil ?", +"Wind down": "Se détendre", +"Wind-down nudge": "Rappel de détente", +"Window": "Fenêtre", +"Windowed rMSSD": "rMSSD par fenêtre", +"Wire NOOP's actions into a Back-Tap, a focus automation, or a longer Shortcut. For example, double-tap the back of your iPhone to buzz the strap.": "Reliez les actions de NOOP à un Toucher au dos, une automatisation de Focus ou un raccourci plus élaboré. Par exemple, appuyez deux fois à l'arrière de votre iPhone pour faire vibrer le bracelet.", +"With": "Avec", +"Without": "Sans", +"Woke": "Réveillé", +"Work": "Travail", +"Working…": "Traitement en cours…", +"Workout": "Entraînement", +"Workout file (GPX / TCX / FIT)": "Fichier d'entraînement (GPX / TCX / FIT)", +"Workout saved": "Entraînement enregistré", +"Workout saved · %@": "Entraînement enregistré · %@", +"Workouts": "Entraînements", +"Worn": "Porté", +"Wrist alerts": "Alertes au poignet", +"Wrist delivery isn't live yet. It needs a small on-device watcher (coming in an update) to read macOS notifications. Everything stays on this Mac. Your choices are saved now and will apply automatically once delivery ships.": "La diffusion au poignet n'est pas encore active. Elle nécessite un petit observateur sur l'appareil (à venir dans une mise à jour) pour lire les notifications macOS. Tout reste sur ce Mac. Vos choix sont enregistrés dès maintenant et s'appliqueront automatiquement une fois la fonction disponible.", +"Wrist temp": "Temp. poignet", +"Wrist temperature needs Series 8 or later, and the newest US units dropped the blood-oxygen sensor. Where a sensor isn't there, NOOP reads “not available” instead of zero.": "La température au poignet nécessite une Series 8 ou ultérieure, et les derniers modèles américains ont perdu le capteur d'oxygène sanguin. Là où un capteur est absent, NOOP affiche « non disponible » plutôt que zéro.", +"Writes a timestamped copy of your strap log to NOOP's folder in the Files app, once a day. Handy for a bug report without remembering to grab it. On iPhone it fires when iOS next wakes NOOP near your chosen time, not guaranteed to the minute (keep NOOP open overnight for the best chance). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans le dossier de NOOP de l'app Fichiers, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur iPhone, cela se déclenche au prochain réveil de NOOP par iOS proche de l'heure choisie, sans garantie à la minute près (laissez NOOP ouvert la nuit pour de meilleures chances). Tout reste sur %@ ; rien n'est envoyé en ligne.", +"Writes a timestamped copy of your strap log to your Documents folder, once a day. Handy for a bug report without remembering to grab it. On Mac it runs while NOOP is open (and catches up on launch if the time passed while it was closed). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans votre dossier Documents, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur Mac, cela s'exécute pendant que NOOP est ouvert (et rattrape au lancement si l'heure est passée pendant sa fermeture). Tout reste sur %@ ; rien n'est envoyé en ligne.", +"YOUR DAILY SCORES": "VOS SCORES QUOTIDIENS", +"YOUR READING": "VOTRE MESURE", +"Yes": "Oui", +"Yesterday": "Hier", +"Yesterday’s Synthesis": "Synthèse d'hier", +"You can change what you share any time in Settings › Health › Data Access & Devices.": "Vous pouvez modifier ce que vous partagez à tout moment dans Paramètres › Santé › Accès aux données et appareils.", +"You can still bring your data in by importing a Health export from Data Sources. A build from the App Store, or one signed with a paid Apple Developer account, connects directly.": "Vous pouvez tout de même importer vos données via un export Santé depuis les Sources de données. Une version issue de l'App Store, ou signée avec un compte Apple Developer payant, se connecte directement.", +"You removed your active strap. Choose which paired band provides your live data, or leave none active and pair one later.": "Vous avez retiré votre bracelet actif. Choisissez quel bracelet associé fournira vos données en direct, ou n'en laissez aucun actif et associez-en un plus tard.", +"You said: %@": "Vous avez dit : %@", +"You'll be reminded around %@.": "Vous recevrez un rappel vers %@.", +"You'll likely wake around %lld ± %lld Charge if you sleep about %@ tonight.": "Vous vous réveillerez probablement avec une Charge d'environ %1$lld ± %2$lld si vous dormez environ %3$@ cette nuit.", +"You're all caught up.": "Vous êtes à jour.", +"You're carrying about %@ of surplus over %@. You've slept past your need on balance. Nicely ahead.": "Vous portez un surplus d'environ %@ sur %@. Vous avez dormi au-delà de vos besoins, en solde. Bien en avance.", +"You're connected. NOOP is reading your Apple Watch data now. Your Charge score will spend its first week or so calibrating, then settle in.": "Vous êtes connecté. NOOP lit désormais les données de votre Apple Watch. Votre score de Charge passera sa première semaine environ à se calibrer, puis se stabilisera.", +"You're in balance with your baseline, so moderate strain is well-judged": "Vous êtes en équilibre avec votre référence, un effort modéré est donc bien choisi", +"You're on the latest (%@).": "Vous avez la dernière version (%@).", +"You're roughly on top of your sleep across %@. Slept minutes balance out against your need.": "Vous êtes globalement à jour sur votre sommeil sur %@. Les minutes dormies compensent vos besoins.", +"You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day.": "Vous êtes proche de votre référence autonome habituelle : stress modéré, une journée normale et équilibrée.", +"You've banked about %@ of sleep debt over %@. Surplus nights count back against it. An earlier night or two would clear it.": "Vous avez accumulé environ %@ de dette de sommeil sur %@. Les nuits en surplus viennent la réduire. Une ou deux nuits plus tôt suffiraient à l'effacer.", +"You've been seated for about %lld min. Time to move.": "Vous êtes assis depuis environ %lld min. Il est temps de bouger.", +"Your Apple Watch as a device": "Votre Apple Watch en tant qu'appareil", +"Your Charge (recovery) on the watch face, with Effort and Rest in the rectangular card.": "Votre Charge (récupération) sur le cadran, avec Effort et Repos dans la carte rectangulaire.", +"Your Charge score learns a personal baseline from your heart-rate variability, resting heart rate and more over time. If a bad first week set it off, you can re-learn it from tonight. Your history stays.": "Votre score de Charge apprend une référence personnelle à partir de votre variabilité de fréquence cardiaque, de votre fréquence cardiaque au repos et plus encore, au fil du temps. Si une mauvaise première semaine l'a faussée, vous pouvez la faire réapprendre dès ce soir. Votre historique reste.", +"Your Data, Fused": "Vos données, fusionnées", +"Your GPS route for this session, recorded and stored on your device. Nothing leaves your phone.": "Votre trajet GPS pour cette séance, enregistré et stocké sur votre appareil. Rien ne quitte votre téléphone.", +"Your HRV dipped while you were still. Want a minute to breathe?": "Votre VFC a chuté pendant que vous étiez immobile. Envie d'une minute pour respirer ?", +"Your Monday-to-Sunday, read in one glance.": "Votre semaine du lundi au dimanche, lue en un coup d'œil.", +"Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings, Experimental). Right now your wake time is saved but the strap is NOT armed. Even with Experimental on, a 5/MG strap-driven wake is still unconfirmed on our side, so keep a backup alarm.": "Votre WHOOP 5/MG n'activera pas cette fonction tant que le mode Expérimental n'est pas activé (Paramètres, Expérimental). Pour l'instant, votre heure de réveil est enregistrée mais le bracelet n'est PAS armé. Même avec le mode Expérimental activé, un réveil piloté par le bracelet 5/MG reste non confirmé de notre côté, gardez donc un réveil de secours.", +"Your WHOOP is at 100%.": "Votre WHOOP est à 100 %.", +"Your WHOOP only talks to one phone at a time.": "Votre WHOOP ne communique qu'avec un seul téléphone à la fois.", +"Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day.": "Vos marqueurs autonomes penchent vers le stress aujourd'hui. Considérez-la comme une journée axée sur la récupération.", +"Your bands report different numbers. Here's every source, and the one NOOP is using.": "Vos bracelets rapportent des chiffres différents. Voici chaque source, et celle que NOOP utilise.", +"Your bloods, BP and body numbers. Kept private, on %@.": "Vos analyses sanguines, votre tension et vos données corporelles. Gardées privées, sur %@.", +"Your cardiovascular load. NOOP turns every second of heart rate into a training-impulse using heart-rate-reserve zones (Karvonen), weights time in harder zones more heavily (Edwards / Banister), and places it on a logarithmic 0–100 scale, so easy days sit low and an all-out day approaches 100, which stays genuinely rare. A long walk with little cardio still counts, through a steps / active-energy floor.": "Votre charge cardiovasculaire. NOOP transforme chaque seconde de fréquence cardiaque en impulsion d'entraînement à l'aide des zones de réserve de fréquence cardiaque (Karvonen), pondère plus fortement le temps passé dans les zones difficiles (Edwards / Banister), et place le résultat sur une échelle logarithmique de 0 à 100, si bien que les jours faciles restent bas et qu'une journée à fond approche 100, ce qui reste vraiment rare. Une longue marche avec peu de cardio compte tout de même, grâce à un seuil minimal basé sur les pas et l'énergie active.", +"Your cards": "Vos cartes", +"Your current strap": "Votre bracelet actuel", +"Your curve fills in as the strap offloads its history.": "Votre courbe se remplit à mesure que le bracelet transfère son historique.", +"Your data has been restored. Quit and reopen NOOP for it to take effect.": "Vos données ont été restaurées. Quittez et rouvrez NOOP pour que cela prenne effet.", +"Your fluid intake today, on %@ only.": "Votre apport en liquides aujourd'hui, sur %@ uniquement.", +"Your last %lld hours have stayed in the high band. A few minutes of paced breathing can help downshift your nervous system.": "Vos dernières %lld heures sont restées dans la bande haute. Quelques minutes de respiration rythmée peuvent aider à apaiser votre système nerveux.", +"Your last %lld readings: %@ %@, holding steady.": "Vos %1$lld dernières mesures : %2$@ %3$@, stables.", +"Your last %lld readings: %@ %@, trending down.": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la baisse.", +"Your last %lld readings: %@ %@, trending up.": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la hausse.", +"Your live heart rate is working from the strap, and charge, effort and rest build from it over your next few nights of wear, sharpening as it learns your baseline. Want your full history instantly? Import your WHOOP export in Data Sources and it backfills in about a minute.": "Votre fréquence cardiaque en direct fonctionne déjà depuis le bracelet, et la charge, l'effort et le repos s'en construisent au fil de vos prochaines nuits de port, s'affinant à mesure qu'ils apprennent votre référence. Vous voulez tout votre historique instantanément ? Importez votre export WHOOP dans les Sources de données, il se complète en une minute environ.", +"Your live heart rate is working from the strap, and recovery, strain and sleep build from it over your next few nights of wear, sharpening as it learns your baseline. Want your full history instantly? Import your WHOOP export in Data Sources and it backfills in about a minute.": "Votre fréquence cardiaque en direct fonctionne depuis le bracelet, et la récupération, l'effort et le sommeil se construisent à partir de là sur vos prochaines nuits de port, s'affinant au fur et à mesure qu'il apprend votre référence. Vous voulez votre historique complet instantanément ? Importez votre export WHOOP dans Sources de données et il se remplit en environ une minute.", +"Your locked pace": "Votre rythme verrouillé", +"Your locked pace · %@ br/min": "Votre rythme verrouillé · %@ resp/min", +"Your nervous system is well-recovered, so you're primed to push": "Votre système nerveux est bien récupéré, vous êtes donc prêt à pousser", +"Your numbers, your strap, and how NOOP works. All on %@.": "Vos chiffres, votre bracelet et le fonctionnement de NOOP. Tout sur %@.", +"Your numbers, your strap, and how NOOP works. All on this Mac.": "Vos chiffres, votre bracelet et comment NOOP fonctionne. Tout sur ce Mac.", +"Your profile photo": "Votre photo de profil", +"Your records never leave %@. There's no account, no cloud, no NOOP server. Because NOOP is an independent app you run yourself (not a healthcare provider), it isn't \"HIPAA-covered,\" and that protection doesn't apply here; the safety comes from the data being local-only and yours.": "Vos données ne quittent jamais %@. Il n'y a ni compte, ni cloud, ni serveur NOOP. Comme NOOP est une application indépendante que vous exécutez vous-même (pas un prestataire de santé), elle n'est pas « couverte par HIPAA », et cette protection ne s'applique pas ici ; la sécurité vient du fait que les données restent uniquement locales et vous appartiennent.", +"Your resonance pace": "Votre rythme de résonance", +"Your rhythm looked steady": "Votre rythme semblait stable", +"Your rhythm varied more than usual": "Votre rythme a varié plus que d'habitude", +"Your ring": "Votre anneau", +"Your ring talks to NOOP only, fully offline, no Oura account.": "Votre anneau ne communique qu'avec NOOP, entièrement hors ligne, sans compte Oura.", +"Your scores build over a few nights": "Vos scores se construisent en quelques nuits", +"Your strap in real time, heart rate and frames as they arrive.": "Votre bracelet en temps réel, fréquence cardiaque et trames à mesure qu'ils arrivent.", +"Your strap is bonded and ready to stream.": "Votre bracelet est appairé et prêt à transmettre.", +"Your strap is bonded · %lld%% battery.": "Votre bracelet est appairé · %lld %% de batterie.", +"Your strap is connected and saving data.": "Votre bracelet est connecté et enregistre les données.", +"Your strap is paired and sending data. Open Live for a real-time heart rate.": "Votre bracelet est associé et envoie des données. Ouvrez En direct pour une fréquence cardiaque en temps réel.", +"Your strap wake-alarm and the evening wind-down reminder, in one place.": "Votre alarme de réveil par bracelet et le rappel de détente du soir, au même endroit.", +"Your thread starts here.": "Votre fil commence ici.", +"Your watch, NOOP's brain": "Votre montre, le cerveau de NOOP", +"Zone %lld · %@": "Zone %1$lld · %2$@", +"Zone %lld · %lld%%": "Zone %1$lld · %2$lld %%", +"Zone %lld: %lld–%lld bpm (%lld–%lld%% max HR)": "Zone %1$lld : %2$lld–%3$lld bpm (%4$lld–%5$lld %% FC max)", +"Zoomed in · drag to pan": "Zoom activé · glissez pour déplacer", +"Zoomed in. Drag to pan": "Zoom activé. Glissez pour déplacer", +"a moderate": "un modéré", +"a strong": "un fort", +"a strong night, supporting recovery": "une nuit forte, favorise la récupération", +"a typical night": "une nuit typique", +"a very strong": "un très fort", +"a weak": "un faible", +"a while ago": "il y a un moment", +"about your age": "environ votre âge", +"above baseline, limiting recovery": "au-dessus de la référence, limite la récupération", +"above baseline, supporting recovery": "au-dessus de la référence, favorise la récupération", +"active": "actif", +"all history": "tout l'historique", +"all time": "depuis toujours", +"all your data, none of the cloud": "toutes vos données, sans le cloud", +"as of %@": "au %@", +"asleep %@": "endormi %@", +"asleep last night": "endormi la nuit dernière", +"at baseline": "au niveau de référence", +"average %@ bpm": "moyenne %@ bpm", +"avg %@": "moy. %@", +"avg %@ · %lldh": "moy. %1$@ · %2$lldh", +"avg %lld": "moy. %lld", +"avg · %lldd": "moy. · %lldj", +"balanced": "équilibré", +"below a good night, limiting recovery": "en dessous d'une bonne nuit, limite la récupération", +"below baseline, limiting recovery": "en dessous de la référence, limite la récupération", +"below baseline, supporting recovery": "en dessous de la référence, favorise la récupération", +"br/min": "resp/min", +"building": "en construction", +"clean intervals": "intervalles propres", +"cooler than baseline, limiting recovery": "plus fraîche que la référence, limite la récupération", +"covered": "couvert", +"down %lld points": "en baisse de %lld points", +"down 1 point": "en baisse de 1 point", +"drink": "boisson", +"e.g. 120": "ex. 120", +"e.g. 2.0–5.0 (your report's own range)": "ex. 2,0–5,0 (la plage de votre rapport)", +"e.g. 3.1": "ex. 3,1", +"e.g. 80": "ex. 80", +"e.g. Magnesium": "ex. Magnésium", +"e.g. Running": "ex. Course à pied", +"e.g. fasting, morning draw": "ex. à jeun, prélèvement matinal", +"e.g. mmol/L": "ex. mmol/L", +"every reading you've entered": "chaque mesure que vous avez saisie", +"fit from your phone": "ajustée depuis votre téléphone", +"flat": "stable", +"from %@": "depuis %@", +"from R-R": "à partir des R-R", +"from profile": "depuis le profil", +"from your report": "depuis votre rapport", +"hatch = typical": "hachures = typique", +"high": "élevé", +"hover for real values": "survolez pour les valeurs réelles", +"just now": "à l'instant", +"large": "grand", +"last 30 days": "30 derniers jours", +"last 7 days": "7 derniers jours", +"last 90 days": "90 derniers jours", +"last taken %@": "dernière prise %@", +"last year": "l'année dernière", +"late-caffeine": "caféine tardive", +"later step": "étape ultérieure", +"latest": "dernier", +"load %@": "charge %@", +"log": "enregistrer", +"logged today": "enregistré aujourd'hui", +"low": "faible", +"low-stress days · %lldd": "jours à faible stress · %lldj", +"mid": "moyen", +"missing": "manquant", +"moderate": "modéré", +"month": "mois", +"more overnight wear to unlock your %@ baseline": "davantage de nuits portées pour débloquer votre référence de %@", +"most recent intake about %@ ago · %lld intakes still in the estimate. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %1$@ · %2$lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez enregistré.", +"most recent intake about %@ ago. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %@. Simple indication, basée sur ce que vous avez enregistré.", +"near baseline": "proche de la référence", +"needs a recent day": "nécessite un jour récent", +"needs a tracked night": "nécessite une nuit suivie", +"needs baseline + logged days": "nécessite une référence + des jours enregistrés", +"needs history": "nécessite un historique", +"negative": "négatif", +"negligible": "négligeable", +"new": "nouveau", +"no": "non", +"no calibration yet": "pas encore de calibration", +"no change": "aucun changement", +"no data": "aucune donnée", +"no estimate yet": "pas encore d'estimation", +"no extra": "aucun extra", +"no import yet": "pas encore d'import", +"no live HR yet": "pas encore de FC en direct", +"no live gravity yet": "pas encore de gravité en direct", +"no night scored yet": "pas encore de nuit évaluée", +"no night yet": "pas encore de nuit", +"no offload yet": "pas encore de transfert", +"no prior %@": "aucun %@ précédent", +"no reading yet": "pas encore de mesure", +"no readings yet": "pas encore de mesures", +"no session yet": "pas encore de séance", +"no window yet": "pas encore de fenêtre", +"not connected": "non connecté", +"not logged today": "non enregistré aujourd'hui", +"not within 7d": "pas dans les 7 derniers j", +"of %@": "sur %@", +"of %@ L": "sur %@ L", +"of 100": "sur 100", +"of 3": "sur 3", +"of 3 · %@": "de 3 · %@", +"of beats": "des battements", +"old": "ancien", +"on demand · today's R-R": "à la demande · R-R du jour", +"open iPhone": "ouvrir iPhone", +"optional": "facultatif", +"out of 100": "sur 100", +"p < 0.05": "p < 0,05", +"partial": "partiel", +"peak %@ · %@": "pic %1$@ · %2$@", +"positive": "positif", +"projected · %@": "prévu · %@", +"quarter": "trimestre", +"range %@ to %@": "plage %@ à %@", +"reading…": "lecture…", +"ready": "prêt", +"recovery": "récupération", +"running balance": "solde courant", +"side by side · %@ before each reading": "côte à côte · %@ avant chaque mesure", +"sleep debt": "dette de sommeil", +"sleep duration": "durée du sommeil", +"slight": "léger", +"small": "petit", +"solid": "solide", +"stale": "obsolète", +"stale · %@": "obsolète · %@", +"steady": "stable", +"steps / motion unit": "pas / unité de mouvement", +"steps per motion unit": "pas par unité de mouvement", +"strain (0–21)": "effort (0–21)", +"strong": "fort", +"tap or drag for real values": "appuyez ou glissez pour les valeurs réelles", +"target": "objectif", +"that signal": "ce signal", +"the last %lld nights": "les %lld dernières nuits", +"the last 7 days": "les 7 derniers jours", +"the last night": "la dernière nuit", +"this %@": "ce %@", +"this Mac": "ce Mac", +"this device": "cet appareil", +"to": "à", +"to baseline": "à la référence", +"today": "aujourd'hui", +"typical": "typique", +"under an hour": "moins d'une heure", +"unscored": "non évalué", +"untouched days": "jours intacts", +"up %lld points": "en hausse de %lld points", +"up 1 point": "en hausse de 1 point", +"used": "utilisé", +"variation index": "indice de variation", +"very strong": "très fort", +"vs 30-day baseline": "vs référence sur 30 jours", +"vs age %lld": "vs âge %lld", +"vs behaviour-free baseline": "vs référence sans comportement", +"vs need": "vs besoin", +"vs prev %@": "vs %@ préc.", +"vs typical": "vs typique", +"vs typical —": "vs typique —", +"warmer than baseline, limiting recovery": "plus chaude que la référence, limite la récupération", +"weak": "faible", +"week": "semaine", +"what are you logging?": "que voulez-vous enregistrer ?", +"year": "année", +"your data": "vos données", +"your number, date and any note": "votre chiffre, la date et une éventuelle note", +"your readings over time": "vos mesures dans le temps", +"zone %lld %lld percent": "zone %1$lld %2$lld pour cent", +"~%lld days left": "~%lld jours restants", +"~%lldh left": "~%lldh restantes", +"~1 day left": "~1 jour restant", +"± %lld yr · a fitness comparison, not a biological age": "± %lld ans · une comparaison de forme physique, pas un âge biologique", +"± %lld · %@": "± %1$lld · %2$@", +"· ~day %lld": "· ~jour %lld", +"· ~day %lld–%lld": "· ~jour %1$lld–%2$lld", +"× %@ on save.": "× %@ à l'enregistrement.", +"Δ vs prev": "Δ vs préc.", +"“Dose” here is timing (later in the day = stronger), not milligrams.": "Ici, la « dose » est une question de moment (plus tard dans la journée = plus fort), pas de milligrammes.", +"≈ %@ steps (auto)": "≈ %@ pas (auto)", +"≈ %@ steps at this setting": "≈ %@ pas avec ce réglage" +} \ No newline at end of file From ae956cbbafe225790fda9e7293ed09c3459a2212 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:10:24 +0200 Subject: [PATCH 17/28] iOS: fill 51 French strings missing from the catalog Source had newer UI copy than the last catalog extraction (Live Sessions, Smart Alarm explainers, Scoring Guide, Trends report legend, sleep-move warnings, a few labels/buttons), so those showed in English on a French device. Added as manual entries with fr translations. Co-Authored-By: Claude Sonnet 5 --- Strand/Resources/Localizable.xcstrings | 561 +++++++++++++++++++++++++ 1 file changed, 561 insertions(+) diff --git a/Strand/Resources/Localizable.xcstrings b/Strand/Resources/Localizable.xcstrings index 800f38f29..683968e26 100644 --- a/Strand/Resources/Localizable.xcstrings +++ b/Strand/Resources/Localizable.xcstrings @@ -125795,6 +125795,567 @@ } } } + }, + "A classic one-glance read of NOOP's own scores. Same data, different lens.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une lecture claire, en un coup d'œil, des scores de NOOP. Mêmes données, autre angle." + } + } + } + }, + "Armed on the strap itself with the experimental 5/MG command. A strap-driven wake is still unconfirmed on 5/MG on our side (confirmed only on WHOOP 4.0), so keep a backup alarm for anything you truly can't miss.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Armée sur le bracelet via la commande expérimentale 5/MG. Un réveil piloté par le bracelet n'est pas encore confirmé sur 5/MG de notre côté (confirmé seulement sur WHOOP 4.0) — garde une alarme de secours pour ce que tu ne peux pas manquer." + } + } + } + }, + "Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Armée sur le bracelet, donc elle peut vibrer à l'heure de réveil même si le téléphone dort ou que NOOP est fermé. Envoie exactement la commande d'alarme de l'appli officielle, vibration confirmée sur une vraie WHOOP 4.0 (capture communautaire + test sur appareil, #535). Garde une alarme de secours pour ce que tu ne peux pas manquer." + } + } + } + }, + "Arms the strap to buzz at your wake time, even if NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arme le bracelet pour vibrer à l'heure de réveil, même si NOOP est fermé. Envoie exactement la commande d'alarme de l'appli officielle, vibration confirmée sur une vraie WHOOP 4.0 (capture communautaire + test sur appareil, #535). Garde une alarme de secours pour ce que tu ne peux pas manquer." + } + } + } + }, + "BEATS PER MINUTE": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "BATTEMENTS PAR MINUTE" + } + } + } + }, + "Band": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bracelet" + } + } + } + }, + "Beta feature": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fonction bêta" + } + } + } + }, + "Change to Number": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passer en nombre" + } + } + } + }, + "Change to Yes/No": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passer en Oui/Non" + } + } + } + }, + "Choose a markers CSV file to import": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un fichier CSV de marqueurs à importer" + } + } + } + }, + "Cues sent": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaux envoyés" + } + } + } + }, + "End session": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer la séance" + } + } + } + }, + "Full day": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journée entière" + } + } + } + }, + "Heads up: this test mode is off, so the report has no capture for it. For a useful report, turn the mode on, reproduce the problem while wearing the strap, then report again.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "À noter : ce mode de test est désactivé, le rapport ne contient donc aucune capture pour lui. Pour un rapport utile, active le mode, reproduis le problème en portant le bracelet, puis renvoie le rapport." + } + } + } + }, + "Help, contact, and attribution.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aide, contact et crédits." + } + } + } + }, + "History stays under the original question so WHOOP imports still line up.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'historique reste sous la question d'origine pour que les imports WHOOP restent alignés." + } + } + } + }, + "How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin temperature are measured from the strap (skin temp is shown as the deviation from your own baseline). Workouts is the count of activities you logged or that were detected. Recovery, Strain and Stress are NOOP's own on-device scores, not clinical measures: Recovery is a daily readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load derived from heart rate, and Stress is a 0-3 autonomic-load index from resting HR and HRV.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comment lire ceci : la VFC, la FC au repos, la durée de sommeil, la fréquence respiratoire et la température cutanée sont mesurées par le bracelet (la temp. cutanée est montrée comme l'écart à ta référence). Entraînements est le nombre d'activités enregistrées ou détectées. Récupération, Effort et Stress sont les scores calculés par NOOP sur l'appareil, pas des mesures cliniques : la Récupération est un indice de forme du jour (VFC, FC au repos, sommeil et tendance de temp. cutanée), l'Effort est la charge cardiovasculaire dérivée de la fréquence cardiaque, et le Stress est un indice de charge autonome de 0 à 3 issu de la FC au repos et de la VFC." + } + } + } + }, + "LIVE SESSION": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "SÉANCE EN DIRECT" + } + } + } + }, + "Liquid Today (prototype)": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Liquid Today (prototype)" + } + } + } + }, + "Live Session": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séance en direct" + } + } + } + }, + "Live Sessions (beta)": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séances en direct (bêta)" + } + } + } + }, + "Long press to show or hide your heart rate.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appui long pour afficher ou masquer ta fréquence cardiaque." + } + } + } + }, + "Longer ranges unlock as more history builds.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les plages plus longues se débloquent à mesure que l'historique s'étoffe." + } + } + } + }, + "Move anyway": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacer quand même" + } + } + } + }, + "NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "NOOP te donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de ton bracelet avec des méthodes de science du sport publiées et revues par les pairs, et calculés entièrement sur ton appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les trois mêmes questions avec de la science ouverte, donc ils suivront généralement la même direction que ceux de WHOOP, sans coïncider chiffre pour chiffre. Et c'est justement l'idée." + } + } + } + }, + "New item group": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau groupe d'éléments" + } + } + } + }, + "No sleep tracked last night": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun sommeil enregistré la nuit dernière" + } + } + } + }, + "No workouts yet": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore d'entraînements" + } + } + } + }, + "Profile and settings": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Profil et réglages" + } + } + } + }, + "Progress toward today's goal": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Progression vers l'objectif du jour" + } + } + } + }, + "RECOVERY VITALS": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "CONSTANTES DE RÉCUPÉRATION" + } + } + } + }, + "Refresh battery": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser la batterie" + } + } + } + }, + "Rename item": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommer l'élément" + } + } + } + }, + "Rename…": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommer…" + } + } + } + }, + "Replaces the Today tab with the prototype redesign. Turn it off any time to return to the classic dashboard. Reads the same live data from your strap.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplace l'onglet Aujourd'hui par la refonte prototype. Désactive-la quand tu veux pour revenir au tableau de bord classique. Elle lit les mêmes données en direct de ton bracelet." + } + } + } + }, + "SILENT GUARDIAN": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "GARDIEN SILENCIEUX" + } + } + } + }, + "Session summary": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résumé de la séance" + } + } + } + }, + "Silence-first strap coaching during workouts.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coaching du bracelet, silencieux d'abord, pendant les entraînements." + } + } + } + }, + "Start a live session. Beta. Silent strap coaching against today's Charge.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancer une séance en direct. Bêta. Coaching silencieux du bracelet en fonction de la Charge du jour." + } + } + } + }, + "Support NOOP: help and contact": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Soutenir NOOP : aide et contact" + } + } + } + }, + "Support NOOP: help and contact.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Soutenir NOOP : aide et contact." + } + } + } + }, + "The line is each waking hour's 0-3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "La courbe est l'indice 0-3 de chaque heure d'éveil, évalué par rapport à tes heures calmes du jour. La barre en dessous répartit ta journée en temps de stress calme, modéré et élevé." + } + } + } + }, + "This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cela déplace la nuit vers une période sans données enregistrées. Les stades ne peuvent pas y être calculés, la nuit peut donc apparaître vide tant que des données ne la couvrent pas." + } + } + } + }, + "Undo": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + } + } + }, + "Undo sleep deletion": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler la suppression du sommeil" + } + } + } + }, + "View sources": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir les sources" + } + } + } + }, + "We compare today's resting heart rate and HRV to your own 30-day baseline. A higher-than-usual resting HR and a lower-than-usual HRV both push the score up, classic signs the body is activated. The combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at your baseline, 3 is highly activated.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous comparons ta fréquence cardiaque au repos et ta VFC du jour à ta référence sur 30 jours. Une FC au repos plus haute que d'habitude et une VFC plus basse que d'habitude poussent toutes deux le score vers le haut — signes classiques d'un corps activé. L'écart combiné est ramené sur une échelle de 0 à 3 : 0 = calme, 1,5 = ta référence, 3 = très activé." + } + } + } + }, + "Wear the strap overnight to score a night first.": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "Porte le bracelet la nuit pour qu'une nuit soit d'abord évaluée." + } + } + } + }, + "YOUR CARDS": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "TES CARTES" + } + } + } + }, + "minus": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "moins" + } + } + } + }, + "plus": { + "extractionState": "manual", + "localizations": { + "fr": { + "stringUnit": { + "state": "translated", + "value": "plus" + } + } + } } }, "version": "1.0" From 3f54bf2647e9fa1ea0810119354336aeb530243a Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:11:16 +0200 Subject: [PATCH 18/28] Docs: point support at this fork, drop upstream community channels SUPPORT.md and the issue-template config linked the upstream project's Discord / subreddit / wiki / contact email, none of which this family fork runs. Now they point at this repo's issues + README. Co-Authored-By: Claude Sonnet 5 --- .github/ISSUE_TEMPLATE/config.yml | 23 ++++------------------- SUPPORT.md | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 317c1ba98..88b618b21 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,20 +1,5 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - - name: 💬 NOOP Discord — questions, setup help & chat - url: https://discord.com/invite/nHK9FHczu - about: "Got a question, stuck on setup, or not sure something's a bug? Discord is the fastest place for a quick answer. (Confirmed bugs still go to a GitHub issue, with a strap log.)" - - name: ❓ FAQ — start here (most questions are answered) - url: https://github.com/ParthJadhav/noop/wiki/FAQ - about: "Live HR, recovery/sleep, steps, importing data, privacy, updates — the common questions are answered here. Please check before filing." - - name: ⌚ WHOOP 5.0 / MG — what works & what doesn't - url: https://github.com/ParthJadhav/noop/wiki/WHOOP-5.0-and-MG-Status - about: "SpO₂, steps, recovery/sleep on the 5.0 and MG — what's supported today, what's still being decoded, and why. Read this before reporting a 5/MG issue." - - name: 🔧 Troubleshooting — pairing, sync & no-data - url: https://github.com/ParthJadhav/noop/wiki/Troubleshooting - about: '"History syncs but no data", pairing/bond failures, generic heart-rate straps, "no recovery/sleep" — most connection issues are solved here first.' - - name: 💡 Feature ideas & questions — Discussions - url: https://github.com/ParthJadhav/noop/discussions - about: "Got an idea, or not sure if something's a bug? Open a Discussion. Issues are for confirmed bugs with a strap log." - - name: 👽 Community, tips & release news — r/NOOPApp - url: https://www.reddit.com/r/NOOPApp/ - about: "Setup help, tips and release news on the subreddit. (Please still file confirmed bugs as a GitHub issue, with a strap log.)" + - name: 📖 README — install, pairing, privacy, importing history + url: https://github.com/MatB57/noop#readme + about: "Install on iPhone / Android / Mac, pair a WHOOP 5.0 / MG, import your WHOOP or Apple Health history, and how the on-device scoring works." diff --git a/SUPPORT.md b/SUPPORT.md index 7378b9271..dd6efd5c9 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,9 +1,16 @@ # Getting help with NOOP -- **🐞 Bugs → open a [GitHub issue](https://github.com/ParthJadhav/noop/issues/new/choose).** There's a template, and bugs are tracked, deduped and linked to fixes here. Include your **NOOP version**, **platform** and **strap model**, plus a **strap log** — NOOP runs entirely on your device, so without one we usually can't tell a real fault from a setup detail. (Android: **Settings → Strap → "Share strap log (for bug reports)"**; macOS: the **Live** screen → **Strap log** card → **Copy**/**Save…**.) -- **💬 Questions, ideas & how-you-can-help → [GitHub Discussions](https://github.com/ParthJadhav/noop/discussions).** Ask, share results, and see [exactly what data would move NOOP forward](https://github.com/ParthJadhav/noop/discussions/147). -- **📣 Setup help, tips & release news → [r/NOOPApp](https://www.reddit.com/r/NOOPApp/).** Every release is announced there too. -- **📖 Docs → the [README](README.md)** covers install (macOS Gatekeeper / Android Play Protect), pairing a WHOOP 5.0/MG, privacy, and importing your history. -- **📧 Other → thenoopapp@gmail.com** +This is a personal continuity fork of NOOP, maintained for a small family. There is no Discord, +subreddit, or public support channel. -NOOP is an independent, unofficial project — **not affiliated with WHOOP, Inc.** — and **not a medical device**. +- **🐞 Bugs → open a [GitHub issue](https://github.com/MatB57/noop/issues/new/choose).** Include your + **NOOP version**, **platform** (iPhone / Android / Mac) and **strap model** (WHOOP 4.0 / 5.0 / MG), + plus a **strap log** — NOOP runs entirely on your device, so without one a real fault is hard to + tell from a setup detail. (Android: **Settings → Strap → "Share strap log (for bug reports)"**; + macOS: the **Live** screen → **Strap log** card → **Copy** / **Save…**.) +- **📖 Docs → the [README](README.md)** covers install (macOS Gatekeeper / Android Play Protect / + iOS sideloading), pairing a WHOOP 5.0 / MG, privacy, and importing your history. + +NOOP is an independent, unofficial project — **not affiliated with WHOOP, Inc.** — and **not a +medical device**. It is built on prior community reverse-engineering work; see +[`ATTRIBUTION.md`](ATTRIBUTION.md). From 39a38f908b9003f078e93e7359e708bccb322b7e Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:41:57 +0200 Subject: [PATCH 19/28] Android: wrap UI string literals in tr() for French MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical pass over ~50 Compose screens: every user-facing English string literal (Text, contentDescription, titles/subtitles, button labels, helper copy, concatenation fragments) is now tr("…"), which returns the string unchanged unless the device language is French. Behaviour-neutral on an English device. French renderings for ~1000 of these already come from the shared iOS dictionary; the rest are being translated next. Co-Authored-By: Claude Sonnet 5 --- .../main/java/com/noop/ui/AddDeviceWizard.kt | 350 +++++++-------- .../app/src/main/java/com/noop/ui/AppRoot.kt | 12 +- .../java/com/noop/ui/AppleHealthScreen.kt | 74 ++-- .../java/com/noop/ui/AutomationsScreen.kt | 72 +-- .../main/java/com/noop/ui/BackupSyncScreen.kt | 50 +-- .../main/java/com/noop/ui/BreatheScreen.kt | 154 +++---- .../src/main/java/com/noop/ui/CaffeineLog.kt | 34 +- .../src/main/java/com/noop/ui/CoachScreen.kt | 94 ++-- .../main/java/com/noop/ui/CompareScreen.kt | 40 +- .../main/java/com/noop/ui/ConnectionHelp.kt | 42 +- .../main/java/com/noop/ui/CoupledScreen.kt | 6 +- .../java/com/noop/ui/DataSourcesScreen.kt | 234 +++++----- .../main/java/com/noop/ui/DevicesScreen.kt | 150 +++---- .../java/com/noop/ui/FullDayChartScreen.kt | 18 +- .../java/com/noop/ui/FusedRecordScreen.kt | 18 +- .../src/main/java/com/noop/ui/HealthScreen.kt | 236 +++++----- .../java/com/noop/ui/HowNoopWorksScreen.kt | 58 +-- .../java/com/noop/ui/HrvSnapshotScreen.kt | 60 +-- .../main/java/com/noop/ui/HydrationScreen.kt | 34 +- .../java/com/noop/ui/InsightsHubScreen.kt | 56 +-- .../main/java/com/noop/ui/InsightsScreen.kt | 108 ++--- .../java/com/noop/ui/IntelligenceScreen.kt | 28 +- .../main/java/com/noop/ui/IntervalsScreen.kt | 50 +-- .../src/main/java/com/noop/ui/JournalLog.kt | 26 +- .../main/java/com/noop/ui/LabBookScreen.kt | 76 ++-- .../src/main/java/com/noop/ui/LiveScreen.kt | 164 +++---- .../java/com/noop/ui/LiveSessionScreen.kt | 22 +- .../java/com/noop/ui/LiveWorkoutScreen.kt | 40 +- .../java/com/noop/ui/MarkerEditorScreen.kt | 24 +- .../src/main/java/com/noop/ui/MindSection.kt | 14 +- .../noop/ui/NotificationsSettingsScreen.kt | 112 ++--- .../main/java/com/noop/ui/OnboardingScreen.kt | 184 ++++---- .../src/main/java/com/noop/ui/RhythmScreen.kt | 104 ++--- .../java/com/noop/ui/ScoringGuideScreen.kt | 54 +-- .../main/java/com/noop/ui/SettingsScreen.kt | 414 +++++++++--------- .../java/com/noop/ui/SkinTempCardsScreen.kt | 66 +-- .../src/main/java/com/noop/ui/SleepScreen.kt | 286 ++++++------ .../main/java/com/noop/ui/SmartAlarmScreen.kt | 66 +-- .../com/noop/ui/StepsCalibrationScreen.kt | 98 ++--- .../src/main/java/com/noop/ui/StressScreen.kt | 114 ++--- .../main/java/com/noop/ui/SupportScreen.kt | 20 +- .../src/main/java/com/noop/ui/TodayScreen.kt | 164 +++---- .../java/com/noop/ui/TrendsExploreScreen.kt | 50 +-- .../src/main/java/com/noop/ui/TrendsScreen.kt | 52 +-- .../java/com/noop/ui/UpdatesInboxScreen.kt | 24 +- .../main/java/com/noop/ui/WeeklyDigestCard.kt | 12 +- .../main/java/com/noop/ui/WhatsNewSheet.kt | 10 +- .../com/noop/ui/WhoopModelComparisonScreen.kt | 26 +- .../main/java/com/noop/ui/WorkoutEditing.kt | 4 +- .../src/main/java/com/noop/ui/WorkoutStart.kt | 14 +- .../main/java/com/noop/ui/WorkoutsScreen.kt | 160 +++---- android/app/src/main/res/raw/fr_strings.json | 51 +++ 52 files changed, 2240 insertions(+), 2189 deletions(-) diff --git a/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt b/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt index ed7350371..b3d622fda 100644 --- a/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt +++ b/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt @@ -103,14 +103,14 @@ private enum class DeviceType { val title: String get() = when (this) { - Whoop5MG -> "WHOOP 5.0 / MG" - Whoop4 -> "WHOOP 4.0" - HrStrap -> "Heart-rate strap" - GymEquipment -> "Gym equipment" - Amazfit -> "Amazfit / Zepp" - MiBand -> "Xiaomi Mi Band" - Garmin -> "Garmin watch" - Oura -> "Oura ring" + Whoop5MG -> tr("WHOOP 5.0 / MG") + Whoop4 -> tr("WHOOP 4.0") + HrStrap -> tr("Heart-rate strap") + GymEquipment -> tr("Gym equipment") + Amazfit -> tr("Amazfit / Zepp") + MiBand -> tr("Xiaomi Mi Band") + Garmin -> tr("Garmin watch") + Oura -> tr("Oura ring") } } @@ -235,21 +235,21 @@ fun AddDeviceWizard( } val confirmAdvertisedName = run { - pickedWhoop?.let { return@run it.name?.takeIf { n -> n.isNotBlank() } ?: (type?.title ?: "Device") } + pickedWhoop?.let { return@run it.name?.takeIf { n -> n.isNotBlank() } ?: (type?.title ?: tr("Device")) } pickedStrap?.let { return@run it.name } pickedMachine?.let { return@run it.name } pickedHuami?.let { return@run it.name } - type?.title ?: "Device" + type?.title ?: tr("Device") } val confirmName = nameDraft.trim().ifEmpty { confirmAdvertisedName } val confirmBrand = when { - type?.isWhoop == true -> "WHOOP" - type == DeviceType.GymEquipment -> "Gym equipment" - type == DeviceType.Amazfit -> "Amazfit" - type == DeviceType.MiBand -> "Mi Band" - type == DeviceType.Garmin -> "Garmin" + type?.isWhoop == true -> tr("WHOOP") + type == DeviceType.GymEquipment -> tr("Gym equipment") + type == DeviceType.Amazfit -> tr("Amazfit") + type == DeviceType.MiBand -> tr("Mi Band") + type == DeviceType.Garmin -> tr("Garmin") pickedStrap != null -> brandGuess(pickedStrap!!.name) - else -> "Heart-rate strap" + else -> tr("Heart-rate strap") } val confirmRssi = pickedWhoop?.rssi ?: pickedStrap?.rssi ?: pickedMachine?.rssi ?: pickedHuami?.rssi ?: -70 @@ -413,7 +413,7 @@ fun AddDeviceWizard( IconButton(onClick = { goBack() }, modifier = Modifier.size(28.dp)) { Icon( Icons.AutoMirrored.Filled.KeyboardArrowLeft, - contentDescription = "Back", + contentDescription = tr("Back"), tint = Palette.textSecondary, modifier = Modifier.size(22.dp), ) @@ -427,7 +427,7 @@ fun AddDeviceWizard( } } IconButton(onClick = { stopAllScans(); onClose() }, modifier = Modifier.size(28.dp)) { - Icon(Icons.Filled.Close, contentDescription = "Close", tint = Palette.textTertiary, modifier = Modifier.size(20.dp)) + Icon(Icons.Filled.Close, contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(20.dp)) } } }, @@ -564,23 +564,23 @@ fun AddDeviceWizard( AlertDialog( onDismissRequest = { askMakeActive = false; finishAdd(makeActive = false) }, containerColor = Palette.surfaceOverlay, - title = { Text("Make this your active device?", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Make this your active device?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( "Make $confirmName your active device now? It will provide your live data. You can change " + - "this any time.", + tr("this any time."), style = NoopType.subhead, color = Palette.textSecondary, ) }, confirmButton = { TextButton(onClick = { askMakeActive = false; finishAdd(makeActive = true) }) { - Text("Make active", style = NoopType.body, color = Palette.accent) + Text(tr("Make active"), style = NoopType.body, color = Palette.accent) } }, dismissButton = { TextButton(onClick = { askMakeActive = false; finishAdd(makeActive = false) }) { - Text("Not now", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Not now"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -592,11 +592,11 @@ fun AddDeviceWizard( AlertDialog( onDismissRequest = { ouraConfirmAdopt = false }, containerColor = Palette.surfaceOverlay, - title = { Text("Take over this ring?", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Take over this ring?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( - "NOOP will install its own key on the ring and become its owner. The Oura app will no " + - "longer control this ring. This is intended and it cannot be undone from NOOP.", + tr("NOOP will install its own key on the ring and become its owner. The Oura app will no ") + + tr("longer control this ring. This is intended and it cannot be undone from NOOP."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -610,12 +610,12 @@ fun AddDeviceWizard( // adoptPhase / needs-pairing drives it to success (close) or a REACHABLE honest Failed. finishAddOura(closeAfter = false) }) { - Text("Take over", style = NoopType.body, color = Palette.statusCritical) + Text(tr("Take over"), style = NoopType.body, color = Palette.statusCritical) } }, dismissButton = { TextButton(onClick = { ouraConfirmAdopt = false }) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -640,34 +640,34 @@ fun AddDeviceWizard( } private fun headerTitle(step: WizardStep, type: DeviceType?): String = when (step) { - WizardStep.Type -> "Add a device" - WizardStep.Prep -> type?.title ?: "Add a device" - WizardStep.Pick -> "Pick your device" - WizardStep.Confirm -> "Name & confirm" + WizardStep.Type -> tr("Add a device") + WizardStep.Prep -> type?.title ?: tr("Add a device") + WizardStep.Pick -> tr("Pick your device") + WizardStep.Confirm -> tr("Name & confirm") } private fun headerSubtitle(step: WizardStep): String? = when (step) { - WizardStep.Type -> "What are you adding?" - WizardStep.Prep -> "Get it ready, then scan." - WizardStep.Pick -> "Tap the one that's yours." + WizardStep.Type -> tr("What are you adding?") + WizardStep.Prep -> tr("Get it ready, then scan.") + WizardStep.Pick -> tr("Tap the one that's yours.") WizardStep.Confirm -> null } // MARK: - Oura header titles (the adopt sub-flow's own steps) private fun ouraHeaderTitle(step: OuraStep, advanced: Boolean): String = when (step) { - OuraStep.Gate -> if (advanced) "Advanced: use your own key" else "Oura ring" - OuraStep.Prep -> "Get your ring ready" - OuraStep.Pick -> "Pick the ring" - OuraStep.Confirm -> "Your ring" - OuraStep.Adopting -> "Taking over your ring" - OuraStep.Failed -> "Could not take over" + OuraStep.Gate -> if (advanced) tr("Advanced: use your own key") else tr("Oura ring") + OuraStep.Prep -> tr("Get your ring ready") + OuraStep.Pick -> tr("Pick the ring") + OuraStep.Confirm -> tr("Your ring") + OuraStep.Adopting -> tr("Taking over your ring") + OuraStep.Failed -> tr("Could not take over") } private fun ouraHeaderSubtitle(step: OuraStep, advanced: Boolean): String? = when (step) { - OuraStep.Gate -> if (advanced) "Power users only." else "Take it over locally. Beta." - OuraStep.Prep -> "Reset it in the Oura app first." - OuraStep.Pick -> "Tap the one that's yours." + OuraStep.Gate -> if (advanced) tr("Power users only.") else tr("Take it over locally. Beta.") + OuraStep.Prep -> tr("Reset it in the Oura app first.") + OuraStep.Pick -> tr("Tap the one that's yours.") OuraStep.Confirm -> null OuraStep.Adopting -> null OuraStep.Failed -> null @@ -678,33 +678,33 @@ private fun ouraHeaderSubtitle(step: OuraStep, advanced: Boolean): String? = whe @Composable private fun TypeStep(onPick: (DeviceType) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - TypeRow(Icons.Filled.Watch, DeviceType.Whoop5MG.title, "Newer WHOOP band. Experimental in NOOP") { + TypeRow(Icons.Filled.Watch, DeviceType.Whoop5MG.title, tr("Newer WHOOP band. Experimental in NOOP")) { onPick(DeviceType.Whoop5MG) } - TypeRow(Icons.Filled.Watch, DeviceType.Whoop4.title, "NOOP's primary, fully-supported band") { + TypeRow(Icons.Filled.Watch, DeviceType.Whoop4.title, tr("NOOP's primary, fully-supported band")) { onPick(DeviceType.Whoop4) } - TypeRow(Icons.Filled.FavoriteBorder, DeviceType.HrStrap.title, "Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio broadcast") { + TypeRow(Icons.Filled.FavoriteBorder, DeviceType.HrStrap.title, tr("Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio broadcast")) { onPick(DeviceType.HrStrap) } - TypeRow(Icons.AutoMirrored.Filled.DirectionsRun, DeviceType.GymEquipment.title, "Treadmill, indoor bike, rower or cross-trainer (Bluetooth FTMS)") { + TypeRow(Icons.AutoMirrored.Filled.DirectionsRun, DeviceType.GymEquipment.title, tr("Treadmill, indoor bike, rower or cross-trainer (Bluetooth FTMS)")) { onPick(DeviceType.GymEquipment) } // EXPERIMENTAL tier - clearly labelled, opt-in, best-effort. Each is honest about what it can // actually read; none fabricates data. - Overline("Experimental", modifier = Modifier.padding(top = 8.dp)) + Overline(tr("Experimental"), modifier = Modifier.padding(top = 8.dp)) ExperimentalTierNote() - TypeRow(Icons.Filled.Circle, DeviceType.Oura.title, "Take over your ring locally. Beta. This replaces the Oura app.") { + TypeRow(Icons.Filled.Circle, DeviceType.Oura.title, tr("Take over your ring locally. Beta. This replaces the Oura app.")) { onPick(DeviceType.Oura) } - TypeRow(Icons.Filled.GraphicEq, DeviceType.Amazfit.title, "Incl. Helio. Live heart rate where the band exposes it. Help us test.") { + TypeRow(Icons.Filled.GraphicEq, DeviceType.Amazfit.title, tr("Incl. Helio. Live heart rate where the band exposes it. Help us test.")) { onPick(DeviceType.Amazfit) } - TypeRow(Icons.Filled.GraphicEq, DeviceType.MiBand.title, "Live heart rate on bands that don't need pairing. Help us test.") { + TypeRow(Icons.Filled.GraphicEq, DeviceType.MiBand.title, tr("Live heart rate on bands that don't need pairing. Help us test.")) { onPick(DeviceType.MiBand) } - TypeRow(Icons.Filled.Watch, DeviceType.Garmin.title, "Uses the watch's Broadcast Heart Rate. We'll show you how.") { + TypeRow(Icons.Filled.Watch, DeviceType.Garmin.title, tr("Uses the watch's Broadcast Heart Rate. We'll show you how.")) { onPick(DeviceType.Garmin) } @@ -727,8 +727,8 @@ private fun ExperimentalTierNote() { ) { Icon(Icons.Filled.Science, contentDescription = null, tint = Palette.statusWarning, modifier = Modifier.size(18.dp)) Text( - "Experimental, best-effort support. We're still testing these, so they might not connect on " + - "every device. They never make up data, and they'll tell you honestly when live isn't possible.", + tr("Experimental, best-effort support. We're still testing these, so they might not connect on ") + + tr("every device. They never make up data, and they'll tell you honestly when live isn't possible."), style = NoopType.footnote, color = Palette.statusWarning, ) @@ -771,8 +771,8 @@ private fun WhoopFirstNote() { ) { Icon(Icons.Filled.FavoriteBorder, contentDescription = null, tint = Palette.textTertiary, modifier = Modifier.size(16.dp)) Text( - "WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps stream live heart rate " + - "and HRV, but not WHOOP's deeper sleep and recovery data.", + tr("WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps stream live heart rate ") + + tr("and HRV, but not WHOOP's deeper sleep and recovery data."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -804,14 +804,14 @@ private fun OnePhoneWarningCard() { ) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( - "One phone at a time", + tr("One phone at a time"), style = NoopType.headline, color = Palette.statusWarning, ) Text( - "A WHOOP strap bonds to a single device. While it's connected to NOOP it won't stream " + - "to the official WHOOP app, and the other way round. It's reversible: pair it in the " + - "other app whenever you want it back.", + tr("A WHOOP strap bonds to a single device. While it's connected to NOOP it won't stream ") + + tr("to the official WHOOP app, and the other way round. It's reversible: pair it in the ") + + tr("other app whenever you want it back."), style = NoopType.footnote, color = Palette.statusWarning, ) @@ -852,7 +852,7 @@ private fun PrepStep(type: DeviceType, onScan: () -> Unit) { ) { Icon(Icons.Filled.Science, contentDescription = null, tint = Palette.statusWarning, modifier = Modifier.size(18.dp)) Text( - "WHOOP 5.0 / MG support is newer and still experimental in NOOP.", + tr("WHOOP 5.0 / MG support is newer and still experimental in NOOP."), style = NoopType.footnote, color = Palette.statusWarning, ) @@ -892,7 +892,7 @@ private fun PrepStep(type: DeviceType, onScan: () -> Unit) { .background(Palette.accent) .semantics { contentDescription = "Scan for ${type.title}" }, ) { - Text("Scan", style = NoopType.headline, color = Palette.goldDeepText) + Text(tr("Scan"), style = NoopType.headline, color = Palette.goldDeepText) } } } @@ -900,34 +900,34 @@ private fun PrepStep(type: DeviceType, onScan: () -> Unit) { /** Type-specific "get it ready" guidance - the point of the branching wizard. US English copy. */ private fun prepInstructions(type: DeviceType): List = when (type) { DeviceType.Whoop4 -> listOf( - "Put your WHOOP 4.0 on your wrist and make sure it's awake.", - "Make sure it's NOT connected to the official WHOOP app right now.", - "NOOP will look for it nearby.", + tr("Put your WHOOP 4.0 on your wrist and make sure it's awake."), + tr("Make sure it's NOT connected to the official WHOOP app right now."), + tr("NOOP will look for it nearby."), ) DeviceType.Whoop5MG -> listOf( - "WHOOP 5.0 / MG bonds to one device at a time, so unpair it from the official WHOOP app first.", - "Put the band into pairing mode, on your wrist and awake.", - "NOOP will look for it nearby.", + tr("WHOOP 5.0 / MG bonds to one device at a time, so unpair it from the official WHOOP app first."), + tr("Put the band into pairing mode, on your wrist and awake."), + tr("NOOP will look for it nearby."), ) DeviceType.HrStrap -> listOf( - "Wake your strap. Put it on, or dampen the contacts.", - "Make sure it isn't connected to another app (a bike computer, the brand's own app…).", - "NOOP will look for it nearby.", + tr("Wake your strap. Put it on, or dampen the contacts."), + tr("Make sure it isn't connected to another app (a bike computer, the brand's own app…)."), + tr("NOOP will look for it nearby."), ) DeviceType.GymEquipment -> listOf( - "Wake the machine. Start pedalling, walking or rowing so it powers on its Bluetooth.", - "Make sure it isn't already connected to another app (Zwift, the gym's app, a bike computer…).", - "NOOP looks for machines that broadcast the standard Bluetooth Fitness Machine service.", + tr("Wake the machine. Start pedalling, walking or rowing so it powers on its Bluetooth."), + tr("Make sure it isn't already connected to another app (Zwift, the gym's app, a bike computer…)."), + tr("NOOP looks for machines that broadcast the standard Bluetooth Fitness Machine service."), ) DeviceType.Amazfit -> listOf( - "Wake your Amazfit / Zepp band and make sure it isn't connected to the Zepp app right now.", - "NOOP reads live heart rate when the band exposes it. Some bands need a pairing we can't do yet. If so, we'll say so honestly.", - "Experimental: this is best-effort. If live doesn't work, you can export from Zepp and import the file.", + tr("Wake your Amazfit / Zepp band and make sure it isn't connected to the Zepp app right now."), + tr("NOOP reads live heart rate when the band exposes it. Some bands need a pairing we can't do yet. If so, we'll say so honestly."), + tr("Experimental: this is best-effort. If live doesn't work, you can export from Zepp and import the file."), ) DeviceType.MiBand -> listOf( - "Wake your Mi Band and make sure it isn't connected to the Mi Fitness / Zepp Life app right now.", - "NOOP reads live heart rate on bands that don't require pairing. Newer bands need an auth handshake we can't do yet.", - "Experimental: if your band needs pairing, we'll tell you honestly rather than show a fake reading.", + tr("Wake your Mi Band and make sure it isn't connected to the Mi Fitness / Zepp Life app right now."), + tr("NOOP reads live heart rate on bands that don't require pairing. Newer bands need an auth handshake we can't do yet."), + tr("Experimental: if your band needs pairing, we'll tell you honestly rather than show a fake reading."), ) DeviceType.Garmin -> com.noop.ble.GarminBroadcast.broadcastHint // Oura runs the factory-reset-and-adopt prep inside OuraFlow (ouraPrepInstructions), so this generic @@ -938,11 +938,11 @@ private fun prepInstructions(type: DeviceType): List = when (type) { /** The factory-reset prep checklist for the Oura adopt flow (Step B of the onboarding UX spec). No * em-dashes; matches the iOS copy. */ private val ouraPrepInstructions: List = listOf( - "Open the official Oura app and remove this ring (Oura calls it \"factory reset\" or \"unpair and " + - "reset\"). This wipes the ring's owner so NOOP can take it over.", - "Keep the ring on the charger or on your finger so it stays awake.", - "Make sure the Oura app is fully closed. A ring answers one owner at a time.", - "When the ring is reset and waking, tap Scan below.", + tr("Open the official Oura app and remove this ring (Oura calls it \"factory reset\" or \"unpair and ") + + tr("reset\"). This wipes the ring's owner so NOOP can take it over."), + tr("Keep the ring on the charger or on your finger so it stays awake."), + tr("Make sure the Oura app is fully closed. A ring answers one owner at a time."), + tr("When the ring is reset and waking, tap Scan below."), ) // MARK: - Step 3 - pick from the live scan @@ -957,8 +957,8 @@ private fun WhoopPickStep( PickList(searching = true, isEmpty = found.isEmpty(), onRescan = onRescan) { found.sortedByDescending { it.rssi }.forEach { strap -> DiscoveredRow( - name = strap.name?.takeIf { it.isNotBlank() } ?: "WHOOP", - subtitle = "WHOOP", + name = strap.name?.takeIf { it.isNotBlank() } ?: tr("WHOOP"), + subtitle = tr("WHOOP"), rssi = strap.rssi, onTap = { onSelect(strap) }, ) @@ -998,7 +998,7 @@ private fun FtmsPickStep( discovered.sortedByDescending { it.rssi }.forEach { machine -> DiscoveredRow( name = machine.name, - subtitle = "Gym equipment", + subtitle = tr("Gym equipment"), rssi = machine.rssi, onTap = { onSelect(machine) }, ) @@ -1018,7 +1018,7 @@ private fun HuamiPickStep( discovered.sortedByDescending { it.rssi }.forEach { dev -> DiscoveredRow( name = dev.name, - subtitle = "Experimental", + subtitle = tr("Experimental"), rssi = dev.rssi, onTap = { onSelect(dev) }, ) @@ -1080,10 +1080,10 @@ private fun OuraGateStep( Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { // Beta banner (amber heads-up pattern). OuraAmberPanel( - "Beta. Read this first.", - "Local Oura support is new and we cannot test every ring here. It may not connect on your " + - "ring, and it can change between updates. NOOP never makes up a number. If something does " + - "not work, it will tell you plainly.", + tr("Beta. Read this first."), + tr("Local Oura support is new and we cannot test every ring here. It may not connect on your ") + + tr("ring, and it can change between updates. NOOP never makes up a number. If something does ") + + tr("not work, it will tell you plainly."), ) // What you get / what you lose, two stacked sections on a frosted card. @@ -1095,25 +1095,25 @@ private fun OuraGateStep( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(14.dp), ) { - Overline("What you get") + Overline(tr("What you get")) OuraBulletList( listOf( - "Your ring talks to NOOP only, fully offline, no Oura account.", - "Live heart rate, and HRV when the ring can measure it.", - "Overnight sleep staging, resting heart rate, skin-temperature trend, motion and " + - "battery, read straight off the ring.", - "NOOP's own Charge, Effort and Rest, computed on your device from published methods.", + tr("Your ring talks to NOOP only, fully offline, no Oura account."), + tr("Live heart rate, and HRV when the ring can measure it."), + tr("Overnight sleep staging, resting heart rate, skin-temperature trend, motion and ") + + tr("battery, read straight off the ring."), + tr("NOOP's own Charge, Effort and Rest, computed on your device from published methods."), ), ) - Overline("What you lose") + Overline(tr("What you lose")) OuraBulletList( listOf( - "The Oura app and your Oura account stop working with this ring. This is the point. " + - "You are replacing Oura.", - "Oura's own Readiness and Sleep scores. NOOP does not copy them. It computes its own.", - "Anything that needs Oura's cloud (web dashboard, Oura's coaching, shared circles).", - "Likely your Oura warranty and support, because the ring is no longer paired to Oura. " + - "Treat this as permanent.", + tr("The Oura app and your Oura account stop working with this ring. This is the point. ") + + tr("You are replacing Oura."), + tr("Oura's own Readiness and Sleep scores. NOOP does not copy them. It computes its own."), + tr("Anything that needs Oura's cloud (web dashboard, Oura's coaching, shared circles)."), + tr("Likely your Oura warranty and support, because the ring is no longer paired to Oura. ") + + tr("Treat this as permanent."), ), ) } @@ -1127,7 +1127,7 @@ private fun OuraGateStep( .clickable { onConsent(!consent) } .semantics { contentDescription = - "I understand this disconnects the ring from Oura and that NOOP cannot undo it for me." + tr("I understand this disconnects the ring from Oura and that NOOP cannot undo it for me.") } .padding(12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -1140,8 +1140,8 @@ private fun OuraGateStep( modifier = Modifier.size(20.dp), ) Text( - "I understand this disconnects the ring from Oura and that NOOP cannot undo it for me. To " + - "go back to Oura I would factory-reset the ring again and set it up in the Oura app.", + tr("I understand this disconnects the ring from Oura and that NOOP cannot undo it for me. To ") + + tr("go back to Oura I would factory-reset the ring again and set it up in the Oura app."), style = NoopType.footnote, color = Palette.statusCritical, ) @@ -1155,21 +1155,21 @@ private fun OuraGateStep( .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(if (consent) Palette.accent else Palette.surfaceInset) - .semantics { contentDescription = "Continue" }, + .semantics { contentDescription = tr("Continue") }, ) { Text( - "Continue", + tr("Continue"), style = NoopType.headline, color = if (consent) Palette.goldDeepText else Palette.textTertiary, ) } // Secondary: keep the Oura app (non-destructive file import) - always one tap away. TextButton(onClick = onUseFileImport, modifier = Modifier.fillMaxWidth()) { - Text("Keep the Oura app instead (import a file)", style = NoopType.subhead, color = Palette.accent) + Text(tr("Keep the Oura app instead (import a file)"), style = NoopType.subhead, color = Palette.accent) } // Tertiary: Advanced power-user key path. TextButton(onClick = onAdvanced, modifier = Modifier.fillMaxWidth()) { - Text("Advanced: I already have my ring's key", style = NoopType.footnote, color = Palette.accent) + Text(tr("Advanced: I already have my ring's key"), style = NoopType.footnote, color = Palette.accent) } } } @@ -1186,13 +1186,13 @@ private fun OuraAdvancedKeyStep( val showError = keyDraft.isNotBlank() && parsed == null Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { OuraAmberPanel( - "For power users.", - "If you extracted your ring's 16-byte key from a previous Oura setup, NOOP can talk to the " + - "ring with that key WITHOUT resetting it, so the Oura app keeps working too. NOOP does not " + - "extract keys for you and cannot help you find one. If you do not know what this means, go " + - "back and use the standard setup or file import.", + tr("For power users."), + tr("If you extracted your ring's 16-byte key from a previous Oura setup, NOOP can talk to the ") + + tr("ring with that key WITHOUT resetting it, so the Oura app keeps working too. NOOP does not ") + + tr("extract keys for you and cannot help you find one. If you do not know what this means, go ") + + tr("back and use the standard setup or file import."), ) - Overline("Ring key (32 hex characters)") + Overline(tr("Ring key (32 hex characters)")) OutlinedTextField( value = keyDraft, onValueChange = { onKeyDraft(it) }, @@ -1204,13 +1204,13 @@ private fun OuraAdvancedKeyStep( colors = wizardFieldColors(), modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "Ring key, 32 hex characters" }, + .semantics { contentDescription = tr("Ring key, 32 hex characters") }, ) if (showError) { - Text("That is not a 32-character hex key.", style = NoopType.footnote, color = Palette.statusCritical) + Text(tr("That is not a 32-character hex key."), style = NoopType.footnote, color = Palette.statusCritical) } Text( - "NOOP stores this key only on this device, in the same place it stores your paired bands.", + tr("NOOP stores this key only on this device, in the same place it stores your paired bands."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1221,10 +1221,10 @@ private fun OuraAdvancedKeyStep( .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(if (parsed != null) Palette.accent else Palette.surfaceInset) - .semantics { contentDescription = "Scan for your ring" }, + .semantics { contentDescription = tr("Scan for your ring") }, ) { Text( - "Scan for your ring", + tr("Scan for your ring"), style = NoopType.headline, color = if (parsed != null) Palette.goldDeepText else Palette.textTertiary, ) @@ -1256,9 +1256,9 @@ private fun OuraPrepStep(advanced: Boolean, onScan: () -> Unit) { // does not reset the ring, so it skips the "force-quit Oura" framing). if (!advanced) { OuraAmberPanel( - "A ring talks to one owner at a time.", - "If the Oura app is still running it will hold the ring and adoption will fail. Force-quit " + - "Oura, then scan.", + tr("A ring talks to one owner at a time."), + tr("If the Oura app is still running it will hold the ring and adoption will fail. Force-quit ") + + tr("Oura, then scan."), ) } TextButton( @@ -1267,9 +1267,9 @@ private fun OuraPrepStep(advanced: Boolean, onScan: () -> Unit) { .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Palette.accent) - .semantics { contentDescription = "Scan for your ring" }, + .semantics { contentDescription = tr("Scan for your ring") }, ) { - Text("Scan for your ring", style = NoopType.headline, color = Palette.goldDeepText) + Text(tr("Scan for your ring"), style = NoopType.headline, color = Palette.goldDeepText) } } } @@ -1287,13 +1287,13 @@ private fun OuraPickStep( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { StatePill( - if (scanning) "Searching…" else "Idle", + if (scanning) tr("Searching…") else tr("Idle"), tone = if (scanning) StrandTone.Accent else StrandTone.Neutral, pulsing = scanning, ) Spacer(Modifier.weight(1f)) TextButton(onClick = onRescan) { - Text("Rescan", style = NoopType.subhead, color = Palette.accent) + Text(tr("Rescan"), style = NoopType.subhead, color = Palette.accent) } } if (discovered.isEmpty()) { @@ -1306,10 +1306,10 @@ private fun OuraPickStep( verticalArrangement = Arrangement.spacedBy(10.dp), ) { CircularProgressIndicator(color = Palette.accent, modifier = Modifier.size(22.dp)) - Text("Searching…", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Searching…"), style = NoopType.body, color = Palette.textPrimary) Text( - "Not showing up? Make sure you reset the ring in the Oura app and force-quit it, then " + - "tap Rescan. A ring still owned by Oura will not list here.", + tr("Not showing up? Make sure you reset the ring in the Oura app and force-quit it, then ") + + tr("tap Rescan. A ring still owned by Oura will not list here."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1320,7 +1320,7 @@ private fun OuraPickStep( DiscoveredRow( name = ring.name, // Subtitle = the detected generation (best-effort); "Oura ring" when undetected. - subtitle = ring.detectedGen?.displayName ?: "Oura ring", + subtitle = ring.detectedGen?.displayName ?: tr("Oura ring"), rssi = ring.rssi, onTap = { onPick(ring) }, ) @@ -1353,7 +1353,7 @@ private fun OuraConfirmStep( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Icon(Icons.Filled.Circle, contentDescription = null, tint = Palette.accent, modifier = Modifier.size(24.dp)) Text(gen.displayName, style = NoopType.headline, color = Palette.textPrimary, modifier = Modifier.weight(1f)) - StatePill("Beta", tone = StrandTone.Warning, showsDot = false) + StatePill(tr("Beta"), tone = StrandTone.Warning, showsDot = false) } // Per-gen capability checklist: tick for supported, dash for not-available, * for an estimate. ouraCapabilityRows(gen).forEach { (mark, label) -> @@ -1363,22 +1363,22 @@ private fun OuraConfirmStep( } } Text( - "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps " + - "are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 " + - "percentage comes off the ring (import an Oura file for those).", + tr("Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps ") + + tr("are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 ") + + tr("percentage comes off the ring (import an Oura file for those)."), style = NoopType.footnote, color = Palette.textTertiary, ) } - Overline("Name") + Overline(tr("Name")) OutlinedTextField( value = name, onValueChange = onName, singleLine = true, - placeholder = { Text("Oura ring", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Oura ring"), style = NoopType.body, color = Palette.textTertiary) }, colors = wizardFieldColors(), - modifier = Modifier.fillMaxWidth().semantics { contentDescription = "Device name" }, + modifier = Modifier.fillMaxWidth().semantics { contentDescription = tr("Device name") }, ) // The adopt action. The destructive (key-install) path is red; the Advanced key path is not @@ -1390,13 +1390,13 @@ private fun OuraConfirmStep( .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Palette.accent) - .semantics { contentDescription = "Connect to this ring" }, + .semantics { contentDescription = tr("Connect to this ring") }, ) { - Text("Connect to this ring", style = NoopType.headline, color = Palette.goldDeepText) + Text(tr("Connect to this ring"), style = NoopType.headline, color = Palette.goldDeepText) } Text( - "Both NOOP and the Oura app can use a ring you own by key, but only one can hold the " + - "Bluetooth link at a time.", + tr("Both NOOP and the Oura app can use a ring you own by key, but only one can hold the ") + + tr("Bluetooth link at a time."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1407,9 +1407,9 @@ private fun OuraConfirmStep( .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Palette.statusCritical.copy(alpha = 0.16f)) - .semantics { contentDescription = "Take over this ring" }, + .semantics { contentDescription = tr("Take over this ring") }, ) { - Text("Take over this ring", style = NoopType.headline, color = Palette.statusCritical) + Text(tr("Take over this ring"), style = NoopType.headline, color = Palette.statusCritical) } } } @@ -1429,11 +1429,11 @@ private fun OuraAdoptingStep() { ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { CircularProgressIndicator(color = Palette.accent, modifier = Modifier.size(22.dp)) - Text("Taking over your ring", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Taking over your ring"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "Installing NOOP's key and confirming the ring answers only to NOOP. Keep the ring close and " + - "do not open the Oura app.", + tr("Installing NOOP's key and confirming the ring answers only to NOOP. Keep the ring close and ") + + tr("do not open the Oura app."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1452,20 +1452,20 @@ private fun OuraFailedStep(reason: String?, onTryAgain: () -> Unit, onUseFileImp .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Text("We could not take over this ring.", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("We could not take over this ring."), style = NoopType.headline, color = Palette.textPrimary) // Surface the live adopt-failure reason when the source reported one; otherwise the static help. // Mirrors the Swift wizard's `model.ouraNeedsPairing ?? `. Text( - reason ?: "The most common cause is the ring was not fully reset in the Oura app, or the Oura " + - "app is still running. Reset the ring again, force-quit Oura, then try once more. If it keeps " + - "failing, your ring may be a generation NOOP cannot adopt yet. You can still use file import.", + reason ?: tr("The most common cause is the ring was not fully reset in the Oura app, or the Oura ") + + tr("app is still running. Reset the ring again, force-quit Oura, then try once more. If it keeps ") + + tr("failing, your ring may be a generation NOOP cannot adopt yet. You can still use file import."), style = NoopType.subhead, color = Palette.textSecondary, ) // Honest recovery reassurance (Swift parity): a failed adopt never bricks the ring. Text( - "The ring is not bricked. To go back to where you started, factory-reset it again and set it " + - "up in the Oura app.", + tr("The ring is not bricked. To go back to where you started, factory-reset it again and set it ") + + tr("up in the Oura app."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1477,7 +1477,7 @@ private fun OuraFailedStep(reason: String?, onTryAgain: () -> Unit, onUseFileImp .clip(RoundedCornerShape(12.dp)) .background(Palette.accent), ) { - Text("Try again", style = NoopType.headline, color = Palette.goldDeepText) + Text(tr("Try again"), style = NoopType.headline, color = Palette.goldDeepText) } TextButton( onClick = onUseFileImport, @@ -1486,7 +1486,7 @@ private fun OuraFailedStep(reason: String?, onTryAgain: () -> Unit, onUseFileImp .clip(RoundedCornerShape(12.dp)) .background(Palette.surfaceInset), ) { - Text("Use file import", style = NoopType.headline, color = Palette.accent) + Text(tr("Use file import"), style = NoopType.headline, color = Palette.accent) } } } @@ -1538,15 +1538,15 @@ private fun ouraCapabilityRows(gen: OuraRingGen): List> { val live = if (gen == OuraRingGen.GEN5) "*" else "✓" // newer rings: live HR is best-effort val firm = if (gen == OuraRingGen.GEN5) "*" else "✓" // resting HR / sleep / battery return listOf( - live to "Live heart rate", - "*" to "HRV (rMSSD)", - firm to "Resting heart rate", - firm to "Sleep staging", - "*" to "Skin-temperature trend", - "*" to "Steps / motion", - firm to "Battery", + live to tr("Live heart rate"), + "*" to tr("HRV (rMSSD)"), + firm to tr("Resting heart rate"), + firm to tr("Sleep staging"), + "*" to tr("Skin-temperature trend"), + "*" to tr("Steps / motion"), + firm to tr("Battery"), "-" to "Blood oxygen (SpO2 %)", - "-" to "Oura Readiness / Sleep score", + "-" to tr("Oura Readiness / Sleep score"), ) } @@ -1575,13 +1575,13 @@ private fun PickList( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { StatePill( - if (searching) "Searching…" else "Idle", + if (searching) tr("Searching…") else tr("Idle"), tone = if (searching) StrandTone.Accent else StrandTone.Neutral, pulsing = searching, ) Spacer(Modifier.weight(1f)) TextButton(onClick = onRescan) { - Text("Rescan", style = NoopType.subhead, color = Palette.accent) + Text(tr("Rescan"), style = NoopType.subhead, color = Palette.accent) } } if (isEmpty) { @@ -1594,9 +1594,9 @@ private fun PickList( verticalArrangement = Arrangement.spacedBy(10.dp), ) { CircularProgressIndicator(color = Palette.accent, modifier = Modifier.size(22.dp)) - Text("Searching…", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Searching…"), style = NoopType.body, color = Palette.textPrimary) Text( - "Make sure it's awake and not connected elsewhere.", + tr("Make sure it's awake and not connected elsewhere."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1662,16 +1662,16 @@ private fun ConfirmStep( } } - Overline("Name") + Overline(tr("Name")) OutlinedTextField( value = name, onValueChange = onName, singleLine = true, - placeholder = { Text("Device name", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Device name"), style = NoopType.body, color = Palette.textTertiary) }, colors = wizardFieldColors(), modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "Device name" }, + .semantics { contentDescription = tr("Device name") }, ) TextButton( @@ -1683,7 +1683,7 @@ private fun ConfirmStep( .background(if (name.trim().isNotEmpty()) Palette.accent else Palette.surfaceInset), ) { Text( - "Add", + tr("Add"), style = NoopType.headline, color = if (name.trim().isNotEmpty()) Palette.goldDeepText else Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/AppRoot.kt b/android/app/src/main/java/com/noop/ui/AppRoot.kt index 3ab5dfbdd..e81edfced 100644 --- a/android/app/src/main/java/com/noop/ui/AppRoot.kt +++ b/android/app/src/main/java/com/noop/ui/AppRoot.kt @@ -451,7 +451,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) { .padding(bottom = 24.dp), ) { Overline( - "Quick actions", + tr("Quick actions"), modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 6.dp), color = Palette.textTertiary, ) @@ -465,7 +465,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) { showUpdatesInbox = true }, icon = { Icon(Icons.Filled.Notifications, contentDescription = null) }, - label = { Text("Updates", style = NoopType.body) }, + label = { Text(tr("Updates"), style = NoopType.body) }, badge = { val unread = updateStore.unreadCount if (unread > 0) { @@ -570,8 +570,8 @@ private fun MoreScreen(onNavigate: (String) -> Unit) { } } ScreenScaffold( - title = "More", - subtitle = "Everything else, one tap away", + title = tr("More"), + subtitle = tr("Everything else, one tap away"), ) { // Mirror the iOS More page: each group is a tappable UPPERCASE overline header (with a disclosure // chevron) over a single grouped white NoopCard whose rows are tight (accent icon + title + @@ -912,9 +912,9 @@ fun ComingSoon(text: String, modifier: Modifier = Modifier) { ) Spacer(Modifier.height(4.dp)) Text(text, style = NoopType.title2, color = Palette.textPrimary, textAlign = TextAlign.Center) - Overline("Coming soon", color = Palette.textSecondary) + Overline(tr("Coming soon"), color = Palette.textSecondary) Text( - "This section is on the way.", + tr("This section is on the way."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, diff --git a/android/app/src/main/java/com/noop/ui/AppleHealthScreen.kt b/android/app/src/main/java/com/noop/ui/AppleHealthScreen.kt index 562bb6445..2947d8276 100644 --- a/android/app/src/main/java/com/noop/ui/AppleHealthScreen.kt +++ b/android/app/src/main/java/com/noop/ui/AppleHealthScreen.kt @@ -162,7 +162,7 @@ fun AppleHealthScreen(vm: AppViewModel) { // so only on-screen sections compose + are accessibility-walked on scroll (this data view is the long, // chart-heavy one). The loading/empty branches stay single items. Order + spacing are unchanged // (LazyColumn reproduces the eager `spacedBy(20.dp)` between the six sections). - LazyScreenScaffold(title = "Apple Health", subtitle = subtitle) { + LazyScreenScaffold(title = tr("Apple Health"), subtitle = subtitle) { when { !loaded -> item { LoadingCard() } !data.hasAnyData -> item { EmptyState() } @@ -183,12 +183,12 @@ fun AppleHealthScreen(vm: AppViewModel) { /** Header subtitle reflects the windowed (visible) per-day span of the steps series. */ private fun spanSubtitle(loaded: Boolean, data: AppleData, range: AppleRange): String { if (!loaded) { - return "Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app." + return tr("Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app.") } // Use steps as the canonical per-day series for the span readout. val rows = resolve(data.raw("steps"), range).rows if (rows.isEmpty()) { - return "Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app." + return tr("Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app.") } val lo = rows.first().day val hi = rows.last().day @@ -235,7 +235,7 @@ private fun LoadingCard() { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { ConnectionDot(tone = StrandTone.Accent, pulsing = true) Text( - "Reading your Apple Health history…", + tr("Reading your Apple Health history…"), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -246,9 +246,9 @@ private fun LoadingCard() { @Composable private fun EmptyState() { DataPendingNote( - title = "Nothing imported yet", - body = "Nothing imported yet. On an iPhone: Health app, tap your photo, Export " + - "All Health Data, then import the .zip here in Data Sources.", + title = tr("Nothing imported yet"), + body = tr("Nothing imported yet. On an iPhone: Health app, tap your photo, Export ") + + tr("All Health Data, then import the .zip here in Data Sources."), ) } @@ -264,31 +264,31 @@ private fun TileGrid(data: AppleData, range: AppleRange) { // Two columns of equal-width fixed-height tiles, mirroring the macOS adaptive grid. Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { TileRow { - MetricTile(Modifier.weight(1f), data, range, "steps", "Steps", Palette.metricCyan) { intString(it) } - MetricTile(Modifier.weight(1f), data, range, "resting_hr", "Resting HR", Palette.metricRose, "bpm") { + MetricTile(Modifier.weight(1f), data, range, "steps", tr("Steps"), Palette.metricCyan) { intString(it) } + MetricTile(Modifier.weight(1f), data, range, "resting_hr", tr("Resting HR"), Palette.metricRose, "bpm") { "${it.roundToInt()}" } } TileRow { - MetricTile(Modifier.weight(1f), data, range, "hrv", "HRV", Palette.metricPurple, "ms") { "${it.roundToInt()}" } - MetricTile(Modifier.weight(1f), data, range, "vo2max", "VO₂ Max", Palette.accent, "ml/kg") { + MetricTile(Modifier.weight(1f), data, range, "hrv", tr("HRV"), Palette.metricPurple, "ms") { "${it.roundToInt()}" } + MetricTile(Modifier.weight(1f), data, range, "vo2max", tr("VO₂ Max"), Palette.accent, "ml/kg") { String.format(Locale.US, "%.1f", it) } } TileRow { - MetricTile(Modifier.weight(1f), data, range, "weight", "Weight", Palette.accent) { + MetricTile(Modifier.weight(1f), data, range, "weight", tr("Weight"), Palette.accent) { UnitFormatter.massFromKilograms(it, unitSystem) } - MetricTile(Modifier.weight(1f), data, range, "body_fat", "Body Fat", Palette.metricAmber, "%") { + MetricTile(Modifier.weight(1f), data, range, "body_fat", tr("Body Fat"), Palette.metricAmber, "%") { String.format(Locale.US, "%.1f", it) } } TileRow { - MetricTile(Modifier.weight(1f), data, range, "lean_mass", "Lean Mass", Palette.accent) { + MetricTile(Modifier.weight(1f), data, range, "lean_mass", tr("Lean Mass"), Palette.accent) { UnitFormatter.massFromKilograms(it, unitSystem) } MetricTile( - Modifier.weight(1f), data, range, "asleep_min", "Asleep avg", Palette.metricPurple, + Modifier.weight(1f), data, range, "asleep_min", tr("Asleep avg"), Palette.metricPurple, aggregate = Aggregate.Mean, ) { durationString(it) } } @@ -353,17 +353,17 @@ private fun MetricTile( @Composable private fun HeartSection(data: AppleData, range: AppleRange) { - ChartSection("Heart & Vitals", "Cardiac", range) { - MetricChartCard(data, range, "resting_hr", "Resting heart rate", Palette.metricRose) { + ChartSection(tr("Heart & Vitals"), tr("Cardiac"), range) { + MetricChartCard(data, range, "resting_hr", tr("Resting heart rate"), Palette.metricRose) { "${it.roundToInt()} bpm" } - MetricChartCard(data, range, "hrv", "Heart rate variability", Palette.metricPurple) { + MetricChartCard(data, range, "hrv", tr("Heart rate variability"), Palette.metricPurple) { "${it.roundToInt()} ms" } - MetricChartCard(data, range, "spo2", "Blood oxygen", Palette.metricCyan) { + MetricChartCard(data, range, "spo2", tr("Blood oxygen"), Palette.metricCyan) { String.format(Locale.US, "%.1f%%", it) } - MetricChartCard(data, range, "resp_rate", "Respiratory rate", Palette.accent) { + MetricChartCard(data, range, "resp_rate", tr("Respiratory rate"), Palette.accent) { String.format(Locale.US, "%.1f rpm", it) } } @@ -371,9 +371,9 @@ private fun HeartSection(data: AppleData, range: AppleRange) { @Composable private fun ActivitySection(data: AppleData, range: AppleRange) { - ChartSection("Activity & Energy", "Movement", range) { - MetricChartCard(data, range, "steps", "Steps", Palette.metricCyan) { intString(it) } - MetricChartCard(data, range, "active_kcal", "Active energy", Palette.metricAmber) { + ChartSection(tr("Activity & Energy"), tr("Movement"), range) { + MetricChartCard(data, range, "steps", tr("Steps"), Palette.metricCyan) { intString(it) } + MetricChartCard(data, range, "active_kcal", tr("Active energy"), Palette.metricAmber) { "${intString(it)} kcal" } } @@ -383,17 +383,17 @@ private fun ActivitySection(data: AppleData, range: AppleRange) { private fun BodySection(data: AppleData, range: AppleRange) { // Weight + lean mass (stored kg) re-label to lb under the imperial preference. val unitSystem = UnitPrefs.system(LocalContext.current) - ChartSection("Body Composition", "Slow threads", range) { - MetricChartCard(data, range, "weight", "Weight", Palette.accent) { + ChartSection(tr("Body Composition"), tr("Slow threads"), range) { + MetricChartCard(data, range, "weight", tr("Weight"), Palette.accent) { UnitFormatter.massFromKilograms(it, unitSystem) } - MetricChartCard(data, range, "body_fat", "Body fat", Palette.metricAmber) { + MetricChartCard(data, range, "body_fat", tr("Body fat"), Palette.metricAmber) { String.format(Locale.US, "%.1f%%", it) } - MetricChartCard(data, range, "lean_mass", "Lean body mass", Palette.accent) { + MetricChartCard(data, range, "lean_mass", tr("Lean body mass"), Palette.accent) { UnitFormatter.massFromKilograms(it, unitSystem) } - MetricChartCard(data, range, "bmi", "BMI", Palette.metricPurple) { + MetricChartCard(data, range, "bmi", tr("BMI"), Palette.metricPurple) { String.format(Locale.US, "%.1f", it) } } @@ -401,8 +401,8 @@ private fun BodySection(data: AppleData, range: AppleRange) { @Composable private fun SleepSection(data: AppleData, range: AppleRange) { - ChartSection("Sleep", "Rest", range) { - MetricChartCard(data, range, "asleep_min", "Asleep", Palette.metricPurple) { durationString(it) } + ChartSection(tr("Sleep"), tr("Rest"), range) { + MetricChartCard(data, range, "asleep_min", tr("Asleep"), Palette.metricPurple) { durationString(it) } } } @@ -475,13 +475,13 @@ private fun MetricChartCard( ChartFooterRow( items = if (n > 0) { listOf( - "Avg" to fmt(values.average()), - "Min" to fmt(values.min()), - "Max" to fmt(values.max()), - "Points" to "$n", + tr("Avg") to fmt(values.average()), + tr("Min") to fmt(values.min()), + tr("Max") to fmt(values.max()), + tr("Points") to "$n", ) } else { - listOf("Avg" to "—", "Min" to "—", "Max" to "—", "Points" to "0") + listOf(tr("Avg") to "—", tr("Min") to "—", tr("Max") to "—", tr("Points") to "0") }, ) } @@ -496,7 +496,7 @@ private fun SinglePoint(value: Double, accent: Color, fmt: (Double) -> String) { contentAlignment = Alignment.CenterStart, ) { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Overline("Latest reading") + Overline(tr("Latest reading")) Text(fmt(value), style = NoopType.number(34f), color = accent) } } @@ -508,7 +508,7 @@ private fun EmptyChart() { modifier = Modifier.fillMaxWidth().height(Metrics.chartHeight), contentAlignment = Alignment.Center, ) { - Text("No readings recorded.", style = NoopType.subhead, color = Palette.textTertiary) + Text(tr("No readings recorded."), style = NoopType.subhead, color = Palette.textTertiary) } } diff --git a/android/app/src/main/java/com/noop/ui/AutomationsScreen.kt b/android/app/src/main/java/com/noop/ui/AutomationsScreen.kt index 229dca190..88f2e6179 100644 --- a/android/app/src/main/java/com/noop/ui/AutomationsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/AutomationsScreen.kt @@ -109,8 +109,8 @@ fun AutomationsScreen(viewModel: AppViewModel) { // reproduced by the LazyColumn), so spacing is byte-identical; only on-screen sections compose + get // accessibility-walked on scroll. LazyScreenScaffold( - title = "Automations", - subtitle = "Make the strap do things: tap to act, walk away to lock, train by feel.", + title = tr("Automations"), + subtitle = tr("Make the strap do things: tap to act, walk away to lock, train by feel."), ) { // Double-tap (parity since 4.2.8): a real, persisted action picker bound to the ViewModel, with a // Test action button. Mirrors AutomationsView.swift's Picker (Apple-applicable subset only; no @@ -118,12 +118,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.TouchApp, - title = "Double-tap", + title = tr("Double-tap"), blurb = "Double-tap the strap to trigger an action on this device. (The strap exposes a single double-tap gesture.)", active = doubleTapAction != DoubleTapAction.NONE, ) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("When I double-tap", style = NoopType.body, color = Palette.textPrimary) + Text(tr("When I double-tap"), style = NoopType.body, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) DoubleTapActionPicker( selected = doubleTapAction, @@ -139,11 +139,11 @@ fun AutomationsScreen(viewModel: AppViewModel) { ) { Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) - Text("Test action", style = NoopType.body) + Text(tr("Test action"), style = NoopType.body) } Spacer(Modifier.weight(1f)) StatePill( - if (live.bonded) "Strap bonded" else "Not connected", + if (live.bonded) tr("Strap bonded") else tr("Not connected"), tone = if (live.bonded) StrandTone.Positive else StrandTone.Warning, ) } @@ -154,12 +154,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.Bolt, - title = "Haptic coaching", + title = tr("Haptic coaching"), blurb = "Train by feel. The strap buzzes so you don't have to watch a screen.", active = zoneCoaching || stressNudge, ) { ToggleRow( - label = "HR-zone coaching", + label = tr("HR-zone coaching"), help = "A triple-buzz when you climb into your top zone (Zone 5, ≥ $zone5Bpm bpm), a cue to ease off. Max HR comes from Settings.", checked = zoneCoaching, onChange = { viewModel.setZoneCoaching(it) }, @@ -167,7 +167,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { if (zoneCoaching) { RowDivider() ToggleRow( - label = "Recovery buzz", + label = tr("Recovery buzz"), help = "Also buzz once when your heart rate drops back to Zone 1, a cue that you've recovered.", checked = zoneCoachRecovery, onChange = { viewModel.setZoneCoachRecovery(it) }, @@ -175,7 +175,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { } RowDivider() ToggleRow( - label = "Resting stress nudge (experimental)", + label = tr("Resting stress nudge (experimental)"), help = "A gentle buzz when your HRV drops while your heart rate is calm, a cue to take a paced breath. Rate-limited to once every 15 minutes; off by default.", checked = stressNudge, onChange = { stressNudge = it }, @@ -187,12 +187,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.TouchApp, - title = "Wear & presence", + title = tr("Wear & presence"), blurb = "React when the strap comes off or goes on.", active = autoLockOnWristOff, ) { ToggleRow( - label = "Lock the device when I take the strap off", + label = tr("Lock the device when I take the strap off"), help = "Fires the moment the strap leaves your wrist.", checked = autoLockOnWristOff, onChange = { autoLockOnWristOff = it }, @@ -208,12 +208,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.Timer, - title = "Inactivity reminder", + title = tr("Inactivity reminder"), blurb = "A gentle wrist buzz when you've been sitting too long, a nudge to get up and move. Inferred from the strap's motion on each history sync, so it lags real time by a sync or two.", active = inactivityEnabled, ) { ToggleRow( - label = "Enable inactivity reminder", + label = tr("Enable inactivity reminder"), help = "Buzzes after you've been sitting past your threshold.", checked = inactivityEnabled, onChange = { @@ -225,14 +225,14 @@ fun AutomationsScreen(viewModel: AppViewModel) { if (!notifMasterOn) { RowDivider() Text( - "Notifications are off, so this can't buzz yet. Turn on the master switch in " + - "Settings → Notifications to let it through.", + tr("Notifications are off, so this can't buzz yet. Turn on the master switch in ") + + tr("Settings → Notifications to let it through."), style = NoopType.footnote, color = Palette.statusWarning, ) } RowDivider() StepperRow( - label = "Sitting for", + label = tr("Sitting for"), help = "Minutes seated before the first nudge.", value = inactivityThreshold, suffix = "min", range = 15..120, step = 15, onChange = { @@ -242,7 +242,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { ) RowDivider() StepperRow( - label = "Re-nudge every", + label = tr("Re-nudge every"), help = "If you're still seated, buzz again this often.", value = inactivityReNudge, suffix = "min", range = 15..120, step = 15, onChange = { @@ -252,7 +252,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { ) RowDivider() StepperRow( - label = "Buzz strength", + label = tr("Buzz strength"), help = "How strong the buzz is.", value = inactivityBuzzLoops, suffix = "×", range = 1..4, step = 1, onChange = { @@ -262,7 +262,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { ) RowDivider() ToggleRow( - label = "Only when worn", + label = tr("Only when worn"), help = "Don't buzz when the strap is off your wrist.", checked = inactivityOnlyWorn, onChange = { @@ -273,7 +273,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { ) RowDivider() ToggleRow( - label = "Only during active hours", + label = tr("Only during active hours"), help = "Only nudge during your active hours.", checked = inactivityActiveHours, onChange = { @@ -284,7 +284,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { if (inactivityActiveHours) { RowDivider() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("From", style = NoopType.body, color = Palette.textPrimary) + Text(tr("From"), style = NoopType.body, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) TimeChip( minutes = inactivityActiveStart, @@ -295,7 +295,7 @@ fun AutomationsScreen(viewModel: AppViewModel) { }, ) Spacer(Modifier.width(8.dp)) - Text("to", style = NoopType.body, color = Palette.textSecondary) + Text(tr("to"), style = NoopType.body, color = Palette.textSecondary) Spacer(Modifier.width(8.dp)) TimeChip( minutes = inactivityActiveEnd, @@ -320,12 +320,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.MonitorHeart, - title = "Illness early-warning", + title = tr("Illness early-warning"), blurb = "Watches your resting HR, HRV, skin temperature and respiration against your own 28-day baseline. On-device and approximate: informational only, not a diagnosis.", active = illnessWatch, ) { ToggleRow( - label = "Watch for early-illness signs", + label = tr("Watch for early-illness signs"), help = "Needs at least 14 days of history. When two or more signals drift together you get a banner on Today and a notification, at most once a day.", checked = illnessWatch, onChange = { viewModel.setIllnessWatchEnabled(it) }, @@ -337,12 +337,12 @@ fun AutomationsScreen(viewModel: AppViewModel) { item { SettingsSection( icon = Icons.Filled.BatteryStd, - title = "Battery alerts", + title = tr("Battery alerts"), blurb = "A heads-up when the strap battery gets low so you can recharge before bed, and a note when it's finished charging.", active = batteryAlerts, ) { ToggleRow( - label = "Notify on low and full battery", + label = tr("Notify on low and full battery"), help = "Sends a notification when the strap drops to 15% or reaches a full charge, at most once per charge cycle.", checked = batteryAlerts, onChange = { viewModel.setBatteryAlertsEnabled(it) }, @@ -369,14 +369,14 @@ private fun NapDetectionSection(viewModel: AppViewModel) { SettingsSection( icon = Icons.Filled.Bedtime, - title = "Nap detection", + title = tr("Nap detection"), blurb = "Spots a likely daytime nap from the strap's motion and heart rate on each history sync, " + "then asks you to confirm it. Inferred and approximate: NOOP never adds a nap to your sleep " + "without your OK.", active = enabled, ) { ToggleRow( - label = "Detect short naps", + label = tr("Detect short naps"), help = "When a sync shows a quiet, settled stretch in the day, NOOP offers it here for you to keep or skip.", checked = enabled, onChange = { @@ -388,7 +388,7 @@ private fun NapDetectionSection(viewModel: AppViewModel) { if (pending.isEmpty()) { RowDivider() Text( - "No naps to review. Detected naps show up here after a history sync.", + tr("No naps to review. Detected naps show up here after a history sync."), style = NoopType.footnote, color = Palette.textTertiary, ) } else { @@ -446,7 +446,7 @@ private fun napWindowLabel(nap: NapCandidate, ctx: android.content.Context): Str } private fun napDetailLabel(nap: NapCandidate): String = - if (nap.meanHr != null) "Quiet and settled, mean HR ~${nap.meanHr} bpm." else "Quiet and settled." + if (nap.meanHr != null) "Quiet and settled, mean HR ~${nap.meanHr} bpm." else tr("Quiet and settled.") // MARK: - Per-weekday wake-time overrides (PR #554 reimpl under NoopApp) @@ -467,7 +467,7 @@ internal fun AlarmDayOverridePicker( ) { val fireDays = SMART_ALARM_WEEKDAY_ORDER.filter { smartAlarmWeekdayIsSelected(it, enabledDays) } Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Per-day wake time", style = NoopType.caption, color = Palette.textTertiary) + Text(tr("Per-day wake time"), style = NoopType.caption, color = Palette.textTertiary) fireDays.forEach { dow -> val effective = overrides[dow] ?: defaultMinutes val hasOverride = overrides.containsKey(dow) @@ -476,7 +476,7 @@ internal fun AlarmDayOverridePicker( Spacer(Modifier.weight(1f)) if (hasOverride) { Text( - "Reset", + tr("Reset"), style = NoopType.caption, color = Palette.accent, modifier = Modifier @@ -494,7 +494,7 @@ internal fun AlarmDayOverridePicker( } } Text( - "Each day uses the time above unless you set a different one here.", + tr("Each day uses the time above unless you set a different one here."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -514,7 +514,7 @@ private fun SettingsSection( Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Automation") + Overline(tr("Automation")) if (active) Overline("ON", color = Palette.accent) } Row(verticalAlignment = Alignment.CenterVertically) { @@ -554,7 +554,7 @@ private fun DoubleTapActionPicker( Spacer(Modifier.width(4.dp)) Icon( Icons.Filled.ArrowDropDown, - contentDescription = "Choose double-tap action", + contentDescription = tr("Choose double-tap action"), tint = Palette.textSecondary, modifier = Modifier.size(18.dp), ) diff --git a/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt b/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt index 66226eb18..35ab69aba 100644 --- a/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt +++ b/android/app/src/main/java/com/noop/ui/BackupSyncScreen.kt @@ -75,7 +75,7 @@ fun BackupSyncScreen() { busy = false when (r) { is DataBackup.ImportResult.NeedsRestart -> { - Toast.makeText(context, "Restored — reloading NOOP…", Toast.LENGTH_SHORT).show() + Toast.makeText(context, tr("Restored — reloading NOOP…"), Toast.LENGTH_SHORT).show() AppRestart.relaunch(context) } is DataBackup.ImportResult.Failed -> @@ -111,26 +111,26 @@ fun BackupSyncScreen() { } LazyScreenScaffold( - title = "Backup & Sync", - subtitle = "Save a full backup to a folder you choose - point it at Google Drive / Dropbox for off-device sync.", + title = tr("Backup & Sync"), + subtitle = tr("Save a full backup to a folder you choose - point it at Google Drive / Dropbox for off-device sync."), ) { // 1 · Destination folder item { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Backup folder", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Backup folder"), style = NoopType.headline, color = Palette.textPrimary) Text( treeUri?.let { "Saving to: ${folderLabel(it)}" } - ?: "No folder chosen yet. Pick one your cloud app already syncs, or any local folder.", + ?: tr("No folder chosen yet. Pick one your cloud app already syncs, or any local folder."), style = NoopType.footnote, color = Palette.textTertiary, ) Text( - "Tip: a desktop Drive / Dropbox app auto-syncs a chosen folder. On the phone, save to a " + - "folder a sync app (e.g. FolderSync / Autosync) keeps in your cloud.", + tr("Tip: a desktop Drive / Dropbox app auto-syncs a chosen folder. On the phone, save to a ") + + tr("folder a sync app (e.g. FolderSync / Autosync) keeps in your cloud."), style = NoopType.caption, color = Palette.accent, ) NoopButton( - text = if (treeUri == null) "Choose folder" else "Change folder", + text = if (treeUri == null) tr("Choose folder") else tr("Change folder"), leadingIcon = Icons.Filled.FolderOpen, kind = NoopButtonKind.Secondary, enabled = !busy, @@ -149,9 +149,9 @@ fun BackupSyncScreen() { modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Text("Daily auto-backup", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Daily auto-backup"), style = NoopType.body, color = Palette.textPrimary) Text( - "Writes a fresh backup to your folder about once a day (keeps the latest " + + tr("Writes a fresh backup to your folder about once a day (keeps the latest ") + "${BackupSyncPrefs.keepCount(context)}). Off by default - flip it on if you want it.", style = NoopType.footnote, color = Palette.textTertiary, ) @@ -178,12 +178,12 @@ fun BackupSyncScreen() { if (lastMs > 0L) { "Last backup: ${DateUtils.getRelativeTimeSpanString(lastMs)}" } else { - "No backup yet." + tr("No backup yet.") }, style = NoopType.caption, color = Palette.textTertiary, ) NoopButton( - text = if (busy) "Working…" else "Back up now", + text = if (busy) tr("Working…") else tr("Back up now"), leadingIcon = Icons.Filled.CloudUpload, fullWidth = true, enabled = treeUri != null && !busy, @@ -196,9 +196,9 @@ fun BackupSyncScreen() { Toast.makeText( context, if (ok) { - "Backed up to your folder." + tr("Backed up to your folder.") } else { - "Backup failed - re-pick the folder and try again." + tr("Backup failed - re-pick the folder and try again.") }, Toast.LENGTH_LONG, ).show() @@ -213,14 +213,14 @@ fun BackupSyncScreen() { item { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Restore", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Restore"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Replace this device's data with one of your backups. This overwrites current data, " + - "so back up first if unsure.", + tr("Replace this device's data with one of your backups. This overwrites current data, ") + + tr("so back up first if unsure."), style = NoopType.footnote, color = Palette.textTertiary, ) NoopButton( - text = "Restore from a backup…", + text = tr("Restore from a backup…"), leadingIcon = Icons.Filled.Restore, kind = NoopButtonKind.Secondary, enabled = !busy, @@ -257,12 +257,12 @@ fun BackupSyncScreen() { onDismissRequest = { showSnapshotPicker = false }, containerColor = Palette.surfaceOverlay, title = { - Text("Choose a backup", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Choose a backup"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( - "Newest first. Restoring replaces this device's data.", + tr("Newest first. Restoring replaces this device's data."), style = NoopType.footnote, color = Palette.textSecondary, ) snapshots.forEach { snap -> @@ -296,7 +296,7 @@ fun BackupSyncScreen() { }, confirmButton = { TextButton(onClick = { showSnapshotPicker = false }) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -308,7 +308,7 @@ fun BackupSyncScreen() { onDismissRequest = { pendingRestore = null }, containerColor = Palette.surfaceOverlay, title = { - Text("Replace all current data?", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Replace all current data?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( @@ -321,12 +321,12 @@ fun BackupSyncScreen() { pendingRestore = null runRestore(uri) }) { - Text("Replace", style = NoopType.body, color = Palette.statusCritical) + Text(tr("Replace"), style = NoopType.body, color = Palette.statusCritical) } }, dismissButton = { TextButton(onClick = { pendingRestore = null }) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -347,6 +347,6 @@ private val RESTORE_MIME_TYPES = arrayOf( /** A short, human label for a SAF tree Uri (the part after the volume colon). */ private fun folderLabel(treeUri: Uri): String { - val seg = treeUri.lastPathSegment ?: return "selected folder" + val seg = treeUri.lastPathSegment ?: return tr("selected folder") return seg.substringAfterLast(':').ifBlank { seg } } diff --git a/android/app/src/main/java/com/noop/ui/BreatheScreen.kt b/android/app/src/main/java/com/noop/ui/BreatheScreen.kt index 6e1ac74ca..b4a9e99ed 100644 --- a/android/app/src/main/java/com/noop/ui/BreatheScreen.kt +++ b/android/app/src/main/java/com/noop/ui/BreatheScreen.kt @@ -80,10 +80,10 @@ import kotlin.math.roundToInt // MARK: - Pace presets (ported from BreathingView.Pace) private enum class Pace(val label: String) { - Relax("Relax 4-6"), - Coherence("Coherence 5.5"), - Box("Box 4-4"), - Resonance("Resonance"); // the user's locked pace (br/min) — only offered once a pace is locked + Relax(tr("Relax 4-6")), + Coherence(tr("Coherence 5.5")), + Box(tr("Box 4-4")), + Resonance(tr("Resonance")); // the user's locked pace (br/min) — only offered once a pace is locked /** Inhale seconds — for [Resonance] it derives from the locked bpm at a 40:60 inhale:exhale split * (mirrors macOS Pace.inhale(lockedBpm:)). */ @@ -111,9 +111,9 @@ private enum class Pace(val label: String) { fun bpm(lockedBpm: Double? = null): Double = 60.0 / cycle(lockedBpm) fun tagline(lockedBpm: Double? = null): String = when (this) { - Relax -> "Long exhale · downshift to rest" - Coherence -> "Equal breath · ~5.5 br/min coherence" - Box -> "Square breath · steady focus" + Relax -> tr("Long exhale · downshift to rest") + Coherence -> tr("Equal breath · ~5.5 br/min coherence") + Box -> tr("Square breath · steady focus") Resonance -> String.format(Locale.US, "Your locked pace · %.1f br/min", lockedBpm ?: ResonanceEngine.FALLBACK_BPM) } } @@ -131,9 +131,9 @@ private val LIQUID_HERO_RADIUS = 26.dp /** The three biofeedback layers as a mode switch (mirrors BreathingView.Mode). */ private enum class BreatheMode(val label: String) { - Breathe("Breathe"), - Resonance("Resonance"), - Calm("Calm me"), + Breathe(tr("Breathe")), + Resonance(tr("Resonance")), + Calm(tr("Calm me")), } /** @@ -290,8 +290,8 @@ fun BreatheScreen(viewModel: AppViewModel) { } ScreenScaffold( - title = "Breathe", - subtitle = "Haptic-paced breathing · find your pace · calm down", + title = tr("Breathe"), + subtitle = tr("Haptic-paced breathing · find your pace · calm down"), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // into the theme canvas behind the header + top card and bleeds full-width up behind the status bar // via the scaffold's topBackground plumbing. The Android equivalent of the iOS @@ -340,15 +340,15 @@ fun BreatheScreen(viewModel: AppViewModel) { // Status row. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { StatePill( - if (running) "Session live" else "Ready", + if (running) tr("Session live") else tr("Ready"), tone = if (running) StrandTone.Accent else StrandTone.Neutral, pulsing = running, ) Spacer(Modifier.width(8.dp)) if (live.bonded) { - StatePill("Haptics on", tone = StrandTone.Positive) + StatePill(tr("Haptics on"), tone = StrandTone.Positive) } else { - StatePill("Visual only", tone = StrandTone.Warning) + StatePill(tr("Visual only"), tone = StrandTone.Warning) } Spacer(Modifier.weight(1f)) Text(timeString(sessionSeconds), style = NoopType.number(15f), color = Palette.textPrimary) @@ -477,7 +477,7 @@ fun BreatheScreen(viewModel: AppViewModel) { contentDescription = null, modifier = Modifier.padding(end = 6.dp), ) - Text(if (running) "Stop session" else "Start session", style = NoopType.headline) + Text(if (running) tr("Stop session") else tr("Start session"), style = NoopType.headline) } OutlinedButton( @@ -486,7 +486,7 @@ fun BreatheScreen(viewModel: AppViewModel) { colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.accent), ) { Icon(Icons.Filled.GraphicEq, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Test buzz", style = NoopType.body) + Text(tr("Test buzz"), style = NoopType.body) } } @@ -494,7 +494,7 @@ fun BreatheScreen(viewModel: AppViewModel) { // Hidden while running and when there is nothing honest to show. val outcomeLine = when { running -> null - endedOutcome == "—" -> "RMSSD - · not enough R-R data" + endedOutcome == "—" -> tr("RMSSD - · not enough R-R data") endedOutcome != null -> "RMSSD $endedOutcome" lastStoredOutcome.isNotEmpty() -> "Last session: $lastStoredOutcome" else -> null @@ -533,25 +533,25 @@ fun BreatheScreen(viewModel: AppViewModel) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { ReadoutTile( modifier = Modifier.weight(1f), - label = "Heart rate", + label = tr("Heart rate"), value = bpm?.toString() ?: "—", - unit = "bpm", + unit = tr("bpm"), accent = Palette.metricRose, - caption = if (live.worn) "Live" else "Strap not worn", + caption = if (live.worn) tr("Live") else tr("Strap not worn"), ) ReadoutTile( modifier = Modifier.weight(1f), - label = "HRV (RMSSD)", + label = tr("HRV (RMSSD)"), value = rmssd?.let { String.format(Locale.US, "%.0f", it) } ?: "—", - unit = "ms", + unit = tr("ms"), accent = Palette.metricPurple, - caption = if (rrBuffer.value.isEmpty()) "Waiting for R-R" else "Last ${rrBuffer.value.size} beats", + caption = if (rrBuffer.value.isEmpty()) tr("Waiting for R-R") else "Last ${rrBuffer.value.size} beats", ) ReadoutTile( modifier = Modifier.weight(1f), - label = "Pace", + label = tr("Pace"), value = String.format(Locale.US, "%.1f", pace.bpm(lockedBpm)), - unit = "br/min", + unit = tr("br/min"), accent = Palette.restBright, caption = String.format(Locale.US, "%.0f / %.0fs", pace.inhale(lockedBpm), pace.exhale(lockedBpm)), ) @@ -576,9 +576,9 @@ private fun AudioCueToggle(checked: Boolean, onChange: (Boolean) -> Unit) { modifier = Modifier.size(16.dp).padding(end = 10.dp), ) Column(modifier = Modifier.weight(1f)) { - Text("Audio cues", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Audio cues"), style = NoopType.footnote, color = Palette.textSecondary) Text( - "Soft tone on each phase · honours silent mode", + tr("Soft tone on each phase · honours silent mode"), style = NoopType.caption, color = Palette.textTertiary, maxLines = 1, ) } @@ -592,7 +592,7 @@ private fun AudioCueToggle(checked: Boolean, onChange: (Boolean) -> Unit) { uncheckedTrackColor = Palette.surfaceInset, uncheckedBorderColor = Palette.hairline, ), - modifier = Modifier.semantics { contentDescription = "Audio cues" }, + modifier = Modifier.semantics { contentDescription = tr("Audio cues") }, ) } } @@ -634,7 +634,7 @@ private fun CoherenceCard(rmssd: Double?) { NoopCard { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Coherence estimate") + Overline(tr("Coherence estimate")) Spacer(Modifier.weight(1f)) StatePill(label, tone = tone) } @@ -648,7 +648,7 @@ private fun CoherenceCard(rmssd: Double?) { modifier = Modifier.fillMaxWidth(), ) Text( - "Estimate only: a higher RMSSD while paced usually means your parasympathetic \"rest\" branch is engaging. It is not a clinical reading; trends over a session matter more than any single number.", + tr("Estimate only: a higher RMSSD while paced usually means your parasympathetic \"rest\" branch is engaging. It is not a clinical reading; trends over a session matter more than any single number."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -656,11 +656,11 @@ private fun CoherenceCard(rmssd: Double?) { } private fun coherenceState(rmssd: Double?): Pair = when { - rmssd == null -> "No data" to StrandTone.Neutral - rmssd < 20 -> "Building" to StrandTone.Warning - rmssd < 45 -> "Settling" to StrandTone.Neutral - rmssd < 80 -> "Coherent" to StrandTone.Positive - else -> "Deep calm" to StrandTone.Positive + rmssd == null -> tr("No data") to StrandTone.Neutral + rmssd < 20 -> tr("Building") to StrandTone.Warning + rmssd < 45 -> tr("Settling") to StrandTone.Neutral + rmssd < 80 -> tr("Coherent") to StrandTone.Positive + else -> tr("Deep calm") to StrandTone.Positive } // MARK: - Session outcome @@ -716,15 +716,15 @@ private fun HapticHint() { ) { Icon(Icons.Filled.GraphicEq, contentDescription = null, tint = Palette.statusWarning) Text( - "Connect your strap for haptic guidance. You'll feel one pulse on the inhale, two on the exhale, so you can breathe with your eyes closed.", + tr("Connect your strap for haptic guidance. You'll feel one pulse on the inhale, two on the exhale, so you can breathe with your eyes closed."), style = NoopType.footnote, color = Palette.textSecondary, ) } } private fun phaseWord(phase: Phase): String = when (phase) { - Phase.Inhale -> "Breathe in…" - Phase.Exhale -> "Breathe out…" + Phase.Inhale -> tr("Breathe in…") + Phase.Exhale -> tr("Breathe out…") } private fun timeString(total: Int): String = @@ -752,12 +752,12 @@ private fun StressCheckInCard(onBreatheNow: () -> Unit) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Air, contentDescription = null, tint = Palette.restBright, modifier = Modifier.size(16.dp).padding(end = 8.dp)) - Overline("Stress check-in") + Overline(tr("Stress check-in")) Spacer(Modifier.weight(1f)) - StatePill("Passive", tone = StrandTone.Neutral) + StatePill(tr("Passive"), tone = StrandTone.Neutral) } Text( - "Your HRV dipped while you were still. Want a minute to breathe?", + tr("Your HRV dipped while you were still. Want a minute to breathe?"), style = NoopType.subhead, color = Palette.textPrimary, ) honestNudgeLine(n)?.let { @@ -769,18 +769,18 @@ private fun StressCheckInCard(onBreatheNow: () -> Unit) { colors = ButtonDefaults.buttonColors( containerColor = Palette.accent, contentColor = Palette.surfaceBase), modifier = Modifier.weight(1f), - ) { Text("Breathe now", style = NoopType.headline) } + ) { Text(tr("Breathe now"), style = NoopType.headline) } OutlinedButton( onClick = { StressNudgeCenter.dismiss() }, colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.textSecondary), - ) { Text("Not now", style = NoopType.body) } + ) { Text(tr("Not now"), style = NoopType.body) } TextButton(onClick = { BiofeedbackPrefs.setCheckInEnabled(context, false) StressNudgeCenter.dismiss() - }) { Text("Turn off", style = NoopType.body, color = Palette.textSecondary) } + }) { Text(tr("Turn off"), style = NoopType.body, color = Palette.textSecondary) } } Text( - "Relaxation guidance from your own numbers: not a health alert, and not a diagnosis. Trends matter more than any single number.", + tr("Relaxation guidance from your own numbers: not a health alert, and not a diagnosis. Trends matter more than any single number."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -865,17 +865,17 @@ private fun ResonanceMode( NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Find your resonance pace") + Overline(tr("Find your resonance pace")) Spacer(Modifier.weight(1f)) - StatePill(if (live.bonded) "Haptics on" else "Visual only", + StatePill(if (live.bonded) tr("Haptics on") else tr("Visual only"), tone = if (live.bonded) StrandTone.Positive else StrandTone.Warning) } Text( - "Everyone has a breathing pace (usually between 4.5 and 7 breaths a minute) where the heart's rhythm swings the most with each breath. We pace you through a few candidate paces, measure how your HRV responds, and lock the one that resonates best for you.", + tr("Everyone has a breathing pace (usually between 4.5 and 7 breaths a minute) where the heart's rhythm swings the most with each breath. We pace you through a few candidate paces, measure how your HRV responds, and lock the one that resonates best for you."), style = NoopType.subhead, color = Palette.textSecondary, ) Text( - "Estimate from PPG-derived R-R: relaxation guidance, not a clinical reading. Your pace drifts, so we date it and you can re-measure anytime.", + tr("Estimate from PPG-derived R-R: relaxation guidance, not a clinical reading. Your pace drifts, so we date it and you can re-measure anytime."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -886,9 +886,9 @@ private fun ResonanceMode( NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text(sweepLabel ?: "Sweeping…", style = NoopType.headline, color = Palette.textPrimary) + Text(sweepLabel ?: tr("Sweeping…"), style = NoopType.headline, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) - StatePill("Live", tone = StrandTone.Accent, pulsing = true) + StatePill(tr("Live"), tone = StrandTone.Accent, pulsing = true) } ProgressBar(sweepProgress.toFloat()) OutlinedButton( @@ -897,7 +897,7 @@ private fun ResonanceMode( modifier = Modifier.fillMaxWidth(), ) { Icon(Icons.Filled.Stop, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Stop sweep", style = NoopType.body) + Text(tr("Stop sweep"), style = NoopType.body) } } } @@ -912,7 +912,7 @@ private fun ResonanceMode( modifier = Modifier.fillMaxWidth(), ) { Icon(Icons.Filled.GraphicEq, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Full sweep · ~13 min", style = NoopType.headline) + Text(tr("Full sweep · ~13 min"), style = NoopType.headline) } OutlinedButton( onClick = { quick = true; result = null; sweeping = true }, @@ -920,10 +920,10 @@ private fun ResonanceMode( modifier = Modifier.fillMaxWidth(), ) { Icon(Icons.Filled.Bolt, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Quick sweep · ~7 min", style = NoopType.body) + Text(tr("Quick sweep · ~7 min"), style = NoopType.body) } Text( - "Sit still and breathe with the buzz. You can stop anytime; a stopped sweep won't lock a pace.", + tr("Sit still and breathe with the buzz. You can stop anytime; a stopped sweep won't lock a pace."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -946,21 +946,21 @@ private fun ResonanceResultCard(result: ResonanceEngine.SweepResult, context: an NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline(if (result.didLock) "Your resonance pace" else "Couldn't lock today") + Overline(if (result.didLock) tr("Your resonance pace") else tr("Couldn't lock today")) Spacer(Modifier.weight(1f)) - StatePill(if (result.didLock) "Locked" else "Fallback", + StatePill(if (result.didLock) tr("Locked") else tr("Fallback"), tone = if (result.didLock) StrandTone.Positive else StrandTone.Neutral) } Row(verticalAlignment = Alignment.Bottom) { Text(String.format(Locale.US, "%.1f", result.lockedBpm), style = NoopType.number(40f), color = Palette.restBright) Spacer(Modifier.width(6.dp)) - Text("br/min", style = NoopType.subhead, color = Palette.textTertiary, + Text(tr("br/min"), style = NoopType.subhead, color = Palette.textTertiary, modifier = Modifier.padding(bottom = 6.dp)) } if (!result.didLock) { Text( - "Not enough clean beat data to lock a pace today. Try again rested, sitting still with the strap snug. For now we'll pace you at 5.5 br/min (coherence).", + tr("Not enough clean beat data to lock a pace today. Try again rested, sitting still with the strap snug. For now we'll pace you at 5.5 br/min (coherence)."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -979,14 +979,14 @@ private fun LockedPaceCard(bpm: Double, context: android.content.Context) { NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Your locked pace") + Overline(tr("Your locked pace")) Spacer(Modifier.weight(1f)) - StatePill("Locked", tone = StrandTone.Positive) + StatePill(tr("Locked"), tone = StrandTone.Positive) } Row(verticalAlignment = Alignment.Bottom) { Text(String.format(Locale.US, "%.1f", bpm), style = NoopType.number(34f), color = Palette.restBright) Spacer(Modifier.width(6.dp)) - Text("br/min", style = NoopType.subhead, color = Palette.textTertiary, + Text(tr("br/min"), style = NoopType.subhead, color = Palette.textTertiary, modifier = Modifier.padding(bottom = 4.dp)) } val dateMs = BiofeedbackPrefs.lockedPaceDateMs(context) @@ -1003,7 +1003,7 @@ private fun LockedPaceCard(bpm: Double, context: android.content.Context) { private fun RsaCurve(scores: List) { val maxRsa = scores.mapNotNull { it.rsaAmplitude }.maxOrNull() ?: 1.0 Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Overline("RSA response by pace") + Overline(tr("RSA response by pace")) scores.forEach { s -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -1086,17 +1086,17 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Calm me") + Overline(tr("Calm me")) Spacer(Modifier.weight(1f)) - StatePill(if (canRun) "Ready" else "Strap needed", + StatePill(if (canRun) tr("Ready") else tr("Strap needed"), tone = if (canRun) StrandTone.Neutral else StrandTone.Warning) } Text( - "The strap buzzes a gentle rhythm just below your current heart rate, a felt metronome to relax toward. It trails your heart down rather than yanking it, and stops on its own.", + tr("The strap buzzes a gentle rhythm just below your current heart rate, a felt metronome to relax toward. It trails your heart down rather than yanking it, and stops on its own."), style = NoopType.subhead, color = Palette.textSecondary, ) Text( - "A relaxation rhythm, not cardiac control. It never paces below a safe rate and you can stop anytime. If your heart rate doesn't settle, we'll say so plainly.", + tr("A relaxation rhythm, not cardiac control. It never paces below a safe rate and you can stop anytime. If your heart rate doesn't settle, we'll say so plainly."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -1106,9 +1106,9 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Settling") + Overline(tr("Settling")) Spacer(Modifier.weight(1f)) - StatePill("Live", tone = StrandTone.Accent, pulsing = true) + StatePill(tr("Live"), tone = StrandTone.Accent, pulsing = true) } Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(10.dp)) { @@ -1116,7 +1116,7 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: Icon(Icons.Filled.ArrowForward, contentDescription = null, tint = Palette.textTertiary, modifier = Modifier.padding(bottom = 8.dp)) Column { - Text("target", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("target"), style = NoopType.footnote, color = Palette.textTertiary) Text(targetBpm?.let { String.format(Locale.US, "%.0f", it) } ?: "—", style = NoopType.number(22f), color = Palette.restBright) } @@ -1132,7 +1132,7 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: modifier = Modifier.fillMaxWidth(), ) { Icon(Icons.Filled.Stop, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Stop", style = NoopType.headline) + Text(tr("Stop"), style = NoopType.headline) } } } @@ -1150,14 +1150,14 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: modifier = Modifier.fillMaxWidth(), ) { Icon(Icons.Filled.Favorite, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text("Calm me · 3 min", style = NoopType.headline) + Text(tr("Calm me · 3 min"), style = NoopType.headline) } when { !canBuzz -> Text( - "Connect your strap. Calm me is a felt rhythm on the wrist, so it needs a bonded connection.", + tr("Connect your strap. Calm me is a felt rhythm on the wrist, so it needs a bonded connection."), style = NoopType.footnote, color = Palette.textTertiary) !canRun -> Text( - "Waiting for a resting heart rate. Start a live reading first, or come back when you're still.", + tr("Waiting for a resting heart rate. Start a live reading first, or come back when you're still."), style = NoopType.footnote, color = Palette.textTertiary) } } @@ -1181,7 +1181,7 @@ private fun CalmMode(viewModel: AppViewModel, live: com.noop.ble.LiveState, bpm: } if (didNotFall) { Text( - "That's normal. A paced breath often settles things when a metronome alone doesn't.", + tr("That's normal. A paced breath often settles things when a metronome alone doesn't."), style = NoopType.footnote, color = Palette.textTertiary) } } @@ -1205,7 +1205,7 @@ private fun calmOutcomeLine( if (startHr != null && endHr != null && endHr < startHr) "HR eased $startHr → $endHr over $mmss." else if (startHr != null && endHr != null) "HR held steady ($startHr → $endHr). Try a paced breath instead." - else "Session ended. Try a paced breath instead." + else tr("Session ended. Try a paced breath instead.") } } diff --git a/android/app/src/main/java/com/noop/ui/CaffeineLog.kt b/android/app/src/main/java/com/noop/ui/CaffeineLog.kt index 2f7126158..778b40d4f 100644 --- a/android/app/src/main/java/com/noop/ui/CaffeineLog.kt +++ b/android/app/src/main/java/com/noop/ui/CaffeineLog.kt @@ -154,15 +154,15 @@ fun CaffeineLogCard() { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f)) { - Overline("Log") - Text("Caffeine", style = NoopType.title2, color = Palette.textPrimary) + Overline(tr("Log")) + Text(tr("Caffeine"), style = NoopType.title2, color = Palette.textPrimary) } } NoopCard { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( - "Log a coffee, tea, or energy drink and NOOP shows a rough estimate of how much may " + - "still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement.", + tr("Log a coffee, tea, or energy drink and NOOP shows a rough estimate of how much may ") + + tr("still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -176,9 +176,9 @@ fun CaffeineLogCard() { // late intake below. A guide from the same half-life model, not a rule. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Text("Late-caffeine nudge", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Late-caffeine nudge"), style = NoopType.body, color = Palette.textPrimary) Text( - "Flag drinks late enough to still be active at bedtime.", + tr("Flag drinks late enough to still be active at bedtime."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -196,12 +196,12 @@ fun CaffeineLogCard() { uncheckedTrackColor = Palette.surfaceInset, uncheckedBorderColor = Palette.hairline, ), - modifier = Modifier.semantics { contentDescription = "Late-caffeine nudge" }, + modifier = Modifier.semantics { contentDescription = tr("Late-caffeine nudge") }, ) } if (cutoffEnabled) { Row(verticalAlignment = Alignment.CenterVertically) { - Text("Bedtime", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Bedtime"), style = NoopType.footnote, color = Palette.textSecondary) Spacer(Modifier.weight(1f)) TimeChip( minutes = bedtimeMinutes, @@ -229,7 +229,7 @@ fun CaffeineLogCard() { value = mgDraft, onValueChange = { mgDraft = it }, placeholder = { - Text("Amount in mg (optional)", style = NoopType.body, color = Palette.textTertiary) + Text(tr("Amount in mg (optional)"), style = NoopType.body, color = Palette.textTertiary) }, singleLine = true, textStyle = NoopType.body, @@ -244,14 +244,14 @@ fun CaffeineLogCard() { // Log "now" or a quick number of hours ago. Row(verticalAlignment = Alignment.CenterVertically) { - Text("Had it", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Had it"), style = NoopType.footnote, color = Palette.textSecondary) Spacer(Modifier.weight(1f)) for (h in intArrayOf(0, 1, 2, 3)) { - CaffeineChip(if (h == 0) "Now" else "${h}h ago") { + CaffeineChip(if (h == 0) tr("Now") else "${h}h ago") { val at = (System.currentTimeMillis() / 1000L) - h * 3600L intakes = addCaffeineIntake(context, at, mgDraft) mgDraft = "" - Toast.makeText(context, "Caffeine logged.", Toast.LENGTH_SHORT).show() + Toast.makeText(context, tr("Caffeine logged."), Toast.LENGTH_SHORT).show() } Spacer(Modifier.width(6.dp)) } @@ -259,7 +259,7 @@ fun CaffeineLogCard() { if (intakes.isNotEmpty()) { CaffeineDivider() - Text("Logged today", style = NoopType.caption, color = Palette.textTertiary) + Text(tr("Logged today"), style = NoopType.caption, color = Palette.textTertiary) intakes.forEach { intake -> val late = cutoffEnabled && isIntakePastCutoff(intake, bedtimeMinutes) Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { @@ -302,8 +302,8 @@ private fun CaffeineActiveHint(estimate: CaffeineActiveEstimate, hasAnyLog: Bool } } else { Text( - if (!hasAnyLog) "No caffeine logged. Log an intake to see an estimate." - else "Estimated mostly cleared. Nothing logged is likely still active.", + if (!hasAnyLog) tr("No caffeine logged. Log an intake to see an estimate.") + else tr("Estimated mostly cleared. Nothing logged is likely still active."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -321,7 +321,7 @@ private fun caffeineActiveDetail(estimate: CaffeineActiveEstimate): String { private fun caffeineHoursLabel(hrs: Double): String { if (hrs < 1) return "under an hour" val r = hrs.roundToInt() - return if (r == 1) "1 hour" else "$r hours" + return if (r == 1) tr("1 hour") else "$r hours" } private fun caffeineIntakeLabel(intake: CaffeineIntake, context: Context): String { @@ -373,7 +373,7 @@ private fun CaffeineChip(label: String, onClick: () -> Unit) { private fun CaffeineRemoveButton(onClick: () -> Unit) { val shape = RoundedCornerShape(50) Text( - "Remove", + tr("Remove"), style = NoopType.caption, color = Palette.statusCritical, modifier = Modifier diff --git a/android/app/src/main/java/com/noop/ui/CoachScreen.kt b/android/app/src/main/java/com/noop/ui/CoachScreen.kt index e887d6157..58b2e75ef 100644 --- a/android/app/src/main/java/com/noop/ui/CoachScreen.kt +++ b/android/app/src/main/java/com/noop/ui/CoachScreen.kt @@ -77,8 +77,8 @@ fun CoachScreen(vm: CoachViewModel = viewModel()) { val showDayCycleBackground = remember { NoopPrefs.showDayCycleBackground(context) } ScreenScaffold( - title = "Coach", - subtitle = "Ask about your recovery, strain, sleep and HRV, grounded in your own numbers.", + title = tr("Coach"), + subtitle = tr("Ask about your recovery, strain, sleep and HRV, grounded in your own numbers."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the liquid sky sits behind the // header and the cards float over the flat canvas below. Reuses the shared LiquidScreenSky() slot // verbatim; when the day-cycle background is off, the scaffold paints the plain surface instead. @@ -109,21 +109,21 @@ private fun CoachSetup(vm: CoachViewModel) { Column(verticalArrangement = Arrangement.spacedBy(18.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.Lock, contentDescription = null, tint = Palette.accent, modifier = Modifier.size(18.dp)) - Text("Connect a provider", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Connect a provider"), style = NoopType.headline, color = Palette.textPrimary) } Text( if (isCustom) - "Point the coach at any OpenAI-compatible server: a local model (Ollama, LM " + - "Studio, llama.cpp) keeps everything on your device; an API key is optional." + tr("Point the coach at any OpenAI-compatible server: a local model (Ollama, LM ") + + tr("Studio, llama.cpp) keeps everything on your device; an API key is optional.") else - "Bring your own API key. It is stored encrypted on this device and only used to " + - "send your question plus a short summary of your metrics to the provider you pick.", + tr("Bring your own API key. It is stored encrypted on this device and only used to ") + + tr("send your question plus a short summary of your metrics to the provider you pick."), style = NoopType.subhead, color = Palette.textSecondary, ) // Provider choice. Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Provider") + Overline(tr("Provider")) SegmentedPillControl( items = AiProvider.entries, selection = provider, @@ -135,13 +135,13 @@ private fun CoachSetup(vm: CoachViewModel) { // Server URL, Custom (local LLM) only. if (isCustom) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Server URL") + Overline(tr("Server URL")) OutlinedTextField( value = customBaseUrl, onValueChange = { vm.setCustomBaseUrl(context, it) }, modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "Server URL" }, + .semantics { contentDescription = tr("Server URL") }, placeholder = { Text("http://localhost:11434/v1", style = NoopType.body, color = Palette.textTertiary) }, textStyle = NoopType.mono(13f), singleLine = true, @@ -155,7 +155,7 @@ private fun CoachSetup(vm: CoachViewModel) { // Model dropdown + live-list refresh. Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Overline("Model") + Overline(tr("Model")) Spacer(Modifier.weight(1f)) RefreshModelsButton( refreshing = refreshingModels, @@ -173,11 +173,11 @@ private fun CoachSetup(vm: CoachViewModel) { // Masked key field, optional for a local Custom server. Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline(if (isCustom) "API Key (optional)" else "API Key") + Overline(if (isCustom) tr("API Key (optional)") else tr("API Key")) CoachKeyField( value = keyInput, onValueChange = { keyInput = it }, - placeholder = if (isCustom) "Only if your server requires one" + placeholder = if (isCustom) tr("Only if your server requires one") else "Paste your ${provider.displayName} key", ) } @@ -185,7 +185,7 @@ private fun CoachSetup(vm: CoachViewModel) { // Connect (Custom) / Save key (cloud). if (isCustom) { CoachPrimaryButton( - label = "Connect", + label = tr("Connect"), enabled = customBaseUrl.isNotBlank(), onClick = { if (keyInput.isNotBlank()) vm.saveKey(context, keyInput) @@ -194,7 +194,7 @@ private fun CoachSetup(vm: CoachViewModel) { ) } else { CoachPrimaryButton( - label = "Save key", + label = tr("Save key"), enabled = keyInput.isNotBlank(), onClick = { vm.saveKey(context, keyInput) }, ) @@ -227,7 +227,7 @@ private fun CoachChat(vm: CoachViewModel) { Spacer(Modifier.weight(1f)) val disconnectInteraction = remember { MutableInteractionSource() } Text( - "Disconnect", + tr("Disconnect"), style = NoopType.caption, color = Palette.textSecondary, modifier = Modifier @@ -235,7 +235,7 @@ private fun CoachChat(vm: CoachViewModel) { .liquidPress(disconnectInteraction) .clickable(interactionSource = disconnectInteraction, indication = null) { vm.disconnect(context) } .padding(horizontal = 10.dp, vertical = 6.dp) - .semantics { contentDescription = "Disconnect provider" }, + .semantics { contentDescription = tr("Disconnect provider") }, ) } } @@ -245,10 +245,10 @@ private fun CoachChat(vm: CoachViewModel) { NoopCard(padding = 14.dp, tint = Palette.chargeColor) { Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Let the coach use my data", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Let the coach use my data"), style = NoopType.subhead, color = Palette.textPrimary) Text( - if (consent) "On: your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching." - else "Off: the coach answers generally and sends none of your metrics.", + if (consent) tr("On: your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching.") + else tr("Off: the coach answers generally and sends none of your metrics."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -268,7 +268,7 @@ private fun CoachChat(vm: CoachViewModel) { NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - "Ask anything about your recent recovery, strain, sleep or HRV.", + tr("Ask anything about your recent recovery, strain, sleep or HRV."), style = NoopType.subhead, color = Palette.textSecondary, ) SuggestedPrompts(onPick = { input = it }) @@ -309,7 +309,7 @@ private fun CoachChat(vm: CoachViewModel) { if (error != null) vm.clearError() }, modifier = Modifier.weight(1f), - placeholder = { Text("Ask your coach…", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Ask your coach…"), style = NoopType.body, color = Palette.textTertiary) }, textStyle = NoopType.body, singleLine = false, maxLines = 4, @@ -354,15 +354,15 @@ private fun CoachInstructions(vm: CoachViewModel) { .liquidPress(headerInteraction) .clickable(interactionSource = headerInteraction, indication = null) { expanded = !expanded } .semantics { - contentDescription = if (expanded) "Collapse coach instructions" else "Edit coach instructions" + contentDescription = if (expanded) tr("Collapse coach instructions") else tr("Edit coach instructions") }, verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Coach instructions", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Coach instructions"), style = NoopType.subhead, color = Palette.textPrimary) Text( - if (hasCustom) "Customised. Your edited instructions frame every reply." - else "Edit how the coach thinks and talks. Takes effect on your next message.", + if (hasCustom) tr("Customised. Your edited instructions frame every reply.") + else tr("Edit how the coach thinks and talks. Takes effect on your next message."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -381,7 +381,7 @@ private fun CoachInstructions(vm: CoachViewModel) { modifier = Modifier .fillMaxWidth() .heightIn(min = 140.dp, max = 260.dp) - .semantics { contentDescription = "Coach instructions editor" }, + .semantics { contentDescription = tr("Coach instructions editor") }, textStyle = NoopType.body, singleLine = false, colors = coachFieldColors(), @@ -393,7 +393,7 @@ private fun CoachInstructions(vm: CoachViewModel) { enabled = hasCustom, ) { Text( - "Reset to default", + tr("Reset to default"), style = NoopType.footnote, color = if (hasCustom) Palette.accent else Palette.textTertiary, ) @@ -434,7 +434,7 @@ private fun ChatBubble(msg: ChatMsg) { ) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Overline( - if (isUser) "You" else "Coach", + if (isUser) tr("You") else tr("Coach"), color = if (isUser) Palette.accentHover else Palette.textTertiary, ) if (isUser) { @@ -456,7 +456,7 @@ private fun ThinkingBubble() { .clip(RoundedCornerShape(16.dp)) .frostedCardSurface(tint = Palette.chargeColor, cornerRadius = 16.dp) .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "Coach is thinking" }, + .semantics { contentDescription = tr("Coach is thinking") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { @@ -465,7 +465,7 @@ private fun ThinkingBubble() { strokeWidth = 2.dp, color = Palette.accent, ) - Text("Thinking…", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("Thinking…"), style = NoopType.subhead, color = Palette.textSecondary) } } } @@ -482,7 +482,7 @@ private val SUGGESTED_PROMPTS = listOf( @Composable private fun SuggestedPrompts(onPick: (String) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Try asking") + Overline(tr("Try asking")) // Simple wrapped column of chips (one per row keeps long prompts readable). SUGGESTED_PROMPTS.forEach { prompt -> val shape = RoundedCornerShape(50) @@ -555,7 +555,7 @@ private fun ModelDropdown( } // Free-text escape hatch, any model id the provider accepts can be entered. DropdownMenuItem( - text = { Text("Custom…", style = NoopType.body, color = Palette.textSecondary) }, + text = { Text(tr("Custom…"), style = NoopType.body, color = Palette.textSecondary) }, onClick = { expanded = false showCustom = true @@ -588,11 +588,11 @@ private fun CustomModelDialog( AlertDialog( onDismissRequest = onDismiss, containerColor = Palette.surfaceOverlay, - title = { Text("Custom model", style = NoopType.headline, color = Palette.textPrimary) }, + title = { Text(tr("Custom model"), style = NoopType.headline, color = Palette.textPrimary) }, text = { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( - "Enter any model id the provider accepts.", + tr("Enter any model id the provider accepts."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -601,8 +601,8 @@ private fun CustomModelDialog( onValueChange = { text = it }, modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "Custom model id" }, - placeholder = { Text("e.g. gpt-4o", style = NoopType.body, color = Palette.textTertiary) }, + .semantics { contentDescription = tr("Custom model id") }, + placeholder = { Text(tr("e.g. gpt-4o"), style = NoopType.body, color = Palette.textTertiary) }, textStyle = NoopType.mono(13f), singleLine = true, colors = coachFieldColors(), @@ -615,12 +615,12 @@ private fun CustomModelDialog( onClick = { onConfirm(text.trim()) }, enabled = text.isNotBlank(), ) { - Text("Use model", style = NoopType.headline, color = Palette.accent) + Text(tr("Use model"), style = NoopType.headline, color = Palette.accent) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.subhead, color = Palette.textSecondary) } }, ) @@ -650,7 +650,7 @@ private fun RefreshModelsButton( else it } .padding(horizontal = 10.dp, vertical = 6.dp) - .semantics { contentDescription = "Fetch models from provider" }, + .semantics { contentDescription = tr("Fetch models from provider") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { @@ -665,7 +665,7 @@ private fun RefreshModelsButton( ) } Text( - if (refreshing) "Fetching…" else "Refresh models", + if (refreshing) tr("Fetching…") else tr("Refresh models"), style = NoopType.caption, color = if (active) Palette.textPrimary else Palette.textTertiary, ) @@ -685,7 +685,7 @@ private fun CoachKeyField( onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "API key (hidden)" }, + .semantics { contentDescription = tr("API key (hidden)") }, placeholder = { Text(placeholder, style = NoopType.body, color = Palette.textTertiary) }, textStyle = NoopType.mono(13f), singleLine = true, @@ -740,7 +740,7 @@ private fun SendButton(enabled: Boolean, sending: Boolean, onClick: () -> Unit) .clickable(interactionSource = interaction, indication = null, onClick = onClick) else it } - .semantics { contentDescription = "Send message" }, + .semantics { contentDescription = tr("Send message") }, contentAlignment = Alignment.Center, ) { if (sending) { @@ -768,11 +768,11 @@ private fun PrivacyNote(local: Boolean = false) { Icon(Icons.Filled.Lock, contentDescription = null, tint = Palette.textTertiary, modifier = Modifier.size(13.dp)) Text( if (local) - "The coach talks only to the server URL you set. Point it at a local model to " + - "keep everything on your device. Nothing is sent until you ask." + tr("The coach talks only to the server URL you set. Point it at a local model to ") + + tr("keep everything on your device. Nothing is sent until you ask.") else - "Private by default: only your question and a short metrics summary are sent, " + - "and only after you set a key.", + tr("Private by default: only your question and a short metrics summary are sent, ") + + tr("and only after you set a key."), style = NoopType.footnote, color = Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/CompareScreen.kt b/android/app/src/main/java/com/noop/ui/CompareScreen.kt index 090d8c0ad..4718e0022 100644 --- a/android/app/src/main/java/com/noop/ui/CompareScreen.kt +++ b/android/app/src/main/java/com/noop/ui/CompareScreen.kt @@ -170,7 +170,7 @@ private object CompareCatalog { CompareMetric("lean_mass", "Lean Body Mass", "Health", "kg", "apple-health", 1), CompareMetric( "bmi", "BMI", "Health", "", "apple-health", 1, - note = "From Health Connect this is derived from your weight and profile height.", + note = tr("From Health Connect this is derived from your weight and profile height."), ), // Nutrition (imported from a food-tracker CSV — calories-in next to calories-out). // Mirrors the macOS MetricCatalog entries exactly (same keys + sources, v2.2.0 parity). @@ -403,14 +403,14 @@ fun CompareScreen(vm: AppViewModel) { val rangeCaption: String = run { val total = activeSeries.sumOf { it.rows.size } - val unit = if (total == 1) "reading" else "readings" + val unit = if (total == 1) tr("reading") else tr("readings") val base = "$total $unit across ${activeSeries.size} · ${range.phrase}" if (anyWidened) "$base · sparse widened" else base } LazyScreenScaffold( - title = "Compare", - subtitle = "Overlay signals, draw conclusions.", + title = tr("Compare"), + subtitle = tr("Overlay signals, draw conclusions."), // Liquid sky backdrop (LiquidScreenSky.kt) in the scaffold's topBackground slot, gated on the // day-cycle preference — the same pilot plumbing the liquid Today uses. topBackground = if (showDayCycleBackground) { { LiquidScreenSky() } } else null, @@ -419,7 +419,7 @@ fun CompareScreen(vm: AppViewModel) { // ── Metric picker section (chips + range control) item { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Metrics", overline = "Overlay 2-4 signals") + SectionHeader(tr("Metrics"), overline = tr("Overlay 2-4 signals")) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -455,7 +455,7 @@ fun CompareScreen(vm: AppViewModel) { if (selected.isEmpty()) { Text( - "Nothing selected yet.", + tr("Nothing selected yet."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -476,7 +476,7 @@ fun CompareScreen(vm: AppViewModel) { if (selected.size < minSelection) { item { - EmptyNote("Pick at least two metrics above to overlay them and read how they move together.") + EmptyNote(tr("Pick at least two metrics above to overlay them and read how they move together.")) } } else { val nonEmpty = activeSeries.filter { it.rows.isNotEmpty() } @@ -484,13 +484,13 @@ fun CompareScreen(vm: AppViewModel) { if (loadedOnce) { item { DataPendingNote( - title = "Compare needs at least two metrics with history", - body = "Compare needs at least two metrics with history. Import your " + - "WHOOP export in Data Sources first.", + title = tr("Compare needs at least two metrics with history"), + body = tr("Compare needs at least two metrics with history. Import your ") + + tr("WHOOP export in Data Sources first."), ) } } else { - item { EmptyNote("Reading your history…") } + item { EmptyNote(tr("Reading your history…")) } } } else { item { OverlaySection(nonEmpty, range, anyWidened) } @@ -557,12 +557,12 @@ private fun AddMetricMenu( ) { Icon( Icons.Filled.Add, - contentDescription = "Add a metric to compare", + contentDescription = tr("Add a metric to compare"), tint = tint, modifier = Modifier.size(16.dp), ) Text( - if (atMax) "Max 4" else "Add metric", + if (atMax) tr("Max 4") else tr("Add metric"), style = NoopType.subhead, color = tint, ) @@ -713,12 +713,12 @@ private fun OverlaySection( anyWidened: Boolean, ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Overlay", overline = range.phrase, trailing = "${series.size} series") + SectionHeader(tr("Overlay"), overline = range.phrase, trailing = "${series.size} series") // Anchor the overlay card to the brand-green chrome world; each line keeps its own categorical // series colour so the overlaid lines stay distinguishable against the wash. NoopCard(tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Normalized overlay") + Overline(tr("Normalized overlay")) Text( if (anyWidened) { "Each line min-max normalized · sparse series widened past ${range.phrase}" @@ -738,9 +738,9 @@ private fun OverlaySection( // Endpoint axis labels (low / high), mirroring the normalized macOS y-axis. Row(modifier = Modifier.fillMaxWidth()) { - Text("low", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("low"), style = NoopType.footnote, color = Palette.textTertiary) Spacer(Modifier.weight(1f)) - Text("high", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("high"), style = NoopType.footnote, color = Palette.textTertiary) } HorizontalDivider(color = Palette.hairline) @@ -948,7 +948,7 @@ private fun CorrelationSection(series: List, range: CompareRange) Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - "How They Move Together", + tr("How They Move Together"), overline = "Pearson r · ${range.phrase}", trailing = if (pairs.isEmpty()) null else "${pairs.size} pairs", ) @@ -1037,7 +1037,7 @@ private fun insightSentence(p: PairResult): String { } val aT = p.a.metric.title.lowercase() val bT = p.b.metric.title.lowercase() - val verb = if (p.r < 0) "tends to fall" else "tends to rise" + val verb = if (p.r < 0) tr("tends to fall") else tr("tends to rise") return "$head When $aT rises, $bT $verb - a ${strengthWord(p.r)} ${directionWord(p.r)} link." } @@ -1059,7 +1059,7 @@ private fun strengthWord(r: Double): String { private fun directionWord(r: Double): String { if (abs(r) < 0.1) return "" - return if (r >= 0) "positive" else "negative" + return if (r >= 0) tr("positive") else tr("negative") } private fun correlationColor(r: Double): Color { diff --git a/android/app/src/main/java/com/noop/ui/ConnectionHelp.kt b/android/app/src/main/java/com/noop/ui/ConnectionHelp.kt index a933c9443..c2835548e 100644 --- a/android/app/src/main/java/com/noop/ui/ConnectionHelp.kt +++ b/android/app/src/main/java/com/noop/ui/ConnectionHelp.kt @@ -65,12 +65,12 @@ fun ConnectionHelp(viewModel: AppViewModel, modifier: Modifier = Modifier) { if (live.whoop5Detected) { NoopCard(modifier = modifier) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("WHOOP 5 / MG (experimental)", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("WHOOP 5 / MG (experimental)"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Your strap is connected and we're trying an experimental handshake to bring up live " + - "heart rate from the standard profile. This isn't verified on 5/MG hardware yet, so " + - "HR may or may not appear, and deeper metrics (recovery, strain, sleep) aren't " + - "decoded for 5/MG yet. Nothing's wrong with your strap - WHOOP 4.0 is fully supported.", + tr("Your strap is connected and we're trying an experimental handshake to bring up live ") + + tr("heart rate from the standard profile. This isn't verified on 5/MG hardware yet, so ") + + tr("HR may or may not appear, and deeper metrics (recovery, strain, sleep) aren't ") + + tr("decoded for 5/MG yet. Nothing's wrong with your strap - WHOOP 4.0 is fully supported."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -81,22 +81,22 @@ fun ConnectionHelp(viewModel: AppViewModel, modifier: Modifier = Modifier) { NoopCard(modifier = modifier) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Text("Won't connect? Run through these", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Won't connect? Run through these"), style = NoopType.headline, color = Palette.textPrimary) HelpStep( done = !whoopInstalled, - title = "Close the official WHOOP app", - body = "Your strap only pairs with ONE app at a time. If the WHOOP app is connected, " + - "NOOP can't reach the strap. Force stop it (swiping it out of recents isn't enough).", - actionLabel = if (whoopInstalled) "Open WHOOP app, then Force stop" else "WHOOP app isn't installed", + title = tr("Close the official WHOOP app"), + body = tr("Your strap only pairs with ONE app at a time. If the WHOOP app is connected, ") + + tr("NOOP can't reach the strap. Force stop it (swiping it out of recents isn't enough)."), + actionLabel = if (whoopInstalled) tr("Open WHOOP app, then Force stop") else tr("WHOOP app isn't installed"), enabled = whoopInstalled, onAction = { openAppInfo(context, WHOOP_PACKAGE) }, ) HelpStep( done = btOn, - title = "Turn Bluetooth on", - body = if (btOn) "Bluetooth is on." else "Bluetooth is currently off.", - actionLabel = if (!btOn) "Turn on Bluetooth" else null, + title = tr("Turn Bluetooth on"), + body = if (btOn) tr("Bluetooth is on.") else tr("Bluetooth is currently off."), + actionLabel = if (!btOn) tr("Turn on Bluetooth") else null, enabled = !btOn, onAction = { runCatching { enableBtLauncher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) } @@ -104,18 +104,18 @@ fun ConnectionHelp(viewModel: AppViewModel, modifier: Modifier = Modifier) { ) HelpStep( done = permGranted, - title = "Allow Nearby devices", - body = if (permGranted) "Permission granted." - else "On Android 12+, \"Nearby devices\" is the Bluetooth permission. NOOP needs it to find your strap.", - actionLabel = if (!permGranted) "Grant permission" else null, + title = tr("Allow Nearby devices"), + body = if (permGranted) tr("Permission granted.") + else tr("On Android 12+, \"Nearby devices\" is the Bluetooth permission. NOOP needs it to find your strap."), + actionLabel = if (!permGranted) tr("Grant permission") else null, enabled = !permGranted, onAction = { permLauncher.launch(perms) }, ) HelpStep( done = false, - title = "Charge it and put it on", - body = "A flat or off-wrist strap won't advertise, so nothing shows up. A real phone is " + - "required too: an emulator has no Bluetooth.", + title = tr("Charge it and put it on"), + body = tr("A flat or off-wrist strap won't advertise, so nothing shows up. A real phone is ") + + tr("required too: an emulator has no Bluetooth."), actionLabel = null, enabled = false, onAction = {}, @@ -124,7 +124,7 @@ fun ConnectionHelp(viewModel: AppViewModel, modifier: Modifier = Modifier) { OutlinedButton( onClick = { if (permGranted) viewModel.connect() else permLauncher.launch(perms) }, modifier = Modifier.fillMaxWidth(), - ) { Text("Try connecting now", style = NoopType.body) } + ) { Text(tr("Try connecting now"), style = NoopType.body) } } } } diff --git a/android/app/src/main/java/com/noop/ui/CoupledScreen.kt b/android/app/src/main/java/com/noop/ui/CoupledScreen.kt index ce0cef5a8..56c8b8952 100644 --- a/android/app/src/main/java/com/noop/ui/CoupledScreen.kt +++ b/android/app/src/main/java/com/noop/ui/CoupledScreen.kt @@ -158,7 +158,7 @@ fun CoupledScreen( var showGuide by remember { mutableStateOf(false) } ScreenScaffold( - title = "Day", + title = tr("Day"), subtitle = subtitleToday(), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the reusable time-of-day liquid sky // sits behind the top region, full-bleed up behind the status bar via the scaffold's topBackground @@ -426,7 +426,7 @@ private fun StrainCard(dayStrain21: Double?, recovery: Double?, calories: Double animated = false, modifier = Modifier.size(148.dp), ) - Text("No effort yet", style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(top = 6.dp)) + Text(tr("No effort yet"), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(top = 6.dp)) } } @@ -516,7 +516,7 @@ private fun SleepCard( Text("${hoursMinutes(asleepMin)} slept", style = NoopType.headline, color = Palette.textPrimary) Text("${hoursMinutes(needMin)} needed", style = NoopType.subhead, color = Palette.textSecondary) } else { - Text("No sleep tracked last night", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("No sleep tracked last night"), style = NoopType.subhead, color = Palette.textSecondary) } if (bedWakeSpan != null) { Text(bedWakeSpan, style = NoopType.footnote, color = Palette.textTertiary) diff --git a/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt b/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt index 84403c782..522bfa43e 100644 --- a/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt +++ b/android/app/src/main/java/com/noop/ui/DataSourcesScreen.kt @@ -168,7 +168,7 @@ fun DataSourcesScreen(vm: AppViewModel) { scope.launch { val message = withContext(Dispatchers.IO) { runCatching { DataBackup.exportTo(context, uri) } - .fold({ "Backup saved." }, { "Backup failed: ${it.message}" }) + .fold({ tr("Backup saved.") }, { "Backup failed: ${it.message}" }) } busy = false Toast.makeText(context, message, Toast.LENGTH_LONG).show() @@ -186,7 +186,7 @@ fun DataSourcesScreen(vm: AppViewModel) { when (result) { is DataBackup.ImportResult.NeedsRestart -> { restartNeeded = true - Toast.makeText(context, "Imported — reloading NOOP…", Toast.LENGTH_SHORT).show() + Toast.makeText(context, tr("Imported — reloading NOOP…"), Toast.LENGTH_SHORT).show() AppRestart.relaunch(context) } is DataBackup.ImportResult.Failed -> @@ -293,7 +293,7 @@ fun DataSourcesScreen(vm: AppViewModel) { if (granted.any { it in HealthConnectImporter.PERMISSIONS }) { runImport { HealthConnectImporter.import(context, vm.repo, ProfileStore.from(context).heightCm) } } else { - Toast.makeText(context, "Health Connect access not granted.", Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Health Connect access not granted."), Toast.LENGTH_LONG).show() } } @@ -329,7 +329,7 @@ fun DataSourcesScreen(vm: AppViewModel) { vm.writebackHealthConnectNow() } else { vm.setHcWriteback(false) - Toast.makeText(context, "Health Connect write access not granted.", Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Health Connect write access not granted."), Toast.LENGTH_LONG).show() } } @@ -359,29 +359,29 @@ fun DataSourcesScreen(vm: AppViewModel) { // cards now compose + get accessibility-walked on scroll — this list of 11 source cards is long. The // confirm dialogs below the scaffold are untouched. LazyScreenScaffold( - title = "Data Sources", - subtitle = "Everything stays on this phone. Bring your history in once, then it's yours.", + title = tr("Data Sources"), + subtitle = tr("Everything stays on this phone. Bring your history in once, then it's yours."), ) { // --- WHOOP data (cached history) --- item { SourceCard( - title = "WHOOP History", + title = tr("WHOOP History"), icon = Icons.Filled.MonitorHeart, - subtitle = "Recovery, strain, sleep and workouts, stored locally. Import a full " + - "WHOOP data export (.zip) from app.whoop.com → Data Management and it " + - "backfills your whole history in about a minute. Working now on Android.", + subtitle = tr("Recovery, strain, sleep and workouts, stored locally. Import a full ") + + tr("WHOOP data export (.zip) from app.whoop.com → Data Management and it ") + + tr("backfills your whole history in about a minute. Working now on Android."), ) { StatePill( - title = if (whoopHasHr) "Streaming locally" else "No samples yet", + title = if (whoopHasHr) tr("Streaming locally") else tr("No samples yet"), tone = if (whoopHasHr) StrandTone.Positive else StrandTone.Neutral, showsDot = true, ) CountLine( primary = whoopDays?.let { "$it days" } ?: "—", - secondary = whoopWorkouts?.let { "$it workouts stored" } ?: "Counting…", + secondary = whoopWorkouts?.let { "$it workouts stored" } ?: tr("Counting…"), ) BackupButton( - label = "Import WHOOP export (.zip)", + label = tr("Import WHOOP export (.zip)"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -392,25 +392,25 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Apple Health --- item { SourceCard( - title = "Apple Health", + title = tr("Apple Health"), icon = Icons.Filled.FavoriteBorder, tint = Palette.metricCyan, - subtitle = "Import HR, HRV, sleep, SpO₂ and steps from an Apple Health export. On " + - "an iPhone: Health app → tap your photo → Export All Health Data, then " + - "import the .zip here. Working now on Android.", + subtitle = tr("Import HR, HRV, sleep, SpO₂ and steps from an Apple Health export. On ") + + tr("an iPhone: Health app → tap your photo → Export All Health Data, then ") + + tr("import the .zip here. Working now on Android."), ) { val hasApple = (appleDays ?: 0) > 0 || (appleWorkouts ?: 0) > 0 StatePill( - title = if (hasApple) "Imported" else "Nothing imported", + title = if (hasApple) tr("Imported") else tr("Nothing imported"), tone = if (hasApple) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = appleDays?.let { "$it days" } ?: "—", - secondary = appleWorkouts?.let { "$it workouts" } ?: "Counting…", + secondary = appleWorkouts?.let { "$it workouts" } ?: tr("Counting…"), ) BackupButton( - label = "Import Apple Health export…", + label = tr("Import Apple Health export…"), icon = Icons.Filled.FileUpload, enabled = !busy, tint = Palette.metricCyan, @@ -421,7 +421,7 @@ fun DataSourcesScreen(vm: AppViewModel) { // the Swift card. Shown only once there's something to remove; a confirm dialog gates it. if (hasApple) { BackupButton( - label = "Remove imported data", + label = tr("Remove imported data"), icon = Icons.Filled.DeleteOutline, enabled = !busy, tint = Palette.statusCritical, @@ -434,23 +434,23 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Health Connect (native Android health data) --- item { SourceCard( - title = "Health Connect", + title = tr("Health Connect"), icon = Icons.Filled.MonitorHeart, - subtitle = "Pull steps, heart rate, HRV, sleep, SpO₂, weight and workouts straight from " + - "Android's Health Connect. No file needed. On-device; it never overwrites richer " + - "WHOOP data, and writes nothing unless you opt in to sharing back below.", + subtitle = tr("Pull steps, heart rate, HRV, sleep, SpO₂, weight and workouts straight from ") + + tr("Android's Health Connect. No file needed. On-device; it never overwrites richer ") + + tr("WHOOP data, and writes nothing unless you opt in to sharing back below."), ) { val hasHc = (hcDays ?: 0) > 0 || (hcWorkouts ?: 0) > 0 if (hasHc) { - StatePill(title = "Imported", tone = StrandTone.Accent, showsDot = true) + StatePill(title = tr("Imported"), tone = StrandTone.Accent, showsDot = true) CountLine( primary = hcDays?.let { "$it days" } ?: "—", - secondary = hcWorkouts?.let { "$it workouts" } ?: "Counting…", + secondary = hcWorkouts?.let { "$it workouts" } ?: tr("Counting…"), ) } if (healthConnectAvailable) { BackupButton( - label = "Import from Health Connect", + label = tr("Import from Health Connect"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -465,11 +465,11 @@ fun DataSourcesScreen(vm: AppViewModel) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column(modifier = Modifier.weight(1f)) { - Text("Auto-sync periodically", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Auto-sync periodically"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Re-pull new Health Connect data (e.g. Samsung Health → Health Connect) each " + - "time you open NOOP, if it's been longer than the interval below. " + - "Read-only; never overwrites strap data.", + tr("Re-pull new Health Connect data (e.g. Samsung Health → Health Connect) each ") + + tr("time you open NOOP, if it's been longer than the interval below. ") + + tr("Read-only; never overwrites strap data."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -489,7 +489,7 @@ fun DataSourcesScreen(vm: AppViewModel) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Auto-sync Health Connect periodically" + contentDescription = tr("Auto-sync Health Connect periodically") }, ) } @@ -499,7 +499,7 @@ fun DataSourcesScreen(vm: AppViewModel) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text("Every", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Every"), style = NoopType.footnote, color = Palette.textSecondary) SegmentedPillControl( items = listOf(6, 12, 24), selection = hcSyncHours, @@ -508,7 +508,7 @@ fun DataSourcesScreen(vm: AppViewModel) { ) } Text( - "Last sync: " + if (hcLastSync == 0L) "not yet" + tr("Last sync: ") + if (hcLastSync == 0L) tr("not yet") else DateUtils.getRelativeTimeSpanString(hcLastSync).toString(), style = NoopType.footnote, color = Palette.textTertiary, @@ -522,12 +522,12 @@ fun DataSourcesScreen(vm: AppViewModel) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column(modifier = Modifier.weight(1f)) { - Text("Share back to Health Connect", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Share back to Health Connect"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Write the metrics NOOP computes from your strap (resting HR, HRV, SpO₂, " + - "respiratory rate, heart rate, steps, active energy and sleep) into " + - "Health Connect so other apps can use them. Only NOOP's own values are " + - "shared. Imported data is never echoed back.", + tr("Write the metrics NOOP computes from your strap (resting HR, HRV, SpO₂, ") + + tr("respiratory rate, heart rate, steps, active energy and sleep) into ") + + tr("Health Connect so other apps can use them. Only NOOP's own values are ") + + tr("shared. Imported data is never echoed back."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -547,12 +547,12 @@ fun DataSourcesScreen(vm: AppViewModel) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Share computed metrics back to Health Connect" + contentDescription = tr("Share computed metrics back to Health Connect") }, ) } } else { - RoadmapNote("Health Connect isn't set up on this device. Install it from Google Play, then return here to import.") + RoadmapNote(tr("Health Connect isn't set up on this device. Install it from Google Play, then return here to import.")) } } } @@ -560,25 +560,25 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Nutrition CSV (calories / macros / body weight) --- item { SourceCard( - title = "Nutrition (CSV)", + title = tr("Nutrition (CSV)"), icon = Icons.Filled.Restaurant, tint = Palette.metricAmber, - subtitle = "Import daily calories, protein, carbs, fat and body weight from a " + - "nutrition CSV: a MyFitnessPal or Cronometer export, or any spreadsheet " + - "with a date column plus those values. Meal-level rows are summed per day.", + subtitle = tr("Import daily calories, protein, carbs, fat and body weight from a ") + + tr("nutrition CSV: a MyFitnessPal or Cronometer export, or any spreadsheet ") + + tr("with a date column plus those values. Meal-level rows are summed per day."), ) { val hasNutrition = (nutritionDays ?: 0) > 0 || (nutritionWeighIns ?: 0) > 0 StatePill( - title = if (hasNutrition) "Imported" else "Nothing imported", + title = if (hasNutrition) tr("Imported") else tr("Nothing imported"), tone = if (hasNutrition) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = nutritionDays?.let { "$it days logged" } ?: "—", - secondary = nutritionWeighIns?.let { "$it weigh-ins" } ?: "Counting…", + secondary = nutritionWeighIns?.let { "$it weigh-ins" } ?: tr("Counting…"), ) BackupButton( - label = "Import nutrition CSV…", + label = tr("Import nutrition CSV…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -589,26 +589,26 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Xiaomi Mi Band (Mi Fitness on-device DB) — #35 --- item { SourceCard( - title = "Xiaomi Mi Band", + title = tr("Xiaomi Mi Band"), icon = Icons.Filled.Watch, tint = Palette.metricPurple, - subtitle = "Import a Mi Band / Smart Band 8, 9 or 10's full history (steps, heart rate, " + - "resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness " + - "app's on-device database. Fully offline; no Xiaomi account or Bluetooth. Export the Mi " + - "Fitness folder (or its .db / a .zip of it) from your phone and choose it here.", + subtitle = tr("Import a Mi Band / Smart Band 8, 9 or 10's full history (steps, heart rate, ") + + tr("resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness ") + + tr("app's on-device database. Fully offline; no Xiaomi account or Bluetooth. Export the Mi ") + + tr("Fitness folder (or its .db / a .zip of it) from your phone and choose it here."), ) { val hasXiaomi = (xiaomiDays ?: 0) > 0 StatePill( - title = if (hasXiaomi) "Imported" else "Nothing imported", + title = if (hasXiaomi) tr("Imported") else tr("Nothing imported"), tone = if (hasXiaomi) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = xiaomiDays?.let { "$it days imported" } ?: "—", - secondary = if (xiaomiDays == null) "Counting…" else "Mi Band / Smart Band 8 · 9 · 10", + secondary = if (xiaomiDays == null) tr("Counting…") else tr("Mi Band / Smart Band 8 · 9 · 10"), ) BackupButton( - label = "Import Mi Band export…", + label = tr("Import Mi Band export…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -619,26 +619,26 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Lifting log (Hevy CSV / Liftosaur JSON) --- item { SourceCard( - title = "Lifting log (Hevy / Liftosaur)", + title = tr("Lifting log (Hevy / Liftosaur)"), icon = Icons.Filled.FitnessCenter, tint = DomainTheme.Effort.color, - subtitle = "Import your strength-training history from a Hevy CSV export or a Liftosaur " + - "JSON export. Each workout becomes a Strength session with a training-volume " + - "estimate (weight × reps). It's a volume figure, not a measured strain, so it never " + - "changes your Effort.", + subtitle = tr("Import your strength-training history from a Hevy CSV export or a Liftosaur ") + + tr("JSON export. Each workout becomes a Strength session with a training-volume ") + + tr("estimate (weight × reps). It's a volume figure, not a measured strain, so it never ") + + tr("changes your Effort."), ) { val hasLifting = (liftingWorkouts ?: 0) > 0 StatePill( - title = if (hasLifting) "Imported" else "Nothing imported", + title = if (hasLifting) tr("Imported") else tr("Nothing imported"), tone = if (hasLifting) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = liftingWorkouts?.let { "$it workouts" } ?: "—", - secondary = "volume load shown per session", + secondary = tr("volume load shown per session"), ) BackupButton( - label = "Import lifting log…", + label = tr("Import lifting log…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -649,25 +649,25 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Workout file (GPX / TCX / FIT) — any brand, on-device --- item { SourceCard( - title = "Workout file (GPX / TCX / FIT)", + title = tr("Workout file (GPX / TCX / FIT)"), icon = Icons.Filled.Map, tint = Palette.metricAmber, - subtitle = "Import a single exported workout file from any brand (Garmin, Coros, Suunto, " + - "Wahoo, Polar, Strava, Apple) straight off your phone. GPS route, distance, heart rate " + - "and calories come in where the file has them. Fully offline; nothing leaves your phone.", + subtitle = tr("Import a single exported workout file from any brand (Garmin, Coros, Suunto, ") + + tr("Wahoo, Polar, Strava, Apple) straight off your phone. GPS route, distance, heart rate ") + + tr("and calories come in where the file has them. Fully offline; nothing leaves your phone."), ) { val hasFiles = (activityFiles ?: 0) > 0 StatePill( - title = if (hasFiles) "Imported" else "Nothing imported", + title = if (hasFiles) tr("Imported") else tr("Nothing imported"), tone = if (hasFiles) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = activityFiles?.let { "$it workouts" } ?: "—", - secondary = "GPX · TCX · FIT (one workout per file)", + secondary = tr("GPX · TCX · FIT (one workout per file)"), ) BackupButton( - label = "Import workout file…", + label = tr("Import workout file…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -678,27 +678,27 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Oura / Fitbit / Garmin own-data export — on-device --- item { SourceCard( - title = "Oura / Fitbit / Garmin export", + title = tr("Oura / Fitbit / Garmin export"), icon = Icons.Filled.Watch, tint = Palette.metricPurple, - subtitle = "Import your own data export from Oura, Fitbit or Garmin: sleep, resting heart " + - "rate, HRV, steps and more, where the export has them. Download it from the brand's app " + - "(Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then " + - "choose the file here. Fully offline; nothing leaves your phone. Each brand's own " + - "readiness or sleep score is kept for reference only. Your scores stay yours.", + subtitle = tr("Import your own data export from Oura, Fitbit or Garmin: sleep, resting heart ") + + tr("rate, HRV, steps and more, where the export has them. Download it from the brand's app ") + + tr("(Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then ") + + tr("choose the file here. Fully offline; nothing leaves your phone. Each brand's own ") + + tr("readiness or sleep score is kept for reference only. Your scores stay yours."), ) { val hasDays = (wearableDays ?: 0) > 0 StatePill( - title = if (hasDays) "Imported" else "Nothing imported", + title = if (hasDays) tr("Imported") else tr("Nothing imported"), tone = if (hasDays) StrandTone.Accent else StrandTone.Neutral, showsDot = true, ) CountLine( primary = wearableDays?.let { "$it day metrics" } ?: "—", - secondary = "Oura JSON · Fitbit Takeout · Garmin GDPR (daily metrics + sleep)", + secondary = tr("Oura JSON · Fitbit Takeout · Garmin GDPR (daily metrics + sleep)"), ) BackupButton( - label = "Import wearable export…", + label = tr("Import wearable export…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.fillMaxWidth(), @@ -709,32 +709,32 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Broadcast heart rate (NOOP as a standard BLE HR peripheral) --- item { SourceCard( - title = "Broadcast heart rate", + title = tr("Broadcast heart rate"), icon = Icons.Filled.MonitorHeart, tint = DomainTheme.Effort.color, - subtitle = "Re-share your live strap heart rate over Bluetooth as a standard heart-rate " + - "sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read " + - "it. Works on any WHOOP (4.0 or 5.0/MG) because your phone does the broadcasting. " + - "Local Bluetooth only. Nothing leaves your phone. Off by default.", + subtitle = tr("Re-share your live strap heart rate over Bluetooth as a standard heart-rate ") + + tr("sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read ") + + tr("it. Works on any WHOOP (4.0 or 5.0/MG) because your phone does the broadcasting. ") + + tr("Local Bluetooth only. Nothing leaves your phone. Off by default."), ) { if (hrBroadcast) { val (label, tone) = - if (hrBroadcastAdvertising) "Broadcasting" to StrandTone.Positive - else "Starting…" to StrandTone.Warning + if (hrBroadcastAdvertising) tr("Broadcasting") to StrandTone.Positive + else tr("Starting…") to StrandTone.Warning StatePill(title = label, tone = tone, showsDot = true, pulsing = !hrBroadcastAdvertising) CountLine( - primary = if (hrBroadcastAdvertising) "Standard HR sensor (0x180D)" else "—", + primary = if (hrBroadcastAdvertising) tr("Standard HR sensor (0x180D)") else "—", secondary = when { hrBroadcastSubscribers > 0 -> "$hrBroadcastSubscribers ${if (hrBroadcastSubscribers == 1) "device" else "devices"} reading" live.heartRate != null -> "Sharing ${live.heartRate} bpm · waiting for a device" - else -> "No live heart rate yet · open Live to pair your strap" + else -> tr("No live heart rate yet · open Live to pair your strap") }, ) } else { // Parity with the Swift card, which shows an explicit "Off" pill when the toggle is off // (DataSourcesView.broadcastHrCard: StatePill("Off", tone: .neutral, showsDot: false)). - StatePill(title = "Off", tone = StrandTone.Neutral, showsDot = false) + StatePill(title = tr("Off"), tone = StrandTone.Neutral, showsDot = false) } hrBroadcastStatus?.let { note -> Text(note, style = NoopType.footnote, color = Palette.statusWarning) @@ -745,10 +745,10 @@ fun DataSourcesScreen(vm: AppViewModel) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column(modifier = Modifier.weight(1f)) { - Text("Broadcast heart rate", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Broadcast heart rate"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Acts as a standard Bluetooth heart-rate strap. Pair NOOP from your treadmill, " + - "bike or app to see your strap's heart rate there.", + tr("Acts as a standard Bluetooth heart-rate strap. Pair NOOP from your treadmill, ") + + tr("bike or app to see your strap's heart rate there."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -768,7 +768,7 @@ fun DataSourcesScreen(vm: AppViewModel) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Broadcast heart rate as a Bluetooth sensor" + contentDescription = tr("Broadcast heart rate as a Bluetooth sensor") }, ) } @@ -790,9 +790,9 @@ fun DataSourcesScreen(vm: AppViewModel) { modifier = Modifier.size(16.dp), ) Text( - "Broadcast HR is ON. Your strap is advertising its heart rate continuously, " + - "which keeps its radio hot and drains the battery faster. Turn it off when " + - "you're not using it with another device.", + tr("Broadcast HR is ON. Your strap is advertising its heart rate continuously, ") + + tr("which keeps its radio hot and drains the battery faster. Turn it off when ") + + tr("you're not using it with another device."), style = NoopType.caption, color = Palette.statusWarning, ) @@ -804,14 +804,14 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Live WHOOP strap over BLE --- item { SourceCard( - title = "WHOOP Strap (Live BLE)", + title = tr("WHOOP Strap (Live BLE)"), icon = Icons.Filled.Bluetooth, - subtitle = "Pairs directly with your strap over Bluetooth: no WHOOP app, no cloud.", + subtitle = tr("Pairs directly with your strap over Bluetooth: no WHOOP app, no cloud."), ) { val (label, tone) = when { - live.bonded -> "Bonded, streaming." to StrandTone.Positive - live.connected -> "Connected, pairing…" to StrandTone.Warning - else -> "Not connected. Open Live to pair." to StrandTone.Critical + live.bonded -> tr("Bonded, streaming.") to StrandTone.Positive + live.connected -> tr("Connected, pairing…") to StrandTone.Warning + else -> tr("Not connected. Open Live to pair.") to StrandTone.Critical } StatePill(title = label, tone = tone, showsDot = true, pulsing = live.connected && !live.bonded) } @@ -820,35 +820,35 @@ fun DataSourcesScreen(vm: AppViewModel) { // --- Whole-store backup (the real Android migration path) --- item { SourceCard( - title = "Backup & Move", + title = tr("Backup & Move"), icon = Icons.Filled.FileDownload, - subtitle = "Your whole history is one file on this phone. Export it to keep a copy " + - "or move to a new phone, then import it there. Nothing leaves the device " + - "except through the file you choose.", + subtitle = tr("Your whole history is one file on this phone. Export it to keep a copy ") + + tr("or move to a new phone, then import it there. Nothing leaves the device ") + + tr("except through the file you choose."), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { BackupButton( - label = "Export…", + label = tr("Export…"), icon = Icons.Filled.FileDownload, enabled = !busy, modifier = Modifier.weight(1f), ) { exportLauncher.launch("noop-backup-${java.time.LocalDate.now()}.noopbak") } BackupButton( - label = "Import…", + label = tr("Import…"), icon = Icons.Filled.FileUpload, enabled = !busy, modifier = Modifier.weight(1f), ) { importLauncher.launch(arrayOf("*/*")) } } if (busy) { - Text("Working…", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("Working…"), style = NoopType.footnote, color = Palette.textTertiary) } if (restartNeeded) { Text( - "Import staged — NOOP is reloading. If it doesn't, fully close and reopen it.", + tr("Import staged — NOOP is reloading. If it doesn't, fully close and reopen it."), style = NoopType.subhead, color = Palette.statusWarning, ) @@ -865,12 +865,12 @@ fun DataSourcesScreen(vm: AppViewModel) { onDismissRequest = { confirmDeleteApple = false }, containerColor = Palette.surfaceOverlay, title = { - Text("Remove Apple Health imported data?", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Remove Apple Health imported data?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( - "This permanently deletes everything imported from Apple Health: heart rate, HRV, " + - "sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.", + tr("This permanently deletes everything imported from Apple Health: heart rate, HRV, ") + + tr("sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -887,15 +887,15 @@ fun DataSourcesScreen(vm: AppViewModel) { refreshCounts() vm.loadWorkouts() busy = false - Toast.makeText(context, "Removed Apple Health imported data.", Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Removed Apple Health imported data."), Toast.LENGTH_LONG).show() } }) { - Text("Remove", style = NoopType.body, color = Palette.statusCritical) + Text(tr("Remove"), style = NoopType.body, color = Palette.statusCritical) } }, dismissButton = { TextButton(onClick = { confirmDeleteApple = false }) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) diff --git a/android/app/src/main/java/com/noop/ui/DevicesScreen.kt b/android/app/src/main/java/com/noop/ui/DevicesScreen.kt index daf1837a6..2484b0db1 100644 --- a/android/app/src/main/java/com/noop/ui/DevicesScreen.kt +++ b/android/app/src/main/java/com/noop/ui/DevicesScreen.kt @@ -122,15 +122,15 @@ fun DevicesScreen( val removedDevices = all.filter { it.status == DeviceStatus.archived.name } val currentActiveName = all.firstOrNull { it.status == DeviceStatus.active.name }?.let { displayName(it) } - ?: "Your current strap" + ?: tr("Your current strap") // PERF (#707): lazy scaffold — each device card is virtualized via `items(...)` (each was a direct // child of the eager `spacedBy(20.dp)` column, so the LazyColumn's matching spacing is identical) and // the static button/footer are single items. Only on-screen cards compose + are accessibility-walked. // Conditional rows use `if (cond) { item/items }` so a hidden section adds no row. LazyScreenScaffold( - title = "Devices", - subtitle = "Pair and manage the bands NOOP reads from.", + title = tr("Devices"), + subtitle = tr("Pair and manage the bands NOOP reads from."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // into the flat canvas behind the top of the screen so the frosted device cards float over it. The // static sky (LiquidSkyStatic inside the helper) carries no per-frame cost on this scrolling list. @@ -141,8 +141,8 @@ fun DevicesScreen( // The registry resolves a beat after launch. Show a calm pending note in that brief window. item { DataPendingNote( - title = "Getting your devices ready", - body = "NOOP is opening your on-device data. Your paired bands will appear here in a moment.", + title = tr("Getting your devices ready"), + body = tr("NOOP is opening your on-device data. Your paired bands will appear here in a moment."), ) } return@LazyScreenScaffold @@ -170,7 +170,7 @@ fun DevicesScreen( item { AddDeviceButton(onClick = { showAddWizard = true }) } if (removedDevices.isNotEmpty()) { - item { Overline("Removed", modifier = Modifier.padding(top = 4.dp)) } + item { Overline(tr("Removed"), modifier = Modifier.padding(top = 4.dp)) } items(removedDevices) { device -> DeviceCard( device = device, @@ -203,10 +203,10 @@ fun DevicesScreen( // --- Switch confirm --- switchTarget?.let { device -> ConfirmDialog( - title = "Make this your active strap?", + title = tr("Make this your active strap?"), message = "Make ${displayName(device)} your active strap? From now on it provides your live data. " + "$currentActiveName's history stays exactly as it is. Only new days come from ${displayName(device)}.", - confirmLabel = "Make active", + confirmLabel = tr("Make active"), onConfirm = { scope.launch { viewModel.setActiveDevice(device.id); reload() } switchTarget = null @@ -230,10 +230,10 @@ fun DevicesScreen( // --- Remove confirm --- removeTarget?.let { device -> ConfirmDialog( - title = "Remove this device?", + title = tr("Remove this device?"), message = "Remove ${displayName(device)}? NOOP will stop connecting to it. Its recorded data is " + - "kept and you can re-add it any time.", - confirmLabel = "Remove", + tr("kept and you can re-add it any time."), + confirmLabel = tr("Remove"), destructive = true, onConfirm = { val wasActive = device.status == DeviceStatus.active.name @@ -255,9 +255,9 @@ fun DevicesScreen( // --- Second, strongly-worded delete-data confirm (from the Removed card's secondary control) --- deleteDataTarget?.let { device -> ConfirmDialog( - title = "Delete all of this device's data?", + title = tr("Delete all of this device's data?"), message = "This permanently deletes all data recorded from ${displayName(device)}. This can't be undone.", - confirmLabel = "Delete data", + confirmLabel = tr("Delete data"), destructive = true, onConfirm = { scope.launch { viewModel.deletePairedDeviceData(device.id); reload() } @@ -345,7 +345,7 @@ private fun DeviceCard( } // Locally-adopted Oura is Beta: a non-dot Beta chip sits beside the usual state pill. if (device.sourceKind == SourceKind.oura.name) { - StatePill("Beta", tone = StrandTone.Warning, showsDot = false) + StatePill(tr("Beta"), tone = StrandTone.Warning, showsDot = false) Spacer(Modifier.width(6.dp)) } StatePill(device, isActive, isLiveConnected) @@ -437,7 +437,7 @@ private fun BatteryTube(pct: Int) { horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.semantics { contentDescription = "Battery $clamped%" }, ) { - Text("Battery", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("Battery"), style = NoopType.footnote, color = Palette.textTertiary) LiquidTube( frac = clamped / 100.0, tint = Palette.accent, @@ -452,14 +452,14 @@ private fun BatteryTube(pct: Int) { private fun StatePill(device: PairedDeviceRow, isActive: Boolean, isLiveConnected: Boolean) { when { device.status == DeviceStatus.archived.name -> - StatePill("Removed", tone = StrandTone.Neutral, showsDot = false) + StatePill(tr("Removed"), tone = StrandTone.Neutral, showsDot = false) isActive -> StatePill( - if (isLiveConnected) "Active · Live" else "Active", + if (isLiveConnected) tr("Active · Live") else tr("Active"), tone = StrandTone.Positive, pulsing = isLiveConnected, ) - else -> StatePill("Paired", tone = StrandTone.Neutral) + else -> StatePill(tr("Paired"), tone = StrandTone.Neutral) } } @@ -488,23 +488,23 @@ private fun DeviceActionsMenu( DropdownMenu(expanded = open, onDismissRequest = { onOpenChange(false) }) { if (device.status == DeviceStatus.archived.name) { if (onReAdd != null) { - MenuItem("Make active", Icons.Filled.Bolt) { onOpenChange(false); onReAdd() } + MenuItem(tr("Make active"), Icons.Filled.Bolt) { onOpenChange(false); onReAdd() } } - MenuItem("Rename", Icons.Filled.Edit) { onOpenChange(false); onRename() } + MenuItem(tr("Rename"), Icons.Filled.Edit) { onOpenChange(false); onRename() } if (onDeleteData != null) { HorizontalDivider(color = Palette.hairline) - MenuItem("Delete this device's data…", Icons.Filled.Delete, destructive = true) { + MenuItem(tr("Delete this device's data…"), Icons.Filled.Delete, destructive = true) { onOpenChange(false); onDeleteData() } } } else { if (!isActive) { - MenuItem("Make active", Icons.Filled.Bolt) { onOpenChange(false); onMakeActive() } + MenuItem(tr("Make active"), Icons.Filled.Bolt) { onOpenChange(false); onMakeActive() } } - MenuItem("Rename", Icons.Filled.Edit) { onOpenChange(false); onRename() } + MenuItem(tr("Rename"), Icons.Filled.Edit) { onOpenChange(false); onRename() } if (onRemove != null) { HorizontalDivider(color = Palette.hairline) - MenuItem("Remove", Icons.Filled.RemoveCircleOutline, destructive = true) { + MenuItem(tr("Remove"), Icons.Filled.RemoveCircleOutline, destructive = true) { onOpenChange(false); onRemove() } } @@ -534,13 +534,13 @@ private fun AddDeviceButton(onClick: () -> Unit) { // filled-accent-blue / white-label primary the iOS DevicesView uses (`NoopButton(... kind: .primary, // fullWidth: true)`) — no hand-rolled gold-text fill, no glow. NoopButton( - text = "Add a device", + text = tr("Add a device"), leadingIcon = Icons.Filled.Add, kind = NoopButtonKind.Primary, fullWidth = true, modifier = Modifier .padding(top = 4.dp) - .semantics { contentDescription = "Add a device" }, + .semantics { contentDescription = tr("Add a device") }, onClick = onClick, ) } @@ -559,9 +559,9 @@ private fun WhoopFirstFooter() { modifier = Modifier.size(16.dp), ) Text( - "WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps are an early, " + - "in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper " + - "sleep and recovery data.", + tr("WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps are an early, ") + + tr("in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper ") + + tr("sleep and recovery data."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -575,7 +575,7 @@ private fun ConfirmDialog( title: String, message: String, confirmLabel: String, - cancelLabel: String = "Cancel", + cancelLabel: String = tr("Cancel"), destructive: Boolean = false, onConfirm: () -> Unit, onDismiss: () -> Unit, @@ -612,7 +612,7 @@ private fun RenameDialog( AlertDialog( onDismissRequest = onDismiss, containerColor = Palette.surfaceOverlay, - title = { Text("Rename device", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Rename device"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( @@ -624,20 +624,20 @@ private fun RenameDialog( value = draft, onValueChange = { draft = it }, singleLine = true, - placeholder = { Text("Name", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Name"), style = NoopType.body, color = Palette.textTertiary) }, colors = devicesFieldColors(), - modifier = Modifier.fillMaxWidth().semantics { contentDescription = "Device name" }, + modifier = Modifier.fillMaxWidth().semantics { contentDescription = tr("Device name") }, ) } }, confirmButton = { TextButton(onClick = { onSave(draft) }) { - Text("Save", style = NoopType.body, color = Palette.accent) + Text(tr("Save"), style = NoopType.body, color = Palette.accent) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -652,12 +652,12 @@ private fun PickActiveDialog( AlertDialog( onDismissRequest = onLeaveNone, containerColor = Palette.surfaceOverlay, - title = { Text("Pick a new active strap", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Pick a new active strap"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( - "You removed your active strap. Choose which paired band provides your live data, or " + - "leave none active and pair one later.", + tr("You removed your active strap. Choose which paired band provides your live data, or ") + + tr("leave none active and pair one later."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -679,7 +679,7 @@ private fun PickActiveDialog( confirmButton = {}, dismissButton = { TextButton(onClick = onLeaveNone) { - Text("Leave none active", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Leave none active"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -776,22 +776,22 @@ private fun deviceProfile(device: PairedDeviceRow): DeviceCapabilityProfile { // live-workout path. Effort-scored only when the machine actually reports heart rate. if (device.sourceKind == SourceKind.ftms.name) { return DeviceCapabilityProfile( - displayModel = "Gym equipment (FTMS)", - captures = "Speed · Cadence · Power · Distance · Energy · Heart rate (if the machine sends it)", - powers = "Records a live machine workout, Effort-scored from HR when the machine reports it", - footnote = "Live machine data over Bluetooth FTMS. No sleep, recovery, skin temp or SpO₂. " + - "Effort needs the machine's heart rate; without it the session logs the machine metrics only.", + displayModel = tr("Gym equipment (FTMS)"), + captures = tr("Speed · Cadence · Power · Distance · Energy · Heart rate (if the machine sends it)"), + powers = tr("Records a live machine workout, Effort-scored from HR when the machine reports it"), + footnote = tr("Live machine data over Bluetooth FTMS. No sleep, recovery, skin temp or SpO₂. ") + + tr("Effort needs the machine's heart rate; without it the session logs the machine metrics only."), ) } // EXPERIMENTAL Huami device (Amazfit / Zepp / Mi Band): best-effort live HR only, honest about it. if (device.sourceKind == SourceKind.huami.name) { return DeviceCapabilityProfile( displayModel = "${device.brand} (experimental)", - captures = "Heart rate (live, best-effort)", - powers = "Powers the live console + Effort. No Charge, Rest or Sleep", - footnote = "Experimental: live heart rate where the band exposes it. Some bands need a pairing " + - "we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, " + - "recovery, skin temp, SpO₂ or steps.", + captures = tr("Heart rate (live, best-effort)"), + powers = tr("Powers the live console + Effort. No Charge, Rest or Sleep"), + footnote = tr("Experimental: live heart rate where the band exposes it. Some bands need a pairing ") + + tr("we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, ") + + tr("recovery, skin temp, SpO₂ or steps."), ) } // EXPERIMENTAL locally-adopted Oura ring (gen 3/4/5). The gen is carried on `model` ("Oura Ring @@ -805,60 +805,60 @@ private fun deviceProfile(device: PairedDeviceRow): DeviceCapabilityProfile { // gen3/4 are verified-shape; gen5 ("newer") carries the least-proven caveat. val newer = gen == com.noop.oura.OuraRingGen.GEN5 val captures = if (newer) - "Heart rate* · HRV* · Sleep* · Resting HR* · Skin temp* · Battery*" + tr("Heart rate* · HRV* · Sleep* · Resting HR* · Skin temp* · Battery*") else - "Heart rate · HRV* · Sleep · Resting HR · Skin temp* · Battery" + tr("Heart rate · HRV* · Sleep · Resting HR · Skin temp* · Battery") val powers = if (newer) - "Powers Effort now; Charge and Rest once enough nights and decode are confirmed" + tr("Powers Effort now; Charge and Rest once enough nights and decode are confirmed") else - "Powers Charge, Effort, Rest and Sleep" + tr("Powers Charge, Effort, Rest and Sleep") return DeviceCapabilityProfile( displayModel = "${gen.displayName} (Beta)", captures = captures, powers = powers, - footnote = "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, " + - "and HRV needs you to be still. No Oura Readiness or SpO₂ " + - "percentage comes off the ring (import an Oura file for those).", + footnote = tr("Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, ") + + tr("and HRV needs you to be still. No Oura Readiness or SpO₂ ") + + tr("percentage comes off the ring (import an Oura file for those)."), ) } // Generic heart-rate strap: live HR + R-R only; drives the live console + Effort, nothing nightly. if (!SourceCoordinator.isWhoop(device)) { return DeviceCapabilityProfile( - displayModel = "Heart-rate strap", - captures = "Heart rate · HRV (live)* · Strain", - powers = "Powers the live console + Effort. No Charge, Rest or Sleep", - footnote = "Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery " + - "(those are WHOOP-only).", + displayModel = tr("Heart-rate strap"), + captures = tr("Heart rate · HRV (live)* · Strain"), + powers = tr("Powers the live console + Effort. No Charge, Rest or Sleep"), + footnote = tr("Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery ") + + tr("(those are WHOOP-only)."), ) } - val whoopPowers = "Powers Charge, Effort, Rest, Sleep + Health Monitor" + val whoopPowers = tr("Powers Charge, Effort, Rest, Sleep + Health Monitor") val model = device.model.lowercase() // WHOOP 5.0 / MG — adds a (raw) step count the 4.0 can't read over BLE. if (model.contains("5") || model.contains("mg")) { return DeviceCapabilityProfile( - displayModel = "WHOOP 5.0 / MG", - captures = "Heart rate · HRV · Skin temp* · Resp rate* · Steps* · Sleep · Strain · Battery", + displayModel = tr("WHOOP 5.0 / MG"), + captures = tr("Heart rate · HRV · Skin temp* · Resp rate* · Steps* · Sleep · Strain · Battery"), powers = whoopPowers, - footnote = "* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw " + + footnote = tr("* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw ") + "motion count (#78). No SpO₂ % off the strap; import a WHOOP CSV for a real %.", ) } // WHOOP 4.0 — NOOP's primary band; no steps over BLE. if (model.contains("4")) { return DeviceCapabilityProfile( - displayModel = "WHOOP 4.0", - captures = "Heart rate · HRV · Skin temp* · Resp rate* · Sleep · Strain · Battery", + displayModel = tr("WHOOP 4.0"), + captures = tr("Heart rate · HRV · Skin temp* · Resp rate* · Sleep · Strain · Battery"), powers = whoopPowers, - footnote = "* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); " + + footnote = tr("* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); ") + "no steps over BLE on a 4.0. No SpO₂ % off the strap; import a WHOOP CSV for a real %.", ) } // Legacy / unknown WHOOP (the seeded device, model just "WHOOP") — show only the common-to-all set. return DeviceCapabilityProfile( - displayModel = "WHOOP", - captures = "Heart rate · HRV · Skin temp* · Resp rate* · Sleep · Strain · Battery", + displayModel = tr("WHOOP"), + captures = tr("Heart rate · HRV · Skin temp* · Resp rate* · Sleep · Strain · Battery"), powers = whoopPowers, - footnote = "Exact model unknown. Shows what every WHOOP can do. * on-device estimate · " + + footnote = tr("Exact model unknown. Shows what every WHOOP can do. * on-device estimate · ") + "no SpO₂ % off the strap (import a WHOOP CSV for that).", ) } @@ -888,8 +888,8 @@ private fun OuraLocalStateNote() { ) { Icon(Icons.Filled.Info, contentDescription = null, tint = Palette.statusWarning, modifier = Modifier.size(14.dp)) Text( - "Paired locally. NOOP owns this ring while it holds the key. If you reset it again or set it " + - "up in the Oura app, NOOP no longer owns it and you would re-add it to take it over.", + tr("Paired locally. NOOP owns this ring while it holds the key. If you reset it again or set it ") + + tr("up in the Oura app, NOOP no longer owns it and you would re-add it to take it over."), style = NoopType.caption, color = Palette.statusWarning, ) @@ -897,8 +897,8 @@ private fun OuraLocalStateNote() { } private fun lastSeenLine(device: PairedDeviceRow, isLiveConnected: Boolean): String = when { - device.status == DeviceStatus.archived.name -> "Removed · data kept" - isLiveConnected -> "Connected now" + device.status == DeviceStatus.archived.name -> tr("Removed · data kept") + isLiveConnected -> tr("Connected now") else -> "Last seen ${relativeAgo(device.lastSeenAt)}" } diff --git a/android/app/src/main/java/com/noop/ui/FullDayChartScreen.kt b/android/app/src/main/java/com/noop/ui/FullDayChartScreen.kt index c0eb52498..e7824440a 100644 --- a/android/app/src/main/java/com/noop/ui/FullDayChartScreen.kt +++ b/android/app/src/main/java/com/noop/ui/FullDayChartScreen.kt @@ -151,8 +151,8 @@ fun FullDayChartScreen(vm: AppViewModel, onBack: () -> Unit) { } ScreenScaffold( - title = "Deep Timeline", - subtitle = "Every second of your day. Drag back up to 3 days.", + title = tr("Deep Timeline"), + subtitle = tr("Every second of your day. Drag back up to 3 days."), ) { // METRIC PILLS — horizontally scrollable so all six fit on a phone. Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { @@ -166,12 +166,12 @@ fun FullDayChartScreen(vm: AppViewModel, onBack: () -> Unit) { // SOURCE PILL — the owned strap, with the #574 owned/all scope toggle. Row(verticalAlignment = Alignment.CenterVertically) { - Text("My WHOOP", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("My WHOOP"), style = NoopType.footnote, color = Palette.textSecondary) Spacer(Modifier.weight(1f)) SegmentedPillControl( items = listOf(true, false), selection = ownedOnly, - label = { if (it) "Owned" else "All" }, + label = { if (it) tr("Owned") else tr("All") }, onSelect = { ownedOnly = it }, ) } @@ -216,7 +216,7 @@ fun FullDayChartScreen(vm: AppViewModel, onBack: () -> Unit) { Box(modifier = Modifier.fillMaxWidth().height(280.dp), contentAlignment = Alignment.Center) { when { loading && points.isEmpty() -> - Text("Loading the day…", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("Loading the day…"), style = NoopType.footnote, color = Palette.textTertiary) points.isEmpty() -> EmptyTimelineState(metric, ownedOnly) else -> TimelineChart( points = points, @@ -244,13 +244,13 @@ fun FullDayChartScreen(vm: AppViewModel, onBack: () -> Unit) { // ZOOM HINT + reset. Row(verticalAlignment = Alignment.CenterVertically) { Text( - if (window == null) "Pinch to zoom · drag to pan" else "Zoomed in - drag to pan", + if (window == null) tr("Pinch to zoom · drag to pan") else tr("Zoomed in - drag to pan"), style = NoopType.footnote, color = Palette.textTertiary, ) Spacer(Modifier.weight(1f)) if (window != null) { Text( - "Reset", + tr("Reset"), style = NoopType.footnote, color = Palette.accent, modifier = Modifier.clickable { window = null }, @@ -270,8 +270,8 @@ private fun EmptyTimelineState(metric: TimelineMetric, ownedOnly: Boolean) { Text("No ${metric.title.lowercase(Locale.US)} here", style = NoopType.body, color = Palette.textSecondary) Text( - if (ownedOnly) "Nothing offloaded for this window yet." - else "Other sources don’t offload raw per-second data on-device.", + if (ownedOnly) tr("Nothing offloaded for this window yet.") + else tr("Other sources don’t offload raw per-second data on-device."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, ) } diff --git a/android/app/src/main/java/com/noop/ui/FusedRecordScreen.kt b/android/app/src/main/java/com/noop/ui/FusedRecordScreen.kt index 42e709462..bd46a3922 100644 --- a/android/app/src/main/java/com/noop/ui/FusedRecordScreen.kt +++ b/android/app/src/main/java/com/noop/ui/FusedRecordScreen.kt @@ -109,13 +109,13 @@ fun FusedRecordScreen( "$dayLabel · your record, on $deviceNoun." } - ScreenScaffold(title = "Your Data, Fused", subtitle = subtitle, modifier = modifier) { + ScreenScaffold(title = tr("Your Data, Fused"), subtitle = subtitle, modifier = modifier) { if (isMultiSource) DayBadgeRow(record.dayOwner) if (record.rows.isEmpty()) { DataPendingNote( - title = "Nothing to fuse yet", - body = "Import a WHOOP export, Health Connect or a second band and your best-sourced record builds here, on this device.", + title = tr("Nothing to fuse yet"), + body = tr("Import a WHOOP export, Health Connect or a second band and your best-sourced record builds here, on this device."), ) } else { NoopCard(padding = 0.dp) { @@ -152,7 +152,7 @@ private fun DayBadgeRow(owner: FusionSource?) { val text = if (owner != null) { "Today's scores owned by ${owner.displayName}" } else { - "Scores still calibrating, no single day-owner yet" + tr("Scores still calibrating, no single day-owner yet") } Row( verticalAlignment = Alignment.CenterVertically, @@ -203,7 +203,7 @@ private fun PrivacyNote(deviceNoun: String) { @Composable private fun DisclaimerNote() { Text( - "NOOP picks the best-sourced number and shows you where each came from. It's for wellness and curiosity. It doesn't diagnose or replace medical advice.", + tr("NOOP picks the best-sourced number and shows you where each came from. It's for wellness and curiosity. It doesn't diagnose or replace medical advice."), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(horizontal = 4.dp), @@ -353,9 +353,9 @@ private fun ConflictCompareDialog(row: FusedRow, onDismiss: () -> Unit) { .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(Metrics.gap), ) { - SectionHeader(title = row.label, overline = "Sources differ") + SectionHeader(title = row.label, overline = tr("Sources differ")) Text( - "Your bands report different numbers. Here's every source, and the one NOOP is using.", + tr("Your bands report different numbers. Here's every source, and the one NOOP is using."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -397,7 +397,7 @@ private fun ConflictCompareDialog(row: FusedRow, onDismiss: () -> Unit) { horizontalArrangement = Arrangement.End, ) { TextButton(onClick = onDismiss) { - Text("Done", style = NoopType.headline, color = Palette.accent) + Text(tr("Done"), style = NoopType.headline, color = Palette.accent) } } } @@ -418,7 +418,7 @@ private fun ContributorRow( .semantics { contentDescription = "${contrib.source.displayName}, ${FusionFormat.value(contrib.value, metricKey)}" + - if (isWinner) ", in use" else "" + if (isWinner) tr(", in use") else "" }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), diff --git a/android/app/src/main/java/com/noop/ui/HealthScreen.kt b/android/app/src/main/java/com/noop/ui/HealthScreen.kt index ea7e07dd6..f5b65a693 100644 --- a/android/app/src/main/java/com/noop/ui/HealthScreen.kt +++ b/android/app/src/main/java/com/noop/ui/HealthScreen.kt @@ -141,8 +141,8 @@ fun HealthScreen( val showDayCycleBackground = remember { NoopPrefs.showDayCycleBackground(context) } LazyScreenScaffold( - title = "Health Monitor", - subtitle = "Live vitals, streamed from the strap.", + title = tr("Health Monitor"), + subtitle = tr("Live vitals, streamed from the strap."), topBackground = if (showDayCycleBackground) { { LiquidScreenSky() } } else null, ) { if (today == null && !hasLiveHr) { @@ -162,8 +162,8 @@ fun HealthScreen( item { Spacer(Modifier.height(Metrics.selectorTopUp)) } item { VitalsSection( - title = "Vital Signs", - overline = "Latest readings", + title = tr("Vital Signs"), + overline = tr("Latest readings"), trailing = null, vitals = latestVitals(days, UnitPrefs.temperature(LocalContext.current)), onVitalClick = onVitalClick, @@ -233,9 +233,9 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { val canSync = live.connected && live.bonded && !live.backfilling Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - "Sync", - overline = "Strap history", - trailing = if (live.connected) (if (live.bonded) "Connected" else "Pairing…") else "Offline", + tr("Sync"), + overline = tr("Strap history"), + trailing = if (live.connected) (if (live.bonded) tr("Connected") else tr("Pairing…")) else tr("Offline"), ) NoopCard(tint = Palette.chargeColor) { @@ -245,7 +245,7 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { when { live.backfilling -> SyncingHistoryNote(chunks = live.syncChunksThisSession) !live.connected -> StatePill( - title = "No strap connected", + title = tr("No strap connected"), tone = StrandTone.Neutral, showsDot = false, ) @@ -253,7 +253,7 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Metrics.space8), ) { - StatePill(title = "History synced", tone = StrandTone.Positive) + StatePill(title = tr("History synced"), tone = StrandTone.Positive) Text( relativeAgo(live.lastSyncAt!!), style = NoopType.footnote, @@ -261,7 +261,7 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { ) } else -> StatePill( - title = if (live.bonded) "Ready to sync" else "Pairing…", + title = if (live.bonded) tr("Ready to sync") else tr("Pairing…"), tone = StrandTone.Accent, showsDot = true, pulsing = !live.bonded, @@ -274,19 +274,19 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { // connected+bonded and not already syncing; the gated BLE entry point is a safe no-op // otherwise. (Total pending records are unknowable from the protocol, so no progress %.) NoopButton( - text = if (live.backfilling) "Syncing…" else "Sync now", + text = if (live.backfilling) tr("Syncing…") else tr("Sync now"), leadingIcon = Icons.Filled.Sync, kind = NoopButtonKind.Secondary, fullWidth = true, enabled = canSync, modifier = Modifier.semantics { contentDescription = if (canSync) { - "Sync now. Pulls your strap's stored history immediately, without waiting " + - "for the next automatic sync." + tr("Sync now. Pulls your strap's stored history immediately, without waiting ") + + tr("for the next automatic sync.") } else if (live.backfilling) { - "Sync now. A sync is already in progress." + tr("Sync now. A sync is already in progress.") } else { - "Sync now. Connect your strap first." + tr("Sync now. Connect your strap first.") } }, onClick = onSyncNow, @@ -305,12 +305,12 @@ private fun SyncStatusSection(vm: AppViewModel, onSyncNow: () -> Unit) { /** The helper line below the Sync-now button: explains the current state (syncing / offline / pairing / * ready), copy-matched to HealthView.swift's SyncStatusSection.helperText. */ private fun syncHelperText(live: LiveState): String = when { - live.backfilling -> "Pulling your strap's stored history. This drains oldest-first; a deep backlog " + - "now continues automatically across passes instead of waiting between syncs." - !live.connected -> "Connect your strap to sync its stored history. Until then, only imported data " + - "shows here." - !live.bonded -> "Finishing the pairing handshake. Sync now becomes available once the strap is paired." - else -> "Syncs your strap's stored history right away, instead of waiting for the next automatic sync." + live.backfilling -> tr("Pulling your strap's stored history. This drains oldest-first; a deep backlog ") + + tr("now continues automatically across passes instead of waiting between syncs.") + !live.connected -> tr("Connect your strap to sync its stored history. Until then, only imported data ") + + tr("shows here.") + !live.bonded -> tr("Finishing the pairing handshake. Sync now becomes available once the strap is paired.") + else -> tr("Syncs your strap's stored history right away, instead of waiting for the next automatic sync.") } // MARK: - Records & sources (Swift parity) — discoverable deep-links into the on-device records @@ -327,19 +327,19 @@ private fun RecordsAndSourcesSection( onOpenFusedRecord: () -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Records & sources", overline = "On this phone") + SectionHeader(tr("Records & sources"), overline = tr("On this phone")) RecordRow( icon = Icons.Filled.MenuBook, tint = Palette.metricCyan, - title = "Lab Book", - subtitle = "Your bloods, BP and body numbers. Kept private here.", + title = tr("Lab Book"), + subtitle = tr("Your bloods, BP and body numbers. Kept private here."), onClick = onOpenLabBook, ) RecordRow( icon = Icons.AutoMirrored.Filled.CompareArrows, tint = Palette.accent, - title = "Your Data, Fused", - subtitle = "The best-sourced number per metric, across your bands.", + title = tr("Your Data, Fused"), + subtitle = tr("The best-sourced number per metric, across your bands."), onClick = onOpenFusedRecord, ) } @@ -426,7 +426,7 @@ private fun SkinTempSuiteSection( onTurnOffCycle: () -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Skin Temperature", overline = "From your nightly readings") + SectionHeader(tr("Skin Temperature"), overline = tr("From your nightly readings")) // Illness heads-up first when it has something to say (it's the most time-sensitive card). signals?.illness?.let { illness -> @@ -448,9 +448,9 @@ private fun SkinTempSuiteSection( signals?.bodyClock?.let { BodyClockCard(estimate = it) } Text( - "Cycle phase, body-clock and illness heads-up are approximations computed on your device from " + - "your own nightly temperature, heart rate and HRV: observations about your own numbers, " + - "never a diagnosis. They never leave this phone.", + tr("Cycle phase, body-clock and illness heads-up are approximations computed on your device from ") + + tr("your own nightly temperature, heart rate and HRV: observations about your own numbers, ") + + tr("never a diagnosis. They never leave this phone."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -479,10 +479,10 @@ private fun HealthContributorsSection(day: DailyMetric?) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier.weight(1f)) { - SectionHeader("Contributors", overline = "Recovery") + SectionHeader(tr("Contributors"), overline = tr("Recovery")) } StatePill( - title = if (solid) "SOLID" else "CALIBRATING", + title = if (solid) tr("SOLID") else tr("CALIBRATING"), tone = if (solid) StrandTone.Accent else StrandTone.Neutral, ) } @@ -493,36 +493,36 @@ private fun HealthContributorsSection(day: DailyMetric?) { // hue), Sleep + Respiratory share the blue sleep world (sleepLight). Each bar reveals with // a staggered fade+rise, mirroring iOS `.staggeredAppear(index:)`. ContributorBar( - label = "HRV", + label = tr("HRV"), readout = hrv?.let { "${it.roundToInt()} ms" } ?: "—", fraction = hrv?.let { (it - 20.0) / 100.0 }, color = Palette.metricCyan, modifier = Modifier.staggeredAppear(0), ) ContributorBar( - label = "Resting HR", + label = tr("Resting HR"), readout = rhr?.let { "${it.roundToInt()} bpm" } ?: "—", fraction = rhr?.let { 1.0 - ((it - 40.0) / 40.0) }, color = Palette.chargeColor, modifier = Modifier.staggeredAppear(1), ) ContributorBar( - label = "Sleep", + label = tr("Sleep"), readout = sleepMin?.let { sleepHoursText(it) } ?: "—", fraction = sleepMin?.let { (it / 60.0) / 8.0 }, color = Palette.sleepLight, modifier = Modifier.staggeredAppear(2), ) ContributorBar( - label = "Respiratory", + label = tr("Respiratory"), readout = resp?.let { String.format(Locale.US, "%.1f rpm", it) } ?: "—", fraction = resp?.let { 1.0 - ((it - 12.0) / 8.0) }, color = Palette.sleepLight, modifier = Modifier.staggeredAppear(3), ) Text( - "Baselines learned on-device over 14 days. Bars read each signal against a " + - "typical adult range (approximate, not medical advice).", + tr("Baselines learned on-device over 14 days. Bars read each signal against a ") + + tr("typical adult range (approximate, not medical advice)."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -615,7 +615,7 @@ private fun FitnessAgeSection(vm: AppViewModel, days: List, profile var showChecklist by remember { mutableStateOf(false) } Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Fitness Age", overline = "Weekly", trailing = "± 5 yr") + SectionHeader(tr("Fitness Age"), overline = tr("Weekly"), trailing = tr("± 5 yr")) val value = fitnessAge if (value != null) { FitnessAgeHero( @@ -673,7 +673,7 @@ private fun VitalitySection(vm: AppViewModel, days: List, profile: val v = vitality; val ba = bodyAge if (v != null && ba != null) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Vitality", overline = "Weekly", trailing = "Body Age ${ba.roundToInt()}") + SectionHeader(tr("Vitality"), overline = tr("Weekly"), trailing = "Body Age ${ba.roundToInt()}") VitalityHero(vitality = v, bodyAge = ba, chronoAge = profile.age, contributions = contributions) } } @@ -694,7 +694,7 @@ private fun VitalityHero( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Vitality") + Overline(tr("Vitality")) // The Vitality 0–100 rides a filling LiquidVessel on the charge world, the count-up // number rolled up over it (white, tabular) — the Today HeroScoreVessel idiom. Same // value + fraction (vitality / 100) as the bare headline this replaced. @@ -704,10 +704,10 @@ private fun VitalityHero( tint = Palette.chargeColor, diameter = 96.dp, ) - Text("out of 100", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("out of 100"), style = NoopType.footnote, color = Palette.textTertiary) } Column(horizontalAlignment = Alignment.End) { - Overline("Body Age") + Overline(tr("Body Age")) CountUpText( value = bodyAge, format = { it.roundToInt().toString() }, @@ -715,7 +715,7 @@ private fun VitalityHero( color = Palette.textPrimary, ) Text( - if (delta == 0) "about your age" + if (delta == 0) tr("about your age") else "${kotlin.math.abs(delta)} ${yearWord(delta)} ${if (younger) "younger" else "older"}", style = NoopType.footnote, color = if (delta == 0) Palette.textSecondary @@ -730,7 +730,7 @@ private fun VitalityHero( Text("Holding you back: ${worst.label}", style = NoopType.footnote, color = Palette.statusWarning) } Text( - "A wellness estimate from your habits, not a clinical biological age.", + tr("A wellness estimate from your habits, not a clinical biological age."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -815,7 +815,7 @@ private fun FitnessAgeHero( val deltaYears = (chronoAge - fitnessAge).roundToInt() val younger = fitnessAge < chronoAge val deltaWord = when { - deltaYears == 0 -> "About your age" + deltaYears == 0 -> tr("About your age") younger -> "$deltaYears ${yearWord(deltaYears)} younger than your age" else -> "${kotlin.math.abs(deltaYears)} ${yearWord(deltaYears)} older than your age" } @@ -835,7 +835,7 @@ private fun FitnessAgeHero( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Fitness Age") + Overline(tr("Fitness Age")) // The hero age rides a filling LiquidVessel on the gold Charge world, the age number // rolled up over it (white, tabular) — the Today HeroScoreVessel idiom. The shown NUMBER // is the same value (fitnessAge, rounded) as the bare headline this replaced. @@ -862,7 +862,7 @@ private fun FitnessAgeHero( } Text( - text = "± 5 yr · a fitness comparison, not a biological age", + text = tr("± 5 yr · a fitness comparison, not a biological age"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -878,12 +878,12 @@ private fun FitnessAgeHero( onClick = onHowAccurate, ) .padding(vertical = Metrics.space4) - .semantics { contentDescription = "How accurate is this Fitness Age?" }, + .semantics { contentDescription = tr("How accurate is this Fitness Age?") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Metrics.space6), ) { Text( - "How accurate is this?", + tr("How accurate is this?"), style = NoopType.captionNumber, color = Palette.accent, ) @@ -914,24 +914,24 @@ private fun FitnessReadinessCard(readiness: FitnessAgeReadiness, headed: Boolean if (headed) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space4)) { Text( - "A few more days and we can show your Fitness Age", + tr("A few more days and we can show your Fitness Age"), style = NoopType.headline, color = Palette.textPrimary, ) Text( - "It compares your resting heart rate and recent activity against people your age. " + - "Wear your strap for a full week and it appears here.", + tr("It compares your resting heart rate and recent activity against people your age. ") + + tr("Wear your strap for a full week and it appears here."), style = NoopType.subhead, color = Palette.textSecondary, ) } } - ReadinessGroup(title = "Drives your Fitness Age", items = drivesAge) - ReadinessGroup(title = "Unlocks your VO₂max", items = unlocksVo2) + ReadinessGroup(title = tr("Drives your Fitness Age"), items = drivesAge) + ReadinessGroup(title = tr("Unlocks your VO₂max"), items = unlocksVo2) Text( - "Weight, height and waist add a VO₂max estimate. They don't change the Fitness Age itself.", + tr("Weight, height and waist add a VO₂max estimate. They don't change the Fitness Age itself."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1012,18 +1012,18 @@ fun VitalSignsScreen(vm: AppViewModel, onVitalClick: (String) -> Unit = {}) { } ScreenScaffold( - title = "Vital Signs", - subtitle = "Historical vitals from your cached daily metrics.", + title = tr("Vital Signs"), + subtitle = tr("Historical vitals from your cached daily metrics."), ) { RecentDaySelectorBar(selectedOffset = selectedDayOffset, onSelect = { selectedDayOffset = it }) if (selectedMetric == null || vitals.all { it.value == null }) { DataPendingNote( title = missingVitalsTitle(selectedDayOffset), - body = "Try Yesterday or 2 days ago from the bar above if the strap or import did not produce a daily vitals snapshot yet.", + body = tr("Try Yesterday or 2 days ago from the bar above if the strap or import did not produce a daily vitals snapshot yet."), ) } else { VitalsSection( - title = "Vital Signs", + title = tr("Vital Signs"), overline = selectedDayLabel(selectedDayOffset), trailing = "as of ${selectedMetric.day}", vitals = vitals, @@ -1166,9 +1166,9 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - title = "Heart Rate", - overline = "Live", - trailing = if (derived) "from R-R" else null, + title = tr("Heart Rate"), + overline = tr("Live"), + trailing = if (derived) tr("from R-R") else null, ) // The live HR hero is Apple-flat — a plain card tinted rose (heart-rate's metric accent) over a @@ -1188,12 +1188,12 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { - Text("Heart Rate", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Heart Rate"), style = NoopType.headline, color = Palette.textPrimary) Text( text = when { - derived -> "Estimated from R-R interval" - hasLiveHr -> "Streaming live" - else -> "Awaiting strap" + derived -> tr("Estimated from R-R interval") + hasLiveHr -> tr("Streaming live") + else -> tr("Awaiting strap") }, style = NoopType.footnote, color = Palette.textSecondary, @@ -1216,7 +1216,7 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { contentDescription = if (hasLiveHr) { "Live heart rate over time, $displayHr beats per minute, zone $zone" } else { - "Live heart rate over time, no data" + tr("Live heart rate over time, no data") } }, ) { @@ -1266,7 +1266,7 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { zone = if (hasLiveHr) "Z$zone" else "—", percentMax = if (hasLiveHr) "${(fraction * 100).roundToInt()}%" else "—", maxHr = "$hrMax", - state = if (hasLiveHr) "STREAMING" else "IDLE", + state = if (hasLiveHr) tr("STREAMING") else tr("IDLE"), ) } } @@ -1275,17 +1275,17 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { } private fun zoneLabel(hasLiveHr: Boolean, zone: Int, fraction: Double): String { - if (!hasLiveHr) return "Idle" + if (!hasLiveHr) return tr("Idle") return "Zone $zone · ${(fraction * 100).roundToInt()}%" } @Composable private fun HeartRateFooter(zone: String, percentMax: String, maxHr: String, state: String) { Row(modifier = Modifier.fillMaxWidth().padding(top = Metrics.space4)) { - FooterStat("Zone", zone, Modifier.weight(1f)) + FooterStat(tr("Zone"), zone, Modifier.weight(1f)) FooterStat("% Max", percentMax, Modifier.weight(1f)) - FooterStat("Max HR", maxHr, Modifier.weight(1f)) - FooterStat("State", state, Modifier.weight(1f)) + FooterStat(tr("Max HR"), maxHr, Modifier.weight(1f)) + FooterStat(tr("State"), state, Modifier.weight(1f)) } } @@ -1478,10 +1478,10 @@ private fun VitalsSection( if (footer) { Text( - text = "SpO₂, respiratory rate and skin temperature are sleep-window " + - "aggregates from your most recent imported day; resting HR and HRV update daily. " + - "Once NOOP has 14 nights of history, in-range compares each vital to your own " + - "baseline (approximate, not medical advice); until then typical adult ranges apply.", + text = tr("SpO₂, respiratory rate and skin temperature are sleep-window ") + + tr("aggregates from your most recent imported day; resting HR and HRV update daily. ") + + tr("Once NOOP has 14 nights of history, in-range compares each vital to your own ") + + tr("baseline (approximate, not medical advice); until then typical adult ranges apply."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1523,11 +1523,11 @@ private data class Vital( /** The in-range caption that stands in for a StatePill inside the fixed-height tile. * The wording says which yardstick judged it: your baseline vs typical ranges. */ val stateCaption: String = when { - banding.band == VitalBands.Band.NO_DATA -> "No data" + banding.band == VitalBands.Band.NO_DATA -> tr("No data") banding.basis == VitalBands.Basis.PERSONAL -> - if (banding.band == VitalBands.Band.IN_RANGE) "In your range" else "Off your baseline" + if (banding.band == VitalBands.Band.IN_RANGE) tr("In your range") else tr("Off your baseline") else -> - if (banding.band == VitalBands.Band.IN_RANGE) "In typical range" else "Outside typical range" + if (banding.band == VitalBands.Band.IN_RANGE) tr("In typical range") else tr("Outside typical range") } val accessibilityText: String = @@ -1622,7 +1622,7 @@ private fun vitalsFor( ) return listOf( Vital( - key = "resp", label = "Resp Rate", unit = "rpm", + key = "resp", label = tr("Resp Rate"), unit = "rpm", value = d?.respRateBpm, format = { String.format("%.1f", it) }, deltaText = deltaText(d?.respRateBpm, previous { it.respRateBpm }), readingDay = todayKey, @@ -1633,7 +1633,7 @@ private fun vitalsFor( sparkline = trail(d?.respRateBpm) { it.respRateBpm }, ), Vital( - key = "spo2", label = "Blood O₂", unit = "%", + key = "spo2", label = tr("Blood O₂"), unit = "%", value = d?.spo2Pct, format = { String.format("%.0f", it) }, deltaText = deltaText(d?.spo2Pct, previous { it.spo2Pct }, decimals = 0), readingDay = todayKey, @@ -1646,7 +1646,7 @@ private fun vitalsFor( sparkline = trail(d?.spo2Pct) { it.spo2Pct }, ), Vital( - key = "rhr", label = "Resting HR", unit = "bpm", + key = "rhr", label = tr("Resting HR"), unit = "bpm", value = d?.restingHr?.toDouble(), format = { it.roundToInt().toString() }, deltaText = deltaText(d?.restingHr?.toDouble(), previous { it.restingHr?.toDouble() }, decimals = 0), readingDay = todayKey, @@ -1660,7 +1660,7 @@ private fun vitalsFor( sparkline = trail(d?.restingHr?.toDouble()) { it.restingHr?.toDouble() }, ), Vital( - key = "hrv", label = "HRV", unit = "ms", + key = "hrv", label = tr("HRV"), unit = "ms", value = d?.avgHrv, format = { it.roundToInt().toString() }, deltaText = deltaText(d?.avgHrv, previous { it.avgHrv }, decimals = 0), readingDay = todayKey, @@ -1671,7 +1671,7 @@ private fun vitalsFor( sparkline = trail(d?.avgHrv) { it.avgHrv }, ), Vital( - key = "skin", label = "Skin Temp", unit = skinUnitLabel, + key = "skin", label = tr("Skin Temp"), unit = skinUnitLabel, value = skin, format = skinFormat, deltaText = deltaText(skin, previousSkin), readingDay = todayKey, @@ -1837,19 +1837,19 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { ScreenScaffold( title = detail?.title ?: "Vital Signs", - subtitle = "Historical trend from cached daily metrics.", + subtitle = tr("Historical trend from cached daily metrics."), ) { if (isSeriesBacked && !seriesLoaded) { DataPendingNote( - title = "Loading…", - body = "Fetching this metric's history.", + title = tr("Loading…"), + body = tr("Fetching this metric's history."), ) return@ScreenScaffold } if (detail == null || detail.points.size < 2) { DataPendingNote( - title = "Not enough history yet", - body = "This vital needs at least two historical readings before NOOP can chart it.", + title = tr("Not enough history yet"), + body = tr("This vital needs at least two historical readings before NOOP can chart it."), ) return@ScreenScaffold } @@ -1862,8 +1862,8 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { val filteredPoints = remember(detail, effectiveRange) { filterVitalPoints(detail.points, effectiveRange) } if (filteredPoints.size < 2) { DataPendingNote( - title = "Not enough history in this range", - body = "Try a longer interval like 3M, 6M, 1Y, or ALL to see this vital’s trend.", + title = tr("Not enough history in this range"), + body = tr("Try a longer interval like 3M, 6M, 1Y, or ALL to see this vital’s trend."), ) return@ScreenScaffold } @@ -1874,12 +1874,12 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { val max = values.maxOrNull() val avg = values.average() - SectionHeader(detail.title, overline = "Vital Signs", trailing = "${filteredPoints.size} readings") + SectionHeader(detail.title, overline = tr("Vital Signs"), trailing = "${filteredPoints.size} readings") NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Latest") + Overline(tr("Latest")) Text( text = "${detail.format(latest.second)} ${detail.unit}".trim(), style = NoopType.chartValueLarge, @@ -1901,7 +1901,7 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { ) if (unlockedRanges.size < VitalDetailRange.entries.size) { Text( - "Longer ranges unlock as more history builds.", + tr("Longer ranges unlock as more history builds."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1921,9 +1921,9 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { ) Row(modifier = Modifier.fillMaxWidth()) { listOf( - "Min" to min, - "Avg" to avg, - "Max" to max, + tr("Min") to min, + tr("Avg") to avg, + tr("Max") to max, ).forEach { (label, metric) -> Column(modifier = Modifier.weight(1f)) { Overline(label, color = Palette.textTertiary) @@ -1971,15 +1971,15 @@ private fun latestVital( } private fun selectedDayLabel(offset: Int): String = when (offset) { - 0 -> "Today" - 1 -> "Yesterday" - else -> "2 days ago" + 0 -> tr("Today") + 1 -> tr("Yesterday") + else -> tr("2 days ago") } private fun missingVitalsTitle(offset: Int): String = when (offset) { - 0 -> "We didn't get today's data" - 1 -> "We didn't get yesterday's data" - else -> "We didn't get data from 2 days ago" + 0 -> tr("We didn't get today's data") + 1 -> tr("We didn't get yesterday's data") + else -> tr("We didn't get data from 2 days ago") } private fun asOfLabel(day: String?): String? { @@ -1987,8 +1987,8 @@ private fun asOfLabel(day: String?): String? { val date = runCatching { LocalDate.parse(day) }.getOrNull() ?: return "as of $day" val today = LocalDate.now() return when (date) { - today -> "as of today" - today.minusDays(1) -> "as of yesterday" + today -> tr("as of today") + today.minusDays(1) -> tr("as of yesterday") else -> "as of ${date.format(DateTimeFormatter.ofPattern("d MMM", Locale.US))}" } } @@ -2069,7 +2069,7 @@ private fun buildVitalDetail( return when (key) { "resp" -> VitalDetailModel( key = key, - title = "Respiratory Rate", + title = tr("Respiratory Rate"), unit = "rpm", color = Palette.metricCyan, points = days.mapNotNull { it.respRateBpm?.let { value -> it.day to value } }, @@ -2077,7 +2077,7 @@ private fun buildVitalDetail( ) "spo2" -> VitalDetailModel( key = key, - title = "Blood Oxygen", + title = tr("Blood Oxygen"), unit = "%", color = Palette.metricCyan, points = days.mapNotNull { it.spo2Pct?.let { value -> it.day to value } }, @@ -2085,7 +2085,7 @@ private fun buildVitalDetail( ) "rhr" -> VitalDetailModel( key = key, - title = "Resting Heart Rate", + title = tr("Resting Heart Rate"), unit = "bpm", color = Palette.metricRose, points = days.mapNotNull { it.restingHr?.toDouble()?.let { value -> it.day to value } }, @@ -2093,7 +2093,7 @@ private fun buildVitalDetail( ) "hrv" -> VitalDetailModel( key = key, - title = "Heart Rate Variability", + title = tr("Heart Rate Variability"), unit = "ms", color = Palette.metricPurple, points = days.mapNotNull { it.avgHrv?.let { value -> it.day to value } }, @@ -2113,7 +2113,7 @@ private fun buildVitalDetail( } VitalDetailModel( key = key, - title = "Skin Temperature", + title = tr("Skin Temperature"), unit = unit, color = Palette.metricAmber, points = days.mapNotNull { row -> @@ -2135,7 +2135,7 @@ private fun buildVitalDetail( private suspend fun buildSeriesVitalDetail(vm: AppViewModel, key: String): VitalDetailModel? = when (key) { "fitness_age" -> VitalDetailModel( key = key, - title = "Fitness Age", + title = tr("Fitness Age"), unit = "yrs", color = Palette.chargeColor, points = vm.repo.metricSeries(COMPUTED_SOURCE, "fitness_age", "0000-01-01", "9999-12-31") @@ -2144,7 +2144,7 @@ private suspend fun buildSeriesVitalDetail(vm: AppViewModel, key: String): Vital ) "vitality" -> VitalDetailModel( key = key, - title = "Vitality", + title = tr("Vitality"), unit = "", color = Palette.metricPurple, points = vm.repo.metricSeries(COMPUTED_SOURCE, "vitality", "0000-01-01", "9999-12-31") @@ -2153,7 +2153,7 @@ private suspend fun buildSeriesVitalDetail(vm: AppViewModel, key: String): Vital ) "steps_est" -> VitalDetailModel( key = key, - title = "Steps", + title = tr("Steps"), unit = "steps", color = Palette.metricCyan, points = vm.repo.resolvedSeries("steps_est", "my-whoop", "0000-00-00", "9999-99-99").values, @@ -2171,7 +2171,7 @@ private suspend fun buildSeriesVitalDetail(vm: AppViewModel, key: String): Vital for (r in rows) r.activeKcal?.let { byDay.putIfAbsent(r.day, it) } VitalDetailModel( key = key, - title = "Active Energy", + title = tr("Active Energy"), unit = "kcal", color = Palette.metricAmber, points = byDay.entries.sortedBy { it.key }.map { it.key to it.value }, @@ -2186,8 +2186,8 @@ private suspend fun buildSeriesVitalDetail(vm: AppViewModel, key: String): Vital @Composable private fun HealthEmptyState() { DataPendingNote( - title = "No biometrics yet", - body = "No biometrics yet. Import your WHOOP export (and Apple Health if you " + - "have it) in Data Sources to fill this in.", + title = tr("No biometrics yet"), + body = tr("No biometrics yet. Import your WHOOP export (and Apple Health if you ") + + tr("have it) in Data Sources to fill this in."), ) } diff --git a/android/app/src/main/java/com/noop/ui/HowNoopWorksScreen.kt b/android/app/src/main/java/com/noop/ui/HowNoopWorksScreen.kt index 1a9ca9ff4..c014f655e 100644 --- a/android/app/src/main/java/com/noop/ui/HowNoopWorksScreen.kt +++ b/android/app/src/main/java/com/noop/ui/HowNoopWorksScreen.kt @@ -64,27 +64,27 @@ private enum class PrimerSection( val icon: ImageVector, ) { SLEEP( - title = "How your sleep is sorted", - body = "NOOP picks your main sleep as your longest real block, and (once it has " + - "learned your usual hours) the one nearest your normal sleep time. Everything " + - "else that day is a nap. You can always edit bed and wake times.", + title = tr("How your sleep is sorted"), + body = tr("NOOP picks your main sleep as your longest real block, and (once it has ") + + tr("learned your usual hours) the one nearest your normal sleep time. Everything ") + + tr("else that day is a nap. You can always edit bed and wake times."), icon = Icons.Filled.Bedtime, ), SCORES( - title = "How your scores work", - body = "Charge, Effort and Rest are scored on your own device from your strap data. " + - "Charge needs about four nights of sleep to learn your baseline (that's \"Calibrating\", " + - "counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. " + + title = tr("How your scores work"), + body = tr("Charge, Effort and Rest are scored on your own device from your strap data. ") + + tr("Charge needs about four nights of sleep to learn your baseline (that's \"Calibrating\", ") + + tr("counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. ") + "On a WHOOP 5 or MG the strap banks little history, so that count can sit at 0 of 4 until you " + "have worn it across a few nights. That's the strap's sync limit, not a fault. " + "Before there's a number, NOOP shows what it can without faking one.", icon = Icons.Filled.Insights, ), SCORE_RECIPE( - title = "How your scores are computed", - body = "Charge weighs five signals against your own baseline: your overnight HRV matters most, " + - "then your resting heart rate, how well you slept, your breathing rate, and how far your skin " + - "temperature drifted from normal. Higher HRV and lower resting heart rate lift Charge; a big " + + title = tr("How your scores are computed"), + body = tr("Charge weighs five signals against your own baseline: your overnight HRV matters most, ") + + tr("then your resting heart rate, how well you slept, your breathing rate, and how far your skin ") + + tr("temperature drifted from normal. Higher HRV and lower resting heart rate lift Charge; a big ") + "skin-temperature drift in either direction lowers it. Each signal is measured as how far " + "tonight sits from your personal baseline, never an absolute target. If a signal is missing, " + "it's dropped and the rest are reweighted, so the number always reflects only what was " + @@ -93,15 +93,15 @@ private enum class PrimerSection( icon = Icons.Filled.Calculate, ), RECORDING( - title = "What \"recording\" means", - body = "When your strap is connected NOOP is saving data live. \"Last synced\" tells " + - "you how fresh it is. If it says \"Not recording\", reconnect.", + title = tr("What \"recording\" means"), + body = tr("When your strap is connected NOOP is saving data live. \"Last synced\" tells ") + + tr("you how fresh it is. If it says \"Not recording\", reconnect."), icon = Icons.Filled.Sensors, ), PROVENANCE( - title = "Where your numbers come from", - body = "A badge shows whether a number was scored on-device by NOOP, or imported " + - "from Whoop or Apple Health.", + title = tr("Where your numbers come from"), + body = tr("A badge shows whether a number was scored on-device by NOOP, or imported ") + + tr("from Whoop or Apple Health."), icon = Icons.Filled.Verified, ); @@ -174,10 +174,10 @@ private fun Header(onClose: () -> Unit) { modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Overline("The basics", color = Palette.textTertiary) - Text("How NOOP works", style = NoopType.display(26f), color = Palette.textPrimary) + Overline(tr("The basics"), color = Palette.textTertiary) + Text(tr("How NOOP works"), style = NoopType.display(26f), color = Palette.textPrimary) Text( - "Sleep · scores · recording · where your numbers come from", + tr("Sleep · scores · recording · where your numbers come from"), style = NoopType.caption, color = Palette.textSecondary, ) @@ -185,7 +185,7 @@ private fun Header(onClose: () -> Unit) { IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) { Icon( Icons.Filled.Close, - contentDescription = "Close", + contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(22.dp), ) @@ -199,11 +199,11 @@ private fun Header(onClose: () -> Unit) { private fun IntroCard() { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("The one rule") + Overline(tr("The one rule")) Text( - "NOOP never shows you a number it had to make up. If a score isn't ready, " + - "it tells you why and what to do next. Everything here runs on your " + - "device, from your strap.", + tr("NOOP never shows you a number it had to make up. If a score isn't ready, ") + + tr("it tells you why and what to do next. Everything here runs on your ") + + tr("device, from your strap."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -257,8 +257,8 @@ private fun PrimerCard(section: PrimerSection) { @Composable private fun FooterNote() { Text( - "NOOP never makes up a number. When it can't compute one honestly it tells you " + - "what's missing and what to do, rather than showing a fake value.", + tr("NOOP never makes up a number. When it can't compute one honestly it tells you ") + + tr("what's missing and what to do, rather than showing a fake value."), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier @@ -282,7 +282,7 @@ private fun Footer(onClose: () -> Unit) { contentColor = Palette.surfaceBase, ), ) { - Text("Got it", style = NoopType.captionNumber) + Text(tr("Got it"), style = NoopType.captionNumber) } } } diff --git a/android/app/src/main/java/com/noop/ui/HrvSnapshotScreen.kt b/android/app/src/main/java/com/noop/ui/HrvSnapshotScreen.kt index cd1bd8d0c..be51d4330 100644 --- a/android/app/src/main/java/com/noop/ui/HrvSnapshotScreen.kt +++ b/android/app/src/main/java/com/noop/ui/HrvSnapshotScreen.kt @@ -160,28 +160,28 @@ fun HrvSnapshotScreen( // items. Conditional sections use `if (cond) { item {} }` so a hidden result/hint adds no row. Order + // spacing identical (LazyColumn reproduces the eager `spacedBy(20.dp)`). LazyScreenScaffold( - title = "HRV Reading", - subtitle = "A still, seated snapshot of your heart-rate variability", + title = tr("HRV Reading"), + subtitle = tr("A still, seated snapshot of your heart-rate variability"), ) { // Status row. item { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { when (phase) { - HrvPhase.Idle -> StatePill("Ready", tone = StrandTone.Neutral) - HrvPhase.Capturing -> StatePill("Capturing", tone = StrandTone.Accent, pulsing = true) - HrvPhase.Done -> StatePill("Reading complete", tone = StrandTone.Positive) + HrvPhase.Idle -> StatePill(tr("Ready"), tone = StrandTone.Neutral) + HrvPhase.Capturing -> StatePill(tr("Capturing"), tone = StrandTone.Accent, pulsing = true) + HrvPhase.Done -> StatePill(tr("Reading complete"), tone = StrandTone.Positive) } Spacer(Modifier.width(8.dp)) if (bonded) { - StatePill("Strap live", tone = StrandTone.Positive) + StatePill(tr("Strap live"), tone = StrandTone.Positive) } else { - StatePill("Not connected", tone = StrandTone.Warning) + StatePill(tr("Not connected"), tone = StrandTone.Warning) } Spacer(Modifier.weight(1f)) IconButton(onClick = onClose) { Icon( Icons.Filled.Close, - contentDescription = "Close HRV reading", + contentDescription = tr("Close HRV reading"), tint = Palette.textTertiary, ) } @@ -210,7 +210,7 @@ fun HrvSnapshotScreen( CaptureDial( fraction = captureFraction(phase, secondsRemaining), value = dialValue(phase, runningRmssd, result), - unit = if (phase == HrvPhase.Idle) "RMSSD" else "MS RMSSD", + unit = if (phase == HrvPhase.Idle) tr("RMSSD") else tr("MS RMSSD"), sub = if (phase == HrvPhase.Capturing) { "${secondsRemaining}s left · ${captureBuffer.value.size} beats" } else null, @@ -285,7 +285,7 @@ fun HrvSnapshotScreen( colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.accent), ) { Icon(Icons.Filled.Download, contentDescription = null, modifier = Modifier.padding(end = 6.dp)) - Text(if (saved) "Saved" else "Save", style = NoopType.body) + Text(if (saved) tr("Saved") else tr("Save"), style = NoopType.body) } } } @@ -303,11 +303,11 @@ fun HrvSnapshotScreen( item { NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("How this is measured") + Overline(tr("How this is measured")) Text( - "A 60-second snapshot of your beat-to-beat (R-R) intervals from the strap, cleaned " + - "(range and ectopic-beat filtering) before computing RMSSD the same way your " + - "overnight HRV is computed.", + tr("A 60-second snapshot of your beat-to-beat (R-R) intervals from the strap, cleaned ") + + tr("(range and ectopic-beat filtering) before computing RMSSD the same way your ") + + tr("overnight HRV is computed."), style = NoopType.footnote, color = Palette.textTertiary, ) Text( @@ -396,7 +396,7 @@ private fun CaptureDial(fraction: Float, value: String, unit: String, sub: Strin private fun ResultCard(result: HrvAnalyzer.HrvResult) { NoopCard(padding = 18.dp, tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("Your reading") + Overline(tr("Your reading")) if (result.rmssd == null) { Row( @@ -414,30 +414,30 @@ private fun ResultCard(result: HrvAnalyzer.HrvResult) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "RMSSD", + label = tr("RMSSD"), value = formatHrv(result.rmssd, "%.0f"), caption = "ms", accent = Palette.metricPurple, ) StatTile( modifier = Modifier.weight(1f), - label = "SDNN", + label = tr("SDNN"), value = formatHrv(result.sdnn, "%.0f"), caption = "ms", accent = Palette.restBright, ) StatTile( modifier = Modifier.weight(1f), - label = "Mean HR", + label = tr("Mean HR"), value = formatHrv(meanHr(result.meanNN), "%.0f"), caption = "bpm", accent = Palette.metricRose, ) StatTile( modifier = Modifier.weight(1f), - label = "Beats", + label = tr("Beats"), value = "${result.nClean}", - caption = "used", + caption = tr("used"), accent = Palette.metricCyan, ) } @@ -461,8 +461,8 @@ private fun NotBondedHint() { ) { Icon(Icons.Filled.MonitorHeart, contentDescription = null, tint = Palette.statusWarning) Text( - "An HRV reading needs the live R-R stream. Open the Live screen and connect your strap, " + - "then come back.", + tr("An HRV reading needs the live R-R stream. Open the Live screen and connect your strap, ") + + tr("then come back."), style = NoopType.footnote, color = Palette.textSecondary, ) } @@ -496,23 +496,23 @@ private fun dialValue(phase: HrvPhase, runningRmssd: Double?, result: HrvAnalyze } private fun primaryLabel(phase: HrvPhase): String = when (phase) { - HrvPhase.Idle -> "Take an HRV reading" - HrvPhase.Capturing -> "Cancel" - HrvPhase.Done -> "Take another reading" + HrvPhase.Idle -> tr("Take an HRV reading") + HrvPhase.Capturing -> tr("Cancel") + HrvPhase.Done -> tr("Take another reading") } private fun instruction(phase: HrvPhase, bonded: Boolean, result: HrvAnalyzer.HrvResult?): String = when (phase) { HrvPhase.Idle -> if (bonded) { - "Sit still and breathe normally. Tap below to take a 60-second reading." + tr("Sit still and breathe normally. Tap below to take a 60-second reading.") } else { - "Connect your strap on the Live screen to take a reading." + tr("Connect your strap on the Live screen to take a reading.") } - HrvPhase.Capturing -> "Sit still, breathe normally. Keep your wrist relaxed and steady." + HrvPhase.Capturing -> tr("Sit still, breathe normally. Keep your wrist relaxed and steady.") HrvPhase.Done -> if (result != null && result.rmssd == null) { - "Not enough clean beats - sit still and try again." + tr("Not enough clean beats - sit still and try again.") } else { - "Done. Save this reading to keep it in your trends." + tr("Done. Save this reading to keep it in your trends.") } } diff --git a/android/app/src/main/java/com/noop/ui/HydrationScreen.kt b/android/app/src/main/java/com/noop/ui/HydrationScreen.kt index 1a3403f9d..a00a1b3f4 100644 --- a/android/app/src/main/java/com/noop/ui/HydrationScreen.kt +++ b/android/app/src/main/java/com/noop/ui/HydrationScreen.kt @@ -176,8 +176,8 @@ fun HydrationScreen(viewModel: AppViewModel) { // pattern — LiquidScreenSky.kt), replacing the classic flat canvas. Gated on the day-cycle pref, so an // opted-out user still gets the plain surface. Mirrors the liquid Today scaffold. LazyScreenScaffold( - title = "Hydration", - subtitle = "Your fluid intake today, on this phone only.", + title = tr("Hydration"), + subtitle = tr("Your fluid intake today, on this phone only."), topBackground = if (showDayCycleBackground) { { LiquidScreenSky() } } else null, ) { // HERO — the day's intake as a LiquidVessel (water in a vessel: the literal fit), with the litre @@ -263,19 +263,19 @@ fun HydrationScreen(viewModel: AppViewModel) { item { Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { LiquidLogTile( - label = "Sip", + label = tr("Sip"), icon = Icons.Filled.WaterDrop, accent = accent, modifier = Modifier.weight(1f), ) { log(HydrationGoal.SIP_ML) } LiquidLogTile( - label = "Cup", + label = tr("Cup"), icon = Icons.Filled.LocalDrink, accent = accent, modifier = Modifier.weight(1f), ) { log(HydrationGoal.CUP_ML) } LiquidLogTile( - label = "Bottle", + label = tr("Bottle"), icon = Icons.Filled.LocalDrink, accent = accent, modifier = Modifier.weight(1f), @@ -287,7 +287,7 @@ fun HydrationScreen(viewModel: AppViewModel) { // NoopButton — it carries its own press feedback and this is a secondary affordance, not a quick-log.) item { NoopButton( - text = "Custom amount", + text = tr("Custom amount"), leadingIcon = Icons.Filled.Add, kind = NoopButtonKind.Secondary, modifier = Modifier.fillMaxWidth(), @@ -306,7 +306,7 @@ fun HydrationScreen(viewModel: AppViewModel) { item { NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Last 7 days") + Overline(tr("Last 7 days")) HydrationHistoryBars(history = history, goalMl = goalMl, accent = accent) } } @@ -317,10 +317,10 @@ fun HydrationScreen(viewModel: AppViewModel) { item { NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Today") + Overline(tr("Today")) if (totalMl <= 0.0) { Text( - "No drinks logged yet. Tap Sip, Cup or Bottle to start.", + tr("No drinks logged yet. Tap Sip, Cup or Bottle to start."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -337,7 +337,7 @@ fun HydrationScreen(viewModel: AppViewModel) { ) Spacer(Modifier.width(10.dp)) Text( - "Logged today", + tr("Logged today"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -362,7 +362,7 @@ fun HydrationScreen(viewModel: AppViewModel) { ) { remove(last) } } NoopButton( - text = "Clear today", + text = tr("Clear today"), leadingIcon = Icons.Filled.Delete, kind = NoopButtonKind.Secondary, modifier = Modifier.weight(1f), @@ -375,7 +375,7 @@ fun HydrationScreen(viewModel: AppViewModel) { item { Text( - "A simple goal that adjusts to your effort. General wellness guidance, not medical advice.", + tr("A simple goal that adjusts to your effort. General wellness guidance, not medical advice."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Start, @@ -444,7 +444,7 @@ private fun HydrationHistoryBars( accent: Color, ) { if (history.isEmpty()) { - Text("No history yet.", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("No history yet."), style = NoopType.footnote, color = Palette.textTertiary) return } val goal = goalMl.coerceAtLeast(1).toDouble() @@ -536,7 +536,7 @@ private fun CustomAmountDialog( modifier = Modifier.size(20.dp), ) Spacer(Modifier.width(10.dp)) - Text("Custom amount", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Custom amount"), style = NoopType.title2, color = Palette.textPrimary) } }, text = { @@ -544,7 +544,7 @@ private fun CustomAmountDialog( OutlinedTextField( value = text, onValueChange = { new -> text = new.filter { it.isDigit() }.take(5) }, - label = { Text("Millilitres (ml)", style = NoopType.footnote) }, + label = { Text(tr("Millilitres (ml)"), style = NoopType.footnote) }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), colors = OutlinedTextFieldDefaults.colors( @@ -569,12 +569,12 @@ private fun CustomAmountDialog( }, confirmButton = { TextButton(onClick = { parsed?.let(onConfirm) }, enabled = parsed != null) { - Text("Log", color = if (parsed != null) accent else Palette.textTertiary) + Text(tr("Log"), color = if (parsed != null) accent else Palette.textTertiary) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel", color = Palette.textSecondary) + Text(tr("Cancel"), color = Palette.textSecondary) } }, ) diff --git a/android/app/src/main/java/com/noop/ui/InsightsHubScreen.kt b/android/app/src/main/java/com/noop/ui/InsightsHubScreen.kt index d51d5b30e..be8f3c65d 100644 --- a/android/app/src/main/java/com/noop/ui/InsightsHubScreen.kt +++ b/android/app/src/main/java/com/noop/ui/InsightsHubScreen.kt @@ -97,11 +97,11 @@ fun InsightsHubScreen(vm: AppViewModel) { // PERF (#707): lazy scaffold — each section (and its standalone Spacer, a real child of the eager // `spacedBy(20.dp)` Column) becomes one `item { }`, so the LazyColumn's matching `spacedBy(20.dp)` // reproduces identical spacing and only on-screen sections compose + are semantics-walked. - LazyScreenScaffold(title = "Insights", subtitle = "Patterns in your own data: association, not cause.") { + LazyScreenScaffold(title = tr("Insights"), subtitle = tr("Patterns in your own data: association, not cause.")) { if (!state.loaded) { item { NoopCard { - Text("Reading your journal and outcomes…", style = NoopType.subhead, color = Palette.textTertiary) + Text(tr("Reading your journal and outcomes…"), style = NoopType.subhead, color = Palette.textTertiary) } } return@LazyScreenScaffold @@ -121,11 +121,11 @@ fun InsightsHubScreen(vm: AppViewModel) { item { NoopCard { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Overline("How to read this", color = Palette.textTertiary) + Overline(tr("How to read this"), color = Palette.textTertiary) Text( - "Everything here is a pattern in your own logged days: an association with an " + - "effect size and confidence, never a cause or a diagnosis. Population patterns " + - "are shown as “typical” and are always overridden by your own data once " + + tr("Everything here is a pattern in your own logged days: an association with an ") + + tr("effect size and confidence, never a cause or a diagnosis. Population patterns ") + + tr("are shown as “typical” and are always overridden by your own data once ") + "you have enough of it. Approximations, not WHOOP’s scores; not a medical device.", style = NoopType.footnote, color = Palette.textTertiary, @@ -149,7 +149,7 @@ private fun MoversSection( // control can't share a row with the weighted header without compressing (matches macOS). SectionHeader( "What moves your ${outcome.outcomeName.lowercase(Locale.US)}", - overline = "Ranked · your data", + overline = tr("Ranked · your data"), ) SegmentedPillControl( items = InsightsOutcome.entries.toList(), @@ -161,9 +161,9 @@ private fun MoversSection( if (ranked.isEmpty()) { NoopCard { Text( - "Not enough overlap between your journal answers and " + + tr("Not enough overlap between your journal answers and ") + "${outcome.outcomeName.lowercase(Locale.US)} yet. Keep logging. Each behaviour " + - "needs days both with and without it before NOOP can read its effect.", + tr("needs days both with and without it before NOOP can read its effect."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -228,7 +228,7 @@ private fun MoverCard(r: RankedEffect, outcome: InsightsOutcome) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "With", + label = tr("With"), value = outcome.format(e.meanWith), caption = "n = ${e.nWith}", accent = tintColor, @@ -237,7 +237,7 @@ private fun MoverCard(r: RankedEffect, outcome: InsightsOutcome) { ) StatTile( modifier = Modifier.weight(1f), - label = "Without", + label = tr("Without"), value = outcome.format(e.meanWithout), caption = "n = ${e.nWithout}", accent = Palette.textPrimary, @@ -247,7 +247,7 @@ private fun MoverCard(r: RankedEffect, outcome: InsightsOutcome) { HorizontalDivider(color = Palette.hairline) Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("Effect size", modifier = Modifier.weight(1f)) + Overline(tr("Effect size"), modifier = Modifier.weight(1f)) Text( String.format(Locale.US, "d = %.2f", e.cohensD), style = NoopType.captionNumber, @@ -265,13 +265,13 @@ private fun MoverCard(r: RankedEffect, outcome: InsightsOutcome) { @Composable private fun DoseSection(cards: List) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Dose-response", overline = "Personal curve · prior-shrunk") + SectionHeader(tr("Dose-response"), overline = tr("Personal curve · prior-shrunk")) if (cards.isEmpty()) { NoopCard { Text( - "Log alcohol or late caffeine with an amount and NOOP fits a personal dose curve: " + - "how much each extra unit tends to move your numbers. Until then it shows " + - "typical patterns, clearly labelled as not yet yours.", + tr("Log alcohol or late caffeine with an amount and NOOP fits a personal dose curve: ") + + tr("how much each extra unit tends to move your numbers. Until then it shows ") + + tr("typical patterns, clearly labelled as not yet yours."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -329,7 +329,7 @@ private fun DoseResponseCard(card: DoseCardData) { if (card.timingProxy) { Text( - "“Dose” here is timing (later in the day = stronger), not milligrams.", + tr("“Dose” here is timing (later in the day = stronger), not milligrams."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -353,7 +353,7 @@ private fun DamageForecast( val fromDose = 1 val delta = r.delta(fromDose, previewDose) val projected = card.latestOutcome?.let { max(0.0, min(card.outcomeCeiling, it + delta)) } - val stepLabel = if (previewDose <= 1) "no extra" else "$previewDose${card.dosePlusSuffix(previewDose)}" + val stepLabel = if (previewDose <= 1) tr("no extra") else "$previewDose${card.dosePlusSuffix(previewDose)}" Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { // Overline then the dose stepper on its own row — the choices (0…max+) overflow a ~360dp @@ -377,14 +377,14 @@ private fun DamageForecast( modifier = Modifier.weight(1f), label = "Per extra ${card.unitNoun}", value = signed(r.perUnit, card.outcomeSuffix), - caption = if (r.priorDominated) "typical" else "your data", + caption = if (r.priorDominated) tr("typical") else tr("your data"), accent = if (r.perUnit < 0) Palette.statusCritical else Palette.statusPositive, ) StatTile( modifier = Modifier.weight(1f), label = "Tomorrow’s ${card.outcomeName}", value = projected?.let { "${it.roundToInt()}${card.outcomeSuffix}" } ?: "—", - caption = if (projected != null) "projected · $stepLabel" else "needs a recent day", + caption = if (projected != null) "projected · $stepLabel" else tr("needs a recent day"), accent = domain.color, ) } @@ -504,15 +504,15 @@ internal data class DoseCardData( ) { val id: String get() = behavior.raw val outcomeName: String get() = response.outcome - val title: String get() = if (behavior == DosedBehavior.ALCOHOL) "Alcohol" else "Caffeine" + val title: String get() = if (behavior == DosedBehavior.ALCOHOL) tr("Alcohol") else tr("Caffeine") val icon get() = if (behavior == DosedBehavior.ALCOHOL) Icons.Filled.LocalBar else Icons.Filled.Coffee - val unitNoun: String get() = if (behavior == DosedBehavior.ALCOHOL) "drink" else "later step" - val unitLabel: String get() = if (behavior == DosedBehavior.ALCOHOL) "drink" else "late-caffeine" + val unitNoun: String get() = if (behavior == DosedBehavior.ALCOHOL) tr("drink") else tr("later step") + val unitLabel: String get() = if (behavior == DosedBehavior.ALCOHOL) tr("drink") else tr("late-caffeine") val timingProxy: Boolean get() = behavior == DosedBehavior.CAFFEINE val outcomeSuffix: String get() = if (outcomeName == "HRV") " ms" else "%" val outcomeCeiling: Double get() = if (outcomeName == "HRV") 400.0 else 100.0 val forecastOverline: String - get() = if (behavior == DosedBehavior.ALCOHOL) "Tonight’s forecast" else "Timing forecast" + get() = if (behavior == DosedBehavior.ALCOHOL) tr("Tonight’s forecast") else tr("Timing forecast") val doseChoices: List get() = (0..DoseResponseEngine.maxCurveDose).toList() @@ -528,7 +528,7 @@ internal data class DoseCardData( fun dosePlusSuffix(d: Int): String = if (behavior == DosedBehavior.ALCOHOL) { - if (d >= DoseResponseEngine.maxCurveDose) "+ drinks" else " drinks" + if (d >= DoseResponseEngine.maxCurveDose) tr("+ drinks") else tr(" drinks") } else "" } @@ -633,9 +633,9 @@ private fun forecastSentence(card: DoseCardData, previewDose: Int, delta: Double return "No extra tonight. Your ${card.outcomeName.lowercase(Locale.US)} forecast stays where it is." } val mag = abs(delta).roundToInt() - val dir = if (delta <= 0) "lower" else "higher" + val dir = if (delta <= 0) tr("lower") else tr("higher") val basis = if (card.response.priorDominated) { - "based on typical patterns" + tr("based on typical patterns") } else { "based on ${card.response.nUser} of your ${card.unitLabel.lowercase(Locale.US)} days" } @@ -646,7 +646,7 @@ private fun forecastSentence(card: DoseCardData, previewDose: Int, delta: Double private fun curveDescription(card: DoseCardData, r: DoseResponse): String = "Dose-response curve. Each extra ${card.unitNoun} lines up with about " + "${signed(r.perUnit, card.outcomeSuffix)} on ${card.outcomeName}, " + - if (r.priorDominated) "typical patterns." else "your own data." + if (r.priorDominated) tr("typical patterns.") else tr("your own data.") private fun signed(v: Double, suffix: String): String { val mag = abs(v) diff --git a/android/app/src/main/java/com/noop/ui/InsightsScreen.kt b/android/app/src/main/java/com/noop/ui/InsightsScreen.kt index c0fd74db2..07f7b6168 100644 --- a/android/app/src/main/java/com/noop/ui/InsightsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/InsightsScreen.kt @@ -100,7 +100,7 @@ private enum class Outcome( val format: (Double) -> String, ) { Recovery( - label = "Charge", outcomeName = "Charge", higherIsBetter = true, domain = DomainTheme.Charge, + label = tr("Charge"), outcomeName = "Charge", higherIsBetter = true, domain = DomainTheme.Charge, pick = { it.recovery }, format = { "${it.roundToInt()}%" }, ), Hrv( @@ -108,7 +108,7 @@ private enum class Outcome( pick = { it.avgHrv }, format = { "${it.roundToInt()} ms" }, ), Sleep( - label = "Rest", outcomeName = "Rest", higherIsBetter = true, domain = DomainTheme.Rest, + label = tr("Rest"), outcomeName = "Rest", higherIsBetter = true, domain = DomainTheme.Rest, pick = { it.efficiency }, format = { "${it.roundToInt()}%" }, ), Rhr( @@ -312,8 +312,8 @@ fun InsightsScreen(vm: AppViewModel, onOpenInsightsHub: () -> Unit = {}) { // `ScreenScaffold(topBackground: liquidScaffoldSky())`; reuses the shared LiquidScreenSky() slot verbatim. // Insights has no day-cycle gate of its own, so the sky is always drawn (matching the liquid explorer). LazyScreenScaffold( - title = "Insights", - subtitle = "Interrogate what affects what.", + title = tr("Insights"), + subtitle = tr("Interrogate what affects what."), topBackground = { LiquidScreenSky() }, ) { @@ -328,7 +328,7 @@ fun InsightsScreen(vm: AppViewModel, onOpenInsightsHub: () -> Unit = {}) { if (preFilledFromYesterday) { item { Text( - "Pre-filled from last night. Tap to confirm or change.", + tr("Pre-filled from last night. Tap to confirm or change."), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.fillMaxWidth(), @@ -478,7 +478,7 @@ fun InsightsScreen(vm: AppViewModel, onOpenInsightsHub: () -> Unit = {}) { if (!journalLoaded) { NoopCard { Text( - "Reading your journal and outcomes…", + tr("Reading your journal and outcomes…"), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -486,10 +486,10 @@ fun InsightsScreen(vm: AppViewModel, onOpenInsightsHub: () -> Unit = {}) { } else if (behaviours.isEmpty()) { // No journal yet, explain, without dead-ending on a paid export. DataPendingNote( - title = "Insights read your journal and outcomes", - body = "Log behaviours above. After a few days of answers, NOOP ranks how each " + - "one moves your recovery, HRV and sleep. Importing a WHOOP export (which " + - "includes its journal) backfills history instantly.", + title = tr("Insights read your journal and outcomes"), + body = tr("Log behaviours above. After a few days of answers, NOOP ranks how each ") + + tr("one moves your recovery, HRV and sleep. Importing a WHOOP export (which ") + + tr("includes its journal) backfills history instantly."), ) } else { BehaviourSection( @@ -533,7 +533,7 @@ private fun WhatMovesYouLink(onOpen: () -> Unit) { .liquidPress(interaction) .semantics { contentDescription = - "What moves you. Ranked patterns in your own data, and your dose-response." + tr("What moves you. Ranked patterns in your own data, and your dose-response.") }, ) { Row( @@ -557,10 +557,10 @@ private fun WhatMovesYouLink(onOpen: () -> Unit) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { // WHOOP tappable-card title: UPPERCASE tracked WHITE label + a trailing "›" chevron // glyph (mirrors the iOS "WHAT MOVES YOU ›" overline). The descriptive line sits beneath. - Overline("What moves you ›", color = Palette.textPrimary) + Overline(tr("What moves you ›"), color = Palette.textPrimary) Text( - "Ranked, lag-aware: which of your habits actually move your Charge, plus your " + - "personal alcohol/caffeine dose-response.", + tr("Ranked, lag-aware: which of your habits actually move your Charge, plus your ") + + tr("personal alcohol/caffeine dose-response."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -619,11 +619,11 @@ internal fun computeActivityCosts( @Composable private fun ActivityCostSection(costs: List) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Activity Cost", overline = "What each activity costs your recovery") + SectionHeader(tr("Activity Cost"), overline = tr("What each activity costs your recovery")) if (costs.isEmpty()) { NoopCard { Text( - "Tag a few sessions of the same activity and NOOP will learn its personal recovery cost.", + tr("Tag a few sessions of the same activity and NOOP will learn its personal recovery cost."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -681,32 +681,32 @@ private fun ActivityCostCard(cost: com.noop.analytics.ActivityCost) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "Next morning", + label = tr("Next morning"), value = "${cost.meanNextMorning.roundToInt()}", caption = "Charge · $pointsLabel pts", accent = accent, ) StatTile( modifier = Modifier.weight(1f), - label = "Rest baseline", + label = tr("Rest baseline"), value = "${cost.baselineMean.roundToInt()}", - caption = "untouched days", + caption = tr("untouched days"), accent = Palette.textPrimary, ) } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "Bounce back", + label = tr("Bounce back"), value = cost.daysToBaseline?.let { "${it}d" } ?: "—", - caption = if (cost.daysToBaseline != null) "to baseline" else "not within 7d", + caption = if (cost.daysToBaseline != null) tr("to baseline") else tr("not within 7d"), accent = Palette.chargeColor, ) StatTile( modifier = Modifier.weight(1f), - label = "Sessions", + label = tr("Sessions"), value = "${cost.n}", - caption = if (solid) "solid" else "building", + caption = if (solid) tr("solid") else tr("building"), accent = Palette.textPrimary, ) } @@ -729,7 +729,7 @@ private fun BehaviourSection( ) { Box(modifier = Modifier.weight(1f)) { SectionHeader( - "Behaviour Effects", + tr("Behaviour Effects"), overline = "What moves your ${outcome.outcomeName.lowercase(Locale.US)}", ) } @@ -744,9 +744,9 @@ private fun BehaviourSection( if (ranked.isEmpty()) { NoopCard { Text( - "Not enough overlap between your journal answers and " + + tr("Not enough overlap between your journal answers and ") + "${outcome.outcomeName.lowercase(Locale.US)} to measure an effect yet. " + - "Keep logging. Effects need days both with and without each behaviour.", + tr("Keep logging. Effects need days both with and without each behaviour."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -821,7 +821,7 @@ private fun EffectCard(e: BehaviorEffect, outcome: Outcome) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "With", + label = tr("With"), value = outcome.format(e.meanWith), caption = "n = ${e.nWith}", accent = tintColor, @@ -830,7 +830,7 @@ private fun EffectCard(e: BehaviorEffect, outcome: Outcome) { ) StatTile( modifier = Modifier.weight(1f), - label = "Without", + label = tr("Without"), value = outcome.format(e.meanWithout), caption = "n = ${e.nWithout}", accent = Palette.textPrimary, @@ -844,7 +844,7 @@ private fun EffectCard(e: BehaviorEffect, outcome: Outcome) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Overline("Effect size", modifier = Modifier.weight(1f)) + Overline(tr("Effect size"), modifier = Modifier.weight(1f)) Text( String.format(Locale.US, "d = %.2f", e.cohensD), style = NoopType.captionNumber, @@ -910,7 +910,7 @@ private data class ExperimentSnapshot( return i - b } val deltaCaption: String get() = - if (delta == null) "needs baseline + logged days" else "vs behaviour-free baseline" + if (delta == null) tr("needs baseline + logged days") else tr("vs behaviour-free baseline") } private data class ExperimentConfidence(val label: String, val tone: StrandTone) @@ -931,8 +931,8 @@ private fun ExperimentSection( ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - "Personal Experiment", - overline = "N-of-1 protocol", + tr("Personal Experiment"), + overline = tr("N-of-1 protocol"), trailing = snapshot?.phaseLabel ?: "Setup", ) NoopCard { @@ -971,12 +971,12 @@ private fun ExperimentSetupCard( verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { - Text("Run a clean personal test", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Run a clean personal test"), style = NoopType.headline, color = Palette.textPrimary) Spacer(Modifier.height(4.dp)) Text( - "Pick one behaviour you log, one outcome, and a short window. NOOP " + - "compares the days you log the behaviour against your behaviour-free " + - "days before the start.", + tr("Pick one behaviour you log, one outcome, and a short window. NOOP ") + + tr("compares the days you log the behaviour against your behaviour-free ") + + tr("days before the start."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -987,7 +987,7 @@ private fun ExperimentSetupCard( if (candidates.isEmpty()) { Text( - "Log at least one behaviour above before starting an experiment.", + tr("Log at least one behaviour above before starting an experiment."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -1018,13 +1018,13 @@ private fun ExperimentSetupCard( // Unified button system (mirrors iOS NoopButton("Start experiment", flask, .primary, fullWidth)). NoopButton( - text = "Start experiment", + text = tr("Start experiment"), leadingIcon = Icons.Filled.Science, kind = NoopButtonKind.Primary, fullWidth = true, enabled = resolvedBehaviour != null, onClick = onStart, - modifier = Modifier.semantics { contentDescription = "Start experiment" }, + modifier = Modifier.semantics { contentDescription = tr("Start experiment") }, ) } } @@ -1074,14 +1074,14 @@ private fun ActiveExperimentCard( Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { ExperimentMeasure( modifier = Modifier.weight(1f), - label = "Baseline", + label = tr("Baseline"), value = snapshot.baselineMean?.let { snapshot.outcome.format(it) } ?: "—", caption = "${snapshot.baselineCount} days without it", tint = Palette.textSecondary, ) ExperimentMeasure( modifier = Modifier.weight(1f), - label = "Intervention", + label = tr("Intervention"), value = snapshot.interventionMean?.let { snapshot.outcome.format(it) } ?: "—", caption = "${snapshot.interventionCount} logged days", tint = Palette.accent, @@ -1090,16 +1090,16 @@ private fun ActiveExperimentCard( Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { ExperimentMeasure( modifier = Modifier.weight(1f), - label = "Change", + label = tr("Change"), value = formatExperimentDelta(snapshot.delta, snapshot.outcome), caption = snapshot.deltaCaption, tint = experimentDeltaColor(snapshot), ) ExperimentMeasure( modifier = Modifier.weight(1f), - label = "Compliance", + label = tr("Compliance"), value = "${snapshot.compliance.roundToInt()}%", - caption = if (snapshot.loggedToday) "logged today" else "not logged today", + caption = if (snapshot.loggedToday) tr("logged today") else tr("not logged today"), tint = if (snapshot.loggedToday) Palette.statusPositive else Palette.statusWarning, ) } @@ -1144,30 +1144,30 @@ private fun ActiveExperimentCard( verticalAlignment = Alignment.CenterVertically, ) { NoopButton( - text = "Mark done", + text = tr("Mark done"), leadingIcon = Icons.Filled.CheckCircle, kind = NoopButtonKind.Primary, enabled = !snapshot.loggedToday, onClick = { onMark(true) }, modifier = Modifier .weight(1f) - .semantics { contentDescription = "Mark done today" }, + .semantics { contentDescription = tr("Mark done today") }, ) NoopButton( - text = "Skip", + text = tr("Skip"), leadingIcon = Icons.Filled.Close, kind = NoopButtonKind.Secondary, onClick = { onMark(false) }, modifier = Modifier .weight(1f) - .semantics { contentDescription = "Skip today" }, + .semantics { contentDescription = tr("Skip today") }, ) NoopButton( - text = "End", + text = tr("End"), leadingIcon = Icons.Filled.Stop, kind = NoopButtonKind.Destructive, onClick = onEnd, - modifier = Modifier.semantics { contentDescription = "End experiment" }, + modifier = Modifier.semantics { contentDescription = tr("End experiment") }, ) } } @@ -1441,12 +1441,12 @@ private fun saveExperimentInt(context: Context, key: String, value: Int) { @Composable private fun RelationshipsSection(rels: List) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Metric Relationships", overline = "Pearson r") + SectionHeader(tr("Metric Relationships"), overline = tr("Pearson r")) if (rels.isEmpty()) { NoopCard { Text( - "Not enough overlapping history to correlate your metrics yet.", + tr("Not enough overlapping history to correlate your metrics yet."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -1499,7 +1499,7 @@ private fun RelationshipRow(rel: Relationship) { color = strength, ) StatePill( - if (rel.significant) "p < 0.05" else "n.s.", + if (rel.significant) tr("p < 0.05") else tr("n.s."), tone = if (rel.significant) StrandTone.Accent else StrandTone.Neutral, showsDot = false, ) @@ -1758,7 +1758,7 @@ private fun strengthWord(r: Double): String { } private fun relationshipSentence(rel: Relationship): String { - val dir = if (rel.r > 0) "positive" else if (rel.r < 0) "negative" else "flat" + val dir = if (rel.r > 0) tr("positive") else if (rel.r < 0) tr("negative") else tr("flat") return "${strengthWord(rel.r)} $dir relationship " + "(r = ${String.format(Locale.US, "%.2f", rel.r)}, n = ${rel.n})." } diff --git a/android/app/src/main/java/com/noop/ui/IntelligenceScreen.kt b/android/app/src/main/java/com/noop/ui/IntelligenceScreen.kt index c56595d38..29153b942 100644 --- a/android/app/src/main/java/com/noop/ui/IntelligenceScreen.kt +++ b/android/app/src/main/java/com/noop/ui/IntelligenceScreen.kt @@ -100,8 +100,8 @@ fun IntelligenceScreen(vm: AppViewModel) { } LazyScreenScaffold( - title = "Intelligence", - subtitle = "Charge, effort and rest - scored with the model, explained in plain terms.", + title = tr("Intelligence"), + subtitle = tr("Charge, effort and rest - scored with the model, explained in plain terms."), ) { item { forecast?.let { ForecastCard(it) } } item { ExplainerCard(effortScale) } @@ -123,8 +123,8 @@ fun IntelligenceScreen(vm: AppViewModel) { horizontalArrangement = Arrangement.spacedBy(Metrics.gap), ) { Column(modifier = Modifier.weight(1f)) { - Overline("Recent") - Text("By Day", style = NoopType.title2, color = Palette.textPrimary) + Overline(tr("Recent")) + Text(tr("By Day"), style = NoopType.title2, color = Palette.textPrimary) } SegmentedPillControl( items = IntelRange.entries.toList(), @@ -146,7 +146,7 @@ fun IntelligenceScreen(vm: AppViewModel) { item { NoopCard(padding = 18.dp) { Text( - "No scored days in this window. Widen the range or import more history.", + tr("No scored days in this window. Widen the range or import more history."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -177,7 +177,7 @@ private fun ForecastCard(f: RecoveryForecast) { val charge = f.charge.roundToInt() val band = f.band.roundToInt() Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Tomorrow's Charge", overline = "Evening forecast", trailing = "Estimate") + SectionHeader(tr("Tomorrow's Charge"), overline = tr("Evening forecast"), trailing = "Estimate") NoopCard(padding = 20.dp) { Column( modifier = Modifier.fillMaxWidth(), @@ -217,7 +217,7 @@ private fun ForecastCard(f: RecoveryForecast) { ) Text( "Estimate from today's effort, your typical sleep and your ${f.nights}-night " + - "recovery baseline - not a measurement. Your real Charge is scored from " + + tr("recovery baseline - not a measurement. Your real Charge is scored from ") + "tomorrow's HRV when you wake.", style = NoopType.footnote, color = Palette.textTertiary, @@ -252,10 +252,10 @@ private fun ExplainerCard(effortScale: EffortScale) { tint = Palette.chargeColor, modifier = Modifier.size(20.dp), ) - Text("How this works", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("How this works"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "Charge weighs your heart-rate variability against your personal baseline " + + tr("Charge weighs your heart-rate variability against your personal baseline ") + "(~55%), resting heart rate (~20%), rest quality (~15%), respiration (~5%) " + "and skin-temperature deviation (~5%). Effort is a 0 - ${UnitFormatter.effortScaleMax(effortScale)} " + "cardiovascular load from time spent in each heart-rate zone. Rest is staged " + @@ -284,9 +284,9 @@ private fun EmptyNote() { modifier = Modifier.size(18.dp), ) Text( - "No scored days yet. Sync your strap to collect raw streams. Effort and Rest are " + - "scored once a day's data is in. Charge needs about four nights of sleep to learn your " + - "baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first " + + tr("No scored days yet. Sync your strap to collect raw streams. Effort and Rest are ") + + tr("scored once a day's data is in. Charge needs about four nights of sleep to learn your ") + + tr("baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first ") + "couple of weeks. On a WHOOP 5 or MG the strap banks little history, so that night count " + "can climb slowly or sit at 0 of 4 until you have worn it across a few nights. That's " + "its sync limit, not a fault. Import your WHOOP export to skip the wait.", @@ -307,7 +307,7 @@ private fun EmptyNote() { private fun ModelBreakdownCard(effortScale: EffortScale) { NoopCard(padding = 20.dp, tint = Palette.chargeColor) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("Charge model") + Overline(tr("Charge model")) WeightRow("Heart-rate variability", "~55%", 0.55f, Palette.metricPurple) WeightRow("Resting heart rate", "~20%", 0.20f, Palette.metricRose) WeightRow("Rest quality", "~15%", 0.15f, Palette.metricCyan) @@ -319,7 +319,7 @@ private fun ModelBreakdownCard(effortScale: EffortScale) { verticalAlignment = Alignment.CenterVertically, ) { Text( - "Effort", + tr("Effort"), style = NoopType.subhead, color = Palette.textSecondary, modifier = Modifier.weight(1f), diff --git a/android/app/src/main/java/com/noop/ui/IntervalsScreen.kt b/android/app/src/main/java/com/noop/ui/IntervalsScreen.kt index 390745a40..ff59f1182 100644 --- a/android/app/src/main/java/com/noop/ui/IntervalsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/IntervalsScreen.kt @@ -60,9 +60,9 @@ import kotlin.math.min // With no strap bonded it still works as a big glanceable visual timer (no haptics). private enum class IntervalPhase(val label: String) { - Work("WORK"), - Rest("REST"), - Done("DONE"), + Work(tr("WORK")), + Rest(tr("REST")), + Done(tr("DONE")), } /** @@ -203,22 +203,22 @@ fun IntervalsScreen(vm: AppViewModel) { // (the heavy per-second BevelGauge hero) and off-screen cards (config) don't recompose or get // semantics-walked. Order/spacing unchanged (LazyColumn reproduces the eager `spacedBy(20.dp)`). LazyScreenScaffold( - title = "Interval Timer", - subtitle = "Silent haptic HIIT - the strap buzzes the transitions", + title = tr("Interval Timer"), + subtitle = tr("Silent haptic HIIT - the strap buzzes the transitions"), ) { // --- Status row --- item { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { if (live.bonded) { - StatePill("Buzz cues on", tone = StrandTone.Positive) + StatePill(tr("Buzz cues on"), tone = StrandTone.Positive) } else { - StatePill("Connect strap for buzz cues", tone = StrandTone.Warning) + StatePill(tr("Connect strap for buzz cues"), tone = StrandTone.Warning) } Spacer(Modifier.weight(1f)) when { - running -> StatePill("Running", tone = StrandTone.Accent, pulsing = true) - isFinished -> StatePill("Complete", tone = StrandTone.Positive) - else -> StatePill("Paused", tone = StrandTone.Neutral, showsDot = false) + running -> StatePill(tr("Running"), tone = StrandTone.Accent, pulsing = true) + isFinished -> StatePill(tr("Complete"), tone = StrandTone.Positive) + else -> StatePill(tr("Paused"), tone = StrandTone.Neutral, showsDot = false) } } } @@ -256,7 +256,7 @@ fun IntervalsScreen(vm: AppViewModel) { stops = phaseStops, tipColor = phaseColor, numberText = if (isFinished) "✓" else remaining.toString(), - captionText = if (isFinished) "SESSION DONE" else "SECONDS", + captionText = if (isFinished) tr("SESSION DONE") else tr("SECONDS"), diameter = 240.dp, lineWidth = 18.dp, ) @@ -284,7 +284,7 @@ fun IntervalsScreen(vm: AppViewModel) { modifier = Modifier.padding(end = 6.dp), ) Text( - if (running) "Pause" else if (isFinished) "Restart" else "Start", + if (running) tr("Pause") else if (isFinished) tr("Restart") else tr("Start"), style = NoopType.headline, ) } @@ -304,7 +304,7 @@ fun IntervalsScreen(vm: AppViewModel) { contentDescription = null, modifier = Modifier.padding(end = 6.dp), ) - Text("Reset", style = NoopType.headline) + Text(tr("Reset"), style = NoopType.headline) } } @@ -322,7 +322,7 @@ fun IntervalsScreen(vm: AppViewModel) { ) Spacer(Modifier.width(6.dp)) Text( - "Bond your strap on the Live screen to feel the transitions hands-free.", + tr("Bond your strap on the Live screen to feel the transitions hands-free."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -342,7 +342,7 @@ fun IntervalsScreen(vm: AppViewModel) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom, ) { - Overline("Session") + Overline(tr("Session")) Spacer(Modifier.weight(1f)) Text( "${timeString(elapsed)} / ${timeString(totalPlanned)}", @@ -378,11 +378,11 @@ fun IntervalsScreen(vm: AppViewModel) { } Row(modifier = Modifier.fillMaxWidth()) { - OverviewStat(Modifier.weight(1f), "Work", "${workSeconds}s", Palette.effortColor) - OverviewStat(Modifier.weight(1f), "Rest", "${restSeconds}s", Palette.restColor) - OverviewStat(Modifier.weight(1f), "Rounds", rounds.toString(), Palette.textPrimary) + OverviewStat(Modifier.weight(1f), tr("Work"), "${workSeconds}s", Palette.effortColor) + OverviewStat(Modifier.weight(1f), tr("Rest"), "${restSeconds}s", Palette.restColor) + OverviewStat(Modifier.weight(1f), tr("Rounds"), rounds.toString(), Palette.textPrimary) OverviewStat( - Modifier.weight(1f), "Remaining", + Modifier.weight(1f), tr("Remaining"), timeString(max(0, totalPlanned - elapsed)), Palette.textSecondary, ) } @@ -394,27 +394,27 @@ fun IntervalsScreen(vm: AppViewModel) { item { NoopCard { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("Configure") + Overline(tr("Configure")) ConfigStepper( - title = "Work", unit = "sec", value = workSeconds, + title = tr("Work"), unit = tr("sec"), value = workSeconds, range = 5..600, step = 5, tint = Palette.effortColor, enabled = !running, onChange = { workSeconds = it }, ) Divider() ConfigStepper( - title = "Rest", unit = "sec", value = restSeconds, + title = tr("Rest"), unit = tr("sec"), value = restSeconds, range = 5..600, step = 5, tint = Palette.restColor, enabled = !running, onChange = { restSeconds = it }, ) Divider() ConfigStepper( - title = "Rounds", unit = null, value = rounds, + title = tr("Rounds"), unit = null, value = rounds, range = 1..30, step = 1, tint = Palette.textPrimary, enabled = !running, onChange = { rounds = it }, ) if (running) { Text( - "Pause to change work, rest, or rounds.", + tr("Pause to change work, rest, or rounds."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -456,7 +456,7 @@ private fun RoundChip(currentRound: Int, rounds: Int) { .border(1.dp, Palette.hairline, shape) .padding(horizontal = 12.dp, vertical = 7.dp), ) { - Overline("Round") + Overline(tr("Round")) Spacer(Modifier.width(6.dp)) Text(currentRound.toString(), style = NoopType.number(18f), color = Palette.textPrimary) Spacer(Modifier.width(2.dp)) diff --git a/android/app/src/main/java/com/noop/ui/JournalLog.kt b/android/app/src/main/java/com/noop/ui/JournalLog.kt index 26e2c022d..f691268b0 100644 --- a/android/app/src/main/java/com/noop/ui/JournalLog.kt +++ b/android/app/src/main/java/com/noop/ui/JournalLog.kt @@ -188,9 +188,9 @@ fun JournalLogCard( // Header: title/overline on the left, the Tomorrow/Today/Yesterday toggle (or Edit/Done) on the right. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Overline("Log") + Overline(tr("Log")) Text( - "Journal", + tr("Journal"), style = NoopType.title2, color = Palette.textPrimary, maxLines = 1, @@ -398,15 +398,15 @@ private fun JournalItemEditControls( modifier = Modifier.clickable { menuOpen = true }.padding(horizontal = 8.dp)) androidx.compose.material3.DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { androidx.compose.material3.DropdownMenuItem( - text = { Text("Rename…") }, + text = { Text(tr("Rename…")) }, onClick = { menuOpen = false; onStartRename() }, ) androidx.compose.material3.DropdownMenuItem( - text = { Text("Group…") }, + text = { Text(tr("Group…")) }, onClick = { menuOpen = false; groupMenuOpen = true }, ) androidx.compose.material3.DropdownMenuItem( - text = { Text(if (item.kind.isNumeric) "Change to Yes/No" else "Change to Number") }, + text = { Text(if (item.kind.isNumeric) tr("Change to Yes/No") else tr("Change to Number")) }, onClick = { menuOpen = false onSetKind(if (item.kind.isNumeric) JournalKind.Bool else JournalKind.Numeric(null)) @@ -436,25 +436,25 @@ private fun JournalRenameDialog( var draft by remember { mutableStateOf(item.displayName ?: item.canonical) } androidx.compose.material3.AlertDialog( onDismissRequest = onDismiss, - title = { Text("Rename item") }, + title = { Text(tr("Rename item")) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( value = draft, onValueChange = { draft = it }, - placeholder = { Text("Display name") }, + placeholder = { Text(tr("Display name")) }, singleLine = true, colors = journalFieldColors(), ) Text( - "History stays under the original question so WHOOP imports still line up.", + tr("History stays under the original question so WHOOP imports still line up."), style = NoopType.footnote, color = Palette.textTertiary, ) } }, - confirmButton = { Text("Save", color = Palette.accent, modifier = Modifier.clickable { onSave(draft) }.padding(8.dp)) }, - dismissButton = { Text("Cancel", color = Palette.textSecondary, modifier = Modifier.clickable { onDismiss() }.padding(8.dp)) }, + confirmButton = { Text(tr("Save"), color = Palette.accent, modifier = Modifier.clickable { onSave(draft) }.padding(8.dp)) }, + dismissButton = { Text(tr("Cancel"), color = Palette.textSecondary, modifier = Modifier.clickable { onDismiss() }.padding(8.dp)) }, ) } @@ -470,7 +470,7 @@ private fun JournalAddRow(onAddCustom: (String, JournalKind, JournalGroup) -> Un OutlinedTextField( value = draft, onValueChange = { draft = it }, - placeholder = { Text("Add a custom item…", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Add a custom item…"), style = NoopType.body, color = Palette.textTertiary) }, singleLine = true, textStyle = NoopType.body, colors = journalFieldColors(), @@ -478,7 +478,7 @@ private fun JournalAddRow(onAddCustom: (String, JournalKind, JournalGroup) -> Un modifier = Modifier.weight(1f), ) Spacer(Modifier.width(8.dp)) - JournalChip(if (numeric) "Number" else "Yes/No", selected = numeric) { numeric = !numeric } + JournalChip(if (numeric) tr("Number") else "Yes/No", selected = numeric) { numeric = !numeric } Spacer(Modifier.width(8.dp)) JournalChip("Add", selected = draft.isNotBlank()) { val t = draft.trim() @@ -538,7 +538,7 @@ private fun JournalChip(label: String, selected: Boolean, onClick: () -> Unit) { private fun JournalRemoveButton(isCustom: Boolean, onClick: () -> Unit) { val shape = RoundedCornerShape(50) Text( - if (isCustom) "Delete" else "Hide", + if (isCustom) tr("Delete") else tr("Hide"), style = NoopType.caption, color = Palette.statusCritical, modifier = Modifier diff --git a/android/app/src/main/java/com/noop/ui/LabBookScreen.kt b/android/app/src/main/java/com/noop/ui/LabBookScreen.kt index 7bc523e03..fa8c087fb 100644 --- a/android/app/src/main/java/com/noop/ui/LabBookScreen.kt +++ b/android/app/src/main/java/com/noop/ui/LabBookScreen.kt @@ -160,8 +160,8 @@ fun LabBookScreen(vm: AppViewModel) { // `when {}` item (it's a user-entered, bounded set, not unbounded history), so its appearance is // byte-identical; the sheets below the scaffold are untouched. LazyScreenScaffold( - title = "Lab Book", - subtitle = "Your bloods, BP and body numbers. Kept private, on this phone.", + title = tr("Lab Book"), + subtitle = tr("Your bloods, BP and body numbers. Kept private, on this phone."), ) { // Header card: count + scope + add action. item { @@ -180,7 +180,7 @@ fun LabBookScreen(vm: AppViewModel) { Column(modifier = Modifier.weight(1f)) { Text(countLine(markers), style = NoopType.headline, color = Palette.textPrimary) Text( - "All stays on this phone. Nothing is sent anywhere.", + tr("All stays on this phone. Nothing is sent anywhere."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -190,15 +190,15 @@ fun LabBookScreen(vm: AppViewModel) { .size(28.dp) .clip(RoundedCornerShape(8.dp)) .clickable { showDisclaimer = true } - .semantics { contentDescription = "What Lab Book is (and isn't)" }, + .semantics { contentDescription = tr("What Lab Book is (and isn't)") }, contentAlignment = Alignment.Center, ) { Icon(Icons.Filled.Info, contentDescription = null, tint = Palette.textTertiary, modifier = Modifier.size(18.dp)) } } Text( - "It's a notebook, not a lab. NOOP lines up the numbers you enter. It doesn't test, " + - "read, or judge them. Not medical advice.", + tr("It's a notebook, not a lab. NOOP lines up the numbers you enter. It doesn't test, ") + + tr("read, or judge them. Not medical advice."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -223,18 +223,18 @@ fun LabBookScreen(vm: AppViewModel) { ) { Icon(Icons.Filled.FileUpload, contentDescription = null, tint = Palette.metricAmber, modifier = Modifier.size(16.dp)) } - Text("Import readings", style = NoopType.headline, color = Palette.textPrimary, modifier = Modifier.weight(1f)) + Text(tr("Import readings"), style = NoopType.headline, color = Palette.textPrimary, modifier = Modifier.weight(1f)) } Text( - "Bring in a markers CSV (date, marker, value, unit). Names that match the catalog " + - "fold onto your existing markers; anything else comes in as a custom marker. " + + tr("Bring in a markers CSV (date, marker, value, unit). Names that match the catalog ") + + tr("fold onto your existing markers; anything else comes in as a custom marker. ") + "Rows that can't be read are skipped and counted, never guessed. Everything " + "you import stays on this phone.", style = NoopType.subhead, color = Palette.textSecondary, ) PrimaryActionButton( - if (csvImporting) "Importing…" else "Choose CSV…", + if (csvImporting) tr("Importing…") else tr("Choose CSV…"), Icons.Filled.FileUpload, enabled = !csvImporting, ) { csvImportLauncher.launch(arrayOf("*/*")) } @@ -252,15 +252,15 @@ fun LabBookScreen(vm: AppViewModel) { item { when { !loaded -> { - Text("Reading your logbook…", style = NoopType.subhead, color = Palette.textTertiary) + Text(tr("Reading your logbook…"), style = NoopType.subhead, color = Palette.textTertiary) } markers.isEmpty() -> { NoopCard { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Keep your own numbers here", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Keep your own numbers here"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Type in a blood-pressure reading or a cholesterol value from your last appointment. " + - "It stays on this phone, and over time you'll see how it lines up with your sleep, " + + tr("Type in a blood-pressure reading or a cholesterol value from your last appointment. ") + + tr("It stays on this phone, and over time you'll see how it lines up with your sleep, ") + "heart rate and recovery.", style = NoopType.subhead, color = Palette.textSecondary, @@ -273,7 +273,7 @@ fun LabBookScreen(vm: AppViewModel) { val keys = markerKeys(markers, category) SectionHeader( title = category.displayName, - overline = if (keys.size == 1) "1 marker" else "${keys.size} markers", + overline = if (keys.size == 1) tr("1 marker") else "${keys.size} markers", ) for (key in keys) { MarkerRow(key = key, readings = readingsFor(markers, key)) { detailKey = key } @@ -287,20 +287,20 @@ fun LabBookScreen(vm: AppViewModel) { item { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Text( - "Lab Book is a private notebook, not a medical service. NOOP stores and lines up the numbers " + - "you enter. It doesn't test, read, diagnose, or advise. Your records never leave this phone; " + + tr("Lab Book is a private notebook, not a medical service. NOOP stores and lines up the numbers ") + + tr("you enter. It doesn't test, read, diagnose, or advise. Your records never leave this phone; ") + "there's no account or cloud, so it isn't \"HIPAA-covered.\" Always rely on your doctor or " + "pharmacist to interpret results.", style = NoopType.footnote, color = Palette.textTertiary, ) Text( - "Read the full note", + tr("Read the full note"), style = NoopType.footnote, color = Palette.accent, modifier = Modifier .clickable { showDisclaimer = true } - .semantics { contentDescription = "Read the full Lab Book note" }, + .semantics { contentDescription = tr("Read the full Lab Book note") }, ) } } @@ -412,7 +412,7 @@ private fun MarkerDetailSheet( ) // Trend (descriptive arithmetic, never interpretation). - SectionHeader("Trend", overline = "your readings over time") + SectionHeader(tr("Trend"), overline = tr("your readings over time")) NoopCard(tint = Palette.metricCyan) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { val nums = numeric.mapNotNull { it.value } @@ -431,7 +431,7 @@ private fun MarkerDetailSheet( // Compare with a signal (reuses the Pearson idiom + restrained copy). if (numeric.isNotEmpty()) { - SectionHeader("Compare with a signal", overline = "side by side · ${window.phrase} before each reading") + SectionHeader(tr("Compare with a signal"), overline = "side by side · ${window.phrase} before each reading") NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -457,7 +457,7 @@ private fun MarkerDetailSheet( } // History table. - SectionHeader("History", overline = "every reading you've entered") + SectionHeader(tr("History"), overline = tr("every reading you've entered")) NoopCard { Column { val reversed = readings.reversed() @@ -471,8 +471,8 @@ private fun MarkerDetailSheet( } Text( - "These are your own numbers shown back to you. NOOP doesn't decide whether any value is " + - "normal, high or low.", + tr("These are your own numbers shown back to you. NOOP doesn't decide whether any value is ") + + tr("normal, high or low."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -492,16 +492,16 @@ private fun CorrelationResult( val n = pairs.size when { signal == null -> Text( - "Pick a wearable signal (resting HR, HRV, sleep, Charge, weight…) to line it up against this " + + tr("Pick a wearable signal (resting HR, HRV, sleep, Charge, weight…) to line it up against this ") + "marker. NOOP averages the signal over the ${window.phrase} before each reading.", style = NoopType.subhead, color = Palette.textTertiary, ) - computing -> Text("Lining them up…", style = NoopType.subhead, color = Palette.textTertiary) + computing -> Text(tr("Lining them up…"), style = NoopType.subhead, color = Palette.textTertiary) n < LAB_FLOOR -> Text( if (n == 0) { "No overlap yet between this marker and ${signal.title.lowercase()}. Log a few more readings " + - "(and keep wearing your strap)." + tr("(and keep wearing your strap).") } else { "$n reading${if (n == 1) "" else "s"} line up so far, not enough to read a trend yet " + "(NOOP waits for $LAB_FLOOR)." @@ -522,7 +522,7 @@ private fun CorrelationResult( Text(insightSentence(markerName, signal.title, r), style = NoopType.subhead, color = Palette.textSecondary) Text( "$n readings used · ${strengthWord(r)} ${directionWord(r)} association. This is your own data " + - "sitting side by side. It's not a medical finding, and it shows association, not cause.", + tr("sitting side by side. It's not a medical finding, and it shows association, not cause."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -543,7 +543,7 @@ private fun SignalPicker(selected: LabSignal?, onSelect: (LabSignal?) -> Unit) { Row( modifier = Modifier .clickable { expanded = true } - .semantics { contentDescription = "Choose a wearable signal to compare" }, + .semantics { contentDescription = tr("Choose a wearable signal to compare") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { @@ -562,7 +562,7 @@ private fun SignalPicker(selected: LabSignal?, onSelect: (LabSignal?) -> Unit) { } if (selected != null) { androidx.compose.material3.DropdownMenuItem( - text = { Text("Clear", style = NoopType.body, color = Palette.textSecondary) }, + text = { Text(tr("Clear"), style = NoopType.body, color = Palette.textSecondary) }, onClick = { onSelect(null); expanded = false }, ) } @@ -589,7 +589,7 @@ private fun HistoryRow(markerKey: String, row: LabMarkerRow, onDelete: (String) .size(28.dp) .clip(RoundedCornerShape(8.dp)) .clickable { onDelete(row.id) } - .semantics { contentDescription = "Delete this reading" }, + .semantics { contentDescription = tr("Delete this reading") }, contentAlignment = Alignment.Center, ) { Icon(Icons.Filled.Delete, contentDescription = null, tint = Palette.statusCritical, modifier = Modifier.size(15.dp)) @@ -603,8 +603,8 @@ private fun HistoryRow(markerKey: String, row: LabMarkerRow, onDelete: (String) private fun LabBookDisclaimerSheet(onDismiss: () -> Unit) { NoopBottomSheet(onDismiss = onDismiss) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("About Lab Book", style = NoopType.title2, color = Palette.textPrimary) - Text("A private notebook, not a medical service.", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("About Lab Book"), style = NoopType.title2, color = Palette.textPrimary) + Text(tr("A private notebook, not a medical service."), style = NoopType.subhead, color = Palette.textSecondary) DisclaimerBullet("NOOP stores and lines up the numbers you enter yourself. It does not test you, read your results, give medical advice, or diagnose anything.") DisclaimerBullet("Anything you see here (including any side-by-side trend) is your own information shown back to you. It's an association, never a cause, and never a medical finding.") DisclaimerBullet("NOOP never decides whether a value is \"normal,\" \"high,\" or \"low.\" Any reference range shown is exactly what you typed from your own report.") @@ -658,8 +658,8 @@ private val LAB_SIGNALS = listOf( private fun countLine(markers: List): String { val keys = markers.map { it.markerKey }.toSet().size - val markerWord = if (keys == 1) "marker" else "markers" - val readingWord = if (markers.size == 1) "reading" else "readings" + val markerWord = if (keys == 1) tr("marker") else tr("markers") + val readingWord = if (markers.size == 1) tr("reading") else tr("readings") return "$keys $markerWord tracked · ${markers.size} $readingWord" } @@ -690,7 +690,7 @@ private fun latestLabel(row: LabMarkerRow?, key: String): String { } private fun lastTakenCaption(row: LabMarkerRow?): String = - if (row == null) "no readings yet" else "last taken ${labDayLabel(row.takenAt)}" + if (row == null) tr("no readings yet") else "last taken ${labDayLabel(row.takenAt)}" private fun formatValue(v: Double, key: String): String { val decimals = MarkerCatalog.definition(key)?.decimals ?: 1 @@ -728,13 +728,13 @@ private fun strengthWord(r: Double): String = when { else -> "very strong" } -private fun directionWord(r: Double): String = if (abs(r) < 0.1) "" else if (r >= 0) "positive" else "negative" +private fun directionWord(r: Double): String = if (abs(r) < 0.1) "" else if (r >= 0) tr("positive") else tr("negative") private fun insightSentence(markerName: String, signalName: String, r: Double): String { if (abs(r) < 0.3) { return "Over your readings, $markerName and ${signalName.lowercase()} move largely independently. No clear relationship." } - val verb = if (r < 0) "tends to be lower" else "tends to be higher" + val verb = if (r < 0) tr("tends to be lower") else tr("tends to be higher") return "When $markerName is higher, ${signalName.lowercase()} $verb." } diff --git a/android/app/src/main/java/com/noop/ui/LiveScreen.kt b/android/app/src/main/java/com/noop/ui/LiveScreen.kt index b079b4060..c3e534b01 100644 --- a/android/app/src/main/java/com/noop/ui/LiveScreen.kt +++ b/android/app/src/main/java/com/noop/ui/LiveScreen.kt @@ -200,8 +200,8 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { } LazyScreenScaffold( - title = "Live Body Console", - subtitle = "Current physiology, strap trust, and session controls", + title = tr("Live Body Console"), + subtitle = tr("Current physiology, strap trust, and session controls"), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // behind the header + hero and the cards float over the flat canvas below. Reuses the shared // LiquidScreenSky() slot verbatim; when the day-cycle background is off, the scaffold paints the @@ -260,7 +260,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { verticalArrangement = Arrangement.spacedBy(3.dp), ) { Text( - "Can't connect - your strap's pairing was reset", + tr("Can't connect - your strap's pairing was reset"), style = NoopType.subhead, color = Palette.textPrimary, ) @@ -294,7 +294,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { Text( if (live.syncChunksThisSession > 0) "Syncing your strap history… ${live.syncChunksThisSession} chunks pulled" - else "Syncing your strap history…", + else tr("Syncing your strap history…"), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -343,7 +343,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { // Session console — record or inspect the current stream. item { - SectionHeader(title = "Session", overline = "Record or inspect the current stream") + SectionHeader(title = tr("Session"), overline = tr("Record or inspect the current stream")) } // Manual workout — start/stop a session yourself; records HR + strain until you end it. @@ -374,17 +374,17 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { ) } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { - StatTile(modifier = Modifier.weight(1f), label = "HR", value = bpm?.toString() ?: "—", + StatTile(modifier = Modifier.weight(1f), label = tr("HR"), value = bpm?.toString() ?: "—", accent = if (bpm == null) Palette.textPrimary else Palette.metricRose) - StatTile(modifier = Modifier.weight(1f), label = "Avg", value = if (w.avgHr > 0) "${w.avgHr}" else "—") - StatTile(modifier = Modifier.weight(1f), label = "Peak", value = if (w.peakHr > 0) "${w.peakHr}" else "—") - StatTile(modifier = Modifier.weight(1f), label = "Effort", value = UnitFormatter.effortDisplay(w.liveStrain, effortScale), + StatTile(modifier = Modifier.weight(1f), label = tr("Avg"), value = if (w.avgHr > 0) "${w.avgHr}" else "—") + StatTile(modifier = Modifier.weight(1f), label = tr("Peak"), value = if (w.peakHr > 0) "${w.peakHr}" else "—") + StatTile(modifier = Modifier.weight(1f), label = tr("Effort"), value = UnitFormatter.effortDisplay(w.liveStrain, effortScale), accent = Palette.strainColor(w.liveStrain)) } if (w.gpsEnabled) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { - StatTile(modifier = Modifier.weight(1f), label = "Distance", value = liveDistance(w.distanceM, unitSystem)) - StatTile(modifier = Modifier.weight(1f), label = "Pace", value = w.paceSecPerKm?.let { livePace(it, unitSystem) } ?: "—") + StatTile(modifier = Modifier.weight(1f), label = tr("Distance"), value = liveDistance(w.distanceM, unitSystem)) + StatTile(modifier = Modifier.weight(1f), label = tr("Pace"), value = w.paceSecPerKm?.let { livePace(it, unitSystem) } ?: "—") } } Button( @@ -394,7 +394,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { colors = ButtonDefaults.buttonColors( containerColor = Palette.statusCritical, contentColor = Palette.surfaceBase, ), - ) { Text("End workout", style = NoopType.captionNumber) } + ) { Text(tr("End workout"), style = NoopType.captionNumber) } } } } else { @@ -411,7 +411,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { ), ) { Text( - "Start workout", style = NoopType.captionNumber, + tr("Start workout"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, overflow = TextOverflow.Clip, ) } @@ -428,7 +428,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { modifier = Modifier.size(18.dp).padding(end = 4.dp), ) Text( - "Refresh", style = NoopType.captionNumber, + tr("Refresh"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, overflow = TextOverflow.Clip, ) } @@ -463,7 +463,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { modifier = Modifier.size(18.dp).padding(end = 4.dp), ) Text( - "Take an HRV reading", style = NoopType.captionNumber, + tr("Take an HRV reading"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, overflow = TextOverflow.Clip, ) } @@ -484,7 +484,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(Metrics.gap), modifier = Modifier.fillMaxWidth(), ) { - Text("Strap", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Strap"), style = NoopType.footnote, color = Palette.textSecondary) SegmentedPillControl( items = WhoopModel.entries.toList(), selection = selectedModel, @@ -496,8 +496,8 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { // nothing while it's still paired in the official WHOOP app. Shown the moment 5/MG is picked. if (selectedModel == WhoopModel.WHOOP5_MG) { Text( - "WHOOP 5.0/MG pairs with one app at a time. If a scan finds nothing, unpair it in " + - "the official WHOOP app and fully close that app, then Connect again.", + tr("WHOOP 5.0/MG pairs with one app at a time. If a scan finds nothing, unpair it in ") + + tr("the official WHOOP app and fully close that app, then Connect again."), style = NoopType.footnote, color = Palette.textSecondary, modifier = Modifier.fillMaxWidth().padding(top = 6.dp), @@ -533,9 +533,9 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { ) Text( when { - live.scanning -> "Searching…" - live.connected -> "Re-scan" - else -> "Connect" + live.scanning -> tr("Searching…") + live.connected -> tr("Re-scan") + else -> tr("Connect") }, style = NoopType.captionNumber, maxLines = 1, @@ -561,7 +561,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { .padding(end = 4.dp), ) Text( - "Buzz", + tr("Buzz"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, @@ -584,7 +584,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { .padding(end = 4.dp), ) Text( - "End", + tr("End"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, @@ -627,7 +627,7 @@ fun LiveScreen(viewModel: AppViewModel, onManageDevices: () -> Unit = {}) { ) } Text( - if (live.backfilling) "Syncing…" else "Sync now", + if (live.backfilling) tr("Syncing…") else tr("Sync now"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, @@ -661,13 +661,13 @@ private fun MaxHrZoneCard(hrMax: Int, zone5Bpm: Int, coachingOn: Boolean) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "Max HR", + label = tr("Max HR"), value = "$hrMax bpm", accent = Palette.textPrimary, ) StatTile( modifier = Modifier.weight(1f), - label = "Top zone", + label = tr("Top zone"), value = "≥ $zone5Bpm bpm", accent = if (coachingOn) Palette.accent else Palette.textTertiary, ) @@ -702,7 +702,7 @@ private fun ActiveBandRow(name: String, onManageDevices: () -> Unit) { ) Spacer(Modifier.size(10.dp)) Column(modifier = Modifier.weight(1f)) { - Overline("Active band") + Overline(tr("Active band")) Text(name, style = NoopType.headline, color = Palette.textPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) } // liquidPress: the "Manage devices" affordance settles inward on press (the iOS LiquidPressStyle @@ -718,10 +718,10 @@ private fun ActiveBandRow(name: String, onManageDevices: () -> Unit) { indication = null, onClick = onManageDevices, ) - .semantics { contentDescription = "Manage devices" } + .semantics { contentDescription = tr("Manage devices") } .padding(horizontal = 8.dp, vertical = 6.dp), ) { - Text("Manage devices", style = NoopType.subhead, color = Palette.accent) + Text(tr("Manage devices"), style = NoopType.subhead, color = Palette.accent) Icon( Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, @@ -739,12 +739,12 @@ private fun ConsoleHeader(live: LiveState, activeConnection: Boolean) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { // Badges row — pill + connection-mode badge + a live SYNCING badge during an offload. val (label, tone) = when { - live.encryptedBond && live.backfilling -> "Bonded · syncing" to StrandTone.Accent - live.encryptedBond -> "Bonded" to StrandTone.Positive - live.bonded -> "Live HR (not fully paired)" to StrandTone.Warning - live.connected -> "Connected" to StrandTone.Warning - live.scanning -> "Searching…" to StrandTone.Warning - else -> "Disconnected" to StrandTone.Critical + live.encryptedBond && live.backfilling -> tr("Bonded · syncing") to StrandTone.Accent + live.encryptedBond -> tr("Bonded") to StrandTone.Positive + live.bonded -> tr("Live HR (not fully paired)") to StrandTone.Warning + live.connected -> tr("Connected") to StrandTone.Warning + live.scanning -> tr("Searching…") to StrandTone.Warning + else -> tr("Disconnected") to StrandTone.Critical } Row( verticalAlignment = Alignment.CenterVertically, @@ -766,12 +766,12 @@ private fun ConsoleHeader(live: LiveState, activeConnection: Boolean) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) { // Charging bolt next to the battery % when the strap reports it's charging (PR #568 reimpl). HeaderStat( - "Battery", + tr("Battery"), live.batteryPct?.let { "${it.toInt()}%" } ?: "—", charging = live.charging == true, ) - HeaderStat("Worn", if (activeConnection) (if (live.worn) "Yes" else "No") else "—") - HeaderStat("Last sync", lastSyncLabel(live)) + HeaderStat(tr("Worn"), if (activeConnection) (if (live.worn) tr("Yes") else tr("No")) else "—") + HeaderStat(tr("Last sync"), lastSyncLabel(live)) } } } @@ -789,7 +789,7 @@ private fun HeaderStat(title: String, value: String, charging: Boolean = false) if (charging) { Icon( Icons.Filled.Bolt, - contentDescription = "Charging", + contentDescription = tr("Charging"), tint = Palette.statusPositive, modifier = Modifier.size(14.dp), ) @@ -822,9 +822,9 @@ private fun OfflineConnectCallout(scanning: Boolean, onConnect: () -> Unit) { modifier = Modifier.size(20.dp), ) Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Start a live stream", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Start a live stream"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Scan and connect to start a live stream.", + tr("Scan and connect to start a live stream."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -846,7 +846,7 @@ private fun OfflineConnectCallout(scanning: Boolean, onConnect: () -> Unit) { modifier = Modifier.size(18.dp).padding(end = 4.dp), ) Text( - if (scanning) "Searching…" else "Scan & Connect", + if (scanning) tr("Searching…") else tr("Scan & Connect"), style = NoopType.captionNumber, maxLines = 1, softWrap = false, @@ -877,7 +877,7 @@ private fun connectionModeColor(live: LiveState, activeConnection: Boolean): Col } private fun lastSyncLabel(live: LiveState): String = - live.lastSyncAt?.let { relativeAgo(it) } ?: "Never" + live.lastSyncAt?.let { relativeAgo(it) } ?: tr("Never") // MARK: - Body console (focal HR ring + live physiology) @@ -926,7 +926,7 @@ private fun HeartReadout(live: LiveState, bpm: Int?, activeConnection: Boolean, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Overline("Heart Rate") + Overline(tr("Heart Rate")) Box( modifier = Modifier .fillMaxWidth(0.6f) @@ -962,7 +962,7 @@ private fun HeartReadout(live: LiveState, bpm: Int?, activeConnection: Boolean, color = Palette.textSecondary, ) } - Text("bpm", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("bpm"), style = NoopType.subhead, color = Palette.textSecondary) if (zone >= 1) { Text("ZONE $zone", style = NoopType.overline, color = tint) } @@ -983,7 +983,7 @@ private fun PhysiologyStack(live: LiveState, activeConnection: Boolean) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row(verticalAlignment = Alignment.Top, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f)) { - Overline("Live Physiology") + Overline(tr("Live Physiology")) Text(connectionModeDetail(live, activeConnection), style = NoopType.headline, color = Palette.textPrimary) } if (rmssd != null) { @@ -999,13 +999,13 @@ private fun PhysiologyStack(live: LiveState, activeConnection: Boolean) { // coloured em-dashes that read as broken live readouts. Real values + accents return on a // stream. Mirrors the macOS liveProofMetric(offline:). LiveProofMetric( - Modifier.weight(1f), "R-R", - if (activeConnection) (live.rr.lastOrNull()?.let { "$it ms" } ?: "—") else "Offline", + Modifier.weight(1f), tr("R-R"), + if (activeConnection) (live.rr.lastOrNull()?.let { "$it ms" } ?: "—") else tr("Offline"), Palette.metricCyan, offline = !activeConnection, ) LiveProofMetric( - Modifier.weight(1f), "Event", - if (activeConnection) (live.lastEvent ?: "—") else "Offline", + Modifier.weight(1f), tr("Event"), + if (activeConnection) (live.lastEvent ?: "—") else tr("Offline"), Palette.statusWarning, offline = !activeConnection, ) } @@ -1048,8 +1048,8 @@ private fun RRStrip(rrRecent: List) { } } Text( - if (values.isEmpty()) "Waiting for R-R intervals." - else "Recent intervals: " + values.takeLast(5).joinToString(" · ") + " ms", + if (values.isEmpty()) tr("Waiting for R-R intervals.") + else tr("Recent intervals: ") + values.takeLast(5).joinToString(" · ") + " ms", style = NoopType.footnote, color = Palette.textTertiary, maxLines = 1, @@ -1113,52 +1113,52 @@ private data class SignalTile( private fun signalTiles(live: LiveState, bpm: Int?, activeConnection: Boolean): List = listOf( SignalTile( - "Heart rate", - bpm?.let { "$it bpm" } ?: "Missing", - if (activeConnection) "Streaming now" else "No active stream", + tr("Heart rate"), + bpm?.let { "$it bpm" } ?: tr("Missing"), + if (activeConnection) tr("Streaming now") else tr("No active stream"), if (bpm == null) Palette.textTertiary else Palette.accent, ), SignalTile( - "R-R intervals", - if (live.rrRecent.isEmpty()) "Missing" else "${live.rrRecent.size} recent", - rollingRMSSD(live.rrRecent)?.let { "RMSSD ${it.roundToInt()} ms" } ?: "Needs interval frames", + tr("R-R intervals"), + if (live.rrRecent.isEmpty()) tr("Missing") else "${live.rrRecent.size} recent", + rollingRMSSD(live.rrRecent)?.let { "RMSSD ${it.roundToInt()} ms" } ?: tr("Needs interval frames"), if (live.rrRecent.isEmpty()) Palette.textTertiary else Palette.metricCyan, ), SignalTile( - "Connection", + tr("Connection"), when { - activeConnection && live.encryptedBond -> "Encrypted" - activeConnection -> "Partial" - live.connected -> "Connected" - else -> "Offline" + activeConnection && live.encryptedBond -> tr("Encrypted") + activeConnection -> tr("Partial") + live.connected -> tr("Connected") + else -> tr("Offline") }, - if (activeConnection && live.encryptedBond) "Controls unlocked" else "Standard HR is not a full bond", + if (activeConnection && live.encryptedBond) tr("Controls unlocked") else tr("Standard HR is not a full bond"), connectionModeColor(live, activeConnection), ), SignalTile( - "History sync", + tr("History sync"), if (live.backfilling) "${live.syncChunksThisSession} chunks" else lastSyncLabel(live), when { live.lastSyncError != null -> live.lastSyncError - live.backfilling -> "Offload in progress" - live.lastSyncAt == null -> "No completed offload yet" - else -> "Last offload completed" + live.backfilling -> tr("Offload in progress") + live.lastSyncAt == null -> tr("No completed offload yet") + else -> tr("Last offload completed") }, if (live.backfilling) Palette.metricCyan else Palette.textSecondary, ), SignalTile( - "Battery", - live.batteryPct?.let { "${it.toInt()}%" } ?: "Unknown", - if (live.charging == true) "Charging" else "Last reported by strap", + tr("Battery"), + live.batteryPct?.let { "${it.toInt()}%" } ?: tr("Unknown"), + if (live.charging == true) tr("Charging") else tr("Last reported by strap"), batteryTint(live.batteryPct), ), // Wear is only trustworthy on a live link: `worn` defaults true and is only updated by // WRIST_ON/OFF events, so while OFFLINE it would read a false-green "On wrist". Gate value + tint // on activeConnection (triage fix for PR#191, parity with the macOS Wear tile). SignalTile( - "Wear state", - if (activeConnection) (if (live.worn) "On wrist" else "Off wrist") else "Unknown", - if (activeConnection) (if (live.worn) "Eligible for live physiology" else "Wear the strap for scoring") else "Connect to read wear state", + tr("Wear state"), + if (activeConnection) (if (live.worn) tr("On wrist") else tr("Off wrist")) else tr("Unknown"), + if (activeConnection) (if (live.worn) tr("Eligible for live physiology") else tr("Wear the strap for scoring")) else tr("Connect to read wear state"), when { !activeConnection -> Palette.textTertiary live.worn -> Palette.accent @@ -1181,19 +1181,19 @@ private fun SignalTrustTile(tile: SignalTile, modifier: Modifier = Modifier) { // MARK: - Pure helpers (shared by the body console + the trust rail) private fun signalTrustSummary(live: LiveState, activeConnection: Boolean): String = when { - activeConnection && live.encryptedBond -> "Encrypted stream - deep controls and history sync available." - activeConnection -> "Live heart rate is flowing; full strap controls need an encrypted bond." - live.connected -> "Connected, waiting for a streaming state." + activeConnection && live.encryptedBond -> tr("Encrypted stream - deep controls and history sync available.") + activeConnection -> tr("Live heart rate is flowing; full strap controls need an encrypted bond.") + live.connected -> tr("Connected, waiting for a streaming state.") // The actionable "Scan and connect…" CTA now lives in the above-the-fold OfflineConnectCallout, // so this ring caption stays a calm empty-state descriptor rather than a competing CTA. - else -> "Live heart rate appears here once a strap is connected." + else -> tr("Live heart rate appears here once a strap is connected.") } private fun connectionModeDetail(live: LiveState, activeConnection: Boolean): String = when { - activeConnection && live.encryptedBond -> "Full strap stream is active." - activeConnection -> "Heart rate stream is active." - live.connected -> "Radio connected, stream not yet trusted." - else -> "No live stream." + activeConnection && live.encryptedBond -> tr("Full strap stream is active.") + activeConnection -> tr("Heart rate stream is active.") + live.connected -> tr("Radio connected, stream not yet trusted.") + else -> tr("No live stream.") } /** A "feel" RMSSD over the recent R-R buffer — time-gap-unaware on purpose (a live indicator, not a diff --git a/android/app/src/main/java/com/noop/ui/LiveSessionScreen.kt b/android/app/src/main/java/com/noop/ui/LiveSessionScreen.kt index 3573b61c3..7b452eb88 100644 --- a/android/app/src/main/java/com/noop/ui/LiveSessionScreen.kt +++ b/android/app/src/main/java/com/noop/ui/LiveSessionScreen.kt @@ -193,7 +193,7 @@ private fun LiveSessionBody( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - Text("Live Session", style = NoopType.title1, color = Palette.textPrimary) + Text(tr("Live Session"), style = NoopType.title1, color = Palette.textPrimary) StatePill("BETA", tone = StrandTone.Accent, showsDot = false) } @@ -213,8 +213,8 @@ private fun LiveSessionBody( // The one line of copy. STALE says so honestly (coaching paused, nothing accrues); otherwise the // guarding promise — the whole design is that this screen has nothing to watch. Text( - if (stale) "Signal lost — coaching paused." - else "Guarding your session. Silence means you're on track.", + if (stale) tr("Signal lost — coaching paused.") + else tr("Guarding your session. Silence means you're on track."), style = NoopType.subhead, color = if (stale) Palette.textTertiary else Palette.textSecondary, textAlign = TextAlign.Center, @@ -246,7 +246,7 @@ private fun LiveSessionBody( colors = ButtonDefaults.buttonColors( containerColor = Palette.statusCritical, contentColor = Palette.surfaceBase, ), - ) { Text("End session", style = NoopType.headline) } + ) { Text(tr("End session"), style = NoopType.headline) } } } @@ -412,8 +412,8 @@ private fun LiveSessionSummary( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Live Session", color = teal) - Text("Session summary", style = NoopType.title1, color = Palette.textPrimary) + Overline(tr("Live Session"), color = teal) + Text(tr("Session summary"), style = NoopType.title1, color = Palette.textPrimary) } StatePill("BETA", tone = StrandTone.Accent, showsDot = false) } @@ -421,7 +421,7 @@ private fun LiveSessionSummary( // The stale auto-end declares itself — an unexplained early end would read as a bug. if (snap.endedAutomatically) { Text( - "The strap signal was gone for 10 minutes, so the session ended itself.", + tr("The strap signal was gone for 10 minutes, so the session ended itself."), style = NoopType.footnote, color = Palette.statusWarning, ) @@ -442,15 +442,15 @@ private fun LiveSessionSummary( // Where the time went — the three accrued buckets, on the shared StatTile. Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap), modifier = Modifier.fillMaxWidth()) { StatTile( - modifier = Modifier.weight(1f), label = "In band", + modifier = Modifier.weight(1f), label = tr("In band"), value = elapsedClock(inBandSec.toLong()), accent = teal, ) StatTile( - modifier = Modifier.weight(1f), label = "Below", + modifier = Modifier.weight(1f), label = tr("Below"), value = elapsedClock(snap.belowSec.toLong()), accent = Palette.textSecondary, ) StatTile( - modifier = Modifier.weight(1f), label = "Above", + modifier = Modifier.weight(1f), label = tr("Above"), value = elapsedClock(snap.aboveSec.toLong()), accent = Palette.statusCritical, ) } @@ -478,7 +478,7 @@ private fun LiveSessionSummary( colors = ButtonDefaults.buttonColors( containerColor = Palette.accent, contentColor = Palette.surfaceBase, ), - ) { Text("Done", style = NoopType.headline) } + ) { Text(tr("Done"), style = NoopType.headline) } } } diff --git a/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt b/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt index a0e5543b5..0375aee08 100644 --- a/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt +++ b/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt @@ -121,7 +121,7 @@ fun LiveWorkoutScreen(vm: AppViewModel, onClose: () -> Unit) { // Header — sport + elapsed clock. Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Recording workout", color = Palette.effortColor) + Overline(tr("Recording workout"), color = Palette.effortColor) Text(w.sport.name, style = NoopType.title1, color = Palette.textPrimary) } Text( @@ -142,11 +142,11 @@ fun LiveWorkoutScreen(vm: AppViewModel, onClose: () -> Unit) { // Live stats grid — avg / peak / effort, from the captured window. Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap), modifier = Modifier.fillMaxWidth()) { - StatTile(modifier = Modifier.weight(1f), label = "Avg", value = if (w.avgHr > 0) "${w.avgHr}" else "—", + StatTile(modifier = Modifier.weight(1f), label = tr("Avg"), value = if (w.avgHr > 0) "${w.avgHr}" else "—", accent = if (w.avgHr > 0) Palette.metricRose else Palette.textPrimary) - StatTile(modifier = Modifier.weight(1f), label = "Peak", value = if (w.peakHr > 0) "${w.peakHr}" else "—", + StatTile(modifier = Modifier.weight(1f), label = tr("Peak"), value = if (w.peakHr > 0) "${w.peakHr}" else "—", accent = if (w.peakHr > 0) Palette.metricRose else Palette.textPrimary) - StatTile(modifier = Modifier.weight(1f), label = "Effort", value = UnitFormatter.effortDisplay(w.liveStrain, effortScale), + StatTile(modifier = Modifier.weight(1f), label = tr("Effort"), value = UnitFormatter.effortDisplay(w.liveStrain, effortScale), accent = Palette.strainColor(w.liveStrain)) } @@ -166,7 +166,7 @@ fun LiveWorkoutScreen(vm: AppViewModel, onClose: () -> Unit) { colors = ButtonDefaults.buttonColors( containerColor = Palette.statusCritical, contentColor = Palette.surfaceBase, ), - ) { Text("End workout", style = NoopType.headline) } + ) { Text(tr("End workout"), style = NoopType.headline) } } } } @@ -186,16 +186,16 @@ private fun SensorRow(sensor: StandardHrSource.SensorMetrics) { val power = StandardHrSource.formatPowerWatts(sensor.powerWatts) if (speed == null && cadence == null && power == null) return Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Sensor") + Overline(tr("Sensor")) Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap), modifier = Modifier.fillMaxWidth()) { if (speed != null) { - StatTile(modifier = Modifier.weight(1f), label = "Speed", value = "$speed km/h", accent = Palette.effortColor) + StatTile(modifier = Modifier.weight(1f), label = tr("Speed"), value = "$speed km/h", accent = Palette.effortColor) } if (cadence != null) { - StatTile(modifier = Modifier.weight(1f), label = "Cadence", value = "$cadence/min", accent = Palette.effortColor) + StatTile(modifier = Modifier.weight(1f), label = tr("Cadence"), value = "$cadence/min", accent = Palette.effortColor) } if (power != null) { - StatTile(modifier = Modifier.weight(1f), label = "Power", value = "$power W", accent = Palette.effortColor) + StatTile(modifier = Modifier.weight(1f), label = tr("Power"), value = "$power W", accent = Palette.effortColor) } } } @@ -209,7 +209,7 @@ private fun EffortGauge(liveStrain: Double, effortScale: EffortScale) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp), ) { - Overline("Effort building", color = Palette.effortColor) + Overline(tr("Effort building"), color = Palette.effortColor) StrainGauge( strain = UnitFormatter.effortValue(liveStrain, effortScale), outOf = if (effortScale == EffortScale.WHOOP) 21.0 else 100.0, @@ -234,7 +234,7 @@ private fun HeroHeartRate(bpm: Int?, zone: Int) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp), ) { - Overline("Heart rate") + Overline(tr("Heart rate")) Box(contentAlignment = Alignment.Center) { // Soft zone-tinted halo behind the numeral — the Bevel glow. Box( @@ -245,9 +245,9 @@ private fun HeroHeartRate(bpm: Int?, zone: Int) { ) Text(bpm?.toString() ?: "—", style = NoopType.number(80f), color = tint) } - Text("bpm", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("bpm"), style = NoopType.subhead, color = Palette.textSecondary) Text( - if (zone >= 1) "Zone $zone · ${zoneName(zone)}" else "Below Zone 1", + if (zone >= 1) "Zone $zone · ${zoneName(zone)}" else tr("Below Zone 1"), style = NoopType.captionNumber, color = tint, textAlign = TextAlign.Center, @@ -259,7 +259,7 @@ private fun HeroHeartRate(bpm: Int?, zone: Int) { @Composable private fun ZoneRail(zone: Int, zoneSet: com.noop.analytics.HrZoneSet) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("HR zone") + Overline(tr("HR zone")) Row(horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { (1..5).forEach { z -> val active = z == zone @@ -292,7 +292,7 @@ private fun ZoneRail(zone: Int, zoneSet: com.noop.analytics.HrZoneSet) { Text( if (band != null) "Zone $zone: ${band.lower.toInt()} - ${band.upper.toInt()} bpm (${(band.lowerPct * 100).toInt()} - ${(band.upperPct * 100).toInt()}% max HR)" - else "Warming up - keep moving to climb into Zone 1.", + else tr("Warming up - keep moving to climb into Zone 1."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -300,10 +300,10 @@ private fun ZoneRail(zone: Int, zoneSet: com.noop.analytics.HrZoneSet) { } private fun zoneName(zone: Int): String = when (zone) { - 1 -> "Recovery" - 2 -> "Fat burn" - 3 -> "Aerobic" - 4 -> "Threshold" - 5 -> "Maximum" + 1 -> tr("Recovery") + 2 -> tr("Fat burn") + 3 -> tr("Aerobic") + 4 -> tr("Threshold") + 5 -> tr("Maximum") else -> "" } diff --git a/android/app/src/main/java/com/noop/ui/MarkerEditorScreen.kt b/android/app/src/main/java/com/noop/ui/MarkerEditorScreen.kt index cfd442020..f9560d9e7 100644 --- a/android/app/src/main/java/com/noop/ui/MarkerEditorScreen.kt +++ b/android/app/src/main/java/com/noop/ui/MarkerEditorScreen.kt @@ -145,15 +145,15 @@ fun MarkerEditorScreen( NoopBottomSheet(onDismiss = onDismiss) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.sectionGap)) { - Text("Add a reading", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Add a reading"), style = NoopType.title2, color = Palette.textPrimary) Text( - "Type in a number from your own report. It stays on this phone.", + tr("Type in a number from your own report. It stays on this phone."), style = NoopType.subhead, color = Palette.textSecondary, ) // --- Marker picker --- - SectionHeader("Marker", overline = "what are you logging?") + SectionHeader(tr("Marker"), overline = tr("what are you logging?")) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { when { @@ -181,11 +181,11 @@ fun MarkerEditorScreen( OutlinedTextField( value = search, onValueChange = { search = it }, - placeholder = { Text("Search markers (e.g. LDL, ferritin)", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Search markers (e.g. LDL, ferritin)"), style = NoopType.body, color = Palette.textTertiary) }, leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null, tint = Palette.textTertiary) }, singleLine = true, colors = editorFieldColors(), - modifier = Modifier.fillMaxWidth().semantics { contentDescription = "Search markers" }, + modifier = Modifier.fillMaxWidth().semantics { contentDescription = tr("Search markers") }, ) CatalogList(search) { def -> selection = def; unitChoice = 0 @@ -198,7 +198,7 @@ fun MarkerEditorScreen( // --- Reading inputs --- if (selection != null || addingCustom) { - SectionHeader("Reading", overline = "your number, date and any note") + SectionHeader(tr("Reading"), overline = tr("your number, date and any note")) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { if (isBloodPressure) { @@ -211,7 +211,7 @@ fun MarkerEditorScreen( } } Text( - "Entered together; stored as two markers so each lines up cleanly against your signals.", + tr("Entered together; stored as two markers so each lines up cleanly against your signals."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -247,7 +247,7 @@ fun MarkerEditorScreen( EditorTextField(referenceText, { referenceText = it }, "e.g. 2.0-5.0 (your report's own range)") } Text( - "NOOP never fills this in - it only shows back exactly what you type from your own report.", + tr("NOOP never fills this in - it only shows back exactly what you type from your own report."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -256,8 +256,8 @@ fun MarkerEditorScreen( } Text( - "Lab Book keeps your own numbers - it doesn't test, read, or judge them, and it's not medical " + - "advice. Everything stays on this phone.", + tr("Lab Book keeps your own numbers - it doesn't test, read, or judge them, and it's not medical ") + + tr("advice. Everything stays on this phone."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -307,7 +307,7 @@ private fun CatalogList(search: String, onPick: (MarkerDefinition) -> Unit) { } } if (filtered.isEmpty()) { - Text("No match. Add it as a custom marker below.", style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(vertical = 8.dp)) + Text(tr("No match. Add it as a custom marker below."), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(vertical = 8.dp)) } } } @@ -339,7 +339,7 @@ private fun DateRow(millis: Long, onPick: (Long) -> Unit) { ).apply { datePicker.maxDate = System.currentTimeMillis() }.show() } .padding(horizontal = 12.dp, vertical = 11.dp) - .semantics { contentDescription = "Date taken" }, + .semantics { contentDescription = tr("Date taken") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/android/app/src/main/java/com/noop/ui/MindSection.kt b/android/app/src/main/java/com/noop/ui/MindSection.kt index b60ffcd75..04c7aab83 100644 --- a/android/app/src/main/java/com/noop/ui/MindSection.kt +++ b/android/app/src/main/java/com/noop/ui/MindSection.kt @@ -97,20 +97,20 @@ fun MindSection(vm: AppViewModel) { } Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Mind", overline = "Mood check-in") + SectionHeader(tr("Mind"), overline = tr("Mood check-in")) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { if (!loaded) { Text( - "Reading your check-ins…", + tr("Reading your check-ins…"), style = NoopType.subhead, color = Palette.textTertiary, ) } else if (todayMood == null || editing) { // --- Open check-in: the 5-face scale ----------------------- Text( - "How are you feeling today?", + tr("How are you feeling today?"), style = NoopType.headline, color = Palette.textPrimary, ) @@ -134,7 +134,7 @@ fun MindSection(vm: AppViewModel) { } } Text( - "One check-in per day; picking another face overwrites today's.", + tr("One check-in per day; picking another face overwrites today's."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -149,7 +149,7 @@ fun MindSection(vm: AppViewModel) { Spacer(Modifier.width(10.dp)) Column(modifier = Modifier.weight(1f)) { Text(face.word, style = NoopType.headline, color = Palette.textPrimary) - Text("Logged today", style = NoopType.caption, color = Palette.textTertiary) + Text(tr("Logged today"), style = NoopType.caption, color = Palette.textTertiary) } MoodChip("Edit") { editing = true } } @@ -168,8 +168,8 @@ fun MindSection(vm: AppViewModel) { ) } else if (lines.isEmpty()) { Text( - "Not enough overlapping history to correlate mood with your " + - "body metrics yet.", + tr("Not enough overlapping history to correlate mood with your ") + + tr("body metrics yet."), style = NoopType.footnote, color = Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/NotificationsSettingsScreen.kt b/android/app/src/main/java/com/noop/ui/NotificationsSettingsScreen.kt index c2160bbb1..b29a76829 100644 --- a/android/app/src/main/java/com/noop/ui/NotificationsSettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/NotificationsSettingsScreen.kt @@ -260,18 +260,18 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { val enabledCount = enabledState.values.count { it } ScreenScaffold( - title = "Notifications", - subtitle = "Buzz your strap when these apps notify you. Everything runs on this device.", + title = tr("Notifications"), + subtitle = tr("Buzz your strap when these apps notify you. Everything runs on this device."), ) { // MARK: Master card AlertSection( icon = Icons.Filled.NotificationsActive, - title = "Wrist alerts", - blurb = "When on, NOOP taps your wrist for the apps you pick below, so you can leave " + - "your phone and still feel what matters.", + title = tr("Wrist alerts"), + blurb = tr("When on, NOOP taps your wrist for the apps you pick below, so you can leave ") + + tr("your phone and still feel what matters."), ) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("Enable wrist alerts", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Enable wrist alerts"), style = NoopType.body, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) NoopSwitch( checked = masterEnabled, @@ -279,7 +279,7 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { masterEnabled = it NotifPrefs.setBool(context, NotifPrefs.MASTER, it) }, - label = "Enable wrist alerts", + label = tr("Enable wrist alerts"), ) } @@ -296,7 +296,7 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { ) Spacer(Modifier.weight(1f)) PillButton( - label = "Test buzz", + label = tr("Test buzz"), icon = Icons.Filled.GraphicEq, enabled = live.bonded, onClick = { vm.buzz(loops = 2) }, @@ -375,12 +375,12 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { // MARK: Behaviour card AlertSection( icon = Icons.Filled.Tune, - title = "Behaviour", - blurb = "Fine-tune when alerts reach your wrist.", + title = tr("Behaviour"), + blurb = tr("Fine-tune when alerts reach your wrist."), ) { FormToggleRow( - label = "Only buzz when worn", - help = "Skip alerts when the strap is off your wrist.", + label = tr("Only buzz when worn"), + help = tr("Skip alerts when the strap is off your wrist."), checked = onlyWhenWorn, onChange = { onlyWhenWorn = it @@ -389,10 +389,10 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { ) RowDivider() FormToggleRow( - label = "All other apps", - help = "Also buzz for apps that aren't in the lists above (e.g. BeReal). Android " + - "doesn't let NOOP see every installed app, so this is how you cover the rest. " + - "Can be chatty; quiet hours and \"only when worn\" still apply.", + label = tr("All other apps"), + help = tr("Also buzz for apps that aren't in the lists above (e.g. BeReal). Android ") + + tr("doesn't let NOOP see every installed app, so this is how you cover the rest. ") + + tr("Can be chatty; quiet hours and \"only when worn\" still apply."), checked = allOtherApps, onChange = { allOtherApps = it @@ -401,8 +401,8 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { ) RowDivider() FormToggleRow( - label = "Quiet hours", - help = "Mute wrist alerts overnight.", + label = tr("Quiet hours"), + help = tr("Mute wrist alerts overnight."), checked = quietHoursEnabled, onChange = { quietHoursEnabled = it @@ -416,19 +416,19 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text("From", style = NoopType.body, color = Palette.textPrimary) + Text(tr("From"), style = NoopType.body, color = Palette.textPrimary) TimeChip( minutes = quietStartMinutes, - accessibilityLabel = "Quiet hours start", + accessibilityLabel = tr("Quiet hours start"), onPicked = { quietStartMinutes = it NotifPrefs.setInt(context, NotifPrefs.QUIET_START, it) }, ) - Text("to", style = NoopType.body, color = Palette.textSecondary) + Text(tr("to"), style = NoopType.body, color = Palette.textSecondary) TimeChip( minutes = quietEndMinutes, - accessibilityLabel = "Quiet hours end", + accessibilityLabel = tr("Quiet hours end"), onPicked = { quietEndMinutes = it NotifPrefs.setInt(context, NotifPrefs.QUIET_END, it) @@ -442,15 +442,15 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { // MARK: Daily reports (#517) — phone notifications, not wrist buzzes. Opt-in, default OFF, no AI. AlertSection( icon = Icons.Filled.NotificationsActive, - title = "Daily reports", - blurb = "Optional phone notifications, off by default. These arrive after your strap syncs " + - "and NOOP scores the data, so they land soon after, not the exact second you wake or " + - "finish a workout. Everything is worked out on this phone.", + title = tr("Daily reports"), + blurb = tr("Optional phone notifications, off by default. These arrive after your strap syncs ") + + tr("and NOOP scores the data, so they land soon after, not the exact second you wake or ") + + tr("finish a workout. Everything is worked out on this phone."), ) { FormToggleRow( - label = "Morning recap", - help = "After last night is processed, a notification with your Charge and Rest. Posts " + - "once a day, after your strap has synced the night.", + label = tr("Morning recap"), + help = tr("After last night is processed, a notification with your Charge and Rest. Posts ") + + tr("once a day, after your strap has synced the night."), checked = morningReport, onChange = { morningReport = it @@ -459,9 +459,9 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { ) RowDivider() FormToggleRow( - label = "Post-workout summary", - help = "When a new workout syncs in, a notification with its Effort, duration and average " + - "heart rate. Shows up after the session reaches NOOP on the next sync.", + label = tr("Post-workout summary"), + help = tr("When a new workout syncs in, a notification with its Effort, duration and average ") + + tr("heart rate. Shows up after the session reaches NOOP on the next sync."), checked = postWorkoutReport, onChange = { postWorkoutReport = it @@ -478,9 +478,9 @@ fun NotificationsSettingsScreen(vm: AppViewModel) { // MARK: - Strap status (mirrors the three-state mapping from the Mac screen) private fun strapPillTitle(live: com.noop.ble.LiveState): String = when { - live.connected -> "Strap connected" - live.bonded -> "Strap idle" - else -> "Strap not connected" + live.connected -> tr("Strap connected") + live.bonded -> tr("Strap idle") + else -> tr("Strap not connected") } private fun strapPillTone(live: com.noop.ble.LiveState): StrandTone = when { @@ -507,8 +507,8 @@ private fun CallsCard( val contentAlpha = if (masterEnabled) 1f else Palette.disabledOpacity AlertSection( icon = Icons.Filled.Call, - title = "Calls", - blurb = "Tap your wrist for incoming phone calls and strict best-effort VoIP calls.", + title = tr("Calls"), + blurb = tr("Tap your wrist for incoming phone calls and strict best-effort VoIP calls."), ) { Column(modifier = Modifier.alphaIf(contentAlpha)) { Row( @@ -519,36 +519,36 @@ private fun CallsCard( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Buzz on incoming calls", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Buzz on incoming calls"), style = NoopType.body, color = Palette.textPrimary) Text( - "Uses the same quiet-hours and worn-only rules.", + tr("Uses the same quiet-hours and worn-only rules."), style = NoopType.footnote, color = Palette.textTertiary, ) } if (callsEnabled) { - PatternMenu(pattern = pattern, enabled = masterEnabled, appName = "calls", onSelect = onPattern) - TestIconButton(enabled = masterEnabled && bonded, appName = "calls", onClick = onTest) + PatternMenu(pattern = pattern, enabled = masterEnabled, appName = tr("calls"), onSelect = onPattern) + TestIconButton(enabled = masterEnabled && bonded, appName = tr("calls"), onClick = onTest) } NoopSwitch( checked = callsEnabled, onChange = onCallsEnabled, enabled = masterEnabled, - label = "Buzz on incoming calls", + label = tr("Buzz on incoming calls"), ) } if (callsEnabled) { RowDivider() FormToggleRow( - label = "Phone calls", - help = "Needs Phone permission; NOOP never reads numbers or call logs.", + label = tr("Phone calls"), + help = tr("Needs Phone permission; NOOP never reads numbers or call logs."), checked = phoneCallsEnabled, enabled = masterEnabled, onChange = onPhoneCallsEnabled, ) if (permissionDenied) { Text( - "Phone permission was denied, so phone-call buzzing is off.", + tr("Phone permission was denied, so phone-call buzzing is off."), style = NoopType.footnote, color = Palette.statusCritical, modifier = Modifier.padding(top = 2.dp, bottom = 8.dp), @@ -556,8 +556,8 @@ private fun CallsCard( } RowDivider() FormToggleRow( - label = "VoIP calls", - help = "Detects call-style notifications from known calling apps.", + label = tr("VoIP calls"), + help = tr("Detects call-style notifications from known calling apps."), checked = voipCallsEnabled, enabled = masterEnabled, onChange = onVoipCallsEnabled, @@ -590,9 +590,9 @@ private fun DeliveryNote() { modifier = Modifier.size(16.dp), ) Text( - "Wrist delivery needs Notification Access so NOOP can read which apps notify " + - "you. Nothing leaves this device. Your choices are saved now and apply " + - "automatically once access is granted.", + tr("Wrist delivery needs Notification Access so NOOP can read which apps notify ") + + tr("you. Nothing leaves this device. Your choices are saved now and apply ") + + tr("automatically once access is granted."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -609,7 +609,7 @@ private fun DeliveryNote() { } } .padding(horizontal = 2.dp, vertical = 2.dp) - .semantics { contentDescription = "Open Notification Access settings" }, + .semantics { contentDescription = tr("Open Notification Access settings") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { @@ -619,7 +619,7 @@ private fun DeliveryNote() { tint = Palette.accent, modifier = Modifier.size(14.dp), ) - Text("Open Notification Access", style = NoopType.caption, color = Palette.accent) + Text(tr("Open Notification Access"), style = NoopType.caption, color = Palette.accent) } } } @@ -694,7 +694,7 @@ private fun AppRow( Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { Text(app.name, style = NoopType.body, color = Palette.textPrimary) Text( - if (enabled) "Buzzes your wrist" else "Off", + if (enabled) tr("Buzzes your wrist") else tr("Off"), style = NoopType.footnote, color = if (enabled) Palette.accent else Palette.textTertiary, ) @@ -877,7 +877,7 @@ internal fun TimeChip( horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), ) { Text( - "Cancel", + tr("Cancel"), style = NoopType.body, color = Palette.textSecondary, modifier = Modifier @@ -886,7 +886,7 @@ internal fun TimeChip( .padding(horizontal = 16.dp, vertical = 8.dp), ) Text( - "Set", + tr("Set"), style = NoopType.body, color = Palette.accent, modifier = Modifier @@ -910,7 +910,7 @@ private fun AlertSection( icon: ImageVector, title: String, blurb: String? = null, - overline: String = "Alerts", + overline: String = tr("Alerts"), content: @Composable () -> Unit, ) { NoopCard(padding = 20.dp, tint = Palette.accent) { diff --git a/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt b/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt index b6e1b84b8..c8907cc7e 100644 --- a/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt +++ b/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt @@ -217,18 +217,18 @@ fun OnboardingScreen(viewModel: AppViewModel, onFinished: () -> Unit) { } private enum class OnboardingPage(val cta: String) { - Welcome("Begin"), - WhatItDoes("Continue"), - Expectations("Continue"), - Bluetooth("Continue"), - Wear("Continue"), - Connect("Continue"), - Bonded("Continue"), - Profile("Save & continue"), - Import("Continue"), - Notifications("Continue"), - Appearance("Continue"), - Done("Enter NOOP"); + Welcome(tr("Begin")), + WhatItDoes(tr("Continue")), + Expectations(tr("Continue")), + Bluetooth(tr("Continue")), + Wear(tr("Continue")), + Connect(tr("Continue")), + Bonded(tr("Continue")), + Profile(tr("Save & continue")), + Import(tr("Continue")), + Notifications(tr("Continue")), + Appearance(tr("Continue")), + Done(tr("Enter NOOP")); } // MARK: - Shell @@ -291,7 +291,7 @@ private fun OnboardingFooter( ), modifier = Modifier.weight(0.9f), ) { - Text("Back", style = NoopType.subhead) + Text(tr("Back"), style = NoopType.subhead) } Button( onClick = onNext, @@ -372,14 +372,14 @@ private fun WelcomeStep() { } Spacer(Modifier.height(18.dp)) Text( - "all your data, none of the cloud", + tr("all your data, none of the cloud"), style = NoopType.title2, color = Palette.textSecondary, textAlign = TextAlign.Center, ) Spacer(Modifier.height(12.dp)) Text( - "A private window into your recovery, sleep and strain. Read straight from your strap, kept only on this phone.", + tr("A private window into your recovery, sleep and strain. Read straight from your strap, kept only on this phone."), style = NoopType.body, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -391,27 +391,27 @@ private fun WelcomeStep() { @Composable private fun WhatItDoesStep() { StepShell( - title = "What NOOP does", - subtitle = "Three quiet promises.", + title = tr("What NOOP does"), + subtitle = tr("Three quiet promises."), ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { FeatureRow( icon = Icons.Filled.AutoGraph, tint = Palette.accent, - title = "See recovery, clearly", - body = "A calm ring rolls HRV, resting heart rate and sleep into one read on whether to push or rest.", + title = tr("See recovery, clearly"), + body = tr("A calm ring rolls HRV, resting heart rate and sleep into one read on whether to push or rest."), ) FeatureRow( icon = Icons.Filled.MonitorHeart, tint = Palette.accent, - title = "Watch your heart, live", - body = "Connect a WHOOP, a heart-rate strap or a gym machine and watch each beat in real time, with zones that match your profile. Already have history elsewhere? Import it from WHOOP, Apple Health, Oura, Fitbit or Garmin.", + title = tr("Watch your heart, live"), + body = tr("Connect a WHOOP, a heart-rate strap or a gym machine and watch each beat in real time, with zones that match your profile. Already have history elsewhere? Import it from WHOOP, Apple Health, Oura, Fitbit or Garmin."), ) FeatureRow( icon = Icons.Filled.Lock, tint = Palette.statusPositive, - title = "Own your data, offline", - body = "Everything lives on this phone. No account, no sync, no cloud.", + title = tr("Own your data, offline"), + body = tr("Everything lives on this phone. No account, no sync, no cloud."), ) } } @@ -420,8 +420,8 @@ private fun WhatItDoesStep() { @Composable private fun ExpectationsStep() { StepShell( - title = "What to expect", - subtitle = "A few honest words, so nothing is a surprise.", + title = tr("What to expect"), + subtitle = tr("A few honest words, so nothing is a surprise."), ) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { AppChangelog.expectations.forEach { e -> @@ -434,8 +434,8 @@ private fun ExpectationsStep() { @Composable private fun BluetoothStep() { StepShell( - title = "A quick word before you connect", - subtitle = "NOOP uses Bluetooth to find your strap. When you continue, allow the permission so it can scan.", + title = tr("A quick word before you connect"), + subtitle = tr("NOOP uses Bluetooth to find your strap. When you continue, allow the permission so it can scan."), ) { Column( modifier = Modifier.fillMaxWidth(), @@ -446,11 +446,11 @@ private fun BluetoothStep() { InfoCard( icon = Icons.Filled.Lock, tint = Palette.statusPositive, - title = "Nothing leaves your phone", - message = "NOOP talks to your strap directly over Bluetooth Low Energy. There's no server in the middle. The connection is local, and so is every reading it pulls in.", + title = tr("Nothing leaves your phone"), + message = tr("NOOP talks to your strap directly over Bluetooth Low Energy. There's no server in the middle. The connection is local, and so is every reading it pulls in."), ) - Checkline("When Android asks, allow Bluetooth so NOOP can scan and connect.") - Checkline("WHOOP 5.0/MG may need pairing mode the first time, with the official WHOOP app closed.") + Checkline(tr("When Android asks, allow Bluetooth so NOOP can scan and connect.")) + Checkline(tr("WHOOP 5.0/MG may need pairing mode the first time, with the official WHOOP app closed.")) } } } @@ -458,8 +458,8 @@ private fun BluetoothStep() { @Composable private fun WearStep() { StepShell( - title = "Put your strap on", - subtitle = "The sensor needs skin contact before data starts to mean anything.", + title = tr("Put your strap on"), + subtitle = tr("The sensor needs skin contact before data starts to mean anything."), ) { Column( modifier = Modifier.fillMaxWidth(), @@ -469,9 +469,9 @@ private fun WearStep() { IconBadge(icon = Icons.Filled.Sensors, tint = Palette.accent, size = 86) NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Checkline("Wear it snug on your wrist or bicep, sensor against skin.") - Checkline("Give it a few minutes of charge if the battery is low.") - Checkline("Keep it near this phone while pairing and during the first sync.") + Checkline(tr("Wear it snug on your wrist or bicep, sensor against skin.")) + Checkline(tr("Give it a few minutes of charge if the battery is low.")) + Checkline(tr("Keep it near this phone while pairing and during the first sync.")) } } } @@ -505,11 +505,11 @@ private fun ConnectStep(viewModel: AppViewModel) { } StepShell( - title = "Find your strap", + title = tr("Find your strap"), subtitle = when { - live.bonded -> "Bonded. You can keep going." - bleGranted -> "NOOP starts looking as soon as this step appears. You can keep going while it bonds." - else -> "Allow Bluetooth and tap Scan to find your strap, or keep going and connect later." + live.bonded -> tr("Bonded. You can keep going.") + bleGranted -> tr("NOOP starts looking as soon as this step appears. You can keep going while it bonds.") + else -> tr("Allow Bluetooth and tap Scan to find your strap, or keep going and connect later.") }, ) { Column( @@ -524,11 +524,11 @@ private fun ConnectStep(viewModel: AppViewModel) { ) val (label, tone, pulsing) = when { - live.encryptedBond -> Triple("Bonded · streaming", StrandTone.Positive, true) - live.bonded -> Triple("Live HR · not fully paired", StrandTone.Warning, true) - live.connected -> Triple("Connected · pairing", StrandTone.Warning, true) - live.scanning -> Triple("Searching", StrandTone.Accent, true) - else -> Triple("Ready to scan", StrandTone.Neutral, false) + live.encryptedBond -> Triple(tr("Bonded · streaming"), StrandTone.Positive, true) + live.bonded -> Triple(tr("Live HR · not fully paired"), StrandTone.Warning, true) + live.connected -> Triple(tr("Connected · pairing"), StrandTone.Warning, true) + live.scanning -> Triple(tr("Searching"), StrandTone.Accent, true) + else -> Triple(tr("Ready to scan"), StrandTone.Neutral, false) } StatePill(label, tone = tone, pulsing = pulsing, showsDot = true) @@ -548,7 +548,7 @@ private fun ConnectStep(viewModel: AppViewModel) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text("Strap", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Strap"), style = NoopType.footnote, color = Palette.textSecondary) SegmentedPillControl( items = WhoopModel.entries.toList(), selection = selectedModel, @@ -575,7 +575,7 @@ private fun ConnectStep(viewModel: AppViewModel) { ) { Icon(Icons.Filled.Bluetooth, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(6.dp)) - Text(if (live.connected || live.scanning) "Re-scan" else "Scan again", style = NoopType.body) + Text(if (live.connected || live.scanning) tr("Re-scan") else tr("Scan again"), style = NoopType.body) } OutlinedButton( onClick = { viewModel.disconnect() }, @@ -583,7 +583,7 @@ private fun ConnectStep(viewModel: AppViewModel) { colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.statusCritical), modifier = Modifier.weight(1f), ) { - Text("Stop", style = NoopType.body) + Text(tr("Stop"), style = NoopType.body) } } } @@ -593,8 +593,8 @@ private fun ConnectStep(viewModel: AppViewModel) { InfoCard( icon = Icons.Filled.Lock, tint = Palette.statusPositive, - title = "This can run while you finish setup", - message = "If the strap is nearby, NOOP will keep the BLE link alive in the background. You can continue through profile and import while it bonds.", + title = tr("This can run while you finish setup"), + message = tr("If the strap is nearby, NOOP will keep the BLE link alive in the background. You can continue through profile and import while it bonds."), ) // WHOOP is NOOP's primary band, so onboarding leads with it — but it isn't required. @@ -602,9 +602,9 @@ private fun ConnectStep(viewModel: AppViewModel) { // they can continue now and pair a heart-rate strap or import data afterwards. if (!live.bonded) { Text( - "No WHOOP? You can still continue. Pair a heart-rate strap (Polar, Wahoo, Coospo, Garmin HRM…) " + - "or a gym machine under Devices, or import from WHOOP, Apple Health, Oura, Fitbit, Garmin " + - "and more under Data Sources. You can do either any time.", + tr("No WHOOP? You can still continue. Pair a heart-rate strap (Polar, Wahoo, Coospo, Garmin HRM…) ") + + tr("or a gym machine under Devices, or import from WHOOP, Apple Health, Oura, Fitbit, Garmin ") + + tr("and more under Data Sources. You can do either any time."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -638,7 +638,7 @@ private fun BondedStep(viewModel: AppViewModel) { } Spacer(Modifier.height(24.dp)) Text( - "You're connected.", + tr("You're connected."), style = NoopType.title1, color = Palette.textPrimary, textAlign = TextAlign.Center, @@ -646,7 +646,7 @@ private fun BondedStep(viewModel: AppViewModel) { Spacer(Modifier.height(10.dp)) Text( live.batteryPct?.let { "Your strap is bonded · ${it.toInt()}% battery." } - ?: "Your strap is bonded and ready to stream.", + ?: tr("Your strap is bonded and ready to stream."), style = NoopType.body, color = Palette.textSecondary, textAlign = TextAlign.Center, @@ -672,12 +672,12 @@ private fun ProfileStep() { @Suppress("UNUSED_VARIABLE") val tick = rev StepShell( - title = "About you", - subtitle = "So your zones, calories and on-device scoring start from the right numbers.", + title = tr("About you"), + subtitle = tr("So your zones, calories and on-device scoring start from the right numbers."), ) { NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - ProfileFieldRow(label = "Age") { + ProfileFieldRow(label = tr("Age")) { StepperField( value = "${profile.age}", unit = "yrs", @@ -688,7 +688,7 @@ private fun ProfileStep() { } ThinDivider() Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Sex", color = Palette.textTertiary) + Overline(tr("Sex"), color = Palette.textTertiary) SegmentedPillControl( items = ONBOARDING_SEX_OPTIONS, selection = ONBOARDING_SEX_OPTIONS.firstOrNull { it.tag == profile.sex } @@ -704,11 +704,11 @@ private fun ProfileStep() { // Units. Mirror the Sex picker idiom; the stored profile stays SI either way, only the // displayed labels re-format (lb / ft-in). Same key Settings → Units writes. Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("Units", color = Palette.textTertiary) + Overline(tr("Units"), color = Palette.textTertiary) SegmentedPillControl( items = listOf(UnitSystem.METRIC, UnitSystem.IMPERIAL), selection = unitSystem, - label = { if (it == UnitSystem.METRIC) "Metric" else "Imperial" }, + label = { if (it == UnitSystem.METRIC) tr("Metric") else tr("Imperial") }, onSelect = { unitSystem = it NoopPrefs.setUnitSystem(context, it) @@ -717,7 +717,7 @@ private fun ProfileStep() { ) } ThinDivider() - ProfileFieldRow(label = "Weight") { + ProfileFieldRow(label = tr("Weight")) { StepperField( // Full re-labelled string (e.g. "74.5 kg" / "164.2 lb"); unit folded into value. value = UnitFormatter.massFromKilograms(profile.weightKg, unitSystem), @@ -727,7 +727,7 @@ private fun ProfileStep() { ) } ThinDivider() - ProfileFieldRow(label = "Height") { + ProfileFieldRow(label = tr("Height")) { StepperField( value = UnitFormatter.heightFromCentimeters(profile.heightCm, unitSystem), accessibility = "Height", @@ -764,7 +764,7 @@ private fun ImportStep(viewModel: AppViewModel) { fun runImport(block: suspend () -> ImportSummary) { busy = true - status = "Importing…" + status = tr("Importing…") scope.launch { val summary = withContext(Dispatchers.IO) { runCatching { block() }.getOrElse { ImportSummary.failure("Import", it.message ?: "failed") } @@ -792,7 +792,7 @@ private fun ImportStep(viewModel: AppViewModel) { if (granted.any { it in HealthConnectImporter.PERMISSIONS }) { runImport { HealthConnectImporter.import(context, viewModel.repo, ProfileStore.from(context).heightCm) } } else { - val message = "Health Connect access not granted." + val message = tr("Health Connect access not granted.") status = message Toast.makeText(context, message, Toast.LENGTH_LONG).show() } @@ -816,8 +816,8 @@ private fun ImportStep(viewModel: AppViewModel) { } StepShell( - title = "Bring your history", - subtitle = "Optional: import now, or skip and return to Data Sources later.", + title = tr("Bring your history"), + subtitle = tr("Optional: import now, or skip and return to Data Sources later."), ) { Column( modifier = Modifier.fillMaxWidth(), @@ -828,24 +828,24 @@ private fun ImportStep(viewModel: AppViewModel) { InfoCard( icon = Icons.Filled.AutoGraph, tint = Palette.accent, - title = "History fills the dashboard immediately", - message = "A WHOOP export backfills recovery, strain, sleep and workouts. Health Connect can add steps, HR, HRV, sleep and weight from Android sources.", + title = tr("History fills the dashboard immediately"), + message = tr("A WHOOP export backfills recovery, strain, sleep and workouts. Health Connect can add steps, HR, HRV, sleep and weight from Android sources."), ) NoopCard(padding = 16.dp) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { OnboardingActionButton( - label = "Import WHOOP export (.zip)", + label = tr("Import WHOOP export (.zip)"), icon = Icons.Filled.FileUpload, enabled = !busy, ) { whoopImportLauncher.launch(arrayOf("*/*")) } OnboardingActionButton( - label = "Import from Health Connect", + label = tr("Import from Health Connect"), icon = Icons.Filled.MonitorHeart, enabled = !busy && healthConnectAvailable, ) { startHealthConnect() } OnboardingActionButton( - label = "Import Apple Health export", + label = tr("Import Apple Health export"), icon = Icons.Filled.FavoriteBorder, enabled = !busy, ) { appleImportLauncher.launch(arrayOf("*/*")) } @@ -854,7 +854,7 @@ private fun ImportStep(viewModel: AppViewModel) { if (!healthConnectAvailable) { Text( - "Health Connect is not available on this device.", + tr("Health Connect is not available on this device."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -875,8 +875,8 @@ private fun ImportStep(viewModel: AppViewModel) { @Composable private fun NotificationsStep() { StepShell( - title = "Stay in the loop", - subtitle = "NOOP keeps your strap connected in the background. When you continue, allow notifications so it can show that link and reach your wrist.", + title = tr("Stay in the loop"), + subtitle = tr("NOOP keeps your strap connected in the background. When you continue, allow notifications so it can show that link and reach your wrist."), ) { Column( modifier = Modifier.fillMaxWidth(), @@ -887,11 +887,11 @@ private fun NotificationsStep() { InfoCard( icon = Icons.Filled.Bluetooth, tint = Palette.statusPositive, - title = "A quiet, ongoing status", - message = "NOOP holds the Bluetooth link open in the background so your data stays current. One low-priority notification shows it's connected. Nothing noisy.", + title = tr("A quiet, ongoing status"), + message = tr("NOOP holds the Bluetooth link open in the background so your data stays current. One low-priority notification shows it's connected. Nothing noisy."), ) - Checkline("Wrist alerts (strain nudges and your smart alarm) arrive as notifications too.") - Checkline("When Android asks, allow notifications so NOOP can keep you informed.") + Checkline(tr("Wrist alerts (strain nudges and your smart alarm) arrive as notifications too.")) + Checkline(tr("When Android asks, allow notifications so NOOP can keep you informed.")) } } } @@ -906,8 +906,8 @@ private fun AppearanceStep() { var mode by remember { mutableStateOf(AppearancePrefs.mode) } StepShell( - title = "Make it yours", - subtitle = "NOOP follows your system by default, or pick Light or Dark. You can change this any time in Settings → Appearance.", + title = tr("Make it yours"), + subtitle = tr("NOOP follows your system by default, or pick Light or Dark. You can change this any time in Settings → Appearance."), ) { Column( modifier = Modifier.fillMaxWidth(), @@ -922,13 +922,13 @@ private fun AppearanceStep() { horizontalArrangement = Arrangement.spacedBy(Metrics.gap), ) { ThemeSwatch( - title = "Light", + title = tr("Light"), tokens = LightTokens, selected = Palette.isLight, modifier = Modifier.weight(1f), ) ThemeSwatch( - title = "Dark", + title = tr("Dark"), tokens = DarkTokens, selected = !Palette.isLight, modifier = Modifier.weight(1f), @@ -937,7 +937,7 @@ private fun AppearanceStep() { NoopCard(padding = 18.dp) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - ProfileFieldRow(label = "Theme") { + ProfileFieldRow(label = tr("Theme")) { SegmentedPillControl( items = listOf(AppearanceMode.SYSTEM, AppearanceMode.LIGHT, AppearanceMode.DARK), selection = mode, @@ -962,9 +962,9 @@ private fun AppearanceStep() { ) Text( when (mode) { - AppearanceMode.SYSTEM -> "Following your phone's light/dark setting." - AppearanceMode.LIGHT -> "Deep blue accent on warm paper." - AppearanceMode.DARK -> "Deep blue accent on a dark blue-grey canvas." + AppearanceMode.SYSTEM -> tr("Following your phone's light/dark setting.") + AppearanceMode.LIGHT -> tr("Deep blue accent on warm paper.") + AppearanceMode.DARK -> tr("Deep blue accent on a dark blue-grey canvas.") }, style = NoopType.footnote, color = Palette.textTertiary, @@ -1057,14 +1057,14 @@ private fun DoneStep() { IconBadge(icon = Icons.Filled.CheckCircle, tint = Palette.statusPositive, size = 100) Spacer(Modifier.height(22.dp)) Text( - "Your thread starts here.", + tr("Your thread starts here."), style = NoopType.title1, color = Palette.textPrimary, textAlign = TextAlign.Center, ) Spacer(Modifier.height(10.dp)) Text( - "Every beat, every night, every day, woven into one quiet picture of you. Welcome to NOOP.", + tr("Every beat, every night, every day, woven into one quiet picture of you. Welcome to NOOP."), style = NoopType.body, color = Palette.textSecondary, textAlign = TextAlign.Center, @@ -1218,7 +1218,7 @@ private fun ThinDivider() { private data class OnboardingSexOption(val tag: String, val label: String) private val ONBOARDING_SEX_OPTIONS = listOf( - OnboardingSexOption("male", "Male"), - OnboardingSexOption("female", "Female"), - OnboardingSexOption("nonbinary", "Other"), + OnboardingSexOption("male", tr("Male")), + OnboardingSexOption("female", tr("Female")), + OnboardingSexOption("nonbinary", tr("Other")), ) diff --git a/android/app/src/main/java/com/noop/ui/RhythmScreen.kt b/android/app/src/main/java/com/noop/ui/RhythmScreen.kt index 580be8b4d..dbcbe5ca1 100644 --- a/android/app/src/main/java/com/noop/ui/RhythmScreen.kt +++ b/android/app/src/main/java/com/noop/ui/RhythmScreen.kt @@ -105,16 +105,16 @@ object RhythmConsent { /** The points the user must read before turning the feature on (spec §9). No condition * name, no diagnosis, no "consider a clinician" verdict. Kept identical to macOS. */ val points: List> = listOf( - "Experimental, and not a medical device" to - "This is an experimental wellness visualization of your beat-to-beat timing. It is NOT an ECG, and it cannot diagnose, detect, or rule out any heart condition.", - "It is a picture, not a verdict" to - "It shows the shape of your heartbeat timing and a plain-language description of how steady it looked. It does not tell you whether anything is right or wrong.", - "Variation is normal and often benign" to - "Beat-to-beat timing varies for many ordinary reasons: breathing, movement, an imperfect optical reading, or the occasional extra or skipped beat that most healthy people have.", - "It is not a substitute for a professional" to - "If you feel unwell or are worried about your heart, contact a qualified professional; in an emergency, your local emergency service. Do not rely on NOOP.", - "Everything stays on your device" to - "All of this is computed on your own device from data you already have. No heartbeat data leaves it.", + tr("Experimental, and not a medical device") to + tr("This is an experimental wellness visualization of your beat-to-beat timing. It is NOT an ECG, and it cannot diagnose, detect, or rule out any heart condition."), + tr("It is a picture, not a verdict") to + tr("It shows the shape of your heartbeat timing and a plain-language description of how steady it looked. It does not tell you whether anything is right or wrong."), + tr("Variation is normal and often benign") to + tr("Beat-to-beat timing varies for many ordinary reasons: breathing, movement, an imperfect optical reading, or the occasional extra or skipped beat that most healthy people have."), + tr("It is not a substitute for a professional") to + tr("If you feel unwell or are worried about your heart, contact a qualified professional; in an emergency, your local emergency service. Do not rely on NOOP."), + tr("Everything stays on your device") to + tr("All of this is computed on your own device from data you already have. No heartbeat data leaves it."), ) } @@ -135,10 +135,10 @@ fun RhythmConsentGate( Surface(modifier = Modifier.fillMaxSize(), color = Palette.surfaceBase) { Column(modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp)) { Spacer(Modifier.height(40.dp)) - Text("Before you turn on Rhythm", style = NoopType.title1, color = Palette.textPrimary) + Text(tr("Before you turn on Rhythm"), style = NoopType.title1, color = Palette.textPrimary) Spacer(Modifier.height(4.dp)) Text( - "An experimental picture of your beat-to-beat timing. Please read these first.", + tr("An experimental picture of your beat-to-beat timing. Please read these first."), style = NoopType.subhead, color = Palette.textSecondary, ) Spacer(Modifier.height(20.dp)) @@ -154,7 +154,7 @@ fun RhythmConsentGate( } } Text( - "This is a wellness visualization, not a screening test. It does not tell you to see a clinician and it names no condition. This is not legal or medical advice.", + tr("This is a wellness visualization, not a screening test. It does not tell you to see a clinician and it names no condition. This is not legal or medical advice."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -168,7 +168,7 @@ fun RhythmConsentGate( ) Spacer(Modifier.width(8.dp)) Text( - "I understand this is an experimental wellness feature, not a medical device or a diagnosis.", + tr("I understand this is an experimental wellness feature, not a medical device or a diagnosis."), style = NoopType.footnote, color = Palette.textPrimary, modifier = Modifier.padding(top = 14.dp), ) @@ -183,11 +183,11 @@ fun RhythmConsentGate( contentColor = Palette.goldDeepText, ), ) { - Text("Turn on Rhythm", style = NoopType.body) + Text(tr("Turn on Rhythm"), style = NoopType.body) } if (onCancel != null) { TextButton(onClick = onCancel, modifier = Modifier.fillMaxWidth()) { - Text("Not now", style = NoopType.body, color = Palette.gold) + Text(tr("Not now"), style = NoopType.body, color = Palette.gold) } } Spacer(Modifier.height(24.dp)) @@ -246,25 +246,25 @@ private fun RhythmVisualization( // only on-screen cards compose + are accessibility-walked, with the LazyColumn's `spacedBy(20.dp)` // reproducing the eager column's inter-card spacing exactly. The Poincaré PlotCard is the heavy one. LazyScreenScaffold( - title = "Rhythm", - subtitle = "An experimental picture of your beat-to-beat timing", + title = tr("Rhythm"), + subtitle = tr("An experimental picture of your beat-to-beat timing"), trailing = if (onClose != null) { { IconButton(onClick = onClose) { - Icon(Icons.Filled.Close, contentDescription = "Close Rhythm", tint = Palette.textTertiary) + Icon(Icons.Filled.Close, contentDescription = tr("Close Rhythm"), tint = Palette.textTertiary) } } } else { null }, ) { - item { SourceBadge("Experimental", tint = Palette.restColor) } + item { SourceBadge(tr("Experimental"), tint = Palette.restColor) } if (allPoints.isEmpty()) { item { DataPendingNote( - title = "No clear reading yet", - body = "Rhythm only looks during quiet, still, resting windows, so it needs a calm night's worth of steady beats. Once there's a clean window, the scatter and its description show here.", + title = tr("No clear reading yet"), + body = tr("Rhythm only looks during quiet, still, resting windows, so it needs a calm night's worth of steady beats. Once there's a clean window, the scatter and its description show here."), ) } } else { @@ -289,7 +289,7 @@ private fun SummaryCard( NoopCard(padding = 18.dp, tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Overline("Last night", modifier = Modifier.weight(1f)) + Overline(tr("Last night"), modifier = Modifier.weight(1f)) ConfidencePill(headline = headline, readable = night?.readableWindows) } Text(headlineLabel(label), style = NoopType.title2, color = Palette.textPrimary) @@ -306,14 +306,14 @@ private fun SummaryCard( private fun ConfidencePill(headline: RhythmScreener.WindowResult?, readable: Int?) { when (headline?.confidence ?: RhythmConfidence.CALIBRATING) { RhythmConfidence.SOLID -> - StatePill("Solid", tone = StrandTone.Accent) + StatePill(tr("Solid"), tone = StrandTone.Accent) RhythmConfidence.BUILDING -> StatePill( - if ((readable ?: 0) <= 1) "Building · 1 window" else "Building", + if ((readable ?: 0) <= 1) tr("Building · 1 window") else tr("Building"), tone = StrandTone.Warning, ) RhythmConfidence.CALIBRATING -> - StatePill("Calibrating", tone = StrandTone.Neutral) + StatePill(tr("Calibrating"), tone = StrandTone.Neutral) } } @@ -323,7 +323,7 @@ private fun ConfidencePill(headline: RhythmScreener.WindowResult?, readable: Int private fun PlotCard(points: List) { NoopCard(padding = 18.dp, tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Beat-to-beat scatter") + Overline(tr("Beat-to-beat scatter")) Box( modifier = Modifier .fillMaxWidth() @@ -341,7 +341,7 @@ private fun PlotCard(points: List) { ) } Text( - "Each dot pairs one heartbeat interval with the next. A tight line along the diagonal means a steady beat; a rounder, more spread-out cloud means the timing varied more.", + tr("Each dot pairs one heartbeat interval with the next. A tight line along the diagonal means a steady beat; a rounder, more spread-out cloud means the timing varied more."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -408,40 +408,40 @@ private fun PoincarePlot( @Composable private fun StatsCard(headline: RhythmScreener.WindowResult?) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader(title = "The numbers", overline = "Descriptive stats") + SectionHeader(title = tr("The numbers"), overline = tr("Descriptive stats")) Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( - label = "Short axis", value = fmt(headline?.sd1, "%.0f"), - caption = "SD1 · ms", accent = Palette.restBright, + label = tr("Short axis"), value = fmt(headline?.sd1, "%.0f"), + caption = tr("SD1 · ms"), accent = Palette.restBright, modifier = Modifier.weight(1f), ) StatTile( - label = "Long axis", value = fmt(headline?.sd2, "%.0f"), - caption = "SD2 · ms", accent = Palette.restColor, + label = tr("Long axis"), value = fmt(headline?.sd2, "%.0f"), + caption = tr("SD2 · ms"), accent = Palette.restColor, modifier = Modifier.weight(1f), ) } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( - label = "Cloud shape", value = fmt(headline?.sd1sd2, "%.2f"), - caption = "SD1:SD2 ratio", accent = Palette.metricCyan, + label = tr("Cloud shape"), value = fmt(headline?.sd1sd2, "%.2f"), + caption = tr("SD1:SD2 ratio"), accent = Palette.metricCyan, modifier = Modifier.weight(1f), ) StatTile( - label = "Beat-to-beat", value = percent(headline?.normRmssd), - caption = "variation index", accent = Palette.metricPurple, + label = tr("Beat-to-beat"), value = percent(headline?.normRmssd), + caption = tr("variation index"), accent = Palette.metricPurple, modifier = Modifier.weight(1f), ) } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( - label = "Extra / skipped", value = percent(headline?.ectopicFraction), - caption = "of beats", accent = Palette.restColor, + label = tr("Extra / skipped"), value = percent(headline?.ectopicFraction), + caption = tr("of beats"), accent = Palette.restColor, modifier = Modifier.weight(1f), ) StatTile( - label = "Beats read", value = headline?.nBeats?.toString() ?: "—", - caption = "clean intervals", accent = Palette.textSecondary, + label = tr("Beats read"), value = headline?.nBeats?.toString() ?: "—", + caption = tr("clean intervals"), accent = Palette.textSecondary, modifier = Modifier.weight(1f), ) } @@ -454,9 +454,9 @@ private fun StatsCard(headline: RhythmScreener.WindowResult?) { private fun MethodologyCard() { NoopCard { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("How this is measured") + Overline(tr("How this is measured")) Text( - "During quiet, still, resting windows, NOOP looks at the timing between your heartbeats (R-R intervals) and draws their Poincaré scatter. From the cloud it computes its short and long axes (SD1, SD2) and a few plain regularity numbers. Movement and noisy windows are skipped, not shown. These are transparent, published descriptive statistics: a picture of your timing, never a clinical measurement.", + tr("During quiet, still, resting windows, NOOP looks at the timing between your heartbeats (R-R intervals) and draws their Poincaré scatter. From the cloud it computes its short and long axes (SD1, SD2) and a few plain regularity numbers. Movement and noisy windows are skipped, not shown. These are transparent, published descriptive statistics: a picture of your timing, never a clinical measurement."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -481,7 +481,7 @@ private fun RhythmDisclaimerNote() { modifier = Modifier.size(18.dp), ) Text( - "Experimental wellness visualization: not a diagnosis, not an ECG, and not a medical device. It cannot detect any heart condition. Beat-to-beat variation has many ordinary, benign causes. If you feel unwell or are worried, contact a qualified professional; in an emergency, your local emergency service. Everything is computed on your device.", + tr("Experimental wellness visualization: not a diagnosis, not an ECG, and not a medical device. It cannot detect any heart condition. Beat-to-beat variation has many ordinary, benign causes. If you feel unwell or are worried, contact a qualified professional; in an emergency, your local emergency service. Everything is computed on your device."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -491,21 +491,21 @@ private fun RhythmDisclaimerNote() { // ── Copy mapping (neutral, non-clinical — NO verdict, NO condition name) ────────────────── private fun headlineLabel(label: RhythmRegularity): String = when (label) { - RhythmRegularity.STEADY -> "Your rhythm looked steady" - RhythmRegularity.OCCASIONAL_ECTOPY -> "Some occasional extra or skipped beats" - RhythmRegularity.VARIED -> "Your rhythm varied more than usual" - RhythmRegularity.UNREADABLE -> "Couldn't read clearly" + RhythmRegularity.STEADY -> tr("Your rhythm looked steady") + RhythmRegularity.OCCASIONAL_ECTOPY -> tr("Some occasional extra or skipped beats") + RhythmRegularity.VARIED -> tr("Your rhythm varied more than usual") + RhythmRegularity.UNREADABLE -> tr("Couldn't read clearly") } private fun headlineDetail(label: RhythmRegularity): String = when (label) { RhythmRegularity.STEADY -> - "Across the quiet windows we could read, your beat-to-beat timing held a tight, even shape." + tr("Across the quiet windows we could read, your beat-to-beat timing held a tight, even shape.") RhythmRegularity.OCCASIONAL_ECTOPY -> - "Mostly steady, with a few isolated extra or skipped beats. Very common and usually nothing." + tr("Mostly steady, with a few isolated extra or skipped beats. Very common and usually nothing.") RhythmRegularity.VARIED -> - "The scatter looked rounder and more spread out than a tight, steady beat. This has many ordinary causes and is not a diagnosis." + tr("The scatter looked rounder and more spread out than a tight, steady beat. This has many ordinary causes and is not a diagnosis.") RhythmRegularity.UNREADABLE -> - "There wasn't a calm, still window clean enough to describe. Try again after a settled night." + tr("There wasn't a calm, still window clean enough to describe. Try again after a settled night.") } // ── Formatting ──────────────────────────────────────────────────────────────────────────── diff --git a/android/app/src/main/java/com/noop/ui/ScoringGuideScreen.kt b/android/app/src/main/java/com/noop/ui/ScoringGuideScreen.kt index 8e32a9869..f44ea65f6 100644 --- a/android/app/src/main/java/com/noop/ui/ScoringGuideScreen.kt +++ b/android/app/src/main/java/com/noop/ui/ScoringGuideScreen.kt @@ -186,10 +186,10 @@ fun ScoringGuideScreen( IntroCard() ScoreCard( section = ScoreSection.CHARGE, - headline = "Charge: how recovered are you?", - body = "Led by your heart-rate variability (HRV) measured against your own " + - "personal baseline, plus resting heart rate, last night's Rest, breathing " + - "rate, and a skin-temperature signal (an early illness or overreach flag). " + + headline = tr("Charge: how recovered are you?"), + body = tr("Led by your heart-rate variability (HRV) measured against your own ") + + tr("personal baseline, plus resting heart rate, last night's Rest, breathing ") + + tr("rate, and a skin-temperature signal (an early illness or overreach flag). ") + "Higher HRV versus your baseline means more Charge. NOOP needs a few nights " + "to learn your baseline first. Until then you'll see “Calibrating”.", vsWhoop = "Same core idea as WHOOP's Recovery % (HRV-led recovery), but our " + @@ -199,10 +199,10 @@ fun ScoringGuideScreen( ) ScoreCard( section = ScoreSection.EFFORT, - headline = "Effort: how hard did your heart work?", - body = "Your cardiovascular load. NOOP turns every second of heart rate into a " + - "training-impulse using heart-rate-reserve zones (Karvonen), weights time in " + - "harder zones more heavily (Edwards / Banister), and places it on a " + + headline = tr("Effort: how hard did your heart work?"), + body = tr("Your cardiovascular load. NOOP turns every second of heart rate into a ") + + tr("training-impulse using heart-rate-reserve zones (Karvonen), weights time in ") + + tr("harder zones more heavily (Edwards / Banister), and places it on a ") + "logarithmic 0-100 scale, so easy days sit low and an all-out day approaches " + "100, which stays genuinely rare. A long walk with little cardio still counts, " + "through a steps / active-energy floor.", @@ -214,10 +214,10 @@ fun ScoringGuideScreen( ) ScoreCard( section = ScoreSection.REST, - headline = "Rest: how restorative was your sleep?", - body = "A blend of how long you slept versus your personal need (the biggest " + - "factor), how efficiently (asleep versus in bed), how much was restorative " + - "(deep + REM sleep), and how consistent your sleep and wake timing is.", + headline = tr("Rest: how restorative was your sleep?"), + body = tr("A blend of how long you slept versus your personal need (the biggest ") + + tr("factor), how efficiently (asleep versus in bed), how much was restorative ") + + tr("(deep + REM sleep), and how consistent your sleep and wake timing is."), vsWhoop = "Similar in spirit to WHOOP's Sleep Performance %; our composite is our own.", highlighted = highlighted == ScoreSection.REST, onPositioned = { if (ScoreSection.REST !in anchors) anchors[ScoreSection.REST] = it }, @@ -247,10 +247,10 @@ private fun Header(onClose: () -> Unit) { modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Overline("Your daily scores", color = Palette.textTertiary) - Text("How your scores work", style = NoopType.display(26f), color = Palette.textPrimary) + Overline(tr("Your daily scores"), color = Palette.textTertiary) + Text(tr("How your scores work"), style = NoopType.display(26f), color = Palette.textPrimary) Text( - "Charge · Effort · Rest", + tr("Charge · Effort · Rest"), style = NoopType.caption, color = Palette.textSecondary, ) @@ -258,7 +258,7 @@ private fun Header(onClose: () -> Unit) { IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) { Icon( Icons.Filled.Close, - contentDescription = "Close", + contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(22.dp), ) @@ -272,11 +272,11 @@ private fun Header(onClose: () -> Unit) { private fun IntroCard() { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("The three scores") + Overline(tr("The three scores")) Text( - "NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 " + - "scale. They're built from your strap's raw signals using published, " + - "peer-reviewed sport science, and computed entirely on your device. They are " + + tr("NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 ") + + tr("scale. They're built from your strap's raw signals using published, ") + + tr("peer-reviewed sport science, and computed entirely on your device. They are ") + "NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend " + "to. They aim at the same three questions using open science, so they'll usually " + "track WHOOP's in direction, but won't match number-for-number. And that's the " + @@ -423,7 +423,7 @@ private fun ConfidenceCard() { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - "How sure is NOOP? · Solid · Building · Calibrating", + tr("How sure is NOOP? · Solid · Building · Calibrating"), style = NoopType.headline, color = Palette.textPrimary, ) @@ -433,9 +433,9 @@ private fun ConfidenceCard() { StatePill("Calibrating", tone = StrandTone.Neutral, showsDot = true) } Text( - "Every score carries a small honesty label. Calibrating means NOOP is still " + - "learning your baseline, or doesn't have enough data yet. Building means there's " + - "enough to show, but it's thin. Solid means full inputs are present. When NOOP " + + tr("Every score carries a small honesty label. Calibrating means NOOP is still ") + + tr("learning your baseline, or doesn't have enough data yet. Building means there's ") + + tr("enough to show, but it's thin. Solid means full inputs are present. When NOOP ") + "can't compute a score honestly, it shows nothing rather than a fake number.", style = NoopType.subhead, color = Palette.textSecondary, @@ -449,8 +449,8 @@ private fun ConfidenceCard() { @Composable private fun FooterNote() { Text( - "These are independent approximations from a consumer strap, built on open science: not " + - "medical advice, and not WHOOP's official scores.", + tr("These are independent approximations from a consumer strap, built on open science: not ") + + tr("medical advice, and not WHOOP's official scores."), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier @@ -474,7 +474,7 @@ private fun Footer(onClose: () -> Unit) { contentColor = Palette.surfaceBase, ), ) { - Text("Got it", style = NoopType.captionNumber) + Text(tr("Got it"), style = NoopType.captionNumber) } } } diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index f6eeddbcb..5fc58984a 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -479,7 +479,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { onSuccess = { Toast.makeText( context, - "Backup exported. Copy this file to your new phone and use Import there to restore everything.", + tr("Backup exported. Copy this file to your new phone and use Import there to restore everything."), Toast.LENGTH_LONG, ).show() }, @@ -527,7 +527,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { when (result) { is DataBackup.ImportResult.NeedsRestart -> Toast.makeText( context, - "Backup imported. Fully close and reopen NOOP for it to take effect.", + tr("Backup imported. Fully close and reopen NOOP for it to take effect."), Toast.LENGTH_LONG, ).show() is DataBackup.ImportResult.Failed -> Toast.makeText( @@ -549,14 +549,14 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ProfileAvatarStore.setAvatarFromUri(context, uri) } if (!ok) { - Toast.makeText(context, "Couldn't use that photo. Try another.", Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Couldn't use that photo. Try another."), Toast.LENGTH_LONG).show() } } } ScreenScaffold( - title = "Settings", - subtitle = "Your numbers, your strap, and how NOOP works. All on this phone.", + title = tr("Settings"), + subtitle = tr("Your numbers, your strap, and how NOOP works. All on this phone."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the static time-of-day sky settles // into the theme canvas behind the top of the list, exactly like the liquid Today. This is a long, // scroll-heavy list with NO hero gauge, so the liquid finish here is just the sky + liquidPress on @@ -576,22 +576,22 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // (snapshot state) so the controls update the instant a photo is set or cleared. SettingsSection( icon = Icons.Outlined.AccountCircle, - title = "Profile photo", - blurb = "Optional. Add a photo for the avatar in the top-left. Stored only on this phone. NOOP is offline, so it's never uploaded.", + title = tr("Profile photo"), + blurb = tr("Optional. Add a photo for the avatar in the top-left. Stored only on this phone. NOOP is offline, so it's never uploaded."), ) { Row( modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - ProfileAvatar(size = 64.dp, contentDescription = "Profile photo") + ProfileAvatar(size = 64.dp, contentDescription = tr("Profile photo")) Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { NoopButton( - text = if (ProfileAvatarStore.hasAvatar) "Change photo" else "Choose photo", + text = if (ProfileAvatarStore.hasAvatar) tr("Change photo") else tr("Choose photo"), kind = NoopButtonKind.Secondary, modifier = Modifier.weight(1f), onClick = { @@ -602,7 +602,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) if (ProfileAvatarStore.hasAvatar) { NoopButton( - text = "Remove photo", + text = tr("Remove photo"), kind = NoopButtonKind.Tertiary, modifier = Modifier.weight(1f), onClick = { ProfileAvatarStore.clearAvatar(context) }, @@ -616,11 +616,11 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // --- Profile --- SettingsSection( icon = Icons.Outlined.Person, - title = "Profile", - blurb = "These power your heart-rate zones, calorie estimates and recovery baselines. Keep them accurate.", + title = tr("Profile"), + blurb = tr("These power your heart-rate zones, calorie estimates and recovery baselines. Keep them accurate."), ) { Column { - FormRow(label = "Age") { + FormRow(label = tr("Age")) { StepperField( value = profile.age.toString(), accessibility = "Age, ${profile.age} years", @@ -632,7 +632,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } RowDivider() - FormRow(label = "Sex") { + FormRow(label = tr("Sex")) { SegmentedPillControl( items = SEX_OPTIONS, selection = SEX_OPTIONS.firstOrNull { it.tag == profile.sex } ?: SEX_OPTIONS[0], @@ -641,7 +641,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } RowDivider() - FormRow(label = "Weight") { + FormRow(label = tr("Weight")) { // Imperial mode steps in whole pounds and stores the kg equivalent; metric steps in // 0.5 kg. The profile is always SI — only the entry unit changes. if (unitSystem == UnitSystem.IMPERIAL) { @@ -657,14 +657,14 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { StepperField( value = "%.1f".format(profile.weightKg), unit = "kg", - accessibility = "Weight in kilograms", + accessibility = tr("Weight in kilograms"), onMinus = { mutate { profile.weightKg -= 0.5 } }, onPlus = { mutate { profile.weightKg += 0.5 } }, ) } } RowDivider() - FormRow(label = "Height") { + FormRow(label = tr("Height")) { // Imperial mode steps in whole inches and stores the cm equivalent; metric steps in cm. if (unitSystem == UnitSystem.IMPERIAL) { val (ft, inch) = UnitFormatter.cmToFeetInches(profile.heightCm) @@ -679,7 +679,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { StepperField( value = "%.0f".format(profile.heightCm), unit = "cm", - accessibility = "Height in centimetres", + accessibility = tr("Height in centimetres"), onMinus = { mutate { profile.heightCm -= 1 } }, onPlus = { mutate { profile.heightCm += 1 } }, ) @@ -690,17 +690,17 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // estimate. Unset (0) by design — the headline Fitness Age never needs it — so it shows // "Add" until entered, then steps like Height (inches in imperial, cm in metric). // First tap from unset seeds a typical adult waist rather than 1 cm. - FormRow(label = "Waist (optional)") { + FormRow(label = tr("Waist (optional)")) { Column(horizontalAlignment = Alignment.End) { val hasWaist = profile.waistCm > 0.0 if (unitSystem == UnitSystem.IMPERIAL) { val totalInches = UnitFormatter.cmToInches(profile.waistCm).roundToInt() StepperField( - value = if (hasWaist) "%d″".format(totalInches) else "Add", + value = if (hasWaist) "%d″".format(totalInches) else tr("Add"), accessibility = if (hasWaist) { "Waist, $totalInches inches" } else { - "Waist, not set. Optional: adds your VO₂max estimate" + tr("Waist, not set. Optional: adds your VO₂max estimate") }, valueColor = if (hasWaist) Palette.textPrimary else Palette.textTertiary, onMinus = { mutate { profile.waistCm = waistInchesStep(profile.waistCm, up = false) } }, @@ -708,12 +708,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } else { StepperField( - value = if (hasWaist) "%.0f".format(profile.waistCm) else "Add", + value = if (hasWaist) "%.0f".format(profile.waistCm) else tr("Add"), unit = if (hasWaist) "cm" else null, accessibility = if (hasWaist) { - "Waist in centimetres" + tr("Waist in centimetres") } else { - "Waist, not set. Optional: adds your VO₂max estimate" + tr("Waist, not set. Optional: adds your VO₂max estimate") }, valueColor = if (hasWaist) Palette.textPrimary else Palette.textTertiary, onMinus = { mutate { profile.waistCm = waistCmStep(profile.waistCm, up = false) } }, @@ -722,20 +722,20 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { } Spacer(Modifier.height(6.dp)) Text( - text = if (hasWaist) "Adds your VO₂max estimate" else "Optional · adds your VO₂max estimate", + text = if (hasWaist) tr("Adds your VO₂max estimate") else tr("Optional · adds your VO₂max estimate"), style = NoopType.footnote, color = if (hasWaist) Palette.accent else Palette.textTertiary, ) } } RowDivider() - FormRow(label = "Max heart rate") { + FormRow(label = tr("Max heart rate")) { Column(horizontalAlignment = Alignment.End) { StepperField( - value = if (profile.hrMaxOverride > 0) profile.hrMaxOverride.toString() else "Auto", + value = if (profile.hrMaxOverride > 0) profile.hrMaxOverride.toString() else tr("Auto"), unit = "bpm", accessibility = if (profile.hrMaxOverride == 0) { - "Max heart rate override, automatic" + tr("Max heart rate override, automatic") } else { "Max heart rate override, ${profile.hrMaxOverride} bpm" }, @@ -746,7 +746,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { Spacer(Modifier.height(6.dp)) Text( text = if (profile.hrMaxOverride > 0) { - "Manual override" + tr("Manual override") } else { "Auto · ${profile.hrMaxAuto} bpm (Tanaka)" }, @@ -760,7 +760,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // 1.0 = raw pass-through until the true 5/MG tick rate is known. The divisor goes // up to 30 because a 5/MG motion counter can overcount by ~24×; the stepper uses a // variable increment (fine near 1.0, coarse up top) so high values stay reachable. - FormRow(label = "Step calibration") { + FormRow(label = tr("Step calibration")) { StepperField( value = "%.1f".format(profile.stepTicksPerStep), accessibility = "Step calibration, %.1f counter ticks per step" @@ -770,7 +770,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } Text( - "Counter ticks per step. Leave at 1.0 unless your steps run high. On a WHOOP 5/MG they can run very high (10× or more), so this goes up to 30. Walk a known 1,000 steps and divide NOOP's count by the real count to get your value.", + tr("Counter ticks per step. Leave at 1.0 unless your steps run high. On a WHOOP 5/MG they can run very high (10× or more), so this goes up to 30. Walk a known 1,000 steps and divide NOOP's count by the real count to get your value."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -780,10 +780,10 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // motion and calibrates that to the phone. Opens the explainer + fit + comparison + manual // override screen. Mirrors the macOS Profile "Steps estimate" row. val stepsSummary = when { - profile.stepsManualCoefficient > 0 -> "Manual" + profile.stepsManualCoefficient > 0 -> tr("Manual") profile.stepsCalibrationCoefficient > 0 -> "Auto · ${StepsCalibrationFormat.confidenceLabel(profile.stepsCalibrationConfidence)} confidence" - else -> "Not calibrated" + else -> tr("Not calibrated") } val stepsRowInteraction = remember { MutableInteractionSource() } Row( @@ -804,7 +804,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - Text("Steps estimate", style = NoopType.body, color = Palette.textPrimary, modifier = Modifier.weight(1f)) + Text(tr("Steps estimate"), style = NoopType.body, color = Palette.textPrimary, modifier = Modifier.weight(1f)) Text( stepsSummary, style = NoopType.footnote, @@ -818,7 +818,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } Text( - "For a WHOOP 4.0, which sends no step count: NOOP estimates steps from motion, calibrated to your phone. Tap to see how close it is and adjust it.", + tr("For a WHOOP 4.0, which sends no step count: NOOP estimates steps from motion, calibrated to your phone. Tap to see how close it is and adjust it."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -831,15 +831,15 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // macOS Settings → Units card. SettingsSection( icon = Icons.Filled.Straighten, - title = "Units", - blurb = "Choose how distances, weights, heights, temperatures and Effort are shown. Your data is always stored the same way. This only changes the display.", + title = tr("Units"), + blurb = tr("Choose how distances, weights, heights, temperatures and Effort are shown. Your data is always stored the same way. This only changes the display."), ) { Column { - FormRow(label = "Measurement system") { + FormRow(label = tr("Measurement system")) { SegmentedPillControl( items = listOf(UnitSystem.METRIC, UnitSystem.IMPERIAL), selection = unitSystem, - label = { if (it == UnitSystem.METRIC) "Metric" else "Imperial" }, + label = { if (it == UnitSystem.METRIC) tr("Metric") else tr("Imperial") }, onSelect = { unitSystem = it NoopPrefs.setUnitSystem(context, it) @@ -847,7 +847,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } RowDivider() - FormRow(label = "Temperature") { + FormRow(label = tr("Temperature")) { // Three-way: "Match" follows the system above; °C / °F pin it explicitly. Stored as an // empty string ("match") or the TemperatureUnit raw value. SegmentedPillControl( @@ -857,7 +857,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { when (it) { TemperatureUnit.CELSIUS.raw -> "°C" TemperatureUnit.FAHRENHEIT.raw -> "°F" - else -> "Match" + else -> tr("Match") } }, onSelect = { @@ -869,7 +869,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { RowDivider() // Effort scale (#268) — NOOP's native 0–100 Effort or WHOOP's 0–21 Day Strain axis. // Display-only; the stored value never changes, so a flip just re-labels every read-out. - FormRow(label = "Effort scale") { + FormRow(label = tr("Effort scale")) { SegmentedPillControl( items = listOf(EffortScale.HUNDRED, EffortScale.WHOOP), selection = effortScale, @@ -886,10 +886,10 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // --- Appearance (Theme) --- SettingsSection( icon = Icons.Filled.Brightness6, - title = "Appearance", - blurb = "Choose Light, Dark, or follow your system. Dark is the signature near-black; Light keeps the same clean look on a bright canvas.", + title = tr("Appearance"), + blurb = tr("Choose Light, Dark, or follow your system. Dark is the signature near-black; Light keeps the same clean look on a bright canvas."), ) { - FormRow(label = "Theme") { + FormRow(label = tr("Theme")) { SegmentedPillControl( items = listOf(AppearanceMode.SYSTEM, AppearanceMode.LIGHT, AppearanceMode.DARK), selection = themeMode, @@ -900,7 +900,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { }, ) } - FormRow(label = "Chart colours") { + FormRow(label = tr("Chart colours")) { // Titanium = brand gold/amber/blue ramps; Classic = throwback red→green readiness scale // (cool→hot zones, green→red stress). Re-colours every gauge/chart, in both schemes. SegmentedPillControl( @@ -924,12 +924,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Day-cycle background", + tr("Day-cycle background"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Shows a soft sunrise, day, dusk and night scene behind the Today screen. Turn it off for a plain dark canvas. Your cards stay exactly as readable.", + tr("Shows a soft sunrise, day, dusk and night scene behind the Today screen. Turn it off for a plain dark canvas. Your cards stay exactly as readable."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -957,14 +957,14 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // the launcher may take a beat (or briefly disappear/redraw) while it re-reads the icon. SettingsSection( icon = Icons.Filled.Palette, - title = "App icon", - blurb = "Choose how NOOP looks on your home screen. The launcher may take a moment to refresh the icon after you change it.", + title = tr("App icon"), + blurb = tr("Choose how NOOP looks on your home screen. The launcher may take a moment to refresh the icon after you change it."), ) { - FormRow(label = "Icon") { + FormRow(label = tr("Icon")) { SegmentedPillControl( items = listOf(false, true), selection = appIconNavy, - label = { if (it) "Blue Titanium" else "Titanium" }, + label = { if (it) tr("Blue Titanium") else tr("Titanium") }, onSelect = { navy -> appIconNavy = navy setAppIcon(context, navy) @@ -976,8 +976,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // --- Strap --- SettingsSection( icon = Icons.Filled.Sensors, - title = "Strap", - blurb = "NOOP pairs directly with your WHOOP over Bluetooth: no WHOOP app, no cloud.", + title = tr("Strap"), + blurb = tr("NOOP pairs directly with your WHOOP over Bluetooth: no WHOOP app, no cloud."), ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row( @@ -992,7 +992,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { live.batteryPct?.let { pct -> StatePill( title = "Battery ${pct.roundToInt()}%" + - if (live.charging == true) " · Charging" else "", + if (live.charging == true) tr(" · Charging") else "", tone = batteryTone(pct), showsDot = false, ) @@ -1005,7 +1005,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { NoopButton( - text = if (live.scanning) "Searching…" else "Re-scan", + text = if (live.scanning) tr("Searching…") else tr("Re-scan"), leadingIcon = Icons.Filled.Refresh, kind = NoopButtonKind.Primary, enabled = !live.scanning, @@ -1013,7 +1013,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) NoopButton( - text = "Disconnect", + text = tr("Disconnect"), leadingIcon = Icons.Filled.Cancel, kind = NoopButtonKind.Secondary, enabled = live.connected || live.bonded, @@ -1027,10 +1027,10 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { if (live.connected && !live.whoop5Detected) { var nameDraft by remember(live.advertisingName) { mutableStateOf(live.advertisingName ?: "") } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Strap name", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Strap name"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Rename your strap's Bluetooth name, useful for a second-hand band. The strap " + - "reboots to apply, then reconnects with the new name.", + tr("Rename your strap's Bluetooth name, useful for a second-hand band. The strap ") + + tr("reboots to apply, then reconnects with the new name."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1038,7 +1038,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { value = nameDraft, onValueChange = { nameDraft = it.take(24) }, singleLine = true, - placeholder = { Text("WHOOP", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("WHOOP"), style = NoopType.body, color = Palette.textTertiary) }, modifier = Modifier.fillMaxWidth(), colors = OutlinedTextFieldDefaults.colors( focusedTextColor = Palette.textPrimary, @@ -1052,7 +1052,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { NoopButton( - text = "Rename", + text = tr("Rename"), leadingIcon = Icons.Filled.Edit, kind = NoopButtonKind.Primary, enabled = live.bonded && nameDraft.isNotBlank(), @@ -1074,12 +1074,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Keep connected in the background", + tr("Keep connected in the background"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Keeps streaming from your strap with an ongoing notification, even after you close NOOP. Turn off to disconnect when the app is closed.", + tr("Keeps streaming from your strap with an ongoing notification, even after you close NOOP. Turn off to disconnect when the app is closed."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1122,18 +1122,18 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) Column(modifier = Modifier.weight(1f)) { Text( - "Allow background running", + tr("Allow background running"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected.", + tr("Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected."), style = NoopType.footnote, color = Palette.textTertiary, ) } NoopButton( - text = "Allow", + text = tr("Allow"), kind = NoopButtonKind.Primary, onClick = { runCatching { context.startActivity(BatteryOptimization.requestIntent(context)) } @@ -1156,12 +1156,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Continuous HRV capture", + tr("Continuous HRV capture"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Keeps the detailed beat-to-beat stream running all day and night, not just while a live screen is open, so NOOP captures much more for overnight HRV, recovery and sleep. Uses more battery (your strap streams heart rate continuously). Needs \"Keep connected in the background\" on.", + tr("Keeps the detailed beat-to-beat stream running all day and night, not just while a live screen is open, so NOOP captures much more for overnight HRV, recovery and sleep. Uses more battery (your strap streams heart rate continuously). Needs \"Keep connected in the background\" on."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1193,12 +1193,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Overnight only", + tr("Overnight only"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Runs the stream only during your quiet hours window (22:00 to 07:00 by default), roughly halving the battery cost. Daytime Stress readings will be sparser, since Stress reads this live stream.", + tr("Runs the stream only during your quiet hours window (22:00 to 07:00 by default), roughly halving the battery cost. Daytime Stress readings will be sparser, since Stress reads this live stream."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1231,12 +1231,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Debug logging", + tr("Debug logging"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Also write the strap log to the system log (logcat) for development over adb. Off by default; the in-app log and “Share strap log” below work either way.", + tr("Also write the strap log to the system log (logcat) for development over adb. Off by default; the in-app log and “Share strap log” below work either way."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1255,14 +1255,14 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Debug logging" + contentDescription = tr("Debug logging") }, ) } // Diagnostics: export the strap connection log so people can attach it to a bug report. NoopButton( - text = "Share strap log (for bug reports)", + text = tr("Share strap log (for bug reports)"), leadingIcon = Icons.Filled.Upload, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1285,7 +1285,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { indication = null, ) { showModelComparison = true } .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "WHOOP 4.0 versus 5.0: what each can read and why" }, + .semantics { contentDescription = tr("WHOOP 4.0 versus 5.0: what each can read and why") }, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -1299,9 +1299,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { modifier = Modifier.size(18.dp), ) Column(modifier = Modifier.weight(1f)) { - Text("WHOOP 4.0 vs 5.0/MG", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("WHOOP 4.0 vs 5.0/MG"), style = NoopType.headline, color = Palette.textPrimary) Text( - "What each strap can read, and why some features differ.", + tr("What each strap can read, and why some features differ."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -1317,8 +1317,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // the experimental probes, diagnostics, raw-capture export and Trends report all stay one tap // away. Mirrors the iOS SettingsView "Advanced" disclosure and the Test Centre Advanced group. SettingsDisclosure( - title = "Advanced", - subtitle = "Experimental probes, diagnostics, raw-sensor export, and the Trends report. Tucked away to keep the everyday screen tidy.", + title = tr("Advanced"), + subtitle = tr("Experimental probes, diagnostics, raw-sensor export, and the Trends report. Tucked away to keep the everyday screen tidy."), expanded = advancedOpen, onToggle = { advancedOpen = !advancedOpen; SettingsDisclosurePrefs.write(NoopPrefs.of(context), advancedOpen) }, ) { @@ -1327,8 +1327,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { if (showFiveMGControls) { SettingsSection( icon = Icons.Filled.Science, - title = "Experimental · WHOOP 5 / MG", - blurb = "Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap. WHOOP 4.0 is never affected.", + title = tr("Experimental · WHOOP 5 / MG"), + blurb = tr("Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap. WHOOP 4.0 is never affected."), ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row( @@ -1337,7 +1337,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - "Try WHOOP 5/MG protocol probes", + tr("Try WHOOP 5/MG protocol probes"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -1356,12 +1356,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Try WHOOP 5/MG protocol probes" + contentDescription = tr("Try WHOOP 5/MG protocol probes") }, ) } Text( - "On a 5/MG connection NOOP will send a puffin realtime-stream request after the handshake, and log what comes back. If you have a 5/MG strap, turning this on and sharing your strap log helps map the protocol. No effect on WHOOP 4.0.", + tr("On a 5/MG connection NOOP will send a puffin realtime-stream request after the handshake, and log what comes back. If you have a 5/MG strap, turning this on and sharing your strap log helps map the protocol. No effect on WHOOP 4.0."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1373,7 +1373,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - "Broadcast heart rate (Garmin/ANT)", + tr("Broadcast heart rate (Garmin/ANT)"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -1393,12 +1393,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Broadcast heart rate" + contentDescription = tr("Broadcast heart rate") }, ) } Text( - "Makes your WHOOP 5.0/MG advertise its heart rate as a standard Bluetooth HR sensor, so a Garmin (Edge/watch), Zwift or gym equipment can use it during a workout. Applied on the next connection (and immediately if connected); writes the strap's whoop_live_hr_in_adv_ind_pkt flag. Reversible. 5/MG only.", + tr("Makes your WHOOP 5.0/MG advertise its heart rate as a standard Bluetooth HR sensor, so a Garmin (Edge/watch), Zwift or gym equipment can use it during a workout. Applied on the next connection (and immediately if connected); writes the strap's whoop_live_hr_in_adv_ind_pkt flag. Reversible. 5/MG only."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1410,7 +1410,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - "Unlock WHOOP 5/MG deep data (R22)", + tr("Unlock WHOOP 5/MG deep data (R22)"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -1429,34 +1429,34 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Unlock WHOOP 5/MG deep data" + contentDescription = tr("Unlock WHOOP 5/MG deep data") }, ) } Text( - "WHOOP 5/MG straps hand a fresh app only live heart rate. The official app switches on the deeper streams (high-rate HR + motion + history) by writing a set of feature flags, a sequence two independent projects have documented. With this on, the button below sends that exact sequence to your strap. Unlike everything else here it does write to the strap, but it's reversible (it only changes which data the strap emits) and is the same thing the official app does. Experimental: it may do nothing on your firmware.", + tr("WHOOP 5/MG straps hand a fresh app only live heart rate. The official app switches on the deeper streams (high-rate HR + motion + history) by writing a set of feature flags, a sequence two independent projects have documented. With this on, the button below sends that exact sequence to your strap. Unlike everything else here it does write to the strap, but it's reversible (it only changes which data the strap emits) and is the same thing the official app does. Experimental: it may do nothing on your firmware."), style = NoopType.caption, color = Palette.textTertiary, ) if (deepData) { NoopButton( - text = "Send enable sequence to strap", + text = tr("Send enable sequence to strap"), leadingIcon = Icons.Filled.Bolt, kind = NoopButtonKind.Primary, enabled = live.encryptedBond && live.worn, onClick = { vm.ble.enableWhoop5DeepData() }, ) Text( - if (!live.encryptedBond) "Needs the full encrypted bond: close the official WHOOP app and pair the strap to NOOP first (a live-HR-only link can't carry the unlock)." - else if (!live.worn) "Put the strap on first. The deep stream is on-wrist only." - else "Wear the strap, tap once, then let it sync and share your strap log.", + if (!live.encryptedBond) tr("Needs the full encrypted bond: close the official WHOOP app and pair the strap to NOOP first (a live-HR-only link can't carry the unlock).") + else if (!live.worn) tr("Put the strap on first. The deep stream is on-wrist only.") + else tr("Wear the strap, tap once, then let it sync and share your strap log."), style = NoopType.caption, color = Palette.textTertiary, ) // Live R22 telemetry (#174): proof of what the strap is doing right now. if (live.r22FlagsAccepted > 0) { Text( - if (live.r22FlagsAccepted >= 15) "✓ Strap accepted all 15 R22 flags" + if (live.r22FlagsAccepted >= 15) tr("✓ Strap accepted all 15 R22 flags") else "Strap accepted ${live.r22FlagsAccepted}/15 R22 flags…", style = NoopType.caption, color = if (live.r22FlagsAccepted >= 15) Palette.statusPositive else Palette.textSecondary, @@ -1470,7 +1470,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } else if (live.r22FlagsAccepted >= 15) { Text( - "Flags accepted, but the enable sequence doesn't start a separate live stream. The deep records arrive as part of the normal history sync (#494).", + tr("Flags accepted, but the enable sequence doesn't start a separate live stream. The deep records arrive as part of the normal history sync (#494)."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1483,7 +1483,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - "Record 5/MG raw capture (research)", + tr("Record 5/MG raw capture (research)"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -1502,17 +1502,17 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Record 5/MG raw capture" + contentDescription = tr("Record 5/MG raw capture") }, ) } Text( - "Records the raw frames of each 5/MG history sync to a file on this phone, so you can share them and help NOOP learn to decode 5/MG sleep, recovery and strain. The file contains raw biometric frames (heart rate, R-R, skin temperature, motion) and the strap's own diagnostic text. Nothing leaves the phone unless you share it. Off by default.", + tr("Records the raw frames of each 5/MG history sync to a file on this phone, so you can share them and help NOOP learn to decode 5/MG sleep, recovery and strain. The file contains raw biometric frames (heart rate, R-R, skin temperature, motion) and the strap's own diagnostic text. Nothing leaves the phone unless you share it. Off by default."), style = NoopType.caption, color = Palette.textTertiary, ) NoopButton( - text = "Share 5/MG capture (for the decode effort)", + text = tr("Share 5/MG capture (for the decode effort)"), leadingIcon = Icons.Filled.Upload, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1523,7 +1523,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // the strap log together (timestamped, same minute) so a protocol-mapping issue arrives // with the frames AND the context that produced them. NoopButton( - text = "Export raw + log (matched pair)", + text = tr("Export raw + log (matched pair)"), leadingIcon = Icons.Filled.IosShare, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1537,8 +1537,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // stays available on a WHOOP 4.0 too (#22): a 4.0 owner still needs it to share decoded streams. SettingsSection( icon = Icons.Filled.Science, - title = "Diagnostics", - blurb = "A read-only export of the decoded sensor streams NOOP already stores. Works on any strap. Nothing is written to your device, and nothing is uploaded.", + title = tr("Diagnostics"), + blurb = tr("A read-only export of the decoded sensor streams NOOP already stores. Works on any strap. Nothing is written to your device, and nothing is uploaded."), ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { // --- Experimental sleep staging (V2) — opt-in, default OFF, every model. (V7 Pillar 3b) --- @@ -1548,7 +1548,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - "Experimental sleep staging (V2)", + tr("Experimental sleep staging (V2)"), style = NoopType.subhead, color = Palette.textPrimary, modifier = Modifier.weight(1f), @@ -1567,15 +1567,15 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Experimental sleep staging V2" + contentDescription = tr("Experimental sleep staging V2") }, ) } Text( - "A transparent cardiorespiratory recipe that recovers deep and REM better than the " + - "default staging. Opt-in and experimental: it only changes how already-detected " + - "nights are split into stages (detection and scores are unchanged), and the default " + - "staging stays in place if you leave this off. Takes effect on the next nights staged.", + tr("A transparent cardiorespiratory recipe that recovers deep and REM better than the ") + + tr("default staging. Opt-in and experimental: it only changes how already-detected ") + + tr("nights are split into stages (detection and scores are unchanged), and the default ") + + tr("staging stays in place if you leave this off. Takes effect on the next nights staged."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1584,14 +1584,14 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // CSV so power users / external devs can prototype sleep/activity/VBT algorithms on real // data without a BLE stream (#308/#276/#322). On-device only; plain text, no BLE hex. NoopButton( - text = "Export raw sensor data (CSV)", + text = tr("Export raw sensor data (CSV)"), leadingIcon = Icons.Filled.Upload, kind = NoopButtonKind.Secondary, fullWidth = true, onClick = { scope.launch { RawSensorExport.export(context, vm.repo) } }, ) Text( - "Saves the last 24h of decoded sensor samples (heart rate, R-R, motion, steps and any 5/MG deep streams you've unlocked) as one CSV you can share, for tinkering with your own data. Nothing leaves the phone unless you share it.", + tr("Saves the last 24h of decoded sensor samples (heart rate, R-R, motion, steps and any 5/MG deep streams you've unlocked) as one CSV you can share, for tinkering with your own data. Nothing leaves the phone unless you share it."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1601,7 +1601,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // "Share strap log" row above, which also doesn't gate on a live strap). 12/24h follows the // phone's own clock setting. NoopButton( - text = "Buzz the time on your strap", + text = tr("Buzz the time on your strap"), leadingIcon = Icons.Filled.Vibration, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1610,7 +1610,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { }, ) Text( - "Feel the current time as a sequence of buzzes (#460). Does nothing unless your strap is connected.", + tr("Feel the current time as a sequence of buzzes (#460). Does nothing unless your strap is connected."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -1624,8 +1624,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // default. SharedPreferences isn't reactive, so the Switch + time mirror into local state. SettingsSection( icon = Icons.Filled.Storage, - title = "Scheduled debug export (#510)", - blurb = "Once a day at a time you choose, NOOP writes a timestamped strap log (plus the raw 5/MG capture, if you have one) to its export folder. No sharing, nothing leaves the phone. Useful for chasing an intermittent overnight fault. Off by default.", + title = tr("Scheduled debug export (#510)"), + blurb = tr("Once a day at a time you choose, NOOP writes a timestamped strap log (plus the raw 5/MG capture, if you have one) to its export folder. No sharing, nothing leaves the phone. Useful for chasing an intermittent overnight fault. Off by default."), ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row( @@ -1635,12 +1635,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) { Column(modifier = Modifier.weight(1f)) { Text( - "Daily auto-export", + tr("Daily auto-export"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "Writes a timestamped strap log (and the raw .bin if a 5/MG capture exists) to the app's export folder once a day at the time below.", + tr("Writes a timestamped strap log (and the raw .bin if a 5/MG capture exists) to the app's export folder once a day at the time below."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1660,7 +1660,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { uncheckedBorderColor = Palette.hairline, ), modifier = Modifier.semantics { - contentDescription = "Daily auto-export" + contentDescription = tr("Daily auto-export") }, ) } @@ -1672,16 +1672,16 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column(modifier = Modifier.weight(1f)) { - Text("Export time", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Export time"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "The daily export runs at this time.", + tr("The daily export runs at this time."), style = NoopType.footnote, color = Palette.textTertiary, ) } TimeChip( minutes = debugExportMinutes, - accessibilityLabel = "Daily export time", + accessibilityLabel = tr("Daily export time"), onPicked = { debugExportMinutes = it debugExportSettings.timeMinutes = it @@ -1695,7 +1695,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // above) and confirms with a Toast naming the folder, so the user sees the feature work // without waiting for the scheduled run. NoopButton( - text = "Export now", + text = tr("Export now"), leadingIcon = Icons.Filled.SaveAlt, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1707,7 +1707,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { Toast.makeText( context, if (files.isNotEmpty()) "Wrote a dated debug export (${files.size} file${if (files.size == 1) "" else "s"}) to the app's export folder." - else "Couldn't write the debug export.", + else tr("Couldn't write the debug export."), Toast.LENGTH_LONG, ).show() } @@ -1725,13 +1725,13 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // --- Health & wellness (v5 opt-in toggles) --- SettingsSection( icon = Icons.Filled.Science, - title = "Health & wellness", - blurb = "Optional, on-device wellness signals. Each is off by default, computed only on this phone from data you already have, and never a medical diagnosis.", + title = tr("Health & wellness"), + blurb = tr("Optional, on-device wellness signals. Each is off by default, computed only on this phone from data you already have, and never a medical diagnosis."), ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { ToggleRow( - title = "Illness heads-up", - detail = "Watches your resting heart rate, HRV and skin temperature for the pattern that often shows up before you feel unwell, and surfaces a gentle heads-up. An observation about your own numbers, not a diagnosis.", + title = tr("Illness heads-up"), + detail = tr("Watches your resting heart rate, HRV and skin temperature for the pattern that often shows up before you feel unwell, and surfaces a gentle heads-up. An observation about your own numbers, not a diagnosis."), checked = illnessWatch, onCheckedChange = { illnessWatch = it @@ -1746,8 +1746,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // Settings toggle was the one surface that was missed, so a male profile could enable it here. if (cycleTracking || cycleOptInApplies(profile.sex)) { ToggleRow( - title = "Cycle awareness", - detail = "Reads a coarse menstrual-cycle phase from your nightly skin-temperature shift, on this device only. Awareness only: not contraception, not a fertility predictor, not a medical service.", + title = tr("Cycle awareness"), + detail = tr("Reads a coarse menstrual-cycle phase from your nightly skin-temperature shift, on this device only. Awareness only: not contraception, not a fertility predictor, not a medical service."), checked = cycleTracking, onCheckedChange = { cycleTracking = it @@ -1757,8 +1757,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { RowDivider() } ToggleRow( - title = "Hydration tracking", - detail = "Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On this phone only. Nothing is synced.", + title = tr("Hydration tracking"), + detail = tr("Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On this phone only. Nothing is synced."), checked = hydrationTracking, onCheckedChange = { hydrationTracking = it @@ -1767,8 +1767,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) RowDivider() ToggleRow( - title = "Auto-detect workouts", - detail = "After a sync, NOOP looks over your recent heart rate for a sustained, raised stretch that looks like exercise and offers to save it. It only ever suggests. Nothing is saved until you tap Save, and you can dismiss any suggestion. Deliberately conservative, so the odd workout may be missed. On this phone only.", + title = tr("Auto-detect workouts"), + detail = tr("After a sync, NOOP looks over your recent heart rate for a sustained, raised stretch that looks like exercise and offers to save it. It only ever suggests. Nothing is saved until you tap Save, and you can dismiss any suggestion. Deliberately conservative, so the odd workout may be missed. On this phone only."), checked = autoDetectWorkouts, onCheckedChange = { autoDetectWorkouts = it @@ -1777,8 +1777,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) RowDivider() ToggleRow( - title = "Keep screen on during a workout", - detail = "Holds the screen awake while you're recording a workout, so your live heart rate stays visible without the phone dimming. Only applies during a recording. The screen sleeps normally the rest of the time. Leaving it on does use a bit more battery, and means your unlocked screen stays visible for the whole workout, so flip it off if that's a concern.", + title = tr("Keep screen on during a workout"), + detail = tr("Holds the screen awake while you're recording a workout, so your live heart rate stays visible without the phone dimming. Only applies during a recording. The screen sleeps normally the rest of the time. Leaving it on does use a bit more battery, and means your unlocked screen stays visible for the whole workout, so flip it off if that's a concern."), checked = workoutKeepScreenOn, onCheckedChange = { workoutKeepScreenOn = it @@ -1789,8 +1789,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // BETA + default ON (the one exception to this section's off-by-default rule): the flag // gates the Today entry so anyone can wave the beta away here with one flip. ToggleRow( - title = "Live Sessions (beta)", - detail = "Silence-first strap coaching during workouts.", + title = tr("Live Sessions (beta)"), + detail = tr("Silence-first strap coaching during workouts."), checked = liveSessionsBeta, onCheckedChange = { liveSessionsBeta = it @@ -1799,8 +1799,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) RowDivider() ToggleRow( - title = "Stress check-ins (haptic)", - detail = "Lets NOOP notice a fresh HRV dip while you're still and offer a minute to breathe. \"Stress\" here is an autonomic proxy from your own baseline, never a diagnosis. The strap gives one light confirming buzz; no push notification.", + title = tr("Stress check-ins (haptic)"), + detail = tr("Lets NOOP notice a fresh HRV dip while you're still and offer a minute to breathe. \"Stress\" here is an autonomic proxy from your own baseline, never a diagnosis. The strap gives one light confirming buzz; no push notification."), checked = stressCheckIn, onCheckedChange = { stressCheckIn = it @@ -1811,8 +1811,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) if (stressCheckIn) { ToggleRow( - title = "Offer a breath automatically", - detail = "When a dip is detected, surface the check-in card on its own (rate-limited, quiet-hours aware). Off keeps it manual.", + title = tr("Offer a breath automatically"), + detail = tr("When a dip is detected, surface the check-in card on its own (rate-limited, quiet-hours aware). Off keeps it manual."), checked = stressAutoNudge, onCheckedChange = { stressAutoNudge = it @@ -1822,8 +1822,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { } RowDivider() ToggleRow( - title = "Rhythm (experimental)", - detail = "An experimental picture of your beat-to-beat timing: a Poincaré scatter and plain regularity stats from quiet resting windows. Not an ECG and not a diagnosis; you'll read a short disclaimer and accept before it turns on.", + title = tr("Rhythm (experimental)"), + detail = tr("An experimental picture of your beat-to-beat timing: a Poincaré scatter and plain regularity stats from quiet resting windows. Not an ECG and not a diagnosis; you'll read a short disclaimer and accept before it turns on."), checked = rhythmEnabled, onCheckedChange = { // Enabling here just un-gates the experimental item; the screen itself still shows @@ -1839,8 +1839,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) RowDivider() ToggleRow( - title = "Share on-device signals with the Coach", - detail = "When the opt-in Coach is set up with your own key, also include a short summary of your strongest on-device patterns and Lab Book markers in its context. Summary only; no raw data leaves your phone. Requires the Coach's own data consent first.", + title = tr("Share on-device signals with the Coach"), + detail = tr("When the opt-in Coach is set up with your own key, also include a short summary of your strongest on-device patterns and Lab Book markers in its context. Summary only; no raw data leaves your phone. Requires the Coach's own data consent first."), checked = coachSignals, onCheckedChange = { coachSignals = it @@ -1856,11 +1856,11 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // on the same bindings, so this is a faster door to the full set without growing this screen. SettingsSection( icon = Icons.Filled.BugReport, - title = "Test Centre", - blurb = "Turn on a test for the thing that's wrong, wear the strap, then tap Report. Your strap log, recalibrate, scheduled export and experimental probes all live here too.", + title = tr("Test Centre"), + blurb = tr("Turn on a test for the thing that's wrong, wear the strap, then tap Report. Your strap log, recalibrate, scheduled export and experimental probes all live here too."), ) { NoopButton( - text = "Open Test Centre", + text = tr("Open Test Centre"), leadingIcon = Icons.Filled.BugReport, kind = NoopButtonKind.Secondary, fullWidth = true, @@ -1877,24 +1877,24 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // foldHistory drops every night before that epoch and re-seeds. Mirrors the iOS/Mac button. SettingsSection( icon = Icons.Filled.Favorite, - title = "Charge", - blurb = "Charge is NOOP's daily readiness score, learned from your own HRV, resting heart rate and more over time. Your history stays.", + title = tr("Charge"), + blurb = tr("Charge is NOOP's daily readiness score, learned from your own HRV, resting heart rate and more over time. Your history stays."), ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Recalibrate Charge baseline", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("Recalibrate Charge baseline"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Restarts the roughly 4-night build-up for Charge and your HRV baseline from tonight. Use it if a bad first week set your baseline off. Your history stays.", + tr("Restarts the roughly 4-night build-up for Charge and your HRV baseline from tonight. Use it if a bad first week set your baseline off. Your history stays."), style = NoopType.footnote, color = Palette.textTertiary, ) } NoopButton( - text = "Recalibrate Charge baseline", + text = tr("Recalibrate Charge baseline"), leadingIcon = Icons.Filled.Autorenew, kind = NoopButtonKind.Secondary, fullWidth = true, - modifier = Modifier.semantics { contentDescription = "Recalibrate Charge baseline" }, + modifier = Modifier.semantics { contentDescription = tr("Recalibrate Charge baseline") }, onClick = { showRecalibrateConfirm = true }, ) } @@ -1904,10 +1904,10 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { AlertDialog( onDismissRequest = { showRecalibrateConfirm = false }, containerColor = Palette.surfaceOverlay, - title = { Text("Recalibrate your Charge baseline?", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Recalibrate your Charge baseline?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( - "This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays. Use it if a bad first week, like wearing it while sick, set your baseline off.", + tr("This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays. Use it if a bad first week, like wearing it while sick, set your baseline off."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1932,15 +1932,15 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { vm.syncNow() Toast.makeText( context, - "Charge baseline reset. NOOP will re-learn it from tonight. Your history stays, and it takes a few nights to settle.", + tr("Charge baseline reset. NOOP will re-learn it from tonight. Your history stays, and it takes a few nights to settle."), Toast.LENGTH_LONG, ).show() }, - ) { Text("Recalibrate", style = NoopType.body, color = Palette.accent) } + ) { Text(tr("Recalibrate"), style = NoopType.body, color = Palette.accent) } }, dismissButton = { TextButton(onClick = { showRecalibrateConfirm = false }) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -1948,8 +1948,8 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { SettingsSection( icon = Icons.Filled.Storage, - title = "Backup & restore", - blurb = "Move all your NOOP data to another phone. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces this phone's data with a backup.", + title = tr("Backup & restore"), + blurb = tr("Move all your NOOP data to another phone. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces this phone's data with a backup."), ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { // Three equal-width buttons share the row (each takes a third via weight) — mirrors the @@ -1960,7 +1960,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { horizontalArrangement = Arrangement.spacedBy(12.dp), ) { NoopButton( - text = "Export…", + text = tr("Export…"), kind = NoopButtonKind.Primary, enabled = !backupBusy, modifier = Modifier.weight(1f), @@ -1971,7 +1971,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) NoopButton( - text = "Import…", + text = tr("Import…"), kind = NoopButtonKind.Secondary, enabled = !backupBusy, modifier = Modifier.weight(1f), @@ -1982,7 +1982,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) NoopButton( - text = "Export CSV…", + text = tr("Export CSV…"), kind = NoopButtonKind.Secondary, enabled = !backupBusy, modifier = Modifier.weight(1f), @@ -2003,15 +2003,15 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { strokeWidth = 2.dp, modifier = Modifier.size(18.dp), ) - Text("Working…", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Working…"), style = NoopType.footnote, color = Palette.textSecondary) } } NoteRow( icon = Icons.Filled.Info, iconTint = Palette.textTertiary, - text = "Importing overwrites everything currently on this phone. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect. " + - "Export CSV writes a WHOOP-format zip of your days, sleeps, workouts and journal that re-imports into NOOP on Android or Mac. On-device computed rows are marked APPROXIMATE in its Source column; the .noopbak backup stays the lossless restore path.", + text = tr("Importing overwrites everything currently on this phone. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect. ") + + tr("Export CSV writes a WHOOP-format zip of your days, sleeps, workouts and journal that re-imports into NOOP on Android or Mac. On-device computed rows are marked APPROXIMATE in its Source column; the .noopbak backup stays the lossless restore path."), ) } } @@ -2019,15 +2019,15 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { // --- About --- SettingsSection( icon = Icons.Filled.Info, - title = "About", - blurb = "NOOP: all your data, none of the cloud.", + title = tr("About"), + blurb = tr("NOOP: all your data, none of the cloud."), ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - Text("NOOP", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("NOOP"), style = NoopType.title2, color = Palette.textPrimary) StatePill("v${BuildConfig.VERSION_NAME}", tone = StrandTone.Neutral, showsDot = false) } @@ -2052,12 +2052,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { } } .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "Project home and source on GitHub" }, + .semantics { contentDescription = tr("Project home and source on GitHub") }, ) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Project home & source", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Project home & source"), style = NoopType.body, color = Palette.textPrimary) Text( - "GitHub: code, releases, issues and the wiki.", + tr("GitHub: code, releases, issues and the wiki."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -2097,9 +2097,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { strokeWidth = 2.dp, color = Palette.accent, ) - Text("Checking…", style = NoopType.captionNumber) + Text(tr("Checking…"), style = NoopType.captionNumber) } else { - Text("Check for updates", style = NoopType.captionNumber) + Text(tr("Check for updates"), style = NoopType.captionNumber) } } when (val r = updResult) { @@ -2110,7 +2110,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) UpdateCheck.Result.Failed -> Text( - "Couldn't check. Try again.", + tr("Couldn't check. Try again."), style = NoopType.footnote, color = Palette.statusWarning, ) else -> {} @@ -2142,7 +2142,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { ) } else { NoopButton( - text = "Update", + text = tr("Update"), leadingIcon = Icons.Filled.Download, kind = NoopButtonKind.Primary, onClick = { @@ -2154,7 +2154,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { InAppUpdate.requestInstallPermission(context) Toast.makeText( context, - "Allow installing apps, then tap Update again.", + tr("Allow installing apps, then tap Update again."), Toast.LENGTH_LONG, ).show() } @@ -2172,7 +2172,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { updDownloadPct = null Toast.makeText( context, - "Download failed — opening the release page.", + tr("Download failed — opening the release page."), Toast.LENGTH_LONG, ).show() InAppUpdate.openReleasePage(context, avail.url) @@ -2203,13 +2203,13 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { } Text( - "Checks GitHub for the latest version when you tap. Nothing else is sent.", + tr("Checks GitHub for the latest version when you tap. Nothing else is sent."), style = NoopType.footnote, color = Palette.textTertiary, ) } Text( - "A standalone companion for your WHOOP. Everything stays on this phone: your history, your live stream, your numbers. Nothing is uploaded.", + tr("A standalone companion for your WHOOP. Everything stays on this phone: your history, your live stream, your numbers. Nothing is uploaded."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -2242,9 +2242,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { modifier = Modifier.size(18.dp), ) Column(modifier = Modifier.weight(1f)) { - Text("What's new", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("What's new"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Recent changes and what to expect", + tr("Recent changes and what to expect"), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -2268,7 +2268,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { indication = null, ) { showScoringGuide = true } .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "How your scores work" }, + .semantics { contentDescription = tr("How your scores work") }, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -2282,9 +2282,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { modifier = Modifier.size(18.dp), ) Column(modifier = Modifier.weight(1f)) { - Text("How your scores work", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("How your scores work"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Charge, Effort and Rest, and how they differ from WHOOP", + tr("Charge, Effort and Rest, and how they differ from WHOOP"), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -2309,7 +2309,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { indication = null, ) { showHowNoopWorks = true } .padding(horizontal = 14.dp, vertical = 12.dp) - .semantics { contentDescription = "How NOOP works" }, + .semantics { contentDescription = tr("How NOOP works") }, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -2323,9 +2323,9 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { modifier = Modifier.size(18.dp), ) Column(modifier = Modifier.weight(1f)) { - Text("How NOOP works", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("How NOOP works"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Sleep sorting, scores, recording, and where your numbers come from.", + tr("Sleep sorting, scores, recording, and where your numbers come from."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -2352,7 +2352,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { modifier = Modifier.size(16.dp), ) Text( - "NOOP is not a medical device. It is for informational and personal-insight purposes only and is not intended to diagnose, treat, cure or prevent any condition. Talk to a clinician for medical advice.", + tr("NOOP is not a medical device. It is for informational and personal-insight purposes only and is not intended to diagnose, treat, cure or prevent any condition. Talk to a clinician for medical advice."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -2361,12 +2361,12 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { RowDivider() Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Overline("Built on") - AttributionRow(repo = "my-whoop", note = "WHOOP 4.0 protocol") - AttributionRow(repo = "goose", note = "WHOOP 5.0 protocol") + Overline(tr("Built on")) + AttributionRow(repo = "my-whoop", note = tr("WHOOP 4.0 protocol")) + AttributionRow(repo = "goose", note = tr("WHOOP 5.0 protocol")) } Text( - "Open-source BLE reverse-engineering work. Thank you.", + tr("Open-source BLE reverse-engineering work. Thank you."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -2401,7 +2401,7 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { - Text("Support & project home", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Support & project home"), style = NoopType.headline, color = Palette.textPrimary) Text( "Code, releases, and where to report an issue: $PROJECT_URL", style = NoopType.footnote, @@ -2545,10 +2545,10 @@ private fun waistInchesStep(current: Double, up: Boolean): Double { // MARK: - Strap status helpers (mirror SettingsView's computed properties) private fun strapStatusTitle(bonded: Boolean, connected: Boolean): String = when { - bonded && connected -> "Bonded · streaming" - connected -> "Connected" - bonded -> "Bonded · idle" - else -> "Disconnected" + bonded && connected -> tr("Bonded · streaming") + connected -> tr("Connected") + bonded -> tr("Bonded · idle") + else -> tr("Disconnected") } private fun strapTone(bonded: Boolean, connected: Boolean): StrandTone = when { @@ -2559,11 +2559,11 @@ private fun strapTone(bonded: Boolean, connected: Boolean): StrandTone = when { // `internal` (not private) so the unit test in the same package can assert the scanning branch. internal fun strapStatusDetail(bonded: Boolean, connected: Boolean, scanning: Boolean): String = when { - scanning -> "Searching for your WHOOP… make sure it's charged, on your wrist, and the official WHOOP app isn't connected to it." - bonded && connected -> "Your strap is paired and sending data. Open Live for a real-time heart rate." - connected -> "Connected. Finishing the secure pairing handshake…" - bonded -> "Previously paired but not currently connected. Re-scan to reconnect." - else -> "No strap connected. Put your WHOOP nearby and tap Re-scan to pair." + scanning -> tr("Searching for your WHOOP… make sure it's charged, on your wrist, and the official WHOOP app isn't connected to it.") + bonded && connected -> tr("Your strap is paired and sending data. Open Live for a real-time heart rate.") + connected -> tr("Connected. Finishing the secure pairing handshake…") + bonded -> tr("Previously paired but not currently connected. Re-scan to reconnect.") + else -> tr("No strap connected. Put your WHOOP nearby and tap Re-scan to pair.") } private fun batteryTone(pct: Double): StrandTone = when { @@ -2633,7 +2633,7 @@ private fun SettingsDisclosure( ) .semantics { contentDescription = title - stateDescription = if (expanded) "Expanded" else "Collapsed" + stateDescription = if (expanded) tr("Expanded") else tr("Collapsed") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -2671,7 +2671,7 @@ private fun SettingsSection( NoopCard(padding = 20.dp, tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Settings") + Overline(tr("Settings")) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), diff --git a/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt b/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt index 34be28326..a21b5aee8 100644 --- a/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt @@ -127,9 +127,9 @@ fun CycleAwarenessCard( // Header: overline + confidence pill. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Cycle awareness") + Overline(tr("Cycle awareness")) Text( - "From your nightly temperature", + tr("From your nightly temperature"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -170,17 +170,17 @@ fun CycleAwarenessCard( if (onLogPeriod != null || onOpenDetail != null || onTurnOff != null) { Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { if (onLogPeriod != null) { - OutlinedButton(onClick = onLogPeriod) { Text("Log period start") } + OutlinedButton(onClick = onLogPeriod) { Text(tr("Log period start")) } } if (onOpenDetail != null) { OutlinedButton( onClick = onOpenDetail, colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.accent), - ) { Text("View detail") } + ) { Text(tr("View detail")) } } // #801: symmetric off-control (turn cycle awareness off where it was turned on). if (onTurnOff != null) { - OutlinedButton(onClick = onTurnOff) { Text("Turn off") } + OutlinedButton(onClick = onTurnOff) { Text(tr("Turn off")) } } } } @@ -204,17 +204,17 @@ fun CycleAwarenessOptInCard(onEnable: () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.Thermostat, contentDescription = null, tint = Palette.restColor, modifier = Modifier.size(18.dp)) - Text("Cycle awareness", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Cycle awareness"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "NOOP can read a coarse menstrual-cycle phase from your nightly skin temperature, " + - "entirely on your device. It is awareness only: not contraception, not a fertility " + - "predictor, not a medical service.", + tr("NOOP can read a coarse menstrual-cycle phase from your nightly skin temperature, ") + + tr("entirely on your device. It is awareness only: not contraception, not a fertility ") + + tr("predictor, not a medical service."), style = NoopType.subhead, color = Palette.textSecondary, ) PrivacyNote() - OutlinedButton(onClick = onEnable) { Text("Turn on cycle awareness") } + OutlinedButton(onClick = onEnable) { Text(tr("Turn on cycle awareness")) } } } } @@ -236,8 +236,8 @@ fun BodyClockCard( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Body clock") - Text("Light + sleep timing only", style = NoopType.footnote, color = Palette.textTertiary) + Overline(tr("Body clock")) + Text(tr("Light + sleep timing only"), style = NoopType.footnote, color = Palette.textTertiary) } StatePill(bodyClockConfidenceLabel(estimate.confidence), tone = bodyClockConfidenceTone(estimate.confidence)) } @@ -275,7 +275,7 @@ fun BodyClockCard( OutlinedButton( onClick = onOpenPlanner, colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.accent), - ) { Text(if (plan == null) "Plan a trip or shift" else "View the full plan") } + ) { Text(if (plan == null) tr("Plan a trip or shift") else tr("View the full plan")) } } } } @@ -317,11 +317,11 @@ fun HeadsUpCard( // The visible "why": which signals fired. if (result.firedSignals.isNotEmpty()) { - WhyRow("Signals up", result.firedSignals, hue) + WhyRow(tr("Signals up"), result.firedSignals, hue) } // ...and what was ruled out (the differentiating part vs a black-box warning). if (result.suppressedBy.isNotEmpty()) { - WhyRow("Explained by", result.suppressedBy, Palette.textTertiary) + WhyRow(tr("Explained by"), result.suppressedBy, Palette.textTertiary) } // Optional confidence read from the parallel Mahalanobis distance, only when the level is // raised. Subtle by design: it augments, never gates (the engine already decided to raise). @@ -385,11 +385,11 @@ private fun WhyRow(label: String, values: List, tint: Color) { // MARK: - Derived copy / presentation (mirror the Swift card exactly) private fun cyclePhaseTitle(phase: CyclePhaseEngine.Phase): String = when (phase) { - CyclePhaseEngine.Phase.FOLLICULAR -> "Follicular" - CyclePhaseEngine.Phase.PERI_OVULATORY -> "Mid-cycle shift" - CyclePhaseEngine.Phase.LUTEAL -> "Luteal" - CyclePhaseEngine.Phase.UNKNOWN -> "No clear pattern" - CyclePhaseEngine.Phase.LEARNING -> "Learning your pattern" + CyclePhaseEngine.Phase.FOLLICULAR -> tr("Follicular") + CyclePhaseEngine.Phase.PERI_OVULATORY -> tr("Mid-cycle shift") + CyclePhaseEngine.Phase.LUTEAL -> tr("Luteal") + CyclePhaseEngine.Phase.UNKNOWN -> tr("No clear pattern") + CyclePhaseEngine.Phase.LEARNING -> tr("Learning your pattern") } /** "~day 18–22" — always a RANGE, never a single point. */ @@ -400,9 +400,9 @@ private fun cycleDayText(r: CyclePhaseEngine.Result): String? { } private fun cycleConfidenceLabel(c: CyclePhaseEngine.Confidence): String = when (c) { - CyclePhaseEngine.Confidence.LEARNING -> "Learning" - CyclePhaseEngine.Confidence.BUILDING -> "Building" - CyclePhaseEngine.Confidence.SOLID -> "Solid" + CyclePhaseEngine.Confidence.LEARNING -> tr("Learning") + CyclePhaseEngine.Confidence.BUILDING -> tr("Building") + CyclePhaseEngine.Confidence.SOLID -> tr("Solid") } private fun cycleConfidenceTone(c: CyclePhaseEngine.Confidence): StrandTone = when (c) { @@ -413,17 +413,17 @@ private fun cycleConfidenceTone(c: CyclePhaseEngine.Confidence): StrandTone = wh /** "About 25 min later than your schedule" — a plain, skimmable headline. */ private fun bodyClockOffsetTitle(e: CircadianEngine.PhaseEstimate): String { - if (e.confidence == CircadianEngine.PhaseConfidence.UNREADABLE) return "Hard to read right now" + if (e.confidence == CircadianEngine.PhaseConfidence.UNREADABLE) return tr("Hard to read right now") val mins = abs(e.offsetVsScheduleMinutes).roundToInt() - if (mins <= 20) return "About in sync with your schedule" + if (mins <= 20) return tr("About in sync with your schedule") val dir = if (e.offsetVsScheduleMinutes > 0) "later" else "earlier" return "About $mins min $dir than your schedule" } private fun bodyClockConfidenceLabel(c: CircadianEngine.PhaseConfidence): String = when (c) { - CircadianEngine.PhaseConfidence.UNREADABLE -> "Calibrating" - CircadianEngine.PhaseConfidence.WIDE -> "Building" - CircadianEngine.PhaseConfidence.SOLID -> "Solid" + CircadianEngine.PhaseConfidence.UNREADABLE -> tr("Calibrating") + CircadianEngine.PhaseConfidence.WIDE -> tr("Building") + CircadianEngine.PhaseConfidence.SOLID -> tr("Solid") } private fun bodyClockConfidenceTone(c: CircadianEngine.PhaseConfidence): StrandTone = when (c) { @@ -448,11 +448,11 @@ private fun headsUpGlyph(level: IllnessSignalEngine.Level): ImageVector = when ( } private fun headsUpTitle(level: IllnessSignalEngine.Level): String = when (level) { - IllnessSignalEngine.Level.RAISED -> "Heads-up" - IllnessSignalEngine.Level.ALREADY_UNWELL -> "Rest up" - IllnessSignalEngine.Level.SUPPRESSED -> "Probably not illness" - IllnessSignalEngine.Level.MILD -> "A few signals are up" - IllnessSignalEngine.Level.QUIET -> "Nothing notable" + IllnessSignalEngine.Level.RAISED -> tr("Heads-up") + IllnessSignalEngine.Level.ALREADY_UNWELL -> tr("Rest up") + IllnessSignalEngine.Level.SUPPRESSED -> tr("Probably not illness") + IllnessSignalEngine.Level.MILD -> tr("A few signals are up") + IllnessSignalEngine.Level.QUIET -> tr("Nothing notable") } // MARK: - Formatting helpers (locale-free, matching the engine's own helpers) diff --git a/android/app/src/main/java/com/noop/ui/SleepScreen.kt b/android/app/src/main/java/com/noop/ui/SleepScreen.kt index 59f31ab65..76d2f7d53 100644 --- a/android/app/src/main/java/com/noop/ui/SleepScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SleepScreen.kt @@ -277,9 +277,9 @@ fun SleepScreen( modifier = Modifier.fillMaxWidth().padding(Metrics.space24), verticalArrangement = Arrangement.spacedBy(Metrics.space16), ) { - Text("Good morning!", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Good morning!"), style = NoopType.title2, color = Palette.textPrimary) Text( - "Your night data is in. Logging how you felt helps NOOP learn what drives your best recovery.", + tr("Your night data is in. Logging how you felt helps NOOP learn what drives your best recovery."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -288,13 +288,13 @@ fun SleepScreen( modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = Palette.accent), ) { - Text("Open Journal", style = NoopType.headline, color = Palette.surfaceBase) + Text(tr("Open Journal"), style = NoopType.headline, color = Palette.surfaceBase) } TextButton( onClick = { showJournalPrompt = false }, modifier = Modifier.fillMaxWidth(), ) { - Text("Maybe later", style = NoopType.subhead, color = Palette.textTertiary) + Text(tr("Maybe later"), style = NoopType.subhead, color = Palette.textTertiary) } } } @@ -361,8 +361,8 @@ fun SleepScreen( } LazyScreenScaffold( - title = "Sleep", - subtitle = "Last night, read in two seconds.", + title = tr("Sleep"), + subtitle = tr("Last night, read in two seconds."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the static time-of-day liquid sky // settles into the theme canvas behind the header + hero, bled full-width up behind the status bar // via the scaffold's topBackground plumbing. Gated on the day-cycle preference exactly like Today @@ -473,7 +473,7 @@ fun SleepScreen( // pickers used to do exactly that): tell the user why nothing changed. (#940) Toast.makeText( context, - "That time can't be saved (it lands in the future or ends before it starts).", + tr("That time can't be saved (it lands in the future or ends before it starts)."), Toast.LENGTH_SHORT, ).show() } @@ -561,18 +561,18 @@ fun SleepScreen( @Composable private fun SleepMarkCard(onMark: (SleepMarkType) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader(title = "Sleep marks", overline = "Tap to log", trailing = "Phase 1") + SectionHeader(title = tr("Sleep marks"), overline = tr("Tap to log"), trailing = tr("Phase 1")) NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - "Tap when you're heading to bed or when you wake. Each tap is logged with the time. It doesn't change tonight's detected sleep.", + tr("Tap when you're heading to bed or when you wake. Each tap is logged with the time. It doesn't change tonight's detected sleep."), style = NoopType.footnote, color = Palette.textTertiary, ) Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { Button( onClick = { onMark(SleepMarkType.BEDTIME) }, - modifier = Modifier.weight(1f).semantics { contentDescription = "Log going to sleep" }, + modifier = Modifier.weight(1f).semantics { contentDescription = tr("Log going to sleep") }, colors = ButtonDefaults.buttonColors( containerColor = Palette.surfaceInset, contentColor = Palette.textPrimary, @@ -580,11 +580,11 @@ private fun SleepMarkCard(onMark: (SleepMarkType) -> Unit) { ) { Icon(Icons.Filled.Bedtime, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) - Text("Going to sleep", style = NoopType.subhead) + Text(tr("Going to sleep"), style = NoopType.subhead) } Button( onClick = { onMark(SleepMarkType.WAKE) }, - modifier = Modifier.weight(1f).semantics { contentDescription = "Log waking up" }, + modifier = Modifier.weight(1f).semantics { contentDescription = tr("Log waking up") }, colors = ButtonDefaults.buttonColors( containerColor = Palette.surfaceInset, contentColor = Palette.textPrimary, @@ -592,7 +592,7 @@ private fun SleepMarkCard(onMark: (SleepMarkType) -> Unit) { ) { Icon(Icons.Filled.WbSunny, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) - Text("I'm awake", style = NoopType.subhead) + Text(tr("I'm awake"), style = NoopType.subhead) } } } @@ -616,7 +616,7 @@ private fun SleepUndoBanner(session: SleepSession, onUndo: () -> Unit) { // re-detected), so the suppression promise would be false for it. Only a DETECTED delete tombstones, // so only it gets the "won't detect ... again" wording. Mirrors the macOS branch. (#65 banner honesty.) val message = if (session.userEdited) { - "Sleep deleted." + tr("Sleep deleted.") } else { "Sleep deleted. NOOP won't detect sleep between $startText and $endText again." } @@ -634,9 +634,9 @@ private fun SleepUndoBanner(session: SleepSession, onUndo: () -> Unit) { ) TextButton( onClick = onUndo, - modifier = Modifier.semantics { contentDescription = "Undo sleep deletion" }, + modifier = Modifier.semantics { contentDescription = tr("Undo sleep deletion") }, ) { - Text("Undo", style = NoopType.subhead, color = Palette.restColor) + Text(tr("Undo"), style = NoopType.subhead, color = Palette.restColor) } } } @@ -663,7 +663,7 @@ private val LIQUID_HERO_RADIUS: Dp = 26.dp @Composable private fun RestHero(score: Double?, asleepMin: Double?, source: String) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Sleep performance", overline = "Last night", trailing = "Rest") + SectionHeader(tr("Sleep performance"), overline = tr("Last night"), trailing = tr("Rest")) Box( modifier = Modifier .fillMaxWidth() @@ -707,7 +707,7 @@ private fun RestHero(score: Double?, asleepMin: Double?, source: String) { style = NoopType.number(46f), color = Palette.restBright, ) - Text("asleep last night", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("asleep last night"), style = NoopType.subhead, color = Palette.textSecondary) } } SourceBadge(text = source, tint = Palette.restColor) @@ -749,10 +749,10 @@ private fun SleepHeroVessel(fraction: Double, value: Double, tint: Color, diamet /** A short Rest state word for the hero gauge — same banding the synthesis hero uses. */ private fun sleepScoreWord(score: Double): String = when { - score < 50.0 -> "Poor" - score < 70.0 -> "Fair" - score < 85.0 -> "Good" - else -> "Optimal" + score < 50.0 -> tr("Poor") + score < 70.0 -> tr("Fair") + score < 85.0 -> tr("Good") + else -> tr("Optimal") } /** @@ -762,7 +762,7 @@ private fun sleepScoreWord(score: Double): String = when { */ private fun restHeroSource(imported: ImportedSleepSeries, days: List): String { val lastDay = days.lastOrNull()?.day - return if (lastDay != null && imported.performance[lastDay] != null) "Whoop" else "On-device" + return if (lastDay != null && imported.performance[lastDay] != null) tr("Whoop") else tr("On-device") } // MARK: - 1. HERO — stage breakdown for the navigated night @@ -802,7 +802,7 @@ private fun Hero( // substitute another night's hypnogram. (#160) NoopCard(tint = Palette.restColor) { Text( - "No stage data recorded for this night.", + tr("No stage data recorded for this night."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -814,7 +814,7 @@ private fun Hero( // EFFECTIVE onset so a hand-edited bedtime is reflected. (#160 / PR #395) val inBedMin = session?.let { (it.endTs - it.effectiveStartTs) / 60.0 } ?: s.total ChartCard( - title = "Stage breakdown", + title = tr("Stage breakdown"), subtitle = "${durationText(inBedMin)} in bed · ${display.efficiencyText} efficiency" + (if (display.realSegments != null) " · approx. stages (on-device)" else ""), trailing = durationText(s.asleep), @@ -845,15 +845,15 @@ private fun Hero( // group). Honest empty state when no fragment has persisted motion (older rows). MotionStrip(motionEpochs) Row(horizontalArrangement = Arrangement.spacedBy(Metrics.space16)) { - StageLegend("Deep", Palette.sleepDeep) - StageLegend("Light", Palette.sleepLight) - StageLegend("REM", Palette.sleepREM) - StageLegend("Awake", Palette.sleepAwake) + StageLegend(tr("Deep"), Palette.sleepDeep) + StageLegend(tr("Light"), Palette.sleepLight) + StageLegend(tr("REM"), Palette.sleepREM) + StageLegend(tr("Awake"), Palette.sleepAwake) } } } else { Text( - "No stage breakdown for this night.", + tr("No stage breakdown for this night."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -897,19 +897,19 @@ private fun NapsCard( val napMin = naps.sumOf { (it.endTs - it.effectiveStartTs) / 60.0 } NoopCard(padding = Metrics.space14, tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space12)) { - Text("DAYTIME SLEEP", style = NoopType.overline, color = Palette.textTertiary) - Text("Naps", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("DAYTIME SLEEP"), style = NoopType.overline, color = Palette.textTertiary) + Text(tr("Naps"), style = NoopType.subhead, color = Palette.textPrimary) if (naps.isNotEmpty()) { // Main / Nap(s) / Total split — only meaningful once a nap exists. Total = main + naps. Row(modifier = Modifier.fillMaxWidth()) { - NapSummaryCell("Main sleep", durationText(mainMin), Modifier.weight(1f)) - NapSummaryCell("Nap(s)", durationText(napMin), Modifier.weight(1f)) - NapSummaryCell("Total", durationText(mainMin + napMin), Modifier.weight(1f)) + NapSummaryCell(tr("Main sleep"), durationText(mainMin), Modifier.weight(1f)) + NapSummaryCell(tr("Nap(s)"), durationText(napMin), Modifier.weight(1f)) + NapSummaryCell(tr("Total"), durationText(mainMin + napMin), Modifier.weight(1f)) } } if (naps.isEmpty()) { Text( - "No naps recorded for this day.", + tr("No naps recorded for this day."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -959,7 +959,7 @@ private fun MainSleepFooter( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier .clickable { showWhy = !showWhy } - .semantics { contentDescription = "Why this is your main sleep" }, + .semantics { contentDescription = tr("Why this is your main sleep") }, ) { Icon( Icons.Filled.Info, @@ -967,12 +967,12 @@ private fun MainSleepFooter( tint = Palette.restColor, modifier = Modifier.size(16.dp), ) - Text("Why this sleep?", style = NoopType.footnote, color = Palette.restColor) + Text(tr("Why this sleep?"), style = NoopType.footnote, color = Palette.restColor) } } } if (showWhy && reason != null) { - Text("About your main sleep", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("About your main sleep"), style = NoopType.subhead, color = Palette.textPrimary) Text(reason, style = NoopType.footnote, color = Palette.textSecondary) } } @@ -996,13 +996,13 @@ internal fun mainSleepReasonText(blocks: List, habitualMidsleepSec val dur = durationText(sel.asleepSec / 60.0) return when (sel.reason) { SleepStageTotals.MainNightReason.onlyBlock -> - "This is your only sleep block today." + tr("This is your only sleep block today.") SleepStageTotals.MainNightReason.longest -> "Picked as your main sleep because it was your longest block ($dur)." SleepStageTotals.MainNightReason.longestNearUsual -> "Picked as your main sleep because it was your longest block ($dur), near your usual bedtime." SleepStageTotals.MainNightReason.alignedToUsual -> - "Picked as your main sleep because it started near your usual sleep time." + tr("Picked as your main sleep because it started near your usual sleep time.") } } @@ -1062,7 +1062,7 @@ private fun NapRow( IconButton(onClick = { showWhy = !showWhy }) { Icon( Icons.Filled.Info, - contentDescription = "Why this is logged as a nap", + contentDescription = tr("Why this is logged as a nap"), tint = Palette.restColor, modifier = Modifier.size(18.dp), ) @@ -1070,7 +1070,7 @@ private fun NapRow( IconButton(onClick = { editingStart = true }) { Icon( Icons.Filled.Edit, - contentDescription = if (nap.userEdited) "Edit nap times (edited)" else "Edit nap times", + contentDescription = if (nap.userEdited) tr("Edit nap times (edited)") else tr("Edit nap times"), tint = Palette.restColor, modifier = Modifier.size(18.dp), ) @@ -1078,16 +1078,16 @@ private fun NapRow( IconButton(onClick = { onDeleteNap(nap) }) { Icon( Icons.Filled.DeleteOutline, - contentDescription = "Delete this nap", + contentDescription = tr("Delete this nap"), tint = Palette.textTertiary, modifier = Modifier.size(18.dp), ) } } if (showWhy) { - Text("About this nap", style = NoopType.subhead, color = Palette.textPrimary) + Text(tr("About this nap"), style = NoopType.subhead, color = Palette.textPrimary) Text( - "Logged as a nap. Wrong? Tap Edit to adjust your sleep and wake times.", + tr("Logged as a nap. Wrong? Tap Edit to adjust your sleep and wake times."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1165,10 +1165,10 @@ private fun NapRow( @Composable private fun StageBreakdownRows(s: Stages) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space12)) { - StageBreakdownRow("REM", s.rem, s.total, Palette.sleepREM) - StageBreakdownRow("Deep", s.deep, s.total, Palette.sleepDeep) - StageBreakdownRow("Light", s.light, s.total, Palette.sleepLight) - StageBreakdownRow("Awake", s.awake, s.total, Palette.sleepAwake) + StageBreakdownRow(tr("REM"), s.rem, s.total, Palette.sleepREM) + StageBreakdownRow(tr("Deep"), s.deep, s.total, Palette.sleepDeep) + StageBreakdownRow(tr("Light"), s.light, s.total, Palette.sleepLight) + StageBreakdownRow(tr("Awake"), s.awake, s.total, Palette.sleepAwake) } } @@ -1352,7 +1352,7 @@ private fun HypnogramWithAxis( private fun MotionStrip(epochs: List) { if (epochs.size < 2) { Text( - "No movement detail for this night.", + tr("No movement detail for this night."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1429,7 +1429,7 @@ private fun SleepWindowRow(session: SleepSession) { tint = Palette.restColor, ) { Row(verticalAlignment = Alignment.CenterVertically) { - SleepTime(icon = Icons.Filled.Bedtime, label = "Asleep", value = asleep) + SleepTime(icon = Icons.Filled.Bedtime, label = tr("Asleep"), value = asleep) Spacer(Modifier.width(Metrics.space12)) Box( modifier = Modifier @@ -1438,7 +1438,7 @@ private fun SleepWindowRow(session: SleepSession) { .background(Palette.hairline), ) Spacer(Modifier.width(Metrics.space12)) - SleepTime(icon = Icons.Filled.WbSunny, label = "Woke", value = woke) + SleepTime(icon = Icons.Filled.WbSunny, label = tr("Woke"), value = woke) Spacer(Modifier.weight(1f)) } } @@ -1512,7 +1512,7 @@ private fun NightNavHeader( containerColor = Palette.surfaceRaised, titleContentColor = Palette.textPrimary, textContentColor = Palette.textSecondary, - title = { Text("Adjust sleep times", style = NoopType.headline) }, + title = { Text(tr("Adjust sleep times"), style = NoopType.headline) }, text = { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space6)) { Row( @@ -1525,7 +1525,7 @@ private fun NightNavHeader( verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { - Overline("Bedtime", color = Palette.textTertiary) + Overline(tr("Bedtime"), color = Palette.textTertiary) Spacer(Modifier.height(Metrics.space4)) Text(bedText, style = NoopType.headline, color = Palette.textPrimary) } @@ -1541,7 +1541,7 @@ private fun NightNavHeader( verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { - Overline("Wake-up", color = Palette.textTertiary) + Overline(tr("Wake-up"), color = Palette.textTertiary) Spacer(Modifier.height(Metrics.space4)) Text(wakeText, style = NoopType.headline, color = Palette.textPrimary) } @@ -1741,10 +1741,10 @@ private fun NightNavHeader( containerColor = Palette.surfaceRaised, titleContentColor = Palette.textPrimary, textContentColor = Palette.textSecondary, - title = { Text("Move this sleep?", style = NoopType.headline) }, + title = { Text(tr("Move this sleep?"), style = NoopType.headline) }, text = { Text( - "This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it.", + tr("This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it."), style = NoopType.subhead, ) }, @@ -1752,19 +1752,19 @@ private fun NightNavHeader( TextButton(onClick = { onUpdateTimes(session, pendingTimes.first, pendingTimes.second) pendingDisjointTimes = null - }) { Text("Move anyway", style = NoopType.subhead, color = Palette.statusWarning) } + }) { Text(tr("Move anyway"), style = NoopType.subhead, color = Palette.statusWarning) } }, dismissButton = { TextButton(onClick = { pendingDisjointTimes = null }) { - Text("Cancel", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.subhead, color = Palette.textSecondary) } }, ) } val nightLabel = when (offset) { - 0 -> "Last night" - 1 -> "1 night ago" + 0 -> tr("Last night") + 1 -> tr("1 night ago") else -> "$offset nights ago" } val blockShape = RoundedCornerShape(Metrics.cornerSm) @@ -1779,7 +1779,7 @@ private fun NightNavHeader( verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = { if (canGoOlder) onNavigate(offset + 1) }, enabled = canGoOlder) { - Icon(Icons.Filled.ChevronLeft, contentDescription = "Previous night", tint = if (canGoOlder) Palette.accent else Palette.textTertiary) + Icon(Icons.Filled.ChevronLeft, contentDescription = tr("Previous night"), tint = if (canGoOlder) Palette.accent else Palette.textTertiary) } Column( modifier = Modifier @@ -1789,7 +1789,7 @@ private fun NightNavHeader( // the gold pop lives only on the date text below. .background(Palette.surfaceInset) .border(Metrics.divider, Palette.hairline, blockShape) - .clickable(enabled = onPickNightDate != null, onClickLabel = "Pick night date") { showDatePicker = true } + .clickable(enabled = onPickNightDate != null, onClickLabel = tr("Pick night date")) { showDatePicker = true } .padding(vertical = Metrics.selectorPadding, horizontal = Metrics.selectorPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -1799,7 +1799,7 @@ private fun NightNavHeader( } } IconButton(onClick = { if (canGoNewer) onNavigate(offset - 1) }, enabled = canGoNewer) { - Icon(Icons.Filled.ChevronRight, contentDescription = "Next night", tint = if (canGoNewer) Palette.accent else Palette.textTertiary) + Icon(Icons.Filled.ChevronRight, contentDescription = tr("Next night"), tint = if (canGoNewer) Palette.accent else Palette.textTertiary) } } Row( @@ -1818,14 +1818,14 @@ private fun NightNavHeader( Spacer(Modifier.width(Metrics.space6)) Icon( Icons.Filled.Edit, - contentDescription = "Adjust sleep times", + contentDescription = tr("Adjust sleep times"), tint = Palette.textTertiary, modifier = Modifier.size(14.dp).clickable { showTimeChoice = true }, ) Spacer(Modifier.width(Metrics.space12)) Icon( Icons.Filled.DeleteOutline, - contentDescription = "Delete this sleep session", + contentDescription = tr("Delete this sleep session"), tint = Palette.textTertiary, modifier = Modifier.size(14.dp).clickable { showDeleteConfirm = true }, ) @@ -1834,7 +1834,7 @@ private fun NightNavHeader( Spacer(Modifier.width(Metrics.space12)) Icon( Icons.Filled.Add, - contentDescription = "Add a nap", + contentDescription = tr("Add a nap"), tint = Palette.textTertiary, modifier = Modifier.size(14.dp).clickable { addingNapStart = true }, ) @@ -1845,7 +1845,7 @@ private fun NightNavHeader( // appear once the strap has offloaded them (typically the next morning sync). (#614 follow-up) if (!canGoOlder) { Text( - "No earlier night stored yet. Earlier nights sync in the morning.", + tr("No earlier night stored yet. Earlier nights sync in the morning."), style = NoopType.footnote, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -1862,15 +1862,15 @@ private fun NightNavHeader( containerColor = Palette.surfaceRaised, titleContentColor = Palette.textPrimary, textContentColor = Palette.textSecondary, - title = { Text("Delete this sleep session?", style = NoopType.headline) }, + title = { Text(tr("Delete this sleep session?"), style = NoopType.headline) }, text = { // A detected night is tombstoned so it won't re-detect; a userEdited/nap row writes no // tombstone, so its copy drops that (false) promise. Mirrors the undo banner. (#65) Text( if (session.userEdited) { - "Removes this sleep and recomputes the day without it. You can undo for a few seconds after." + tr("Removes this sleep and recomputes the day without it. You can undo for a few seconds after.") } else { - "Removes this recorded sleep and recomputes the day without it. NOOP won't re-detect sleep in this window. You can undo for a few seconds after." + tr("Removes this recorded sleep and recomputes the day without it. NOOP won't re-detect sleep in this window. You can undo for a few seconds after.") }, style = NoopType.subhead, ) @@ -1880,12 +1880,12 @@ private fun NightNavHeader( showDeleteConfirm = false onDeleteSession(session) }) { - Text("Delete", style = NoopType.headline, color = Palette.statusCritical) + Text(tr("Delete"), style = NoopType.headline, color = Palette.statusCritical) } }, dismissButton = { TextButton(onClick = { showDeleteConfirm = false }) { - Text("Cancel", style = NoopType.subhead, color = Palette.textTertiary) + Text(tr("Cancel"), style = NoopType.subhead, color = Palette.textTertiary) } }, ) @@ -1916,7 +1916,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { val tiles = listOf<@Composable (Modifier) -> Unit>( { mod -> SparkTile( - mod, "Rest", + mod, tr("Rest"), value = pctValue(m.performance.latest), caption = vsTypical(m.performance.latest, m.performance.typical, "%"), accent = m.performance.latest?.let { Palette.recoveryColor(it) } ?: Palette.textPrimary, @@ -1926,7 +1926,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Efficiency", + mod, tr("Efficiency"), value = pctValue(m.efficiency.latest), caption = vsTypical(m.efficiency.latest, m.efficiency.typical, "%"), accent = Palette.statusPositive, @@ -1936,7 +1936,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Consistency", + mod, tr("Consistency"), value = pctValue(m.consistency.latest), caption = vsTypical(m.consistency.latest, m.consistency.typical, "%"), accent = m.consistency.latest?.let { Palette.recoveryColor(it) } ?: Palette.textPrimary, @@ -1946,7 +1946,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Hours vs Needed", + mod, tr("Hours vs Needed"), value = pctValue(m.hoursVsNeeded.latest), caption = vsTypical(m.hoursVsNeeded.latest, m.hoursVsNeeded.typical, "%"), accent = m.hoursVsNeeded.latest?.let { Palette.recoveryColor(minOf(100.0, it)) } ?: Palette.textPrimary, @@ -1956,7 +1956,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Restorative", + mod, tr("Restorative"), value = pctValue(m.restorative.latest), caption = vsTypical(m.restorative.latest, m.restorative.typical, "%"), accent = Palette.sleepREM, @@ -1966,7 +1966,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Respiratory", + mod, tr("Respiratory"), value = m.respiratory.latest?.let { String.format(Locale.US, "%.1f", it) } ?: "—", caption = vsTypical(m.respiratory.latest, m.respiratory.typical, " rpm", decimals = 1), accent = Palette.metricPurple, @@ -1976,7 +1976,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { }, { mod -> SparkTile( - mod, "Sleep Debt", + mod, tr("Sleep Debt"), value = m.sleepDebt.latest?.let { durationText(it) } ?: "—", caption = debtCaption(m.sleepDebt.latest), accent = debtColor(m.sleepDebt.latest), @@ -1987,7 +1987,7 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { ) Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Night detail", overline = "Metrics", trailing = "vs typical") + SectionHeader(tr("Night detail"), overline = tr("Metrics"), trailing = tr("vs typical")) // Two-up rows keep every tile the same fixed height with no empty cells. tiles.chunked(2).forEach { rowTiles -> Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { @@ -2010,11 +2010,11 @@ private fun MetricGrid(m: SleepModel, onMetricClick: (String) -> Unit = {}) { @Composable internal fun SleepDebtLedgerCard(ledger: SleepDebtLedger) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Sleep-debt ledger", overline = "Last 14 nights", trailing = "running balance") + SectionHeader(tr("Sleep-debt ledger"), overline = tr("Last 14 nights"), trailing = tr("running balance")) NoopCard(padding = Metrics.cardPadding, tint = Palette.restColor) { if (ledger.nightCount == 0) { Text( - "No nights with sleep data yet. Your ledger fills in as you wear the strap to bed.", + tr("No nights with sleep data yet. Your ledger fills in as you wear the strap to bed."), style = NoopType.subhead, color = Palette.textTertiary, ) @@ -2047,9 +2047,9 @@ internal fun SleepDebtLedgerCard(ledger: SleepDebtLedger) { Hairline() ChartFooter( listOf( - "Balance" to debtSigned(ledger.balanceMin), - "Per-night need" to durationText(ledger.needMin), - "Nights" to "${ledger.nightCount}", + tr("Balance") to debtSigned(ledger.balanceMin), + tr("Per-night need") to durationText(ledger.needMin), + tr("Nights") to "${ledger.nightCount}", ), ) } @@ -2112,14 +2112,14 @@ private fun DebtDeltaBars(ledger: SleepDebtLedger) { private fun StagesVsTypical(m: SleepModel) { val s = m.stages Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Stages vs typical", overline = "Selected night", trailing = "marker = your mean") + SectionHeader(tr("Stages vs typical"), overline = tr("Selected night"), trailing = tr("marker = your mean")) NoopCard(tint = Palette.restColor) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space14)) { - StageRow("Deep", last = s.deep, typical = m.typicalDeepMin, color = Palette.sleepDeep) + StageRow(tr("Deep"), last = s.deep, typical = m.typicalDeepMin, color = Palette.sleepDeep) Hairline() - StageRow("REM", last = s.rem, typical = m.typicalRemMin, color = Palette.sleepREM) + StageRow(tr("REM"), last = s.rem, typical = m.typicalRemMin, color = Palette.sleepREM) Hairline() - StageRow("Light", last = s.light, typical = m.typicalLightMin, color = Palette.sleepLight) + StageRow(tr("Light"), last = s.light, typical = m.typicalLightMin, color = Palette.sleepLight) } } } @@ -2205,19 +2205,19 @@ private fun DurationTrend(m: SleepModel) { val pts = m.trendHours val avg = pts.averageOrNull() Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Trend", overline = "Sleep", trailing = "Last 14 days") + SectionHeader(tr("Trend"), overline = tr("Sleep"), trailing = tr("Last 14 days")) ChartCard( - title = "Hours asleep", - subtitle = "Per night, trailing 14 days", + title = tr("Hours asleep"), + subtitle = tr("Per night, trailing 14 days"), trailing = avg?.let { String.format(Locale.US, "%.1f h avg", it) }, tint = Palette.restColor, footer = { ChartFooter( listOf( - "Avg" to (avg?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), - "Min" to (pts.minOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), - "Max" to (pts.maxOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), - "Nights" to "${pts.size}", + tr("Avg") to (avg?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), + tr("Min") to (pts.minOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), + tr("Max") to (pts.maxOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), + tr("Nights") to "${pts.size}", ), ) }, @@ -2227,7 +2227,7 @@ private fun DurationTrend(m: SleepModel) { LineChart( values = pts, modifier = Modifier.fillMaxWidth().height(Metrics.compactChartHeight) - .semantics { contentDescription = "Sleep hours trend chart" }, + .semantics { contentDescription = tr("Sleep hours trend chart") }, color = Palette.restColor, fill = true, selectionEnabled = true, @@ -2240,16 +2240,16 @@ private fun DurationTrend(m: SleepModel) { } ChartCard( - title = "Sleep Debt", - subtitle = "Hours of sleep debt per day", + title = tr("Sleep Debt"), + subtitle = tr("Hours of sleep debt per day"), trailing = m.trendDebtHours.lastOrNull()?.let { String.format(Locale.US, "%.1f h", it) }, tint = Palette.restColor, footer = { ChartFooter( listOf( - "Avg" to (m.trendDebtHours.averageOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), - "Max" to (m.trendDebtHours.maxOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), - "Days" to "${m.trendDebtHours.size}", + tr("Avg") to (m.trendDebtHours.averageOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), + tr("Max") to (m.trendDebtHours.maxOrNull()?.let { String.format(Locale.US, "%.1f h", it) } ?: "—"), + tr("Days") to "${m.trendDebtHours.size}", ), ) }, @@ -2259,7 +2259,7 @@ private fun DurationTrend(m: SleepModel) { BarChart( values = m.trendDebtHours, modifier = Modifier.fillMaxWidth().height(Metrics.compactChartHeight) - .semantics { contentDescription = "Sleep debt trend chart" }, + .semantics { contentDescription = tr("Sleep debt trend chart") }, color = Palette.metricRose, selectionEnabled = true, ) @@ -2278,7 +2278,7 @@ private fun TrendPlaceholder() { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - InsetChartPlaceholder(message = "Not enough nights yet.") + InsetChartPlaceholder(message = tr("Not enough nights yet.")) } } @@ -2444,9 +2444,9 @@ private fun SparkTile( @Composable private fun SleepEmptyState() { DataPendingNote( - title = "No nights here yet", - body = "No nights here yet. Import your WHOOP export in Data Sources to see " + - "every night, your sleep stages and trends straight away.", + title = tr("No nights here yet"), + body = tr("No nights here yet. Import your WHOOP export in Data Sources to see ") + + tr("every night, your sleep stages and trends straight away."), ) } @@ -3031,7 +3031,7 @@ private fun pctValue(v: Double?): String = v?.let { "${it.roundToInt()}%" } ?: " /** "+12% vs typical" / "−0.4 rpm vs typical" — the latest-vs-mean caption every tile carries. */ private fun vsTypical(latest: Double?, typical: Double?, suffix: String, decimals: Int = 0): String { - if (latest == null || typical == null || typical == 0.0) return "vs typical - " + if (latest == null || typical == null || typical == 0.0) return tr("vs typical - ") val diff = latest - typical val sign = if (diff >= 0) "+" else "−" val mag = abs(diff) @@ -3040,8 +3040,8 @@ private fun vsTypical(latest: Double?, typical: Double?, suffix: String, decimal } private fun debtCaption(debt: Double?): String { - if (debt == null) return "vs need" - return if (debt < 15.0) "On target" else "Below need" + if (debt == null) return tr("vs need") + return if (debt < 15.0) tr("On target") else tr("Below need") } private fun debtColor(debt: Double?): Color = when { @@ -3058,14 +3058,14 @@ private fun debtColor(debt: Double?): Color = when { * "On target" inside the deadband so a few stray minutes don't show as debt. */ private fun debtHeadline(ledger: SleepDebtLedger): String = - if (ledger.magnitudeMin < SleepDebt.ON_TARGET_BAND_MIN) "On target" + if (ledger.magnitudeMin < SleepDebt.ON_TARGET_BAND_MIN) tr("On target") else "≈${durationText(ledger.magnitudeMin)}" /** Short tag beside the headline: sleep debt / surplus / balanced. */ private fun debtTag(ledger: SleepDebtLedger): String = when { - ledger.magnitudeMin < SleepDebt.ON_TARGET_BAND_MIN -> "balanced" - ledger.isDebt -> "sleep debt" - else -> "surplus" + ledger.magnitudeMin < SleepDebt.ON_TARGET_BAND_MIN -> tr("balanced") + ledger.isDebt -> tr("sleep debt") + else -> tr("surplus") } /** Plain-English read of the running balance over the window. */ @@ -3209,8 +3209,8 @@ internal fun HoursVsNeededCard(m: SleepModel) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space14)) { Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Overline("Sleep") - Text("Hours vs Needed", style = NoopType.headline, color = Palette.textPrimary) + Overline(tr("Sleep")) + Text(tr("Hours vs Needed"), style = NoopType.headline, color = Palette.textPrimary) } Text(trendArrow, style = NoopType.title2, color = arrowColor) Spacer(Modifier.width(Metrics.space6)) @@ -3246,17 +3246,17 @@ internal fun HoursVsNeededCard(m: SleepModel) { if (debtRepay > 0) Box(modifier = Modifier.weight((debtRepay / totalBar).toFloat()).fillMaxHeight().background(Palette.statusCritical)) } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.space14)) { - LegendDot("Healthy Min", Palette.metricPurple) - LegendDot("Strain", Palette.strain066) - LegendDot("Debt", Palette.statusCritical) + LegendDot(tr("Healthy Min"), Palette.metricPurple) + LegendDot(tr("Strain"), Palette.strain066) + LegendDot(tr("Debt"), Palette.statusCritical) } Hairline() Row(modifier = Modifier.fillMaxWidth()) { listOf( - "Slept" to String.format(Locale.US, "%.1f h", sleptH), - "Needed" to String.format(Locale.US, "%.1f h", neededH), - "Debt" to if (debtH > 0.05) String.format(Locale.US, "%.1f h", debtH) else "None", + tr("Slept") to String.format(Locale.US, "%.1f h", sleptH), + tr("Needed") to String.format(Locale.US, "%.1f h", neededH), + tr("Debt") to if (debtH > 0.05) String.format(Locale.US, "%.1f h", debtH) else tr("None"), ).forEach { (lbl, v) -> Column(modifier = Modifier.weight(1f)) { Overline(lbl, color = Palette.textTertiary) @@ -3334,9 +3334,9 @@ internal fun SleepConsistencyCard(sleeps: List) { // Header: title + trend-score. Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Overline("Schedule") - Text("Bedtime & wake time", style = NoopType.headline, color = Palette.textPrimary) - Text("Sleep window over recent nights", style = NoopType.footnote, color = Palette.textSecondary) + Overline(tr("Schedule")) + Text(tr("Bedtime & wake time"), style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Sleep window over recent nights"), style = NoopType.footnote, color = Palette.textSecondary) } Text("${consistencyPct.roundToInt()}%", style = NoopType.chartValue, color = Palette.restColor) } @@ -3353,7 +3353,7 @@ internal fun SleepConsistencyCard(sleeps: List) { .fillMaxWidth() .height(160.dp) .clip(RoundedCornerShape(Metrics.cornerSm)) - .semantics { contentDescription = "Sleep consistency nightly bed and wake chart" } + .semantics { contentDescription = tr("Sleep consistency nightly bed and wake chart") } .drawBehind { val yAxisW = 52f val chartW = size.width - yAxisW @@ -3433,9 +3433,9 @@ internal fun SleepConsistencyCard(sleeps: List) { Hairline() Row(modifier = Modifier.fillMaxWidth()) { listOf( - "Score" to "${consistencyPct.roundToInt()}%", - "Typical" to "${((bedSdH + wakeSdH) / 2f * 60f).roundToInt()} min SD", - "Nights" to "${recent.size}", + tr("Score") to "${consistencyPct.roundToInt()}%", + tr("Typical") to "${((bedSdH + wakeSdH) / 2f * 60f).roundToInt()} min SD", + tr("Nights") to "${recent.size}", ).forEach { (lbl, v) -> Column(modifier = Modifier.weight(1f)) { Overline(lbl, color = Palette.textTertiary) @@ -3462,13 +3462,13 @@ private data class SleepMetricSpec( ) private fun sleepMetricSpec(key: String): SleepMetricSpec = when (key) { - "performance" -> SleepMetricSpec("Rest", "%", Palette.restColor) { "${it.roundToInt()}" } - "efficiency" -> SleepMetricSpec("Sleep Efficiency", "%", Palette.statusPositive) { "${it.roundToInt()}" } - "consistency" -> SleepMetricSpec("Consistency", "%", Palette.metricCyan) { "${it.roundToInt()}" } - "hours_vs_needed" -> SleepMetricSpec("Hours vs Needed", "%", Palette.restColor) { "${it.roundToInt()}" } - "restorative" -> SleepMetricSpec("Restorative", "%", Palette.sleepREM) { "${it.roundToInt()}" } - "respiratory" -> SleepMetricSpec("Respiratory Rate", "rpm", Palette.metricPurple) { String.format(Locale.US, "%.1f", it) } - "sleep_debt" -> SleepMetricSpec("Sleep Debt", "h", Palette.metricRose) { String.format(Locale.US, "%.1f", it) } + "performance" -> SleepMetricSpec(tr("Rest"), "%", Palette.restColor) { "${it.roundToInt()}" } + "efficiency" -> SleepMetricSpec(tr("Sleep Efficiency"), "%", Palette.statusPositive) { "${it.roundToInt()}" } + "consistency" -> SleepMetricSpec(tr("Consistency"), "%", Palette.metricCyan) { "${it.roundToInt()}" } + "hours_vs_needed" -> SleepMetricSpec(tr("Hours vs Needed"), "%", Palette.restColor) { "${it.roundToInt()}" } + "restorative" -> SleepMetricSpec(tr("Restorative"), "%", Palette.sleepREM) { "${it.roundToInt()}" } + "respiratory" -> SleepMetricSpec(tr("Respiratory Rate"), "rpm", Palette.metricPurple) { String.format(Locale.US, "%.1f", it) } + "sleep_debt" -> SleepMetricSpec(tr("Sleep Debt"), "h", Palette.metricRose) { String.format(Locale.US, "%.1f", it) } else -> SleepMetricSpec(key, "", Palette.accent) { "${it.roundToInt()}" } } @@ -3534,16 +3534,16 @@ private fun SleepMetricDetailSheetContent(vm: AppViewModel, key: String) { verticalArrangement = Arrangement.spacedBy(Metrics.space16), ) { if (allPoints.size < 2) { - Text("Not enough history yet", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Not enough history yet"), style = NoopType.headline, color = Palette.textPrimary) Text( - "This metric needs at least two nights of data.", + tr("This metric needs at least two nights of data."), style = NoopType.subhead, color = Palette.textSecondary, ) Spacer(Modifier.height(Metrics.space16)) } else if (filteredPoints.size < 2) { Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Overline("Sleep") + Overline(tr("Sleep")) Text(spec.title, style = NoopType.title2, color = Palette.textPrimary) } } @@ -3553,7 +3553,7 @@ private fun SleepMetricDetailSheetContent(vm: AppViewModel, key: String) { label = { it.label }, onSelect = { range = it }, ) - Text("Not enough history in this range. Try 3M, 6M, or ALL.", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("Not enough history in this range. Try 3M, 6M, or ALL."), style = NoopType.subhead, color = Palette.textSecondary) Spacer(Modifier.height(Metrics.space16)) } else { val values = filteredPoints.map { it.second } @@ -3613,7 +3613,7 @@ private fun SleepMetricDetailSheetContent(vm: AppViewModel, key: String) { } Hairline() Row(modifier = Modifier.fillMaxWidth()) { - listOf("Min" to minV, "Avg" to avgV, "Max" to maxV).forEach { (lbl, v) -> + listOf(tr("Min") to minV, tr("Avg") to avgV, tr("Max") to maxV).forEach { (lbl, v) -> Column(modifier = Modifier.weight(1f)) { Overline(lbl, color = Palette.textTertiary) Text( diff --git a/android/app/src/main/java/com/noop/ui/SmartAlarmScreen.kt b/android/app/src/main/java/com/noop/ui/SmartAlarmScreen.kt index e0ecccb02..206cbf457 100644 --- a/android/app/src/main/java/com/noop/ui/SmartAlarmScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SmartAlarmScreen.kt @@ -77,8 +77,8 @@ fun SmartAlarmScreen(vm: AppViewModel) { LazyScreenScaffold( // #766: "Alarms" because this screen now holds the phone Wake Window, the strap's firmware // wake-alarm (moved here from Automations), and the wind-down reminder, so the broader title fits. - title = "Alarms", - subtitle = "Your wake window, the strap wake-alarm, and the evening wind-down reminder, in one place.", + title = tr("Alarms"), + subtitle = tr("Your wake window, the strap wake-alarm, and the evening wind-down reminder, in one place."), ) { // The guaranteed-wake card always shows so the safety promise is the first thing read. item { WindowCard(enabled = enabled, targetMinutes = targetMinutes, windowMinutes = windowMinutes) } @@ -86,7 +86,7 @@ fun SmartAlarmScreen(vm: AppViewModel) { item { AlarmSettingsCard { ToggleRowLocal( - label = "Wake me with a smart alarm", + label = tr("Wake me with a smart alarm"), help = "A guaranteed OS alarm is set for the end of your window; the strap stream can move it earlier if you're sleeping lightly.", checked = enabled, onChange = { want -> @@ -106,8 +106,8 @@ fun SmartAlarmScreen(vm: AppViewModel) { if (enabled && !canSchedule) { RowDividerLocal() Text( - "NOOP doesn't have permission to set exact alarms, so your wake isn't guaranteed. " + - "Tap to allow it in system settings.", + tr("NOOP doesn't have permission to set exact alarms, so your wake isn't guaranteed. ") + + tr("Tap to allow it in system settings."), style = NoopType.footnote, color = Palette.statusWarning, modifier = Modifier @@ -123,8 +123,8 @@ fun SmartAlarmScreen(vm: AppViewModel) { RowDividerLocal() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Wake me no earlier than", style = NoopType.body, color = Palette.textPrimary) - Text("The earliest NOOP will wake you.", style = NoopType.footnote, color = Palette.textTertiary) + Text(tr("Wake me no earlier than"), style = NoopType.body, color = Palette.textPrimary) + Text(tr("The earliest NOOP will wake you."), style = NoopType.footnote, color = Palette.textTertiary) } Spacer(Modifier.width(16.dp)) TimeChip( @@ -137,9 +137,9 @@ fun SmartAlarmScreen(vm: AppViewModel) { RowDividerLocal() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Window length", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Window length"), style = NoopType.body, color = Palette.textPrimary) Text( - "The guaranteed alarm fires this long after your earliest time.", + tr("The guaranteed alarm fires this long after your earliest time."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -160,7 +160,7 @@ fun SmartAlarmScreen(vm: AppViewModel) { help = if (bonded) "Also arms your $strapName to buzz at your earliest wake time, so the strap wakes you first and the phone alarm is the guaranteed backup." else - "Connect your strap to use this. It arms the strap to buzz at your earliest wake time as a gentler first wake-up.", + tr("Connect your strap to use this. It arms the strap to buzz at your earliest wake time as a gentler first wake-up."), checked = buzzWhoop4, onChange = { vm.setBuzzWhoop4Enabled(it) }, ) @@ -204,18 +204,18 @@ private fun StrapAlarmCard(vm: AppViewModel) { NoopCard(padding = 20.dp, tint = if (smartAlarm) Palette.accent else null) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Morning") + Overline(tr("Morning")) Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Alarm, contentDescription = null, tint = Palette.accent) Spacer(Modifier.width(10.dp)) - Text("Strap wake-alarm", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Strap wake-alarm"), style = NoopType.title2, color = Palette.textPrimary) } } // Truth-sync (#535): the WHOOP 4.0 alarm payload was captured from the official app and // confirmed buzzing on a real 4.0 by the capture author, so the copy no longer calls the // 4.0 path experimental. The 5/MG Experimental-gate branch below is deliberately untouched. ToggleRowLocal( - label = "Wake me with a strap buzz", + label = tr("Wake me with a strap buzz"), help = "Arms the strap to buzz at your wake time, even if NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.", checked = smartAlarm, onChange = { vm.setSmartAlarmEnabled(it) }, @@ -223,7 +223,7 @@ private fun StrapAlarmCard(vm: AppViewModel) { if (smartAlarm) { RowDividerLocal() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("Wake at", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Wake at"), style = NoopType.body, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) TimeChip( minutes = alarmMinutes, @@ -247,8 +247,8 @@ private fun StrapAlarmCard(vm: AppViewModel) { RowDividerLocal() if (live.whoop5Detected && !experimentalOn) { Text( - "Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings → " + - "Experimental). Right now your wake time is saved but the strap is NOT armed.", + tr("Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings → ") + + tr("Experimental). Right now your wake time is saved but the strap is NOT armed."), style = NoopType.footnote, color = Palette.statusWarning, ) } else if (live.whoop5Detected) { @@ -257,9 +257,9 @@ private fun StrapAlarmCard(vm: AppViewModel) { // NOT show here (#864 honesty). Byte-identical wording to the Swift SmartAlarmView twin. Text( if (live.bonded) - "Armed on the strap itself with the experimental 5/MG command. A strap-driven wake is still unconfirmed on 5/MG on our side (confirmed only on WHOOP 4.0), so keep a backup alarm for anything you truly can't miss." + tr("Armed on the strap itself with the experimental 5/MG command. A strap-driven wake is still unconfirmed on 5/MG on our side (confirmed only on WHOOP 4.0), so keep a backup alarm for anything you truly can't miss.") else - "Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss.", + tr("Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss."), style = NoopType.footnote, color = Palette.textTertiary, ) } else { @@ -269,7 +269,7 @@ private fun StrapAlarmCard(vm: AppViewModel) { // wording to the Swift SmartAlarmView. "Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss." else - "Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss.", + tr("Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -298,7 +298,7 @@ private fun WindowCard(enabled: Boolean, targetMinutes: Int, windowMinutes: Int) Icon(Icons.Filled.Shield, contentDescription = null, tint = DomainTheme.Rest.color) Spacer(Modifier.width(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Overline("Guaranteed wake") + Overline(tr("Guaranteed wake")) if (enabled) { Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text(hhmm(targetMinutes), style = NoopType.number(28f), color = DomainTheme.Rest.color) @@ -310,9 +310,9 @@ private fun WindowCard(enabled: Boolean, targetMinutes: Int, windowMinutes: Int) style = NoopType.footnote, color = Palette.textSecondary, ) } else { - Text("Off", style = NoopType.title2, color = Palette.textSecondary) + Text(tr("Off"), style = NoopType.title2, color = Palette.textSecondary) Text( - "Turn on the smart alarm to wake inside a window you choose.", + tr("Turn on the smart alarm to wake inside a window you choose."), style = NoopType.footnote, color = Palette.textTertiary, ) } @@ -328,7 +328,7 @@ private fun AlarmSettingsCard(content: @Composable () -> Unit) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Alarm, contentDescription = null, tint = Palette.accent) Spacer(Modifier.width(10.dp)) - Text("Wake alarm", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Wake alarm"), style = NoopType.headline, color = Palette.textPrimary) } content() } @@ -342,15 +342,15 @@ private fun WindDownCard(vm: AppViewModel) { NoopCard(padding = 20.dp, tint = if (enabled) DomainTheme.Rest.color else null) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Evening") + Overline(tr("Evening")) Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Bedtime, contentDescription = null, tint = DomainTheme.Rest.color) Spacer(Modifier.width(10.dp)) - Text("Wind-down nudge", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Wind-down nudge"), style = NoopType.title2, color = Palette.textPrimary) } } ToggleRowLocal( - label = "Remind me to wind down", + label = tr("Remind me to wind down"), help = "A gentle evening notification, timed from your wake time and usual sleep need, so you can settle in time. It's a suggestion, not an alarm.", checked = enabled, onChange = { vm.setWindDownEnabled(it) }, @@ -366,18 +366,18 @@ private fun ExplanationCard() { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Bedtime, contentDescription = null, tint = Palette.accent) Spacer(Modifier.width(10.dp)) - Text("How the smart wake works", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("How the smart wake works"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "While you're inside the window, NOOP watches your live heart rate from the strap. Deep " + - "sleep sits near your nightly low and stays steady; when your heart rate lifts above " + + tr("While you're inside the window, NOOP watches your live heart rate from the strap. Deep ") + + tr("sleep sits near your nightly low and stays steady; when your heart rate lifts above ") + "that (a sign you're sleeping more lightly or starting to stir), NOOP wakes you a " + "little early so you come up from a lighter phase.", style = NoopType.footnote, color = Palette.textSecondary, ) Text( - "This is a coarse cue from heart rate, not a clinical sleep-stage reading. If the strap " + - "isn't streaming (Bluetooth off, not worn, app killed), no early wake happens and the " + + tr("This is a coarse cue from heart rate, not a clinical sleep-stage reading. If the strap ") + + tr("isn't streaming (Bluetooth off, not worn, app killed), no early wake happens and the ") + "guaranteed alarm at the window's end still wakes you.", style = NoopType.footnote, color = Palette.textTertiary, ) @@ -393,9 +393,9 @@ private fun WindowStepper(windowMinutes: Int, onChange: (Int) -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - StepperButton(symbol = "−", onClick = { onChange((windowMinutes - 5).coerceAtLeast(5)) }, label = "Shorten window") + StepperButton(symbol = "−", onClick = { onChange((windowMinutes - 5).coerceAtLeast(5)) }, label = tr("Shorten window")) Text("$windowMinutes min", style = NoopType.bodyNumber, color = Palette.textPrimary) - StepperButton(symbol = "+", onClick = { onChange((windowMinutes + 5).coerceAtMost(60)) }, label = "Lengthen window") + StepperButton(symbol = "+", onClick = { onChange((windowMinutes + 5).coerceAtMost(60)) }, label = tr("Lengthen window")) } } diff --git a/android/app/src/main/java/com/noop/ui/StepsCalibrationScreen.kt b/android/app/src/main/java/com/noop/ui/StepsCalibrationScreen.kt index 77d08940c..9f62aa30d 100644 --- a/android/app/src/main/java/com/noop/ui/StepsCalibrationScreen.kt +++ b/android/app/src/main/java/com/noop/ui/StepsCalibrationScreen.kt @@ -64,9 +64,9 @@ object StepsCalibrationFormat { /** A 0–1 confidence as Low / Medium / High. Thirds: < 0.34 Low, < 0.67 Medium, else High. A manual * coefficient is confidence 1.0 → "High". */ fun confidenceLabel(confidence: Double): String = when { - confidence < 0.34 -> "Low" - confidence < 0.67 -> "Medium" - else -> "High" + confidence < 0.34 -> tr("Low") + confidence < 0.67 -> tr("Medium") + else -> tr("High") } } @@ -201,12 +201,12 @@ private fun Header(onClose: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Overline("Steps estimate", color = Palette.textTertiary) - Text("Calibrate your steps", style = NoopType.display(26f), color = Palette.textPrimary) - Text("WHOOP 4.0 · motion → steps", style = NoopType.caption, color = Palette.textSecondary) + Overline(tr("Steps estimate"), color = Palette.textTertiary) + Text(tr("Calibrate your steps"), style = NoopType.display(26f), color = Palette.textPrimary) + Text(tr("WHOOP 4.0 · motion → steps"), style = NoopType.caption, color = Palette.textSecondary) } IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) { - Icon(Icons.Filled.Close, contentDescription = "Close", tint = Palette.textTertiary, modifier = Modifier.size(22.dp)) + Icon(Icons.Filled.Close, contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(22.dp)) } } } @@ -218,7 +218,7 @@ private fun Footer(onClose: () -> Unit) { onClick = onClose, colors = ButtonDefaults.buttonColors(containerColor = Palette.accent, contentColor = Palette.surfaceBase), ) { - Text("Done", modifier = Modifier.padding(horizontal = 24.dp)) + Text(tr("Done"), modifier = Modifier.padding(horizontal = 24.dp)) } } } @@ -242,18 +242,18 @@ private fun ExplainerCard() { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.DirectionsWalk, contentDescription = null, tint = Palette.accent, modifier = Modifier.size(20.dp)) - Text("How this works", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("How this works"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step " + - "count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.", + tr("NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step ") + + tr("count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps."), style = NoopType.subhead, color = Palette.textSecondary, ) Text( - "On the days your phone also counted steps, NOOP learns how much your motion maps to " + - "steps, then applies that to the strap-only days. The more matching days it has, the " + - "more it trusts the estimate.", + tr("On the days your phone also counted steps, NOOP learns how much your motion maps to ") + + tr("steps, then applies that to the strap-only days. The more matching days it has, the ") + + tr("more it trusts the estimate."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -270,19 +270,19 @@ private fun NoMotionNote() { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.SyncProblem, contentDescription = null, tint = Palette.metricAmber, modifier = Modifier.size(20.dp)) - Text("No motion synced yet", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("No motion synced yet"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's " + - "banked motion history, so your strap needs to sync that history before NOOP has " + - "anything to count.", + tr("We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's ") + + tr("banked motion history, so your strap needs to sync that history before NOOP has ") + + tr("anything to count."), style = NoopType.subhead, color = Palette.textSecondary, ) Text( - "Open NOOP near your strap and let it catch up (a full history sync can take a while on " + - "first run). Once a day or two of motion lands, your step estimate and the calibration " + - "below will start to fill in.", + tr("Open NOOP near your strap and let it catch up (a full history sync can take a while on ") + + tr("first run). Once a day or two of motion lands, your step estimate and the calibration ") + + tr("below will start to fill in."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -296,7 +296,7 @@ private fun NoMotionNote() { private fun CurrentFitCard(profile: ProfileStore, matchedDays: Int) { NoopCard(padding = 20.dp, tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Current calibration") + Overline(tr("Current calibration")) if (profile.stepsCalibrationCoefficient > 0 || profile.stepsManualCoefficient > 0) { val coeff = if (profile.stepsManualCoefficient > 0) { profile.stepsManualCoefficient @@ -305,21 +305,21 @@ private fun CurrentFitCard(profile: ProfileStore, matchedDays: Int) { } Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text(String.format(Locale.US, "%.1f", coeff), style = NoopType.number(30f), color = Palette.accent) - Text("steps per motion unit", style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(bottom = 4.dp)) + Text(tr("steps per motion unit"), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(bottom = 4.dp)) } if (profile.stepsManualCoefficient > 0) { - StatLine("Source", "Manual (you set this by hand)") + StatLine(tr("Source"), tr("Manual (you set this by hand)")) } else { val days = profile.stepsCalibrationSampleDays - StatLine("Fitted from", "$days day${if (days == 1) "" else "s"} your phone also counted") + StatLine(tr("Fitted from"), "$days day${if (days == 1) "" else "s"} your phone also counted") StatLine( - "Confidence", + tr("Confidence"), "${StepsCalibrationFormat.confidenceLabel(profile.stepsCalibrationConfidence)} · " + "${(profile.stepsCalibrationConfidence * 100).roundToInt()}%", ) } } else { - Text("Not calibrated yet", style = NoopType.bodyNumber, color = Palette.textPrimary) + Text(tr("Not calibrated yet"), style = NoopType.bodyNumber, color = Palette.textPrimary) // #589: a concrete countdown instead of a vague "a few days". Headline comes straight from // the engine's NeedsMoreDays state so the wording matches the Today steps tile + the Swift card. Text( @@ -330,8 +330,8 @@ private fun CurrentFitCard(profile: ProfileStore, matchedDays: Int) { color = Palette.accent, ) Text( - "These are the days where your phone also counted steps, so NOOP can learn how your " + - "motion maps to steps. Or set the coefficient manually below.", + tr("These are the days where your phone also counted steps, so NOOP can learn how your ") + + tr("motion maps to steps. Or set the coefficient manually below."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -346,20 +346,20 @@ private fun CurrentFitCard(profile: ProfileStore, matchedDays: Int) { private fun ComparisonCard(rows: List) { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Estimated vs your phone") + Overline(tr("Estimated vs your phone")) if (rows.isEmpty()) { Text( - "No days yet where both NOOP and your phone counted steps. Once your phone logs a " + - "few days alongside the strap, they'll appear here so you can see how close the " + - "estimate is.", + tr("No days yet where both NOOP and your phone counted steps. Once your phone logs a ") + + tr("few days alongside the strap, they'll appear here so you can see how close the ") + + tr("estimate is."), style = NoopType.footnote, color = Palette.textTertiary, ) } else { Row(modifier = Modifier.fillMaxWidth()) { - Text("Day", style = NoopType.caption, color = Palette.textTertiary, modifier = Modifier.weight(1f)) - Text("Est.", style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.End, modifier = Modifier.width(64.dp)) - Text("Phone", style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.End, modifier = Modifier.width(64.dp)) + Text(tr("Day"), style = NoopType.caption, color = Palette.textTertiary, modifier = Modifier.weight(1f)) + Text(tr("Est."), style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.End, modifier = Modifier.width(64.dp)) + Text(tr("Phone"), style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.End, modifier = Modifier.width(64.dp)) Text("Δ", style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.End, modifier = Modifier.width(52.dp)) } for (row in rows) { @@ -384,8 +384,8 @@ private fun ComparisonCard(rows: List) { } } Text( - "These days are excluded from the estimate (your phone's real count is shown instead). " + - "They're here only so you can judge the estimate's accuracy.", + tr("These days are excluded from the estimate (your phone's real count is shown instead). ") + + tr("They're here only so you can judge the estimate's accuracy."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -407,29 +407,29 @@ private fun ManualAdjustCard( ) { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Adjust manually") + Overline(tr("Adjust manually")) Text( - "Override the automatic fit with your own steps-per-motion value. Useful if your phone " + - "has no step history to learn from, or the estimate runs consistently high or low. " + - "Set it back to auto by dragging to the far left.", + tr("Override the automatic fit with your own steps-per-motion value. Useful if your phone ") + + tr("has no step history to learn from, or the estimate runs consistently high or low. ") + + tr("Set it back to auto by dragging to the far left."), style = NoopType.footnote, color = Palette.textTertiary, ) Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text( - if (draftManual > 0) String.format(Locale.US, "%.1f", draftManual) else "Auto", + if (draftManual > 0) String.format(Locale.US, "%.1f", draftManual) else tr("Auto"), style = NoopType.number(24f), color = if (draftManual > 0) Palette.accent else Palette.textSecondary, ) Text( - if (draftManual > 0) "steps / motion unit" else "fit from your phone", + if (draftManual > 0) tr("steps / motion unit") else tr("fit from your phone"), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.padding(bottom = 4.dp), ) } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Auto", style = NoopType.caption, color = Palette.textTertiary) + Text(tr("Auto"), style = NoopType.caption, color = Palette.textTertiary) Slider( // Continuous (no discrete `steps`): the coefficient range can be large, so a 0.5-tick // grid would mean thousands of ticks. The commit rounds to 0.5 instead (see onCommit). @@ -448,11 +448,11 @@ private fun ManualAdjustCard( contentDescription = if (draftManual > 0) { String.format(Locale.US, "Manual steps coefficient, %.1f steps per motion unit", draftManual) } else { - "Manual steps coefficient, automatic" + tr("Manual steps coefficient, automatic") } }, ) - Text("High", style = NoopType.caption, color = Palette.textTertiary) + Text(tr("High"), style = NoopType.caption, color = Palette.textTertiary) } // Live preview: a typical recent day re-estimated at the draft (or auto) coefficient. if (sampleMotion != null) { @@ -460,14 +460,14 @@ private fun ManualAdjustCard( if (effective > 0) { val preview = (sampleMotion * effective).roundToInt() StatLine( - "A typical recent day", + tr("A typical recent day"), "≈ ${grouped(preview)} steps${if (draftManual > 0) " at this setting" else " (auto)"}", ) } } if (draftManual > 0) { Text( - "Takes effect on the next analytics pass (after the next sync).", + tr("Takes effect on the next analytics pass (after the next sync)."), style = NoopType.caption, color = Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/StressScreen.kt b/android/app/src/main/java/com/noop/ui/StressScreen.kt index fe1c1992e..07458d3fc 100644 --- a/android/app/src/main/java/com/noop/ui/StressScreen.kt +++ b/android/app/src/main/java/com/noop/ui/StressScreen.kt @@ -130,8 +130,8 @@ fun StressScreen(vm: AppViewModel, onBreathe: () -> Unit = {}) { val model = remember(days, stored) { StressModel.build(days, stored) } LazyScreenScaffold( - title = "Stress", - subtitle = "Autonomic load from HRV and resting heart rate", + title = tr("Stress"), + subtitle = tr("Autonomic load from HRV and resting heart rate"), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // into the theme canvas behind the header + hero vessel, full-bleed (full-width, up behind the // status bar via the scaffold's topBackground plumbing), and the cards float OVER it on the flat @@ -217,7 +217,7 @@ private fun androidx.compose.foundation.lazy.LazyListScope.StressContent( modifier = Modifier.staggeredAppear(1), verticalArrangement = Arrangement.spacedBy(Metrics.gap), ) { - SectionHeader("Today", overline = "Markers", trailing = "vs 30-day baseline") + SectionHeader(tr("Today"), overline = tr("Markers"), trailing = tr("vs 30-day baseline")) StressTiles(model) } } @@ -274,7 +274,7 @@ private fun StressHeroCard(model: StressModel, modifier: Modifier = Modifier) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Overline("Stress monitor", modifier = Modifier.weight(1f)) + Overline(tr("Stress monitor"), modifier = Modifier.weight(1f)) StatePill(model.band.title, tone = model.band.tone, showsDot = true) } @@ -324,14 +324,14 @@ private fun StressHeroCard(model: StressModel, modifier: Modifier = Modifier) { color = bandColor, ) Text( - "of 3", + tr("of 3"), style = NoopType.number(14f, FontWeight.Medium), color = Palette.textTertiary, modifier = Modifier.padding(start = 6.dp, bottom = 5.dp), ) } Text( - "on the 0-3 autonomic-load scale", + tr("on the 0-3 autonomic-load scale"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -380,9 +380,9 @@ private fun StressAdvancedCard( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Overline("Advanced HRV", modifier = Modifier.weight(1f)) + Overline(tr("Advanced HRV"), modifier = Modifier.weight(1f)) Text( - "on demand · today's R-R", + tr("on demand · today's R-R"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -395,9 +395,9 @@ private fun StressAdvancedCard( tiles.add { m -> StatTile( modifier = m, - label = "Baevsky Stress Index", + label = tr("Baevsky Stress Index"), value = "${stressIndex.si.roundToInt()}", - caption = "Autonomic rigidity from your heart-rate rhythm. Higher means a more rigid, stressed rhythm.", + caption = tr("Autonomic rigidity from your heart-rate rhythm. Higher means a more rigid, stressed rhythm."), accent = StressRamp.TENSE, ) } @@ -410,9 +410,9 @@ private fun StressAdvancedCard( tiles.add { m -> StatTile( modifier = m, - label = "Autonomic balance (LF/HF)", + label = tr("Autonomic balance (LF/HF)"), value = String.format(Locale.US, "%.1f", ratio), - caption = "Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward).", + caption = tr("Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward)."), accent = StressRamp.STEADY, ) } @@ -420,9 +420,9 @@ private fun StressAdvancedCard( tiles.add { m -> StatTile( modifier = m, - label = "HF power", + label = tr("HF power"), value = "${freqHrv.hf.roundToInt()}", - caption = "Parasympathetic (rest) band of your HRV.", + caption = tr("Parasympathetic (rest) band of your HRV."), accent = StressRamp.STEADY, ) } @@ -439,8 +439,8 @@ private fun StressAdvancedCard( } Text( - "These are extra, on-demand HRV lenses computed from today's R-R intervals. They " + - "are informational and do not change the stress score above.", + tr("These are extra, on-demand HRV lenses computed from today's R-R intervals. They ") + + tr("are informational and do not change the stress score above."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -457,7 +457,7 @@ private fun StressDaytimeSection( modifier: Modifier = Modifier, ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Today's Timeline", overline = "Intraday", trailing = timelineTrailing(day)) + SectionHeader(tr("Today's Timeline"), overline = tr("Intraday"), trailing = timelineTrailing(day)) NoopCard(tint = Palette.stressColor) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { @@ -465,7 +465,7 @@ private fun StressDaytimeSection( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Overline("Stress through the day", modifier = Modifier.weight(1f)) + Overline(tr("Stress through the day"), modifier = Modifier.weight(1f)) val peak = day.peak val peakLevel = peak?.level if (peak != null && peakLevel != null) { @@ -496,9 +496,9 @@ private fun StressDaytimeSection( } Text( - "The line traces your autonomic load across the waking day, scored " + - "against your own calm hours today (the same 0-3 proxy as the score " + - "above, read hour by hour). Hours without enough data are skipped.", + tr("The line traces your autonomic load across the waking day, scored ") + + tr("against your own calm hours today (the same 0-3 proxy as the score ") + + tr("above, read hour by hour). Hours without enough data are skipped."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -720,9 +720,9 @@ private fun DaytimeStressLine(hours: List) { private fun daytimeLineDescription(hours: List): String { val scored = hours.mapNotNull { p -> p.level?.let { p.hour to it } } - if (scored.isEmpty()) return "No intraday stress data yet today." + if (scored.isEmpty()) return tr("No intraday stress data yet today.") val parts = scored.map { "${it.first}:00 ${String.format(Locale.US, "%.1f", it.second)}" } - return "Hourly stress today: " + parts.joinToString(", ") + return tr("Hourly stress today: ") + parts.joinToString(", ") } // MARK: - Time-in-band — Calm / Moderate / High split of the scored waking hours (liquid tubes) @@ -746,7 +746,7 @@ private fun StressTotalsBar(day: DaytimeStress.Result) { NoopCard(tint = Palette.stressColor) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("Time in band") + Overline(tr("Time in band")) TimeInBandRow(StressTotalsBand.Calm, calm, total) TimeInBandRow(StressTotalsBand.Moderate, moderate, total) @@ -822,7 +822,7 @@ private fun SustainedBreatheCard(day: DaytimeStress.Result, onBreathe: () -> Uni verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - Overline("Sustained high stress", modifier = Modifier.weight(1f)) + Overline(tr("Sustained high stress"), modifier = Modifier.weight(1f)) StatePill("${day.sustainedRun}h elevated", tone = StrandTone.Warning, showsDot = true) } Text( @@ -832,7 +832,7 @@ private fun SustainedBreatheCard(day: DaytimeStress.Result, onBreathe: () -> Uni color = Palette.textSecondary, ) NoopButton( - text = "Start a Breathe session", + text = tr("Start a Breathe session"), leadingIcon = Icons.Filled.Air, kind = NoopButtonKind.Primary, fullWidth = true, @@ -860,7 +860,7 @@ private fun StressTiles(model: StressModel) { // Today's stress value, with its band as the caption. StatTile( modifier = m, - label = "Stress", + label = tr("Stress"), value = String.format(Locale.US, "%.1f", model.score), caption = "of 3 · ${model.band.title}", accent = StressRamp.color(model.score), @@ -870,7 +870,7 @@ private fun StressTiles(model: StressModel) { // Resting HR — an INCREASE is the stressful direction. MarkerTile( modifier = m, - label = "Resting HR", + label = tr("Resting HR"), value = model.rhrToday?.let { "$it bpm" } ?: "—", delta = model.rhrDelta, accent = Palette.metricRose, @@ -881,7 +881,7 @@ private fun StressTiles(model: StressModel) { // HRV — a DECREASE is the stressful direction. MarkerTile( modifier = m, - label = "HRV", + label = tr("HRV"), value = model.hrvToday?.let { "${it.roundToInt()} ms" } ?: "—", delta = model.hrvDelta, accent = Palette.metricPurple, @@ -892,7 +892,7 @@ private fun StressTiles(model: StressModel) { // Estimated calm time — share of recent days spent in the LOW band. StatTile( modifier = m, - label = "Calm time", + label = tr("Calm time"), value = model.calmTimeValue, caption = model.calmTimeCaption, accent = StressRamp.CALM, @@ -930,7 +930,7 @@ private fun MarkerTile( deltaText = "${if (up) "+" else "−"}${kotlin.math.abs(delta).roundToInt()} vs base" deltaColor = if (isStressful) Palette.statusWarning else Palette.statusPositive } else { - deltaText = "at baseline" + deltaText = tr("at baseline") deltaColor = Palette.textTertiary } StatTile( @@ -951,7 +951,7 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier val points = remember(model, range) { model.windowedTrend(range) } Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Stress Trend", overline = "History", trailing = range.label) + SectionHeader(tr("Stress Trend"), overline = tr("History"), trailing = range.label) if (points.size >= 2) { val avg = points.average() NoopCard(tint = Palette.stressColor) { @@ -963,7 +963,7 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier Column(modifier = Modifier.weight(1f)) { Overline("Stress · ${range.label}") Text( - "Daily 0-3 proxy", + tr("Daily 0-3 proxy"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -983,9 +983,9 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier ) HorizontalDivider(color = Palette.hairline) Row(modifier = Modifier.fillMaxWidth()) { - TrendFooterItem("Today", String.format(Locale.US, "%.1f", model.score)) - TrendFooterItem("Average", String.format(Locale.US, "%.1f", avg)) - TrendFooterItem("Days", points.size.toString()) + TrendFooterItem(tr("Today"), String.format(Locale.US, "%.1f", model.score)) + TrendFooterItem(tr("Average"), String.format(Locale.US, "%.1f", avg)) + TrendFooterItem(tr("Days"), points.size.toString()) } } } @@ -1008,7 +1008,7 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier contentAlignment = Alignment.Center, ) { Text( - "Not enough recent days to chart a trend yet. Keep wearing your strap to populate it.", + tr("Not enough recent days to chart a trend yet. Keep wearing your strap to populate it."), style = NoopType.subhead, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -1033,22 +1033,22 @@ private fun androidx.compose.foundation.layout.RowScope.TrendFooterItem(label: S private fun StressMethodologyCard(model: StressModel, modifier: Modifier = Modifier) { NoopCard(tint = Palette.stressColor, modifier = modifier) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Overline("How this is computed") + Overline(tr("How this is computed")) Text( if (model.usingStored) { - "Today's value is your recorded daily stress score (0-3)." + tr("Today's value is your recorded daily stress score (0-3).") } else { - "Stress is derived from two autonomic signals." + tr("Stress is derived from two autonomic signals.") }, style = NoopType.body, color = Palette.textPrimary, ) Text( - "We compare today's resting heart rate and HRV to your own 30-day " + - "baseline. A higher-than-usual resting HR and a lower-than-usual HRV " + - "both push the score up, classic signs the body is activated. The " + - "combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at " + - "your baseline, 3 is highly activated.", + tr("We compare today's resting heart rate and HRV to your own 30-day ") + + tr("baseline. A higher-than-usual resting HR and a lower-than-usual HRV ") + + tr("both push the score up, classic signs the body is activated. The ") + + tr("combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at ") + + tr("your baseline, 3 is highly activated."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -1094,7 +1094,7 @@ private fun StressLoading() { contentAlignment = Alignment.Center, ) { Text( - "Reading your heart-rate variability and resting heart rate…", + tr("Reading your heart-rate variability and resting heart rate…"), style = NoopType.subhead, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -1106,8 +1106,8 @@ private fun StressLoading() { @Composable private fun StressEmpty() { DataPendingNote( - title = "No stress history yet", - body = "No stress history yet. Import your WHOOP export in Data Sources to see it.", + title = tr("No stress history yet"), + body = tr("No stress history yet. Import your WHOOP export in Data Sources to see it."), ) } @@ -1253,7 +1253,7 @@ internal class StressModel private constructor( val calmCaption: String if (recent.isEmpty()) { calmValue = "—" - calmCaption = "needs history" + calmCaption = tr("needs history") } else { val calm = recent.count { it.value < 1.0 } val pct = (calm.toDouble() / recent.size * 100).roundToInt() @@ -1314,19 +1314,19 @@ internal class StressModel private constructor( val rhrDn = (rhrDelta ?: 0.0) < -1.0 return when (band) { StressBand.High -> when { - rhrUp && hrvDn -> "Resting HR is elevated and HRV is below your baseline, both classic signs of high activation. Prioritise rest, hydration and an easy day." - hrvDn -> "HRV has dropped well below your baseline, pointing to elevated stress or fatigue. Ease off and give your body time to recover." - rhrUp -> "Resting heart rate is running high versus your norm. Your body is under load today. Keep effort light." - else -> "Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day." + rhrUp && hrvDn -> tr("Resting HR is elevated and HRV is below your baseline, both classic signs of high activation. Prioritise rest, hydration and an easy day.") + hrvDn -> tr("HRV has dropped well below your baseline, pointing to elevated stress or fatigue. Ease off and give your body time to recover.") + rhrUp -> tr("Resting heart rate is running high versus your norm. Your body is under load today. Keep effort light.") + else -> tr("Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day.") } StressBand.Medium -> when { rhrUp || hrvDn -> "Slightly off baseline (${if (rhrUp) "resting HR is a touch high" else "HRV is a little low"}), so you're moderately activated. Nothing alarming; just don't overreach." - else -> "You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day." + else -> tr("You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day.") } StressBand.Low -> when { - rhrDn && hrvUp -> "Resting heart rate is low and HRV is up. Your nervous system looks well-recovered and calm. A great day to push if you want to." - hrvUp -> "HRV is above baseline, a sign of a relaxed, well-recovered nervous system. Stress is low." - else -> "Resting heart rate and HRV are sitting at or below baseline: low physiological stress. You're in a calm, recovered state." + rhrDn && hrvUp -> tr("Resting heart rate is low and HRV is up. Your nervous system looks well-recovered and calm. A great day to push if you want to.") + hrvUp -> tr("HRV is above baseline, a sign of a relaxed, well-recovered nervous system. Stress is low.") + else -> tr("Resting heart rate and HRV are sitting at or below baseline: low physiological stress. You're in a calm, recovered state.") } } } diff --git a/android/app/src/main/java/com/noop/ui/SupportScreen.kt b/android/app/src/main/java/com/noop/ui/SupportScreen.kt index d0a51d38b..c6d687433 100644 --- a/android/app/src/main/java/com/noop/ui/SupportScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SupportScreen.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.unit.dp private data class Attribution(val repo: String, val note: String) private val attributions = listOf( - Attribution("my-whoop", "BLE protocol reverse-engineering"), - Attribution("goose", "historical-data decode + offload format"), + Attribution("my-whoop", tr("BLE protocol reverse-engineering")), + Attribution("goose", tr("historical-data decode + offload format")), ) /** @@ -40,19 +40,19 @@ private val attributions = listOf( @Composable fun SupportScreen() { ScreenScaffold( - title = "Support", - subtitle = "Project information, attribution, and contact details.", + title = tr("Support"), + subtitle = tr("Project information, attribution, and contact details."), ) { - SectionHeader("Help & Contact", overline = "Get in touch") + SectionHeader(tr("Help & Contact"), overline = tr("Get in touch")) // Contact — a frosted row with a tinted glyph chip. NoopCard(padding = 18.dp) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { GlyphChip(Icons.Filled.Email, Palette.accent) Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Get in touch", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Project home"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Questions, feedback, bugs - thenoopapp@gmail.com", + tr("Code, releases, and where to report an issue: github.com/MatB57/noop"), style = NoopType.subhead, color = Palette.textSecondary, ) } @@ -64,10 +64,10 @@ fun SupportScreen() { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { GlyphChip(Icons.Filled.VolunteerActivism, Palette.accent) - Text("Built on", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Built on"), style = NoopType.headline, color = Palette.textPrimary) } Text( - "This stands on community reverse-engineering. Huge thanks:", + tr("This stands on community reverse-engineering. Huge thanks:"), style = NoopType.subhead, color = Palette.textSecondary, ) attributions.forEachIndexed { idx, a -> @@ -100,7 +100,7 @@ fun SupportScreen() { Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) { Icon(Icons.Filled.Info, contentDescription = null, tint = Palette.textTertiary) Text( - "Not affiliated with, endorsed by, or connected to WHOOP. Interoperability software for your own device and data. Not a medical device.", + tr("Not affiliated with, endorsed by, or connected to WHOOP. Interoperability software for your own device and data. Not a medical device."), style = NoopType.footnote, color = Palette.textTertiary, ) } diff --git a/android/app/src/main/java/com/noop/ui/TodayScreen.kt b/android/app/src/main/java/com/noop/ui/TodayScreen.kt index e52e0f32b..cfbe90c9b 100644 --- a/android/app/src/main/java/com/noop/ui/TodayScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TodayScreen.kt @@ -616,7 +616,7 @@ fun TodayScreen( store.post( UpdateItem( kind = UpdateKind.READING, - title = "New data added", + title = tr("New data added"), message = "$added new $daysWord of history is ready in Trends.", deepLink = "trends", ), @@ -1085,11 +1085,11 @@ fun TodayScreen( if (selectedDayOffset != 0 || !scoresBuildingDismissed) { Box(modifier = Modifier.fillMaxWidth()) { DataPendingNote( - title = "Live now. Your scores are building.", - body = "Your live heart rate is working from the strap, and recovery, strain " + - "and sleep build from it over your next few nights of wear, sharpening as it " + - "learns your baseline. Want your full history instantly? Import your WHOOP " + - "export in Data Sources and it backfills in about a minute.", + title = tr("Live now. Your scores are building."), + body = tr("Your live heart rate is working from the strap, and recovery, strain ") + + tr("and sleep build from it over your next few nights of wear, sharpening as it ") + + tr("learns your baseline. Want your full history instantly? Import your WHOOP ") + + tr("export in Data Sources and it backfills in about a minute."), ) // The × is only meaningful for today's card (a past day's note isn't dismissed). if (selectedDayOffset == 0 && updateStore != null) { @@ -1282,7 +1282,7 @@ fun TodayScreen( modifier = Modifier.size(Metrics.iconSmall), ) Text( - "No cardio load yet. Effort builds once your heart rate climbs into your effort " + + tr("No cardio load yet. Effort builds once your heart rate climbs into your effort ") + "zone (around 50% of your heart-rate reserve). A calm day honestly reads near zero.", style = NoopType.footnote, color = Palette.textTertiary, @@ -1318,7 +1318,7 @@ fun TodayScreen( item { Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier.weight(1f)) { - SectionHeader("Key Metrics", overline = dayLabel, trailing = "14-day trend") + SectionHeader(tr("Key Metrics"), overline = dayLabel, trailing = tr("14-day trend")) } TextButton( onClick = { showMetricsEditor = true }, @@ -1326,11 +1326,11 @@ fun TodayScreen( ) { Icon( Icons.Filled.Tune, - contentDescription = "Edit Key Metrics", + contentDescription = tr("Edit Key Metrics"), modifier = Modifier.size(Metrics.iconSmall), ) Spacer(Modifier.width(4.dp)) - Text("Edit", style = NoopType.footnote) + Text(tr("Edit"), style = NoopType.footnote) } } } @@ -1538,7 +1538,7 @@ private fun WorkoutInProgressCard( ) Spacer(Modifier.width(Metrics.space8)) Text( - "WORKOUT IN PROGRESS", + tr("WORKOUT IN PROGRESS"), style = NoopType.overline, color = Palette.metricRose, ) @@ -1564,7 +1564,7 @@ private fun WorkoutInProgressCard( containerColor = Palette.accent, contentColor = Palette.surfaceBase, ), ) { - Text("Return to workout", style = NoopType.captionNumber) + Text(tr("Return to workout"), style = NoopType.captionNumber) Spacer(Modifier.width(Metrics.space6)) Icon( Icons.AutoMirrored.Filled.KeyboardArrowRight, @@ -1666,7 +1666,7 @@ private fun TodayCardDismissButton(onClick: () -> Unit, modifier: Modifier = Mod onClick = onClick, modifier = modifier .size(Metrics.iconButton) - .semantics { contentDescription = "Dismiss to Updates" }, + .semantics { contentDescription = tr("Dismiss to Updates") }, ) { Icon( Icons.Filled.Close, @@ -1694,7 +1694,7 @@ private fun QuickActionDisc(onClick: () -> Unit) { indication = null, onClick = onClick, ) - .semantics { contentDescription = "Quick actions" }, + .semantics { contentDescription = tr("Quick actions") }, contentAlignment = Alignment.Center, ) { Icon( @@ -1751,13 +1751,13 @@ private fun ScoringGuideIntroCard(onOpen: () -> Unit, onDismiss: () -> Unit) { modifier = Modifier.size(18.dp), ) Spacer(Modifier.width(8.dp)) - Text("New here?", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("New here?"), style = NoopType.headline, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) IconButton( onClick = onDismiss, modifier = Modifier .size(Metrics.iconButton) - .semantics { contentDescription = "Dismiss" }, + .semantics { contentDescription = tr("Dismiss") }, ) { Icon( Icons.Filled.Close, @@ -1768,13 +1768,13 @@ private fun ScoringGuideIntroCard(onOpen: () -> Unit, onDismiss: () -> Unit) { } } Text( - "See how Charge, Effort and Rest are calculated, and how they differ from WHOOP.", + tr("See how Charge, Effort and Rest are calculated, and how they differ from WHOOP."), style = NoopType.subhead, color = Palette.textSecondary, ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { TextButton(onClick = onOpen) { - Text("See how it works", style = NoopType.captionNumber, color = Palette.accent) + Text(tr("See how it works"), style = NoopType.captionNumber, color = Palette.accent) } } } @@ -1948,7 +1948,7 @@ private fun LiquidTodayHeader( indication = null, onClick = onOpenSettings, ) - .semantics { contentDescription = "Profile and settings" }, + .semantics { contentDescription = tr("Profile and settings") }, contentAlignment = Alignment.Center, ) { ProfileAvatar(size = 34.dp) @@ -1979,7 +1979,7 @@ private fun HeaderHeartButton(onSupport: () -> Unit) { ) .semantics { contentDescription = - "Support NOOP: help and contact." + tr("Support NOOP: help and contact.") }, contentAlignment = Alignment.Center, ) { @@ -2145,7 +2145,7 @@ private fun SupportRow(onSupport: () -> Unit) { indication = null, onClick = onSupport, ) - .semantics { contentDescription = "Support NOOP: help and contact" }, + .semantics { contentDescription = tr("Support NOOP: help and contact") }, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -2161,9 +2161,9 @@ private fun SupportRow(onSupport: () -> Unit) { modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(Metrics.space4), ) { - Text("Support NOOP", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Support NOOP"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Help, contact, and attribution.", + tr("Help, contact, and attribution."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -2626,7 +2626,7 @@ private fun SynthesisHeroCard( ) { Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("SYNTHESIS", style = NoopType.overline, color = Palette.textTertiary) + Text(tr("SYNTHESIS"), style = NoopType.overline, color = Palette.textTertiary) Text( status, style = NoopType.headline, @@ -2680,7 +2680,7 @@ private fun RingEmptyOverlay( ) { if (calibratingNights != null) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Calibrating", style = NoopType.headline, color = Palette.textTertiary, maxLines = 1) + Text(tr("Calibrating"), style = NoopType.headline, color = Palette.textTertiary, maxLines = 1) Text( "$calibratingNights of ${Baselines.minNightsSeed}", style = NoopType.footnote, @@ -2704,9 +2704,9 @@ private fun RingNoData() { @Composable private fun RingNeedsTrackedNight() { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Calibrating", style = NoopType.headline, color = Palette.textTertiary, maxLines = 1) + Text(tr("Calibrating"), style = NoopType.headline, color = Palette.textTertiary, maxLines = 1) Text( - "needs a tracked night", + tr("needs a tracked night"), style = NoopType.footnote, color = Palette.textSecondary, maxLines = 1, @@ -2746,7 +2746,7 @@ private fun HeroMetricRows(day: DailyMetric?, carriedDay: DailyMetric? = null, v verticalArrangement = Arrangement.spacedBy(Metrics.space12), ) { Row(verticalAlignment = Alignment.CenterVertically) { - Overline("Recovery vitals", modifier = Modifier.weight(1f)) + Overline(tr("Recovery vitals"), modifier = Modifier.weight(1f)) // iOS `lastNightLine` — today's own "Last night · " unless the shown vitals are a carry. Text( if (carriedFromVitals) carriedCaption(vitalsDay!!.day) else heroVitalsLastNightLine(), @@ -2755,19 +2755,19 @@ private fun HeroMetricRows(day: DailyMetric?, carriedDay: DailyMetric? = null, v ) } HeroVitalRow( - label = "Heart-rate variability", + label = tr("Heart-rate variability"), value = hrv?.let { "${it.roundToInt()} ms" } ?: NO_DATA, tint = Palette.metricCyan, fraction = hrv?.let { (it / 120.0).coerceIn(0.0, 1.0) }, ) HeroVitalRow( - label = "Resting heart rate", + label = tr("Resting heart rate"), value = rhr?.let { "$it bpm" } ?: NO_DATA, tint = Palette.metricRose, fraction = rhr?.let { (it / 100.0).coerceIn(0.0, 1.0) }, ) HeroVitalRow( - label = "Breaths per minute", + label = tr("Breaths per minute"), value = resp?.let { String.format(Locale.US, "%.1f rpm", it) } ?: NO_DATA, tint = Palette.accent, fraction = resp?.let { (it / 24.0).coerceIn(0.0, 1.0) }, @@ -2842,11 +2842,11 @@ private fun YourCardsSection( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { // Header: "YOUR CARDS" overline + a right-aligned blue CUSTOMISE action (the WHOOP ✎ affordance). Row(verticalAlignment = Alignment.CenterVertically) { - Overline("Your cards", modifier = Modifier.weight(1f)) + Overline(tr("Your cards"), modifier = Modifier.weight(1f)) TextButton( onClick = onCustomise, colors = ButtonDefaults.textButtonColors(contentColor = Palette.accent), - modifier = Modifier.semantics { contentDescription = "Customise your cards" }, + modifier = Modifier.semantics { contentDescription = tr("Customise your cards") }, ) { Icon( Icons.Filled.Tune, @@ -2855,7 +2855,7 @@ private fun YourCardsSection( ) Spacer(Modifier.width(4.dp)) Text( - "CUSTOMISE", + tr("CUSTOMISE"), style = NoopType.overline.copy(letterSpacing = 0.4.sp), color = Palette.accent, ) @@ -3251,10 +3251,10 @@ private fun DashboardCardsEditorDialog( verticalArrangement = Arrangement.spacedBy(16.dp), ) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("My Dashboard", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("My Dashboard"), style = NoopType.title2, color = Palette.textPrimary) Text( - "Choose which cards show on Today and reorder them with the arrows. " + - "Cards with no value yet show a dash.", + tr("Choose which cards show on Today and reorder them with the arrows. ") + + tr("Cards with no value yet show a dash."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -3331,7 +3331,7 @@ private fun DashboardCardsEditorDialog( .forEach { items.add(EditableDashboardCard(it, false)) } }, colors = ButtonDefaults.textButtonColors(contentColor = Palette.textSecondary), - ) { Text("Reset", style = NoopType.body) } + ) { Text(tr("Reset"), style = NoopType.body) } Spacer(Modifier.weight(1f)) Button( onClick = { onSave(items.filter { it.enabled }.map { it.card }) }, @@ -3341,7 +3341,7 @@ private fun DashboardCardsEditorDialog( containerColor = Palette.accent, contentColor = Palette.surfaceBase, ), - ) { Text("Done", style = NoopType.captionNumber) } + ) { Text(tr("Done"), style = NoopType.captionNumber) } } } } @@ -3378,13 +3378,13 @@ internal fun ChargeBreakdownSheet( verticalAlignment = Alignment.CenterVertically, ) { Text( - "What shaped your Charge", + tr("What shaped your Charge"), style = NoopType.headline, color = Palette.textPrimary, modifier = Modifier.weight(1f), ) IconButton(onClick = onClose) { - Icon(Icons.Filled.Close, contentDescription = "Close", tint = Palette.textSecondary) + Icon(Icons.Filled.Close, contentDescription = tr("Close"), tint = Palette.textSecondary) } } Column( @@ -3417,7 +3417,7 @@ internal fun ChargeBreakdownSheet( .background(Palette.surfaceInset) .padding(14.dp) .semantics { - contentDescription = "How Charge is calculated. The method behind the score." + contentDescription = tr("How Charge is calculated. The method behind the score.") }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -3433,12 +3433,12 @@ internal fun ChargeBreakdownSheet( verticalArrangement = Arrangement.spacedBy(1.dp), ) { Text( - "How Charge is calculated", + tr("How Charge is calculated"), style = NoopType.subhead, color = Palette.textPrimary, ) Text( - "The method behind the score, not today's values.", + tr("The method behind the score, not today's values."), style = NoopType.caption, color = Palette.textTertiary, ) @@ -3484,7 +3484,7 @@ private fun RecoveryDriversSection( // Header row: section title + the SURFACED confidence pill (dot + tier tag) on the right. Row(verticalAlignment = Alignment.Top) { Box(modifier = Modifier.weight(1f)) { - SectionHeader("What shaped it", overline = overline, trailing = "vs your baseline") + SectionHeader(tr("What shaped it"), overline = overline, trailing = tr("vs your baseline")) } ChargeConfidencePill(tier) } @@ -3492,8 +3492,8 @@ private fun RecoveryDriversSection( Column(verticalArrangement = Arrangement.spacedBy(Metrics.space16)) { drivers.forEach { DriverRow(it) } Text( - "Each line is how many points that signal moved Charge versus sitting at your " + - "on-device baseline. Approximate, not medical advice.", + tr("Each line is how many points that signal moved Charge versus sitting at your ") + + tr("on-device baseline. Approximate, not medical advice."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -3587,12 +3587,12 @@ private fun RecoveryContributorsSection(day: DailyMetric?, carriedDay: DailyMetr if (hrv == null && rhr == null && sleepMin == null && resp == null) return val overline = carriedDay?.let { "Recovery · ${carriedCaption(it.day)}" } ?: "Recovery" - SectionHeader("Contributors", overline = overline, trailing = "What drove Charge") + SectionHeader(tr("Contributors"), overline = overline, trailing = tr("What drove Charge")) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(Metrics.space16)) { // HRV, higher is better; map a typical 20–120 ms span. Teal (its biometric hue; iOS metricCyan). ContributorBar( - label = "HRV", + label = tr("HRV"), readout = hrv?.let { "${it.roundToInt()} ms" } ?: NO_DATA, fraction = hrv?.let { ((it - 20.0) / 100.0) }, color = Palette.metricCyan, @@ -3600,28 +3600,28 @@ private fun RecoveryContributorsSection(day: DailyMetric?, carriedDay: DailyMetr // Resting HR, lower is better, so invert a typical 40–80 bpm span. Charge/recovery world (iOS // chargeColor, the recovery contributor reads on the WHOOP-green Charge world, not gold). ContributorBar( - label = "Resting HR", + label = tr("Resting HR"), readout = rhr?.let { "${it.roundToInt()} bpm" } ?: NO_DATA, fraction = rhr?.let { 1.0 - ((it - 40.0) / 40.0) }, color = Palette.chargeColor, ) // Sleep, hours in bed against an 8h target. Blue (sleep world). ContributorBar( - label = "Sleep", + label = tr("Sleep"), readout = sleepMin?.let { sleepValue(cd) } ?: NO_DATA, fraction = sleepMin?.let { (it / 60.0) / 8.0 }, color = Palette.sleepLight, ) // Respiratory, stability around a typical 12–20 rpm span. Deep blue (sleep world). ContributorBar( - label = "Respiratory", + label = tr("Respiratory"), readout = resp?.let { String.format(Locale.US, "%.1f rpm", it) } ?: NO_DATA, fraction = resp?.let { 1.0 - ((it - 12.0) / 8.0) }, color = Palette.sleepDeep, ) Text( - "Baselines learned on-device over 14 days. Bars are an approximate read of each " + - "signal against a typical adult range, not medical advice.", + tr("Baselines learned on-device over 14 days. Bars are an approximate read of each ") + + tr("signal against a typical adult range, not medical advice."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -4174,7 +4174,7 @@ private fun MetricGrid( KeyMetric.CHARGE to run { val v = d?.recovery ?: lastScoredCharge?.value KeyTileData( - label = "Recovery", + label = tr("Recovery"), value = d?.recovery?.let { "${it.roundToInt()}" } ?: recoveryCalibration?.let { "$it/${Baselines.minNightsSeed}" } ?: lastScoredCharge?.let { "${it.value.roundToInt()}" } ?: NO_DATA, @@ -4184,14 +4184,14 @@ private fun MetricGrid( ) }, KeyMetric.EFFORT to KeyTileData( - label = "Strain", + label = tr("Strain"), value = d?.strain?.let { UnitFormatter.effortDisplay(it, effortScale) } ?: NO_DATA, unit = if (d?.strain != null) "%" else "", tint = d?.strain?.let { Palette.effortTint(it / StrainScorer.maxStrain) } ?: Palette.effortColor, frac = d?.strain?.let { (it / 100.0).coerceIn(0.0, 1.0) }, ), KeyMetric.REST to KeyTileData( - label = "Rest", + label = tr("Rest"), value = restScore?.let { "${it.roundToInt()}" } ?: NO_DATA, unit = if (restScore != null) "%" else "", tint = restScore?.let { Palette.recoveryColor(it) } ?: Palette.restColor, @@ -4200,7 +4200,7 @@ private fun MetricGrid( KeyMetric.HRV to run { val v = d?.avgHrv ?: carriedDay?.avgHrv KeyTileData( - label = "HRV", + label = tr("HRV"), value = v?.let { "${it.roundToInt()}" } ?: NO_DATA, unit = if (v != null) "ms" else "", tint = Palette.metricCyan, @@ -4210,7 +4210,7 @@ private fun MetricGrid( KeyMetric.RESTING_HR to run { val v = d?.restingHr ?: carriedDay?.restingHr KeyTileData( - label = "Rest HR", + label = tr("Rest HR"), value = v?.toString() ?: NO_DATA, unit = if (v != null) "bpm" else "", tint = Palette.metricRose, @@ -4220,7 +4220,7 @@ private fun MetricGrid( KeyMetric.BLOOD_OXYGEN to run { val v = d?.spo2Pct ?: carriedDay?.spo2Pct KeyTileData( - label = "Blood Oxygen", + label = tr("Blood Oxygen"), value = v?.let { String.format(Locale.US, "%.0f", it) } ?: NO_DATA, unit = if (v != null) "%" else "", tint = Palette.metricCyan, @@ -4230,7 +4230,7 @@ private fun MetricGrid( KeyMetric.RESPIRATORY to run { val v = d?.respRateBpm ?: carriedDay?.respRateBpm KeyTileData( - label = "Respiratory", + label = tr("Respiratory"), value = v?.let { String.format(Locale.US, "%.1f", it) } ?: NO_DATA, unit = if (v != null) "rpm" else "", tint = Palette.accent, @@ -4242,7 +4242,7 @@ private fun MetricGrid( val realSteps = d?.steps ?: importedStepsForDay val steps = realSteps ?: estimatedStepsForDay KeyTileData( - label = "Steps", + label = tr("Steps"), value = steps?.let { intString(it.toDouble()) } ?: NO_DATA, unit = "", tint = Palette.metricCyan, @@ -4252,7 +4252,7 @@ private fun MetricGrid( KeyMetric.WEIGHT to run { val weight = weightTile(latestWeightKg, profileWeightKg, unitSystem) KeyTileData( - label = "Weight", + label = tr("Weight"), value = weight.value, unit = "", tint = Palette.accent, @@ -4260,7 +4260,7 @@ private fun MetricGrid( ) }, KeyMetric.CALORIES to KeyTileData( - label = "Calories", + label = tr("Calories"), value = d?.activeKcalEst?.let { intString(it) } ?: NO_DATA, unit = if (d?.activeKcalEst != null) "kcal" else "", tint = Palette.metricAmber, @@ -4294,7 +4294,7 @@ private fun MetricGrid( colors = ButtonDefaults.textButtonColors(contentColor = Palette.accent), ) { Text( - if (metricsExpanded) "Show fewer" else "Show all metrics ($hidden)", + if (metricsExpanded) tr("Show fewer") else "Show all metrics ($hidden)", style = NoopType.subhead, ) Spacer(Modifier.width(4.dp)) @@ -4518,21 +4518,21 @@ private fun HeartRateTrendCard( // too-narrow rolling window (say 1h with no recent offload) is never a dead end — the user widens it // or steps back to Today, and the message says which window came up empty. if (winBuckets.size < 2) { - SectionHeader("Heart Rate", overline = selectedLabel) + SectionHeader(tr("Heart Rate"), overline = selectedLabel) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Overline("Beats per minute") + Overline(tr("Beats per minute")) if (selectedDay == today) { HrWindowPills(hrWindow) { hrWindowOrdinal = it.ordinal } } Text( when { selectedDay != today -> - "No heart rate for this day. Step back to a day the strap was worn." + tr("No heart rate for this day. Step back to a day the strap was worn.") hrWindow != HrWindow.TODAY && buckets.size >= 2 -> "No heart rate in the last ${hrWindow.label}. Try a wider window or Today." else -> - "Calibrating , no heart rate banked yet today. Your curve fills in as the strap offloads." + tr("Calibrating , no heart rate banked yet today. Your curve fills in as the strap offloads.") }, style = NoopType.footnote, color = Palette.textTertiary, @@ -4569,13 +4569,13 @@ private fun HeartRateTrendCard( val visAvg = visBpm.average().roundToInt() val visMin = visBpm.min().roundToInt() - SectionHeader("Heart Rate", overline = selectedLabel) + SectionHeader(tr("Heart Rate"), overline = selectedLabel) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { // Header, mirrors the macOS ChartCard (title + subtitle, trailing read-out). Row(verticalAlignment = Alignment.Top) { Column(modifier = Modifier.weight(1f)) { - Overline("Beats per minute") + Overline(tr("Beats per minute")) // #985: the buckets stay the same 5-minute means whatever the window (view-only // narrowing, no re-read), so the resolution half of the label never changes — only // the span half tells the truth about what's on screen. @@ -4687,14 +4687,14 @@ private fun HeartRateTrendCard( verticalAlignment = Alignment.CenterVertically, ) { Text( - if (hrZoom == null) "Pinch to zoom · drag to pan" else "Zoomed in · drag to pan", + if (hrZoom == null) tr("Pinch to zoom · drag to pan") else tr("Zoomed in · drag to pan"), style = NoopType.footnote, color = Palette.textTertiary, modifier = Modifier.weight(1f), ) if (hrZoom != null) { Text( - "Reset", + tr("Reset"), style = NoopType.footnote, color = Palette.accent, modifier = Modifier @@ -5133,7 +5133,7 @@ data class TodayFooterState( private fun TodayWorkoutsSection(workouts: List) { if (workouts.isEmpty()) return - SectionHeader("Last Workouts", overline = "Activity", trailing = "14 days") + SectionHeader(tr("Last Workouts"), overline = tr("Activity"), trailing = tr("14 days")) Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { workouts.take(4).chunked(2).forEach { rowWorkouts -> Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { @@ -5167,7 +5167,7 @@ private fun TodaySourcesSection( expanded: Boolean = true, onToggle: () -> Unit = {}, ) { - SectionHeader("Data Sources", overline = "Provenance") + SectionHeader(tr("Data Sources"), overline = tr("Provenance")) val whoopPresent = (footer.whoopDays ?: 0) > 0 || strapBatteryPct != null val applePresent = (footer.appleDays ?: 0) > 0 || (footer.appleWorkouts ?: 0) > 0 val hcPresent = (footer.hcDays ?: 0) > 0 || (footer.hcWorkouts ?: 0) > 0 @@ -5215,7 +5215,7 @@ private fun TodaySourcesSection( .clickable(onClickLabel = "Hide data source detail", onClick = onToggle), verticalAlignment = Alignment.CenterVertically, ) { - Text("Synced from", style = NoopType.overline, color = Palette.textTertiary, modifier = Modifier.weight(1f)) + Text(tr("Synced from"), style = NoopType.overline, color = Palette.textTertiary, modifier = Modifier.weight(1f)) Icon( Icons.Filled.KeyboardArrowUp, contentDescription = null, @@ -5291,7 +5291,7 @@ private fun SourceRow( } Spacer(Modifier.weight(1f)) Text( - text = if (present) detail else "Not connected", + text = if (present) detail else tr("Not connected"), style = NoopType.captionNumber, color = if (present) Palette.textSecondary else Palette.textTertiary, maxLines = 1, @@ -5323,7 +5323,7 @@ private fun ReadinessSection(days: List, carriedDay: DailyMetric? = if (readiness.level == ReadinessEngine.Level.INSUFFICIENT) return val overline = carriedDay?.let { carriedCaption(it.day) } ?: "Should you push today?" - SectionHeader("Readiness", overline = overline) + SectionHeader(tr("Readiness"), overline = overline) NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { // Headline row: level dot + headline, then the ACWR load read-out. @@ -5886,9 +5886,9 @@ private fun KeyMetricsEditorDialog( verticalArrangement = Arrangement.spacedBy(16.dp), ) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("Edit Key Metrics", style = NoopType.title2, color = Palette.textPrimary) + Text(tr("Edit Key Metrics"), style = NoopType.title2, color = Palette.textPrimary) Text( - "Choose which tiles show on your Control Center and reorder them with the arrows.", + tr("Choose which tiles show on your Control Center and reorder them with the arrows."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -5962,7 +5962,7 @@ private fun KeyMetricsEditorDialog( KeyMetric.defaultOrder.forEach { items.add(EditableMetric(it, true)) } }, colors = ButtonDefaults.textButtonColors(contentColor = Palette.textSecondary), - ) { Text("Reset", style = NoopType.body) } + ) { Text(tr("Reset"), style = NoopType.body) } Spacer(Modifier.weight(1f)) Button( onClick = { onSave(items.filter { it.enabled }.map { it.metric }) }, @@ -5972,7 +5972,7 @@ private fun KeyMetricsEditorDialog( containerColor = Palette.accent, contentColor = Palette.surfaceBase, ), - ) { Text("Done", style = NoopType.captionNumber) } + ) { Text(tr("Done"), style = NoopType.captionNumber) } } } } diff --git a/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt b/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt index e2fe733fd..1c3048a95 100644 --- a/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt @@ -138,16 +138,16 @@ private data class MetricSpec( /** The built-in DailyMetric-backed metrics, in the macOS ordering (Charge first). */ private val builtInMetrics: List = listOf( MetricSpec( - key = "recovery", title = "Charge", unit = "%", category = "Charge", + key = "recovery", title = tr("Charge"), unit = "%", category = "Charge", accent = Palette.accent, higherIsBetter = true, decimals = 0, dailyPick = { it.recovery }, - description = "How recovered you are , led by HRV versus your personal baseline.", + description = tr("How recovered you are , led by HRV versus your personal baseline."), ), MetricSpec( - key = "strain", title = "Effort", unit = "/100", category = "Effort", + key = "strain", title = tr("Effort"), unit = "/100", category = "Effort", accent = Palette.strain066, higherIsBetter = null, decimals = 1, dailyPick = { it.strain }, - description = "Cardiovascular load for the day, on a 0-100 scale (was 0-21).", + description = tr("Cardiovascular load for the day, on a 0-100 scale (was 0-21)."), ), MetricSpec( key = "hrv", title = "HRV", unit = "ms", category = "Charge", @@ -155,30 +155,30 @@ private val builtInMetrics: List = listOf( dailyPick = { it.avgHrv }, ), MetricSpec( - key = "rhr", title = "Resting HR", unit = "bpm", category = "Charge", + key = "rhr", title = tr("Resting HR"), unit = "bpm", category = "Charge", accent = Palette.metricRose, higherIsBetter = false, decimals = 0, dailyPick = { it.restingHr?.toDouble() }, ), MetricSpec( - key = "sleep", title = "Sleep", unit = "h", category = "Rest", + key = "sleep", title = tr("Sleep"), unit = "h", category = "Rest", // Rest-score accent rides the reset accent token (iOS metricAccent maps every Rest metric , // sleep_performance / sleep_total_min , to StrandPalette.accent), not a stray metric hue. accent = Palette.accent, higherIsBetter = true, decimals = 1, dailyPick = { it.totalSleepMin?.let { m -> m / 60.0 } }, - description = "How restorative your sleep was , duration, efficiency, deep+REM, timing.", + description = tr("How restorative your sleep was , duration, efficiency, deep+REM, timing."), ), MetricSpec( - key = "efficiency", title = "Sleep Efficiency", unit = "%", category = "Rest", + key = "efficiency", title = tr("Sleep Efficiency"), unit = "%", category = "Rest", accent = Palette.accent, higherIsBetter = true, decimals = 0, dailyPick = { it.efficiency }, ), MetricSpec( - key = "spo2", title = "Blood Oxygen", unit = "%", category = "Health", + key = "spo2", title = tr("Blood Oxygen"), unit = "%", category = "Health", accent = Palette.metricCyan, higherIsBetter = true, decimals = 0, dailyPick = { it.spo2Pct }, ), MetricSpec( - key = "resp", title = "Respiratory Rate", unit = "rpm", category = "Health", + key = "resp", title = tr("Respiratory Rate"), unit = "rpm", category = "Health", accent = Palette.accent, higherIsBetter = null, decimals = 1, dailyPick = { it.respRateBpm }, ), @@ -374,7 +374,7 @@ fun TrendsExploreScreen(vm: AppViewModel) { // are accessibility-walked on scroll. Each top-level child is one `item { }` in the same order; the // conditional empty-state note uses `if (cond) { item {} }` so it adds no row when hidden. No standalone // Spacers here , the LazyColumn's `spacedBy(20.dp)` reproduces the eager column's row spacing exactly. - LazyScreenScaffold(title = "Explore", subtitle = "Every signal, one tap deep.") { + LazyScreenScaffold(title = tr("Explore"), subtitle = tr("Every signal, one tap deep.")) { // The headline tap-through (#575): a full-day, full-resolution, zoomable timeline. Sits above the // per-metric catalog because it's a different kind of view , every second of one day, not one @@ -386,9 +386,9 @@ fun TrendsExploreScreen(vm: AppViewModel) { if (series.isEmpty()) { item { DataPendingNote( - title = "Import your history first", - body = "Import your history first. A WHOOP export in Data Sources fills " + - "every metric you can explore here in about a minute.", + title = tr("Import your history first"), + body = tr("Import your history first. A WHOOP export in Data Sources fills ") + + tr("every metric you can explore here in about a minute."), ) } } @@ -480,9 +480,9 @@ private fun DeepTimelineEntry(onClick: () -> Unit) { Text("∿", style = NoopType.title2, color = Palette.metricRose) } Column(modifier = Modifier.weight(1f)) { - Text("Deep Timeline", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Deep Timeline"), style = NoopType.headline, color = Palette.textPrimary) Text( - "Every second of your day, zoomable.", + tr("Every second of your day, zoomable."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -531,7 +531,7 @@ private fun MetricDropdown( } Icon( if (expanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown, - contentDescription = "Pick metric", + contentDescription = tr("Pick metric"), tint = if (expanded) Palette.accent else Palette.textSecondary, ) } @@ -696,7 +696,7 @@ private fun HeroChartCard( if (windowed.isEmpty()) { "No ${metric.title.lowercase()} recorded yet. Sync your strap to populate this trend." } else { - "Only one reading in range , widen the window to see a trend." + tr("Only one reading in range , widen the window to see a trend.") }, style = NoopType.subhead, color = Palette.textTertiary, @@ -789,25 +789,25 @@ private fun StatRow( } Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { - SectionHeader("Summary", overline = "Over the visible window", trailing = "${s.n} pts") + SectionHeader(tr("Summary"), overline = tr("Over the visible window"), trailing = "${s.n} pts") Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "Average", + label = tr("Average"), value = if (s.n > 0) metric.format(s.mean) else ",", caption = "${s.n} days", accent = metric.accent, ) StatTile( modifier = Modifier.weight(1f), - label = "Min", + label = tr("Min"), value = if (s.n > 0) metric.format(s.min) else ",", accent = Palette.textPrimary, ) StatTile( modifier = Modifier.weight(1f), - label = "Max", + label = tr("Max"), value = if (s.n > 0) metric.format(s.max) else ",", accent = Palette.textPrimary, ) @@ -819,14 +819,14 @@ private fun StatRow( Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap)) { StatTile( modifier = Modifier.weight(1f), - label = "Latest", + label = tr("Latest"), value = latest?.let { metric.format(it.value) } ?: ",", caption = latest?.day, accent = metric.accent, ) StatTile( modifier = Modifier.weight(1f), - label = "Δ vs prev", + label = tr("Δ vs prev"), value = deltaText, caption = deltaCaption, accent = Palette.textPrimary, @@ -867,7 +867,7 @@ private fun rangeCaption( ): String { if (series.isEmpty()) return "," val n = windowed.size - val unit = if (n == 1) "reading" else "readings" + val unit = if (n == 1) tr("reading") else tr("readings") return if (fellBack) "$n $unit · sparse , widened to ${effectiveRange.windowName}" else "$n $unit · ${range.windowName}" } diff --git a/android/app/src/main/java/com/noop/ui/TrendsScreen.kt b/android/app/src/main/java/com/noop/ui/TrendsScreen.kt index 02e178d9b..8410d8bf2 100644 --- a/android/app/src/main/java/com/noop/ui/TrendsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TrendsScreen.kt @@ -143,8 +143,8 @@ fun TrendsScreen(vm: AppViewModel) { val recAvg = recovery.values.averageOrNull() LazyScreenScaffold( - title = "Trends", - subtitle = "The thread of you over time.", + title = tr("Trends"), + subtitle = tr("The thread of you over time."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // into the theme canvas behind the header + top rows, full-bleed via the scaffold's topBackground // plumbing. Static (LiquidSkyStatic, inside the helper) — never an animated sky behind a scrolling @@ -217,7 +217,7 @@ fun TrendsScreen(vm: AppViewModel) { item { ChartCard( modifier = Modifier.staggeredAppear(index = 3), - title = "Charge", + title = tr("Charge"), // The range bar above already prints the authoritative reading-count caption; // the hero only names its window so the count isn't doubled in one card height. subtitle = range.subtitle, @@ -256,9 +256,9 @@ fun TrendsScreen(vm: AppViewModel) { modifier = Modifier.staggeredAppear(index = 4), verticalArrangement = Arrangement.spacedBy(Metrics.gap), ) { - SectionHeader("Daily signals", overline = "Trends") + SectionHeader(tr("Daily signals"), overline = tr("Trends")) MetricTrendCard( - title = "Heart rate variability", unit = "ms", + title = tr("Heart rate variability"), unit = "ms", color = Palette.metricPurple, tint = Palette.chargeColor, higherIsBetter = true, @@ -266,7 +266,7 @@ fun TrendsScreen(vm: AppViewModel) { fmt = { "${it.roundToInt()}" }, ) MetricTrendCard( - title = "Resting heart rate", unit = "bpm", + title = tr("Resting heart rate"), unit = "bpm", color = Palette.metricRose, tint = Palette.chargeColor, higherIsBetter = false, @@ -276,7 +276,7 @@ fun TrendsScreen(vm: AppViewModel) { MetricTrendCard( // Plotted values stay on the stored 0–100 scale (line shape unchanged); only the displayed // numbers + unit follow the Effort-scale toggle, converted inside `fmt`. (#268) - title = "Effort", unit = "/ ${UnitFormatter.effortScaleMax(effortScale)}", + title = tr("Effort"), unit = "/ ${UnitFormatter.effortScaleMax(effortScale)}", // WHOOP: Effort/Strain is always BLUE , a deep→bright blue line, not the amber ramp. color = Palette.effortColor, tint = Palette.effortColor, @@ -357,8 +357,8 @@ private fun WeeklyDigestNav( WeekNavBar(weekOffset = weekOffset, minWeekOffset = minWeekOffset, onStep = onStep) if (digest.isEmpty) { DataPendingNote( - title = "No readings this week", - body = "Step to another week with the arrows above to see its review.", + title = tr("No readings this week"), + body = tr("Step to another week with the arrows above to see its review."), ) } else { NoopCard { WeeklyDigestContent(digest = digest, compact = true) } @@ -395,7 +395,7 @@ private fun WeekNavBar(weekOffset: Int, minWeekOffset: Int, onStep: (Int) -> Uni ) { Icon( Icons.Filled.ChevronLeft, - contentDescription = "Previous week", + contentDescription = tr("Previous week"), tint = if (atOldest) Palette.textTertiary else Palette.accent, ) } @@ -405,7 +405,7 @@ private fun WeekNavBar(weekOffset: Int, minWeekOffset: Int, onStep: (Int) -> Uni verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text(label, style = NoopType.headline, color = Palette.textPrimary) - Overline("Week in review", color = Palette.textSecondary) + Overline(tr("Week in review"), color = Palette.textSecondary) } Spacer(Modifier.weight(1f)) IconButton( @@ -416,7 +416,7 @@ private fun WeekNavBar(weekOffset: Int, minWeekOffset: Int, onStep: (Int) -> Uni ) { Icon( Icons.Filled.ChevronRight, - contentDescription = "Next week", + contentDescription = tr("Next week"), tint = if (atNewest) Palette.textTertiary else Palette.accent, ) } @@ -445,10 +445,10 @@ private fun WeekInReviewCard( NoopCard(modifier = modifier, tint = Palette.chargeColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - SectionHeader("Week in review", overline = "Charge · Effort · Rest") + SectionHeader(tr("Week in review"), overline = tr("Charge · Effort · Rest")) if (chargeAvg != null) { PipScoreRow( - label = "Charge", value = chargeAvg, range = 0f..100f, + label = tr("Charge"), value = chargeAvg, range = 0f..100f, tint = Palette.chargeColor, format = { "${it.roundToInt()}" }, ) } @@ -460,14 +460,14 @@ private fun WeekInReviewCard( val maxV = UnitFormatter.effortValue(100.0, effortScale) val oneDecimal = effortScale == EffortScale.WHOOP PipScoreRow( - label = "Effort", value = display, range = 0f..maxV.toFloat(), + label = tr("Effort"), value = display, range = 0f..maxV.toFloat(), tint = Palette.effortColor, format = { if (oneDecimal) String.format(Locale.US, "%.1f", it) else "${it.roundToInt()}" }, ) } if (restAvg != null) { PipScoreRow( - label = "Rest", value = restAvg, range = 0f..100f, + label = tr("Rest"), value = restAvg, range = 0f..100f, tint = Palette.restColor, format = { "${it.roundToInt()}" }, ) } @@ -595,7 +595,7 @@ private fun windowPoints( /** Caption text, mirroring TrendsView.caption(count:eff:). */ private fun caption(count: Int, eff: TrendsRange, selected: TrendsRange): String { - val unit = if (count == 1) "reading" else "readings" + val unit = if (count == 1) tr("reading") else tr("readings") return if (eff != selected) { "$count $unit · sparse , widened to ${eff.longName}" } else { @@ -917,14 +917,14 @@ private fun RecoveryHistoryCard(days: List, range: TrendsRange) { days.takeLast(span).mapNotNull { it.recovery } } val title = if (range == TrendsRange.All && days.size > 365) { - "Charge , all history" + tr("Charge , all history") } else { - "Charge , past year" + tr("Charge , past year") } NoopCard(tint = Palette.chargeColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - SectionHeader(title, overline = "Calendar", trailing = "${recovery.size} days") + SectionHeader(title, overline = tr("Calendar"), trailing = "${recovery.size} days") if (recovery.size >= 2) { BarChart( values = recovery, @@ -936,8 +936,8 @@ private fun RecoveryHistoryCard(days: List, range: TrendsRange) { } HorizontalDivider(color = Palette.hairline) Text( - "Each bar is one day's Charge score, low to high. The 53-week calendar " + - "heat-grid is part of the desktop app.", + tr("Each bar is one day's Charge score, low to high. The 53-week calendar ") + + tr("heat-grid is part of the desktop app."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -987,7 +987,7 @@ private fun SparsePlaceholder(height: Dp = Metrics.chartHeight) { contentAlignment = Alignment.Center, ) { Text( - "Not enough data for this window.", + tr("Not enough data for this window."), style = NoopType.subhead, color = Palette.textTertiary, textAlign = TextAlign.Center, @@ -998,9 +998,9 @@ private fun SparsePlaceholder(height: Dp = Metrics.chartHeight) { @Composable private fun EmptyTrends() { DataPendingNote( - title = "Trends need history to draw", - body = "Trends need history to draw. Import your WHOOP export in Data Sources " + - "to see weeks, months and years instantly.", + title = tr("Trends need history to draw"), + body = tr("Trends need history to draw. Import your WHOOP export in Data Sources ") + + tr("to see weeks, months and years instantly."), ) } diff --git a/android/app/src/main/java/com/noop/ui/UpdatesInboxScreen.kt b/android/app/src/main/java/com/noop/ui/UpdatesInboxScreen.kt index e88267dc7..7d9cfb356 100644 --- a/android/app/src/main/java/com/noop/ui/UpdatesInboxScreen.kt +++ b/android/app/src/main/java/com/noop/ui/UpdatesInboxScreen.kt @@ -89,8 +89,8 @@ fun UpdatesInboxScreen( ) { // Header — "INBOX" overline + "Updates" title + a live subtitle. Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Overline("Inbox", color = Palette.textTertiary) - Text("Updates", style = NoopType.title1, color = Palette.textPrimary) + Overline(tr("Inbox"), color = Palette.textTertiary) + Text(tr("Updates"), style = NoopType.title1, color = Palette.textPrimary) Text(subtitle(store), style = NoopType.caption, color = Palette.textSecondary) } @@ -99,7 +99,7 @@ fun UpdatesInboxScreen( } else { if (unread.isNotEmpty()) { InboxSection( - label = "New", + label = tr("New"), items = unread, onTap = { handleTap(it, store, onDeepLink, onClose) }, onRestore = { handleRestore(it, store, onRestore, onClose) }, @@ -109,7 +109,7 @@ fun UpdatesInboxScreen( } if (read.isNotEmpty()) { InboxSection( - label = "Earlier", + label = tr("Earlier"), items = read, onTap = { handleTap(it, store, onDeepLink, onClose) }, onRestore = { handleRestore(it, store, onRestore, onClose) }, @@ -134,7 +134,7 @@ fun UpdatesInboxScreen( modifier = Modifier.size(Metrics.iconSmall), ) Spacer(Modifier.width(6.dp)) - Text("Clear all", style = NoopType.subhead, color = Palette.textSecondary) + Text(tr("Clear all"), style = NoopType.subhead, color = Palette.textSecondary) } Spacer(Modifier.weight(1f)) Button( @@ -157,7 +157,7 @@ fun UpdatesInboxScreen( modifier = Modifier.size(Metrics.iconSmall), ) Spacer(Modifier.width(6.dp)) - Text("Mark all read", style = NoopType.subhead) + Text(tr("Mark all read"), style = NoopType.subhead) } } } @@ -165,9 +165,9 @@ fun UpdatesInboxScreen( } private fun subtitle(store: UpdateStore): String { - if (store.items.isEmpty()) return "What's new in the app and your data" + if (store.items.isEmpty()) return tr("What's new in the app and your data") val n = store.unreadCount - return if (n == 0) "All caught up" else "$n unread" + return if (n == 0) tr("All caught up") else "$n unread" } @Composable @@ -270,7 +270,7 @@ private fun SwipeBackground(direction: SwipeToDismissBoxValue) { tint = contentColor, modifier = Modifier.size(Metrics.iconSmall), ) - Text("Mark read", style = NoopType.subhead, color = contentColor) + Text(tr("Mark read"), style = NoopType.subhead, color = contentColor) } } } @@ -354,7 +354,7 @@ private fun UpdateRow( modifier = Modifier.size(Metrics.iconSmall), ) Spacer(Modifier.width(6.dp)) - Text("Restore to Today", style = NoopType.subhead, color = Palette.accent) + Text(tr("Restore to Today"), style = NoopType.subhead, color = Palette.accent) } } } @@ -377,9 +377,9 @@ private fun EmptyInboxState() { tint = Palette.textTertiary, modifier = Modifier.size(34.dp), ) - Text("You're all caught up.", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("You're all caught up."), style = NoopType.headline, color = Palette.textPrimary) Text( - "New release notes and fresh data will land here.", + tr("New release notes and fresh data will land here."), style = NoopType.subhead, color = Palette.textSecondary, ) diff --git a/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt b/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt index 7ccff90d1..1adac129d 100644 --- a/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt +++ b/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt @@ -124,13 +124,13 @@ fun WeeklyDigestCard(vm: AppViewModel, modifier: Modifier = Modifier) { fun WeeklyDigestScreen(vm: AppViewModel) { val days by vm.recentDays.collectAsStateWithLifecycle() val factor = effortDisplayFactor(UnitPrefs.effortScale(LocalContext.current)) - ScreenScaffold(title = "Week in review", subtitle = "Your Monday-to-Sunday, read in one glance.") { + ScreenScaffold(title = tr("Week in review"), subtitle = tr("Your Monday-to-Sunday, read in one glance.")) { val digest = buildWeeklyDigest(days, effortDisplayFactor = factor) if (digest.isEmpty) { DataPendingNote( - title = "No readings this week yet", - body = "Wear your strap or import your WHOOP export in Data Sources. Once this week has a " + - "day or two of data, your week-in-review appears here.", + title = tr("No readings this week yet"), + body = tr("Wear your strap or import your WHOOP export in Data Sources. Once this week has a ") + + tr("day or two of data, your week-in-review appears here."), ) } else { NoopCard { WeeklyDigestContent(digest = digest, compact = false) } @@ -165,7 +165,7 @@ fun WeeklyDigestContent(digest: WeeklyDigest, compact: Boolean = false) { verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Overline("Week in review") + Overline(tr("Week in review")) Text(weekRangeLabel(digest), style = NoopType.title2, color = Palette.textPrimary) } Text( @@ -206,7 +206,7 @@ fun WeeklyDigestContent(digest: WeeklyDigest, compact: Boolean = false) { } Text(digest.balance.sentence, style = NoopType.footnote, color = Palette.textTertiary) Text( - "Informational only, not medical advice.", + tr("Informational only, not medical advice."), style = NoopType.footnote, color = Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/WhatsNewSheet.kt b/android/app/src/main/java/com/noop/ui/WhatsNewSheet.kt index 4e1f7e8a3..c69642bfe 100644 --- a/android/app/src/main/java/com/noop/ui/WhatsNewSheet.kt +++ b/android/app/src/main/java/com/noop/ui/WhatsNewSheet.kt @@ -94,14 +94,14 @@ private fun Header(onClose: () -> Unit) { modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Overline("What's new", color = Palette.textTertiary) + Overline(tr("What's new"), color = Palette.textTertiary) Text("NOOP ${AppChangelog.CURRENT_VERSION}", style = NoopType.display(26f), color = Palette.textPrimary) - Text("Release notes", style = NoopType.caption, color = Palette.textSecondary) + Text(tr("Release notes"), style = NoopType.caption, color = Palette.textSecondary) } IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) { Icon( Icons.Filled.Close, - contentDescription = "Close", + contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(22.dp), ) @@ -115,7 +115,7 @@ private fun Header(onClose: () -> Unit) { private fun ExpectationsCard() { NoopCard(padding = 20.dp, tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("What to expect") + Overline(tr("What to expect")) AppChangelog.expectations.forEach { e -> Row( modifier = Modifier.fillMaxWidth(), @@ -205,7 +205,7 @@ private fun Footer(onClose: () -> Unit) { contentColor = Palette.surfaceBase, ), ) { - Text("Got it", style = NoopType.captionNumber) + Text(tr("Got it"), style = NoopType.captionNumber) } } } diff --git a/android/app/src/main/java/com/noop/ui/WhoopModelComparisonScreen.kt b/android/app/src/main/java/com/noop/ui/WhoopModelComparisonScreen.kt index dd8aa984e..19985d6e4 100644 --- a/android/app/src/main/java/com/noop/ui/WhoopModelComparisonScreen.kt +++ b/android/app/src/main/java/com/noop/ui/WhoopModelComparisonScreen.kt @@ -125,10 +125,10 @@ fun WhoopModelComparisonScreen(onClose: () -> Unit) { private fun IntroCard() { NoopCard(padding = 20.dp, tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Both straps are supported", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("Both straps are supported"), style = NoopType.headline, color = Palette.textPrimary) Text( - "NOOP pairs with the WHOOP 4.0 and the WHOOP 5.0/MG. They share most of what matters (live " + - "heart rate, your scores, buzzing the strap), but a few firmware features differ. Here's " + + tr("NOOP pairs with the WHOOP 4.0 and the WHOOP 5.0/MG. They share most of what matters (live ") + + tr("heart rate, your scores, buzzing the strap), but a few firmware features differ. Here's ") + "what each can do, and why.", style = NoopType.subhead, color = Palette.textSecondary, @@ -141,10 +141,10 @@ private fun IntroCard() { private fun CapabilityTableCard() { NoopCard(padding = 20.dp) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { - Overline("Feature by strap") + Overline(tr("Feature by strap")) // Column header. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("Feature", style = NoopType.caption, color = Palette.textTertiary, modifier = Modifier.weight(1f)) + Text(tr("Feature"), style = NoopType.caption, color = Palette.textTertiary, modifier = Modifier.weight(1f)) Text("4.0", style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.Center, modifier = Modifier.width(48.dp)) Text("5/MG", style = NoopType.caption, color = Palette.textTertiary, textAlign = TextAlign.Center, modifier = Modifier.width(48.dp)) } @@ -191,10 +191,10 @@ private fun SupportGlyph(icon: ImageVector, tint: Color, label: String) { private fun ReassuranceCard() { NoopCard(padding = 20.dp, tint = Palette.metricCyan) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("On a WHOOP 4.0?", style = NoopType.headline, color = Palette.textPrimary) + Text(tr("On a WHOOP 4.0?"), style = NoopType.headline, color = Palette.textPrimary) Text( - "You're not missing the broadcast feature. To share your heart rate with a gym machine, " + - "Zwift, Peloton or a Garmin, open Data Sources and turn on \"Broadcast heart rate\": " + + tr("You're not missing the broadcast feature. To share your heart rate with a gym machine, ") + + tr("Zwift, Peloton or a Garmin, open Data Sources and turn on \"Broadcast heart rate\": ") + "your phone becomes a standard Bluetooth HR sensor using your strap's live reading. The " + "firmware-only flag a 5/MG has just does the same job from the strap instead of the phone.", style = NoopType.subhead, @@ -214,12 +214,12 @@ private fun Header(onClose: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Overline("Your strap", color = Palette.textTertiary) - Text("4.0 vs 5.0/MG", style = NoopType.display(26f), color = Palette.textPrimary) - Text("What each can read, and why", style = NoopType.caption, color = Palette.textSecondary) + Overline(tr("Your strap"), color = Palette.textTertiary) + Text(tr("4.0 vs 5.0/MG"), style = NoopType.display(26f), color = Palette.textPrimary) + Text(tr("What each can read, and why"), style = NoopType.caption, color = Palette.textSecondary) } IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) { - Icon(Icons.Filled.Close, contentDescription = "Close", tint = Palette.textTertiary, modifier = Modifier.size(22.dp)) + Icon(Icons.Filled.Close, contentDescription = tr("Close"), tint = Palette.textTertiary, modifier = Modifier.size(22.dp)) } } } @@ -231,7 +231,7 @@ private fun Footer(onClose: () -> Unit) { onClick = onClose, colors = ButtonDefaults.buttonColors(containerColor = Palette.accent, contentColor = Palette.surfaceBase), ) { - Text("Done", modifier = Modifier.padding(horizontal = 24.dp)) + Text(tr("Done"), modifier = Modifier.padding(horizontal = 24.dp)) } } } diff --git a/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt b/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt index 8b2fffdc5..9cb9688d9 100644 --- a/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt +++ b/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt @@ -366,8 +366,8 @@ object WorkoutEditing { /** Common sports offered when re-labelling a detected bout (the user can fine-tune via Edit). */ val relabelSports: List = listOf( - "Running", "Walking", "Cycling", "Strength Training", "Swimming", "Rowing", "Yoga", "HIIT", - "CrossFit", "Hiking", "Tennis", + tr("Running"), tr("Walking"), tr("Cycling"), tr("Strength Training"), tr("Swimming"), tr("Rowing"), tr("Yoga"), tr("HIIT"), + tr("CrossFit"), tr("Hiking"), tr("Tennis"), ) } diff --git a/android/app/src/main/java/com/noop/ui/WorkoutStart.kt b/android/app/src/main/java/com/noop/ui/WorkoutStart.kt index 8463f2a93..60e7782b5 100644 --- a/android/app/src/main/java/com/noop/ui/WorkoutStart.kt +++ b/android/app/src/main/java/com/noop/ui/WorkoutStart.kt @@ -78,12 +78,12 @@ fun StartWorkoutSheet(vm: AppViewModel, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Start a workout") }, + title = { Text(tr("Start a workout")) }, text = { Column { OutlinedTextField( value = query, onValueChange = { query = it }, - label = { Text("Search sport") }, singleLine = true, + label = { Text(tr("Search sport")) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) Column( @@ -113,7 +113,7 @@ fun StartWorkoutSheet(vm: AppViewModel, onDismiss: () -> Unit) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), ) { - Text("Track GPS route", style = NoopType.body, color = Palette.textPrimary) + Text(tr("Track GPS route"), style = NoopType.body, color = Palette.textPrimary) Spacer(Modifier.weight(1f)) Switch(checked = gpsOn, onCheckedChange = { gpsOn = it }) } @@ -132,7 +132,7 @@ fun StartWorkoutSheet(vm: AppViewModel, onDismiss: () -> Unit) { } }, dismissButton = { - OutlinedButton(onClick = onDismiss) { Text("Cancel") } + OutlinedButton(onClick = onDismiss) { Text(tr("Cancel")) } }, ) } @@ -174,7 +174,7 @@ fun WorkoutStartSection(vm: AppViewModel) { onClick = { showLiveWorkout = true }, contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp), colors = ButtonDefaults.outlinedButtonColors(contentColor = Palette.accent), - ) { Text("Open", style = NoopType.captionNumber) } + ) { Text(tr("Open"), style = NoopType.captionNumber) } Spacer(Modifier.width(8.dp)) Button( onClick = { vm.endWorkout() }, @@ -182,7 +182,7 @@ fun WorkoutStartSection(vm: AppViewModel) { colors = ButtonDefaults.buttonColors( containerColor = Palette.statusCritical, contentColor = Palette.surfaceBase, ), - ) { Text("End", style = NoopType.captionNumber) } + ) { Text(tr("End"), style = NoopType.captionNumber) } } } } else if (live.bonded) { @@ -193,7 +193,7 @@ fun WorkoutStartSection(vm: AppViewModel) { colors = ButtonDefaults.buttonColors( containerColor = Palette.accent, contentColor = Palette.surfaceBase, ), - ) { Text("Start workout", style = NoopType.captionNumber) } + ) { Text(tr("Start workout"), style = NoopType.captionNumber) } } if (showSportPicker) { diff --git a/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt b/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt index 1b7c86407..01c9ca03c 100644 --- a/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt @@ -197,8 +197,8 @@ fun WorkoutsScreen(vm: AppViewModel) { // scaffold is untouched. The All-Sessions list still lives inside its single enclosing card (appearance // is byte-identical) — see the report note on why it isn't flattened to top-level items here. LazyScreenScaffold( - title = "Workouts", - subtitle = "Every session, threaded together.", + title = tr("Workouts"), + subtitle = tr("Every session, threaded together."), // LIQUID SKY BACKDROP (the pilot pattern — LiquidScreenSky.kt): the time-of-day liquid sky settles // into the theme canvas behind the header + top rows (bled full-width up behind the status bar via // the scaffold's topBackground plumbing), and the cards float OVER it on the flat surface below. The @@ -322,7 +322,7 @@ private data class DialogTarget(val editing: WorkoutRow?) private fun EmptyWorkouts(loaded: Boolean, onAdd: () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { DataPendingNote( - title = "No workouts yet", + title = tr("No workouts yet"), body = "No workouts yet. They come from your WHOOP and Apple Health history. " + "Import in Data Sources to bring them in, or add one you tracked elsewhere.", ) @@ -373,7 +373,7 @@ private fun AddWorkoutButton(onAdd: () -> Unit) { ) { Icon(Icons.Filled.Add, contentDescription = null, tint = Palette.accent, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(6.dp)) - Text("Add workout", style = NoopType.subhead, color = Palette.accent) + Text(tr("Add workout"), style = NoopType.subhead, color = Palette.accent) } } @@ -454,12 +454,12 @@ private fun FilterBar( verticalAlignment = Alignment.CenterVertically, ) { FilterPillMenu( - title = filter.sport ?: "All sports", + title = filter.sport ?: tr("All sports"), active = filter.sport != null, - contentDescription = "Filter by sport", + contentDescription = tr("Filter by sport"), ) { dismiss -> DropdownMenuItem( - text = { Text("All sports", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("All sports"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { onSport(null); dismiss() }, ) availableSports.forEach { s -> @@ -470,12 +470,12 @@ private fun FilterBar( } } FilterPillMenu( - title = filter.sourceClass?.let { sourceFilterLabel(it) } ?: "All sources", + title = filter.sourceClass?.let { sourceFilterLabel(it) } ?: tr("All sources"), active = filter.sourceClass != null, - contentDescription = "Filter by source", + contentDescription = tr("Filter by source"), ) { dismiss -> DropdownMenuItem( - text = { Text("All sources", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("All sources"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { onSource(null); dismiss() }, ) SOURCE_FILTER_OPTIONS.forEach { opt -> @@ -491,12 +491,12 @@ private fun FilterBar( .clip(RoundedCornerShape(50)) .clickable(onClick = onClear) .padding(horizontal = 8.dp, vertical = 6.dp) - .semantics { contentDescription = "Clear filters" }, + .semantics { contentDescription = tr("Clear filters") }, verticalAlignment = Alignment.CenterVertically, ) { Icon(Icons.Filled.Close, contentDescription = null, tint = Palette.textSecondary, modifier = Modifier.size(14.dp)) Spacer(Modifier.width(4.dp)) - Text("Clear", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Clear"), style = NoopType.footnote, color = Palette.textSecondary) } } } @@ -507,11 +507,11 @@ private fun FilterBar( trailingIcon = { if (filter.search.isNotEmpty()) { IconButton(onClick = { onSearch("") }) { - Icon(Icons.Filled.Close, contentDescription = "Clear search", tint = Palette.textTertiary, modifier = Modifier.size(16.dp)) + Icon(Icons.Filled.Close, contentDescription = tr("Clear search"), tint = Palette.textTertiary, modifier = Modifier.size(16.dp)) } } }, - placeholder = { Text("Search sport", style = NoopType.body, color = Palette.textTertiary) }, + placeholder = { Text(tr("Search sport"), style = NoopType.body, color = Palette.textTertiary) }, singleLine = true, colors = workoutFieldColors(), modifier = Modifier.fillMaxWidth(), @@ -565,11 +565,11 @@ private fun MergeSportDialog(onDismiss: () -> Unit, onPick: (String) -> Unit) { AlertDialog( onDismissRequest = onDismiss, containerColor = Palette.surfaceOverlay, - title = { Text("Name the merged session", style = NoopType.title2, color = Palette.textPrimary) }, + title = { Text(tr("Name the merged session"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - "These sessions have no sport label yet. Pick one for the merged session.", + tr("These sessions have no sport label yet. Pick one for the merged session."), style = NoopType.footnote, color = Palette.textSecondary, ) @@ -578,11 +578,11 @@ private fun MergeSportDialog(onDismiss: () -> Unit, onPick: (String) -> Unit) { }, confirmButton = { TextButton(onClick = { if (sport.isNotBlank()) onPick(sport.trim()) }, enabled = sport.isNotBlank()) { - Text("Merge", style = NoopType.body, color = if (sport.isNotBlank()) Palette.accent else Palette.textTertiary) + Text(tr("Merge"), style = NoopType.body, color = if (sport.isNotBlank()) Palette.accent else Palette.textTertiary) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel", style = NoopType.body, color = Palette.textSecondary) } + TextButton(onClick = onDismiss) { Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) } @@ -642,7 +642,7 @@ private fun EffortHero( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(18.dp), ) { - Overline("Typical effort", color = Palette.effortColor) + Overline(tr("Typical effort"), color = Palette.effortColor) Box(modifier = Modifier.size(140.dp), contentAlignment = Alignment.Center) { LiquidVessel( value = fraction, @@ -678,8 +678,8 @@ private fun EffortHero( color = Palette.textPrimary, ) Row(horizontalArrangement = Arrangement.spacedBy(Metrics.gap), modifier = Modifier.fillMaxWidth()) { - HeroStat("Sessions", "${rows.size}", Palette.effortColor, Modifier.weight(1f)) - HeroStat("Active", oneDecimal(totalTimeH) + "h", Palette.textPrimary, Modifier.weight(1f)) + HeroStat(tr("Sessions"), "${rows.size}", Palette.effortColor, Modifier.weight(1f)) + HeroStat(tr("Active"), oneDecimal(totalTimeH) + "h", Palette.textPrimary, Modifier.weight(1f)) } Text( if (modal != null) "Mostly ${WorkoutEditing.displaySport(modal.sport)} (${effectiveRange.caption})." @@ -721,7 +721,7 @@ private fun SummarySection( { m -> StatTile( modifier = m, - label = "Total Workouts", + label = tr("Total Workouts"), value = "$totalCount", caption = effectiveRange.caption, accent = Palette.effortColor, @@ -730,7 +730,7 @@ private fun SummarySection( { m -> StatTile( modifier = m, - label = "Total Time", + label = tr("Total Time"), value = oneDecimal(totalTimeH) + "h", caption = "active", accent = Palette.textPrimary, @@ -739,7 +739,7 @@ private fun SummarySection( { m -> StatTile( modifier = m, - label = "Total Calories", + label = tr("Total Calories"), value = grouped(totalKcal), caption = "kcal", accent = Palette.metricAmber, @@ -748,7 +748,7 @@ private fun SummarySection( { m -> StatTile( modifier = m, - label = "Total Distance", + label = tr("Total Distance"), value = UnitFormatter.distanceFromKilometers(totalKm, unitSystem), caption = "covered", accent = Palette.metricCyan, @@ -757,7 +757,7 @@ private fun SummarySection( { m -> StatTile( modifier = m, - label = "Most Active", + label = tr("Most Active"), value = modal?.sport ?: "–", caption = modal?.let { "${it.count} session${if (it.count == 1) "" else "s"}" }, accent = Palette.textPrimary, @@ -782,8 +782,8 @@ private fun SummarySection( private fun BreakdownSection(groups: List, rows: List) { Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - title = "Activity Breakdown", - overline = "By sport", + title = tr("Activity Breakdown"), + overline = tr("By sport"), trailing = "${groups.size} sport${if (groups.size == 1) "" else "s"}", ) // This sport's own sessions, so each card can carry an HR-zone mini-bar. @@ -828,10 +828,10 @@ private fun SportCard(g: SportGroup, zones: ZoneSummary?) { CardDivider() // Identical 4-up stat strip for every card. Row(modifier = Modifier.fillMaxWidth()) { - MiniStat("Sessions", "${g.count}", Modifier.weight(1f)) - MiniStat("Time", oneDecimal(g.totalTimeH) + "h", Modifier.weight(1f)) - MiniStat("Kcal", grouped(g.totalKcal), Modifier.weight(1f), tint = Palette.metricAmber) - MiniStat("Avg/sess", "${g.avgTimePerSessionMin.roundToInt()}m", Modifier.weight(1f)) + MiniStat(tr("Sessions"), "${g.count}", Modifier.weight(1f)) + MiniStat(tr("Time"), oneDecimal(g.totalTimeH) + "h", Modifier.weight(1f)) + MiniStat(tr("Kcal"), grouped(g.totalKcal), Modifier.weight(1f), tint = Palette.metricAmber) + MiniStat(tr("Avg/sess"), "${g.avgTimePerSessionMin.roundToInt()}m", Modifier.weight(1f)) } } } @@ -858,8 +858,8 @@ private fun ZonesSection(rows: List) { val z = remember(rows) { zoneSummary(rows) } ?: return Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { SectionHeader( - title = "HR Zones", - overline = "Whoop import", + title = tr("HR Zones"), + overline = tr("Whoop import"), trailing = "${z.sessionsWithZones} of ${rows.size} session${if (rows.size == 1) "" else "s"}", ) NoopCard(tint = Palette.effortColor) { @@ -880,7 +880,7 @@ private fun ZonesSection(rows: List) { } } Text( - "Share of imported zone time, duration-weighted across sessions (approximate).", + tr("Share of imported zone time, duration-weighted across sessions (approximate)."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -948,7 +948,7 @@ private fun SessionsSection( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier.weight(1f)) { - SectionHeader(title = "All Sessions", overline = "Log", trailing = "${rows.size} total") + SectionHeader(title = tr("All Sessions"), overline = tr("Log"), trailing = "${rows.size} total") } if (anySelectable) SelectPill(selectionMode, onToggleSelectMode) } @@ -1010,12 +1010,12 @@ private fun SelectPill(selectionMode: Boolean, onToggle: () -> Unit) { .clickable(onClick = onToggle) .padding(horizontal = 12.dp, vertical = 6.dp) .semantics { - contentDescription = if (selectionMode) "Finish selecting" else "Select sessions to merge or delete" + contentDescription = if (selectionMode) tr("Finish selecting") else tr("Select sessions to merge or delete") }, verticalAlignment = Alignment.CenterVertically, ) { Text( - if (selectionMode) "Done" else "Select", + if (selectionMode) tr("Done") else tr("Select"), style = NoopType.footnote, color = if (selectionMode) Palette.effortColor else Palette.accent, ) @@ -1053,7 +1053,7 @@ private fun SelectionToolbar( ) Spacer(Modifier.weight(1f)) Text( - "Cancel", + tr("Cancel"), style = NoopType.subhead, color = Palette.textSecondary, modifier = Modifier.clickable(onClick = onCancel).padding(4.dp), @@ -1093,12 +1093,12 @@ private fun SessionHeaderRow(selectionMode: Boolean = false) { // #64: a leading spacer over the per-row selection glyph, so the columns stay aligned in select mode. if (selectionMode) Spacer(Modifier.width(30.dp)) // Weights mirror SessionRow (#157: Date widened for the time range, taken from Sport). - ColHeader("Date", Modifier.weight(1.7f), TextAlign.Start) - ColHeader("Sport", Modifier.weight(1.3f), TextAlign.Start) - ColHeader("Dur", Modifier.weight(1f), TextAlign.End) - ColHeader("HR", Modifier.weight(1.1f), TextAlign.End) - ColHeader("Kcal", Modifier.weight(1f), TextAlign.End) - ColHeader("Src", Modifier.weight(1f), TextAlign.End) + ColHeader(tr("Date"), Modifier.weight(1.7f), TextAlign.Start) + ColHeader(tr("Sport"), Modifier.weight(1.3f), TextAlign.Start) + ColHeader(tr("Dur"), Modifier.weight(1f), TextAlign.End) + ColHeader(tr("HR"), Modifier.weight(1.1f), TextAlign.End) + ColHeader(tr("Kcal"), Modifier.weight(1f), TextAlign.End) + ColHeader(tr("Src"), Modifier.weight(1f), TextAlign.End) // Trailing spacer column over the per-row overflow menu, so headers line up with the cells. Spacer(Modifier.width(32.dp)) } @@ -1281,16 +1281,16 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () SourceBadge(srcLabel, tint = srcTint) } CardDivider() - DetailRow("Time", timeRangeLabel(row.startTs, row.endTs)) - DetailRow("Duration", durationLabel(row.durationS)) - if (row.avgHr != null) DetailRow("Avg HR", "${row.avgHr} bpm") - if (row.maxHr != null) DetailRow("Max HR", "${row.maxHr} bpm") - if (row.energyKcal != null) DetailRow("Calories", "${grouped(row.energyKcal)} kcal") + DetailRow(tr("Time"), timeRangeLabel(row.startTs, row.endTs)) + DetailRow(tr("Duration"), durationLabel(row.durationS)) + if (row.avgHr != null) DetailRow(tr("Avg HR"), "${row.avgHr} bpm") + if (row.maxHr != null) DetailRow(tr("Max HR"), "${row.maxHr} bpm") + if (row.energyKcal != null) DetailRow(tr("Calories"), "${grouped(row.energyKcal)} kcal") if (row.distanceM != null) { val unitSystem = UnitPrefs.system(LocalContext.current) - DetailRow("Distance", UnitFormatter.distanceFromKilometers(row.distanceM / 1000.0, unitSystem)) + DetailRow(tr("Distance"), UnitFormatter.distanceFromKilometers(row.distanceM / 1000.0, unitSystem)) } - if (!row.notes.isNullOrBlank()) DetailRow("Notes", row.notes) + if (!row.notes.isNullOrBlank()) DetailRow(tr("Notes"), row.notes) // #796 - per-session Effort contribution. The session's captured strain re-homed from a plain // value row into a prominent Effort-amber card (the big count-up value + the "This session" @@ -1307,7 +1307,7 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () // HR curve over the session window (#410). A faint baseline shows under 2 points. if (hrCurve.size > 1) { CardDivider() - Overline("Heart rate") + Overline(tr("Heart rate")) LineChart( values = hrCurve, modifier = Modifier.height(Metrics.compactChartHeight), @@ -1317,9 +1317,9 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { val lo = hrCurve.minOrNull()?.roundToInt() ?: 0 val hi = hrCurve.maxOrNull()?.roundToInt() ?: 0 - MiniStat("Avg", row.avgHr?.let { "$it bpm" } ?: "–", Modifier.weight(1f)) - MiniStat("Peak", (row.maxHr ?: hi).let { "$it bpm" }, Modifier.weight(1f)) - MiniStat("Low", "$lo bpm", Modifier.weight(1f)) + MiniStat(tr("Avg"), row.avgHr?.let { "$it bpm" } ?: "–", Modifier.weight(1f)) + MiniStat(tr("Peak"), (row.maxHr ?: hi).let { "$it bpm" }, Modifier.weight(1f)) + MiniStat(tr("Low"), "$lo bpm", Modifier.weight(1f)) } // #18: the Avg HR shown above can be EDITED on the manual sheet while the graph, zones and // Effort stay from the recorded session (preservingCaptured keeps the captured strain/zones). @@ -1330,7 +1330,7 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () val captured = row.strain != null || !row.zonesJSON.isNullOrEmpty() if (captured && row.avgHr != null && kotlin.math.abs(row.avgHr - traceMean) > 3.0) { Text( - "The average above was edited. The graph, zones and Effort stay from the recorded session.", + tr("The average above was edited. The graph, zones and Effort stay from the recorded session."), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1343,9 +1343,9 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () if (total > 0.0) { CardDivider() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Overline("HR zones", modifier = Modifier.weight(1f)) + Overline(tr("HR zones"), modifier = Modifier.weight(1f)) Text( - if (zonesFromImport) "Whoop import" else "From strap HR", + if (zonesFromImport) tr("Whoop import") else tr("From strap HR"), style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1359,7 +1359,7 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () z.forEachIndexed { i, m -> ZoneStat(i + 1, m, total, Modifier.weight(1f)) } } Text( - if (zonesFromImport) "WHOOP's imported per-zone split for this session." + if (zonesFromImport) tr("WHOOP's imported per-zone split for this session.") else "Time in each %HRmax zone, derived from the strap's heart rate over this window (approximate).", style = NoopType.footnote, color = Palette.textTertiary, @@ -1381,7 +1381,7 @@ private fun WorkoutDetailSheet(vm: AppViewModel, row: WorkoutRow, onDismiss: () private fun SessionEffortCard(strain: Double, effortScale: EffortScale) { val shown = UnitFormatter.effortValue(strain, effortScale) Column(verticalArrangement = Arrangement.spacedBy(Metrics.space8)) { - SectionHeader("Effort", overline = "This session") + SectionHeader(tr("Effort"), overline = tr("This session")) NoopCard(tint = Palette.effortColor) { Row( verticalAlignment = Alignment.CenterVertically, @@ -1403,13 +1403,13 @@ private fun SessionEffortCard(strain: Double, effortScale: EffortScale) { color = Palette.effortBright, ) Text( - if (effortScale == EffortScale.WHOOP) "strain (0-21)" else "Effort (0-100)", + if (effortScale == EffortScale.WHOOP) tr("strain (0-21)") else tr("Effort (0-100)"), style = NoopType.footnote, color = Palette.textTertiary, ) } Text( - "This session's contribution to the day's Effort, as captured during the workout.", + tr("This session's contribution to the day's Effort, as captured during the workout."), style = NoopType.subhead, color = Palette.textSecondary, modifier = Modifier.weight(1f), @@ -1456,38 +1456,38 @@ private fun RowActionsMenu( var relabelOpen by remember { mutableStateOf(false) } Box { IconButton(onClick = { open = true }, modifier = Modifier.size(32.dp)) { - Icon(Icons.Filled.MoreVert, contentDescription = "Workout actions", + Icon(Icons.Filled.MoreVert, contentDescription = tr("Workout actions"), tint = Palette.textTertiary, modifier = Modifier.size(18.dp)) } DropdownMenu(expanded = open, onDismissRequest = { open = false }) { when (WorkoutEditing.classify(row.source)) { WorkoutSource.DETECTED -> { DropdownMenuItem( - text = { Text("Re-label as…", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("Re-label as…"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { open = false; relabelOpen = true }, ) DropdownMenuItem( - text = { Text("Edit details…", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("Edit details…"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { open = false; onEdit(row) }, ) DropdownMenuItem( - text = { Text("Dismiss (not a workout)", style = NoopType.body, color = Palette.statusCritical) }, + text = { Text(tr("Dismiss (not a workout)"), style = NoopType.body, color = Palette.statusCritical) }, onClick = { open = false; onDismiss(row) }, ) } WorkoutSource.MANUAL -> { DropdownMenuItem( - text = { Text("Edit…", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("Edit…"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { open = false; onEdit(row) }, ) DropdownMenuItem( - text = { Text("Delete", style = NoopType.body, color = Palette.statusCritical) }, + text = { Text(tr("Delete"), style = NoopType.body, color = Palette.statusCritical) }, onClick = { open = false; onDelete(row) }, ) } WorkoutSource.WHOOP, WorkoutSource.APPLE, WorkoutSource.LIFTING, WorkoutSource.ACTIVITY_FILE -> { DropdownMenuItem( - text = { Text("Duplicate as manual…", style = NoopType.body, color = Palette.textPrimary) }, + text = { Text(tr("Duplicate as manual…"), style = NoopType.body, color = Palette.textPrimary) }, onClick = { open = false; onEdit(row.copy(source = "manual", sport = WorkoutEditing.displaySport(row.sport))) }, ) } @@ -1595,7 +1595,7 @@ private fun ManualWorkoutDialog( ) } Spacer(Modifier.width(10.dp)) - Text(if (editing == null) "Add Workout" else "Edit Workout", + Text(if (editing == null) tr("Add Workout") else tr("Edit Workout"), style = NoopType.title2, color = Palette.textPrimary) } }, @@ -1603,12 +1603,12 @@ private fun ManualWorkoutDialog( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { SportPickerField(sport, onChange = { sport = it }) StartTimeField(startMillis, onPick = { startMillis = it }) - DialogField("Duration (minutes)", durationMin, onChange = { durationMin = it }, numeric = true) - DialogField("Avg HR (bpm, optional)", avgHr, onChange = { avgHr = it }, numeric = true) - DialogField("Calories (kcal, optional)", kcal, onChange = { kcal = it }, numeric = true) + DialogField(tr("Duration (minutes)"), durationMin, onChange = { durationMin = it }, numeric = true) + DialogField(tr("Avg HR (bpm, optional)"), avgHr, onChange = { avgHr = it }, numeric = true) + DialogField(tr("Calories (kcal, optional)"), kcal, onChange = { kcal = it }, numeric = true) if (built == null) { Text( - "Enter a sport, a positive duration (≤ 24h), and valid HR (25-250) / calories (0-20,000).", + tr("Enter a sport, a positive duration (≤ 24h), and valid HR (25-250) / calories (0-20,000)."), style = NoopType.footnote, color = Palette.statusWarning, ) } @@ -1618,7 +1618,7 @@ private fun ManualWorkoutDialog( // We do NOT re-score from one number. Parity with macOS ManualWorkoutSheet.avgHrEditedNote. if (built != null && WorkoutEditing.avgHrEdited(built, editing)) { Text( - "Avg HR is shown as typed. The HR graph, zones and Effort stay from the recorded session.", + tr("Avg HR is shown as typed. The HR graph, zones and Effort stay from the recorded session."), style = NoopType.footnote, color = Palette.statusWarning, ) } @@ -1634,13 +1634,13 @@ private fun ManualWorkoutDialog( c == WorkoutSource.MANUAL || c == WorkoutSource.DETECTED } TextButton(onClick = { built?.let { onSave(it, replacing) } }, enabled = built != null) { - Text(if (editing == null) "Add" else "Save", + Text(if (editing == null) tr("Add") else tr("Save"), style = NoopType.body, color = if (built != null) Palette.accent else Palette.textTertiary) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel", style = NoopType.body, color = Palette.textSecondary) + Text(tr("Cancel"), style = NoopType.body, color = Palette.textSecondary) } }, ) @@ -1664,7 +1664,7 @@ private fun StartTimeField(millis: Long, onPick: (Long) -> Unit) { val context = LocalContext.current val label = remember(millis) { SimpleDateFormat("d MMM yyyy, h:mm a", Locale.US).format(java.util.Date(millis)) } Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Text("Started", style = NoopType.footnote, color = Palette.textSecondary) + Text(tr("Started"), style = NoopType.footnote, color = Palette.textSecondary) Row( modifier = Modifier .fillMaxWidth() @@ -1713,7 +1713,7 @@ private fun SportPickerField(value: String, onChange: (String) -> Unit) { val exact = WorkoutSport.all.any { it.name.equals(q, ignoreCase = true) } val showList = matches.isNotEmpty() && !exact - DialogField("Sport", value, onChange = onChange, placeholder = "e.g. Running") + DialogField(tr("Sport"), value, onChange = onChange, placeholder = tr("e.g. Running")) if (showList) { Column( modifier = Modifier diff --git a/android/app/src/main/res/raw/fr_strings.json b/android/app/src/main/res/raw/fr_strings.json index 389907da1..9e2e29f96 100644 --- a/android/app/src/main/res/raw/fr_strings.json +++ b/android/app/src/main/res/raw/fr_strings.json @@ -255,6 +255,7 @@ "A calm hour now helps you hit your wake time well-rested.": "Une heure calme maintenant vous aide à atteindre votre heure de réveil bien reposé.", "A calm nudge %lldh %lldm before your wake time.": "Un rappel discret %1$lldh %2$lldm avant votre heure de réveil.", "A cardiovascular load in the Banister TRIMP family: time spent in each heart-rate zone, weighted so harder zones count for more, summed into one daily figure.": "Une charge cardiovasculaire de la famille Banister TRIMP : le temps passé dans chaque zone de fréquence cardiaque, pondéré pour que les zones plus intenses comptent davantage, additionné en un seul chiffre quotidien.", +"A classic one-glance read of NOOP's own scores. Same data, different lens.": "Une lecture claire, en un coup d'œil, des scores de NOOP. Mêmes données, autre angle.", "A clean, shareable one-page PDF of your recovery, sleep, HRV, resting heart rate and strain over a date range. Saved on your %@. Nothing leaves the device.": "Un PDF d'une page, clair et partageable, de votre récupération, sommeil, VFC, fréquence cardiaque au repos et effort sur une période donnée. Enregistré sur votre %@. Rien ne quitte l'appareil.", "A couple of metrics depend on which Apple Watch you wear. Wrist temperature, which feeds skin temp, arrived with Series 8, so older watches don't report it. Blood oxygen is the bigger one: Apple removed the SpO₂ sensor from the newest US units over a patent dispute, so those simply don't measure it.": "Quelques métriques dépendent de l'Apple Watch que vous portez. La température du poignet, qui alimente la température cutanée, est arrivée avec la Series 8 ; les montres plus anciennes ne la fournissent donc pas. L'oxygène sanguin est le cas le plus notable : Apple a retiré le capteur SpO₂ des derniers modèles américains à la suite d'un litige de brevet, ces derniers ne le mesurent donc tout simplement pas.", "A couple of metrics depend on your model": "Quelques métriques dépendent de votre modèle", @@ -396,7 +397,10 @@ "Apple's sleep stages are strong, and they drive your Rest score directly.": "Les phases de sommeil d'Apple sont fiables et alimentent directement votre score de Repos.", "Apple-logged": "Enregistré par Apple", "Apr": "Avr", +"Armed on the strap itself with the experimental 5/MG command. A strap-driven wake is still unconfirmed on 5/MG on our side (confirmed only on WHOOP 4.0), so keep a backup alarm for anything you truly can't miss.": "Armée sur le bracelet via la commande expérimentale 5/MG. Un réveil piloté par le bracelet n'est pas encore confirmé sur 5/MG de notre côté (confirmé seulement sur WHOOP 4.0) — garde une alarme de secours pour ce que tu ne peux pas manquer.", +"Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.": "Armée sur le bracelet, donc elle peut vibrer à l'heure de réveil même si le téléphone dort ou que NOOP est fermé. Envoie exactement la commande d'alarme de l'appli officielle, vibration confirmée sur une vraie WHOOP 4.0 (capture communautaire + test sur appareil, #535). Garde une alarme de secours pour ce que tu ne peux pas manquer.", "Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. We send the same alarm command the official app sends, but a strap-driven wake-up hasn't been confirmed on our side yet, so please keep a backup alarm for now.": "Armée sur le bracelet lui-même, elle peut donc vibrer à votre heure de réveil même si votre téléphone est en veille ou NOOP fermé. Nous envoyons la même commande d'alarme que l'application officielle, mais un réveil piloté par le bracelet n'a pas encore été confirmé de notre côté, alors gardez pour l'instant une alarme de secours.", +"Arms the strap to buzz at your wake time, even if NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.": "Arme le bracelet pour vibrer à l'heure de réveil, même si NOOP est fermé. Envoie exactement la commande d'alarme de l'appli officielle, vibration confirmée sur une vraie WHOOP 4.0 (capture communautaire + test sur appareil, #535). Garde une alarme de secours pour ce que tu ne peux pas manquer.", "Arms the strap to buzz at your wake time, even if NOOP is closed. Still experimental on WHOOP 4.0, so keep a backup alarm until you've confirmed it wakes you.": "Arme le bracelet pour vibrer à votre heure de réveil, même si NOOP est fermé. Encore expérimental sur WHOOP 4.0, gardez donc une alarme de secours jusqu'à ce que vous ayez confirmé qu'il vous réveille.", "Ask Coach about your data…": "Demandez à Coach à propos de vos données…", "Ask about your charge, effort, rest and workouts, grounded in your own numbers.": "Posez des questions sur votre charge, votre effort, votre repos et vos entraînements, ancrées dans vos propres chiffres.", @@ -436,6 +440,7 @@ "BANISTER TRIMP / HR ZONES": "BANISTER TRIMP / ZONES DE FC", "BEAT-TO-BEAT": "BATTEMENT PAR BATTEMENT", "BEAT-TO-BEAT SCATTER": "NUAGE BATTEMENT PAR BATTEMENT", +"BEATS PER MINUTE": "BATTEMENTS PAR MINUTE", "BEATS READ": "BATTEMENTS LUS", "BMI": "IMC", "Back": "Retour", @@ -458,6 +463,7 @@ "Baevsky Stress Index": "Indice de stress de Baevsky", "Balance": "Équilibre", "Balanced": "Équilibré", +"Band": "Bracelet", "Based mostly on typical patterns, not yet yours. Log a few more %@ days and this becomes yours.": "Basé principalement sur des tendances types, pas encore les vôtres. Enregistrez encore quelques jours de %@ et cela deviendra le vôtre.", "Baseline": "Référence", "Baselines are learned on-device over your first 14 days. Until then, typical ranges apply.": "Les références sont apprises sur l'appareil au cours de vos 14 premiers jours. D'ici là, des plages types s'appliquent.", @@ -478,6 +484,7 @@ "Below Zone 1": "Sous la zone 1", "Below need": "Sous les besoins", "Beta": "Bêta", +"Beta feature": "Fonction bêta", "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, and HRV needs you to be still. No Oura Readiness or SpO₂ percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO₂ ne provient de la bague (importez un fichier Oura pour cela).", "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, les pas sont un simple comptage de mouvement, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO2 ne provient de la bague (importez un fichier Oura pour cela).", "Beta. Read this first.": "Bêta. Lisez ceci d'abord.", @@ -599,6 +606,8 @@ "Change folder": "Changer de dossier", "Change marker": "Changer de marqueur", "Change photo": "Changer la photo", +"Change to Number": "Passer en nombre", +"Change to Yes/No": "Passer en Oui/Non", "Changes the Bluetooth name your WHOOP 4.0 advertises (what you see when pairing). The strap reboots to apply, so the new name appears the next time it connects. WHOOP 4.0 only.": "Modifie le nom Bluetooth que diffuse votre WHOOP 4.0 (celui que vous voyez lors de l'appairage). Le bracelet redémarre pour appliquer le changement, le nouveau nom apparaît donc à la prochaine connexion. WHOOP 4.0 uniquement.", "Charge %lld out of 100": "Charge %lld sur 100", "Charge (all history)": "Charge (tout l'historique)", @@ -643,6 +652,7 @@ "Choose Mi Fitness export…": "Choisir l'export Mi Fitness…", "Choose a backup": "Choisir une sauvegarde", "Choose a folder for NOOP backups (for example a Google Drive or iCloud folder).": "Choisissez un dossier pour les sauvegardes NOOP (par exemple un dossier Google Drive ou iCloud).", +"Choose a markers CSV file to import": "Choisir un fichier CSV de marqueurs à importer", "Choose a wearable signal to compare": "Choisissez un signal d'appareil connecté à comparer", "Choose export.zip…": "Choisir export.zip…", "Choose export…": "Choisir l'export…", @@ -761,6 +771,7 @@ "Couldn't write the strap log right now.": "Impossible d'écrire le journal du bracelet pour le moment.", "Counter ticks per step. Leave at 1.0 unless your steps run high. On a WHOOP 5/MG they can run very high (10× or more), so this goes up to 30. Walk a known 1,000 steps and divide NOOP's count by the real count to get your value.": "Impulsions du compteur par pas. Laissez à 1,0 sauf si votre nombre de pas est trop élevé. Sur un WHOOP 5/MG, il peut être très élevé (10× ou plus), d'où une plage allant jusqu'à 30. Marchez 1 000 pas connus et divisez le comptage de NOOP par le nombre réel pour obtenir votre valeur.", "Crunching your raw streams…": "Traitement de vos flux bruts…", +"Cues sent": "Signaux envoyés", "Cup": "Tasse", "Current calibration": "Calibration actuelle", "Current charge": "Charge actuelle", @@ -909,6 +920,7 @@ "Encrypted": "Chiffré", "Encrypted stream: deep controls and history sync available.": "Flux chiffré : commandes avancées et synchronisation de l'historique disponibles.", "End": "Terminer", +"End session": "Terminer la séance", "End the experiment plan. Journal and metric history stay untouched.": "Terminer le plan d'expérience. Le journal et l'historique des métriques restent intacts.", "End workout": "Terminer l'entraînement", "Enjoying NOOP?": "Vous appréciez NOOP ?", @@ -1026,6 +1038,7 @@ "From the watch's wrist-temperature sensor during sleep, on Series 8 and later. Older models don't have the sensor, so it reads “not available” rather than zero.": "Depuis le capteur de température au poignet de la montre pendant le sommeil, sur Series 8 et ultérieures. Les modèles plus anciens n'ont pas le capteur, il affiche donc « non disponible » plutôt que zéro.", "From your nightly sensor": "Depuis votre capteur nocturne", "From your nightly temperature": "Depuis votre température nocturne", +"Full day": "Journée entière", "Full strap stream is active.": "Le flux complet du bracelet est actif.", "Full sweep · ~13 min": "Balayage complet · ~13 min", "Fully quit and reopen NOOP to load it.": "Fermez complètement puis rouvrez NOOP pour le charger.", @@ -1091,6 +1104,7 @@ "Haptic-paced breathing · watch your HRV respond": "Respiration rythmée haptiquement · observez votre VFC réagir", "Haptics on": "Haptiques activés", "Hard to read right now": "Difficile à lire pour le moment", +"Heads up: this test mode is off, so the report has no capture for it. For a useful report, turn the mode on, reproduce the problem while wearing the strap, then report again.": "À noter : ce mode de test est désactivé, le rapport ne contient donc aucune capture pour lui. Pour un rapport utile, active le mode, reproduis le problème en portant le bracelet, puis renvoie le rapport.", "Heads-up": "Attention", "Heads-up. Your WHOOP only talks to one phone at a time. Force-quit the official WHOOP app first, or pairing may fail.": "Attention. Votre WHOOP ne communique qu'avec un seul téléphone à la fois. Forcez d'abord la fermeture de l'application WHOOP officielle, sinon le jumelage risque d'échouer.", "Health": "Santé", @@ -1122,6 +1136,7 @@ "Height in inches": "Taille en pouces", "Height, %lld feet %lld inches": "Taille, %1$lld pieds %2$lld pouces", "Help & Contact": "Aide et contact", +"Help, contact, and attribution.": "Aide, contact et crédits.", "Helping most: %@": "Aide le plus : %@", "Hidden": "Masqué", "Hide %@": "Masquer %@", @@ -1130,6 +1145,7 @@ "High": "Élevé", "History": "Historique", "History fills the dashboard immediately": "L'historique remplit immédiatement le tableau de bord", +"History stays under the original question so WHOOP imports still line up.": "L'historique reste sous la question d'origine pour que les imports WHOOP restent alignés.", "History sync": "Synchronisation de l'historique", "History sync is experimental on 5.0.": "La synchronisation de l'historique est expérimentale sur 5.0.", "History synced": "Historique synchronisé", @@ -1164,6 +1180,7 @@ "How this score is calculated": "Comment ce score est calculé", "How this works": "Comment ça fonctionne", "How to read this": "Comment lire ceci", +"How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin temperature are measured from the strap (skin temp is shown as the deviation from your own baseline). Workouts is the count of activities you logged or that were detected. Recovery, Strain and Stress are NOOP's own on-device scores, not clinical measures: Recovery is a daily readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load derived from heart rate, and Stress is a 0-3 autonomic-load index from resting HR and HRV.": "Comment lire ceci : la VFC, la FC au repos, la durée de sommeil, la fréquence respiratoire et la température cutanée sont mesurées par le bracelet (la temp. cutanée est montrée comme l'écart à ta référence). Entraînements est le nombre d'activités enregistrées ou détectées. Récupération, Effort et Stress sont les scores calculés par NOOP sur l'appareil, pas des mesures cliniques : la Récupération est un indice de forme du jour (VFC, FC au repos, sommeil et tendance de temp. cutanée), l'Effort est la charge cardiovasculaire dérivée de la fréquence cardiaque, et le Stress est un indice de charge autonome de 0 à 3 issu de la FC au repos et de la VFC.", "How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin temperature are measured from the strap (skin temp is shown as the deviation from your own baseline). Workouts is the count of activities you logged or that were detected. Recovery, Strain and Stress are NOOP's own on-device scores, not clinical measures: Recovery is a daily readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load derived from heart rate, and Stress is a 0–3 autonomic-load index from resting HR and HRV.": "Comment lire ceci : la VFC, la FC au repos, la durée du sommeil, la fréquence respiratoire et la température cutanée sont mesurées par le bracelet (la température cutanée est affichée comme un écart par rapport à votre propre référence). Entraînements correspond au nombre d'activités que vous avez enregistrées ou qui ont été détectées. Récupération, Effort et Stress sont des scores propres à NOOP calculés sur l'appareil, pas des mesures cliniques : la Récupération est un indice composite de forme quotidienne (VFC, FC au repos, sommeil et tendance de température cutanée), l'Effort est la charge cardiovasculaire dérivée de la fréquence cardiaque, et le Stress est un indice de charge autonome de 0 à 3 basé sur la FC au repos et la VFC.", "How your scores are computed": "Comment vos scores sont calculés", "How your scores work": "Comment fonctionnent vos scores", @@ -1259,6 +1276,7 @@ "LIGHT": "LÉGER", "LIVE HR ONLY": "FC EN DIRECT UNIQUEMENT", "LIVE PHYSIOLOGY": "PHYSIOLOGIE EN DIRECT", +"LIVE SESSION": "SÉANCE EN DIRECT", "LOCAL ONLY": "LOCAL UNIQUEMENT", "LONG AXIS": "GRAND AXE", "LOW": "FAIBLE", @@ -1321,12 +1339,15 @@ "Light Sleep": "Sommeil léger", "Likely your Oura warranty and support, because the ring is no longer paired to Oura. Treat this as permanent.": "Probablement votre garantie et le support Oura, car la bague n'est plus jumelée à Oura. Considérez ceci comme permanent.", "Lining them up…": "Alignement en cours…", +"Liquid Today (prototype)": "Liquid Today (prototype)", "Live": "En direct", "Live Body Console": "Console corporelle en direct", "Live HR": "FC en direct", "Live HR (not fully paired)": "FC en direct (jumelage incomplet)", "Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery (those are WHOOP-only).": "FC en direct + R-R uniquement · pas de sommeil, récupération, température cutanée, SpO₂, pas ou batterie (réservés à WHOOP).", "Live HR works. Free the strap to unlock buzz, alarms & sync": "La FC en direct fonctionne. Libérez le bracelet pour débloquer vibrations, alarmes et synchro", +"Live Session": "Séance en direct", +"Live Sessions (beta)": "Séances en direct (bêta)", "Live heart rate": "Fréquence cardiaque en direct", "Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap, WHOOP 4.0 is never affected.": "La fréquence cardiaque en direct fonctionne déjà sur un bracelet WHOOP 5/MG. Ces sondes vont plus loin et essaient d'en extraire davantage. Ce sont des estimations, désactivées par défaut, et elles ne touchent jamais qu'un bracelet 5/MG, le WHOOP 4.0 n'est jamais affecté.", "Live heart rate already works on a WHOOP 5/MG strap. These probes go further and try to coax more out of it. They are guesses, off by default, and only ever touch a 5/MG strap. WHOOP 4.0 is never affected.": "La fréquence cardiaque en direct fonctionne déjà sur un bracelet WHOOP 5/MG. Ces sondes vont plus loin et tentent d'en tirer davantage. Ce sont des suppositions, désactivées par défaut, qui ne touchent jamais qu'un bracelet 5/MG. WHOOP 4.0 n'est jamais concerné.", @@ -1372,6 +1393,8 @@ "Logged wake-up at %@.": "Réveil enregistré à %@.", "Logging ahead for tomorrow: today's activities inform tomorrow's recovery, just as yesterday's are reflected in today's. Tomorrow's answers line up with tomorrow's morning.": "Enregistrement anticipé pour demain : les activités d'aujourd'hui influencent la récupération de demain, tout comme celles d'hier se reflètent dans celle d'aujourd'hui. Les réponses de demain correspondront au matin de demain.", "Long exhale · downshift to rest": "Expiration longue · bascule vers le repos", +"Long press to show or hide your heart rate.": "Appui long pour afficher ou masquer ta fréquence cardiaque.", +"Longer ranges unlock as more history builds.": "Les plages plus longues se débloquent à mesure que l'historique s'étoffe.", "Looks like a workout": "Ressemble à un entraînement", "Looks like a workout around %@–%@ (avg HR %lld, %lld min). Save it?": "Ressemble à un entraînement vers %@–%@ (FC moy. %lld, %lld min). L'enregistrer ?", "Looks like a workout on %@ around %@–%@ (avg HR %lld, %lld min). Save it?": "Ressemble à un entraînement le %1$@ vers %2$@–%3$@ (FC moy. %4$lld, %5$lld min). L'enregistrer ?", @@ -1457,6 +1480,7 @@ "Move %@ up": "Déplacer %@ vers le haut", "Move all your NOOP data to another machine. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces %@'s data with a backup.": "Déplacez toutes vos données NOOP vers un autre appareil. L'export enregistre tout (historique, sommeils, entraînements, paramètres) dans un seul fichier que vous pouvez copier ; l'import remplace les données de %@ par une sauvegarde.", "Move all your NOOP data to another machine. Export saves everything, history, sleeps, workouts, settings, to a single file you can copy across; import replaces this Mac's data with a backup.": "Transférez toutes vos données NOOP vers une autre machine. L'export sauvegarde tout, historique, nuits, entraînements, paramètres, dans un seul fichier à copier ; l'import remplace les données de ce Mac par une sauvegarde.", +"Move anyway": "Déplacer quand même", "Move reminder": "Rappel de mouvement", "Movement": "Mouvement", "Mute wrist alerts overnight.": "Couper les alertes au poignet pendant la nuit.", @@ -1472,6 +1496,7 @@ "NOOP can tap your wrist when your %@ needs you. No glance at the screen required.": "NOOP peut tapoter votre poignet quand votre %@ a besoin de vous. Pas besoin de regarder l'écran.", "NOOP computed": "Calculé par NOOP", "NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.": "NOOP estime vos pas à partir du mouvement de votre WHOOP, calibré sur le nombre de pas de votre téléphone. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas.", +"NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": "NOOP te donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de ton bracelet avec des méthodes de science du sport publiées et revues par les pairs, et calculés entièrement sur ton appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les trois mêmes questions avec de la science ouverte, donc ils suivront généralement la même direction que ceux de WHOOP, sans coïncider chiffre pour chiffre. Et c'est justement l'idée.", "NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0–100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": "NOOP vous donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de votre bracelet en utilisant une science du sport publiée et évaluée par des pairs, et calculés entièrement sur votre appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les mêmes trois questions avec une science ouverte, donc ils suivront généralement la direction de ceux de WHOOP, mais ne correspondront pas chiffre pour chiffre. Et c'est bien le but.", "NOOP is a personal, open project: not the WHOOP app, and not affiliated with WHOOP. It reads a strap you own, on your own device. Treat it as a capable work-in-progress rather than a finished product.": "NOOP est un projet personnel et ouvert : ce n'est pas l'application WHOOP et il n'est pas affilié à WHOOP. Il lit un bracelet que vous possédez, sur votre propre appareil. Considérez-le comme un travail en cours capable plutôt qu'un produit fini.", "NOOP is an unofficial project: not affiliated with, endorsed by, or sponsored by WHOOP, Inc. \"WHOOP\" is their trademark, used only to name the hardware NOOP works with.": "NOOP est un projet non officiel : il n'est ni affilié, ni approuvé, ni sponsorisé par WHOOP, Inc. « WHOOP » est leur marque déposée, utilisée uniquement pour nommer le matériel avec lequel NOOP fonctionne.", @@ -1528,6 +1553,7 @@ "Never": "Jamais", "New data added": "Nouvelles données ajoutées", "New here?": "Nouveau ici ?", +"New item group": "Nouveau groupe d'éléments", "New release notes and fresh data will land here.": "Les nouvelles notes de version et les données récentes arriveront ici.", "New strap name": "Nouveau nom du bracelet", "Newer WHOOP band, experimental in NOOP": "Bracelet WHOOP plus récent, expérimental dans NOOP", @@ -1592,6 +1618,7 @@ "No respiratory-rate value": "Aucune valeur de fréquence respiratoire", "No resting HR value": "Aucune valeur de fréquence cardiaque au repos", "No scored days in this window. Widen the range or import more history.": "Aucun jour avec score dans cette période. Élargissez la plage ou importez plus d'historique.", +"No sleep tracked last night": "Aucun sommeil enregistré la nuit dernière", "No sources yet": "Aucune source pour l'instant", "No stage data recorded for this night.": "Aucune donnée de phase enregistrée pour cette nuit.", "No strap connected": "Aucun bracelet connecté", @@ -1603,6 +1630,7 @@ "No warranty; liability limited": "Aucune garantie ; responsabilité limitée", "No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or skin-temp readings fell inside %@. Wear your strap a few more days, or pick a wider range, then export again.": "Aucune mesure d'entraînement, de stress, de récupération, de sommeil, de VFC, de fréquence cardiaque au repos, d'effort, de fréquence respiratoire ou de température cutanée ne se situait dans %@. Portez votre bracelet quelques jours de plus, ou choisissez une plage plus large, puis exportez à nouveau.", "No workouts found. Point at a Hevy CSV export or a Liftosaur JSON export.": "Aucun entraînement trouvé. Indiquez un export CSV Hevy ou un export JSON Liftosaur.", +"No workouts yet": "Pas encore d'entraînements", "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in, or add one you tracked elsewhere.": "Aucun entraînement pour l'instant. Ils proviennent de l'historique de votre WHOOP et d'Apple Santé. Importez dans Sources de données pour les récupérer, ou ajoutez-en un que vous avez suivi ailleurs.", "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in.": "Pas encore d'entraînements. Ils proviennent de votre historique WHOOP et Apple Santé. Importez dans Sources de données pour les intégrer.", "Non-binary": "Non-binaire", @@ -1797,7 +1825,9 @@ "Primed": "Prêt", "Probably not illness": "Probablement pas une maladie", "Profile": "Profil", +"Profile and settings": "Profil et réglages", "Profile photo": "Photo de profil", +"Progress toward today's goal": "Progression vers l'objectif du jour", "Project home & source": "Page et code source du projet", "Project home and source code on GitHub": "Page et code source du projet sur GitHub", "Proof that the console is current": "Preuve que la console est à jour", @@ -1820,6 +1850,7 @@ "R-R intervals": "Intervalles R-R", "RECORDING": "ENREGISTREMENT", "RECORDING WORKOUT": "ENREGISTREMENT DE L'ENTRAÎNEMENT", +"RECOVERY VITALS": "CONSTANTES DE RÉCUPÉRATION", "REM Sleep": "Sommeil paradoxal", "REM sleep": "Sommeil paradoxal", "REST": "REPOS", @@ -1902,6 +1933,7 @@ "Reference range from my report (optional)": "Plage de référence de mon bilan (facultatif)", "Reference range from your own report, optional": "Plage de référence de votre propre bilan, facultatif", "Refresh": "Actualiser", +"Refresh battery": "Actualiser la batterie", "Refresh models": "Actualiser les modèles", "Refresh models from provider": "Actualiser les modèles depuis le fournisseur", "Refresh strap battery and connection state.": "Actualiser la batterie et l'état de connexion du bracelet.", @@ -1928,10 +1960,13 @@ "Removing…": "Suppression…", "Rename": "Renommer", "Rename device": "Renommer l'appareil", +"Rename item": "Renommer l'élément", +"Rename…": "Renommer…", "Replace all current data with the backup %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde %@ ? Cette action est irréversible.", "Replace all current data with the backup from %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde du %@ ? Cette action est irréversible.", "Replace all data": "Remplacer toutes les données", "Replace this device's data with one of the backups in your folder. This overwrites current data, so back up first if you're unsure.": "Remplacez les données de cet appareil par l'une des sauvegardes de votre dossier. Cela écrase les données actuelles, faites donc d'abord une sauvegarde en cas de doute.", +"Replaces the Today tab with the prototype redesign. Turn it off any time to return to the classic dashboard. Reads the same live data from your strap.": "Remplace l'onglet Aujourd'hui par la refonte prototype. Désactive-la quand tu veux pour revenir au tableau de bord classique. Elle lit les mêmes données en direct de ton bracelet.", "Report": "Signaler", "Report a %@ bug": "Signaler un bug de %@", "Report a bug with my log": "Signaler un bug avec mon journal", @@ -2010,6 +2045,7 @@ "SESSIONS": "SÉANCES", "SHORT AXIS": "AXE COURT", "SIGNIFICANT": "SIGNIFICATIF", +"SILENT GUARDIAN": "GARDIEN SILENCIEUX", "SLEEP": "SOMMEIL", "SPEED": "VITESSE", "STEPS ESTIMATE": "ESTIMATION DES PAS", @@ -2073,6 +2109,7 @@ "Session ended. Try a paced breath instead.": "Séance terminée. Essayez plutôt une respiration cadencée.", "Session live": "Séance en direct", "Session progress": "Progression de la séance", +"Session summary": "Résumé de la séance", "Sessions": "Séances", "Set a wake time for specific days (a lie-in at the weekend, say). Days you leave alone use the time above.": "Définissez une heure de réveil pour des jours précis (une grasse matinée le week-end, par exemple). Les jours non modifiés utilisent l'heure ci-dessus.", "Set custom container size": "Définir une taille de récipient personnalisée", @@ -2103,6 +2140,7 @@ "Shows your live heart rate on the Lock Screen and in the Dynamic Island while the strap is connected. Turn it off to keep your live HR out of the Dynamic Island. (Any one already showing clears within a moment.)": "Affiche votre fréquence cardiaque en direct sur l'écran verrouillé et dans la Dynamic Island tant que le bracelet est connecté. Désactivez-le pour garder votre FC en direct hors de la Dynamic Island. (Une FC déjà affichée disparaît sous peu.)", "Signal Trust": "Fiabilité du signal", "Signals up": "Signaux en hausse", +"Silence-first strap coaching during workouts.": "Coaching du bracelet, silencieux d'abord, pendant les entraînements.", "Silent haptic HIIT, the strap buzzes the transitions": "HIIT haptique silencieux, le bracelet vibre aux transitions", "Silent haptic HIIT: the strap buzzes the transitions": "HIIT haptique silencieux : le bracelet vibre aux transitions", "Similar in spirit to WHOOP's Sleep Performance %; our composite is our own.": "Similaire dans l'esprit au % de Performance de sommeil de WHOOP ; notre composite nous est propre.", @@ -2177,6 +2215,7 @@ "Start": "Démarrer", "Start %@": "Démarrer %@", "Start a Breathe session": "Démarrer une séance de Respiration", +"Start a live session. Beta. Silent strap coaching against today's Charge.": "Lancer une séance en direct. Bêta. Coaching silencieux du bracelet en fonction de la Charge du jour.", "Start a live stream": "Démarrer un flux en direct", "Start a local experiment using today's date as day one.": "Démarrez une expérience locale en utilisant la date d'aujourd'hui comme jour un.", "Start a workout": "Démarrer un entraînement", @@ -2259,6 +2298,8 @@ "Sunday": "Dimanche", "Support": "Assistance", "Support NOOP": "Soutenir NOOP", +"Support NOOP: help and contact": "Soutenir NOOP : aide et contact", +"Support NOOP: help and contact.": "Soutenir NOOP : aide et contact.", "Support the build": "Soutenir le développement", "Suppress auto-nudges overnight (10pm–7am).": "Désactiver les alertes automatiques la nuit (22h–7h).", "Sustained high stress": "Stress élevé prolongé", @@ -2321,6 +2362,7 @@ "The best-sourced number per metric across every band you use.": "Le chiffre le mieux documenté pour chaque métrique, parmi tous les bracelets que vous utilisez.", "The full terms are in TERMS.md, shipped with NOOP. This is not legal advice.": "Les conditions complètes se trouvent dans TERMS.md, fourni avec NOOP. Ceci n'est pas un avis juridique.", "The honest catch: a watch isn't a chest strap. It's brilliant at sleep, steps, workouts and fitness, and lighter on the dense heart-rate-variability a strap measures all night. So recovery takes about a week to calibrate, and a couple of metrics depend on your watch model. NOOP is upfront about all of it. Every watch-derived number carries a confidence, and where the watch can't be honest, NOOP shows nothing instead of a made-up figure.": "Le hic honnête : une montre n'est pas un bracelet pectoral. Elle excelle pour le sommeil, les pas, les entraînements et la forme physique, et est plus légère sur la VFC dense qu'un bracelet mesure toute la nuit. La récupération met donc environ une semaine à s'étalonner, et quelques métriques dépendent du modèle de votre montre. NOOP est transparent sur tout cela. Chaque chiffre dérivé de la montre porte un niveau de confiance, et là où la montre ne peut pas être honnête, NOOP n'affiche rien plutôt qu'un chiffre inventé.", +"The line is each waking hour's 0-3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": "La courbe est l'indice 0-3 de chaque heure d'éveil, évalué par rapport à tes heures calmes du jour. La barre en dessous répartit ta journée en temps de stress calme, modéré et élevé.", "The line is each waking hour's 0–3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": "La ligne représente l'indicateur 0 à 3 de chaque heure éveillée, évalué par rapport à vos propres heures calmes du jour. La barre ci-dessous répartit votre journée en temps de stress calme, modéré et élevé.", "The master switch for every wrist buzz (inactivity, stress, alerts). Off keeps the strap quiet no matter what else is on.": "L'interrupteur principal de chaque vibration au poignet (inactivité, stress, alertes). Désactivé, il garde le bracelet silencieux quoi qu'il arrive d'autre.", "The method behind the score, not today's values.": "La méthode derrière le score, pas les valeurs du jour.", @@ -2363,6 +2405,7 @@ "This is your last scored session. Wear the strap overnight for a fresh score.": "Voici votre dernière séance notée. Portez le bracelet une nuit pour obtenir un nouveau score.", "This is your only sleep block today.": "C'est votre seul bloc de sommeil aujourd'hui.", "This isn't a NOOP backup from this app. It's missing the migration bookkeeping a NOOP backup carries (it looks like an Android backup or another app's database), and restoring it would strand your store. To move your history across platforms, export the WHOOP-format CSV on the other device (Settings → Export data) and import that here, or import your original WHOOP / Apple Health export.": "Ce n'est pas une sauvegarde NOOP provenant de cette application. Il lui manque les informations de migration qu'une sauvegarde NOOP contient (elle ressemble à une sauvegarde Android ou à la base de données d'une autre application), et la restaurer bloquerait votre stockage. Pour transférer votre historique entre plateformes, exportez le CSV au format WHOOP sur l'autre appareil (Paramètres → Exporter les données) et importez-le ici, ou importez votre export WHOOP / Apple Santé d'origine.", +"This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it.": "Cela déplace la nuit vers une période sans données enregistrées. Les stades ne peuvent pas y être calculés, la nuit peut donc apparaître vide tant que des données ne la couvrent pas.", "This night scored high efficiency but very little deep or REM, more likely a staging estimate miss than a real restorative shortfall. The totals are kept as-is; read the split with care.": "Cette nuit a obtenu une efficacité élevée mais très peu de sommeil profond ou paradoxal, ce qui ressemble plus à une erreur d'estimation des phases qu'à un réel déficit de récupération. Les totaux sont conservés tels quels ; interprétez la répartition avec prudence.", "This permanently deletes all data recorded from %@. This can't be undone.": "Cela supprime définitivement toutes les données enregistrées depuis %@. Cette action est irréversible.", "This permanently deletes everything imported from Apple Health: heart rate, HRV, sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "Cela supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, sommeil, pas, entraînements et plus encore. Les données en direct de votre bracelet ne sont pas concernées. Cette action est irréversible.", @@ -2439,6 +2482,8 @@ "Type in a number from your own report. It stays on %@.": "Saisissez un chiffre issu de votre propre rapport. Il reste sur %@.", "Typical for you": "Typique pour vous", "Typical range": "Plage typique", +"Undo": "Annuler", +"Undo sleep deletion": "Annuler la suppression du sommeil", "Unit": "Unité", "Units": "Unités", "Unknown": "Inconnu", @@ -2468,6 +2513,7 @@ "Vibrate your connected WHOOP strap.": "Faites vibrer votre bracelet WHOOP connecté.", "View active workout": "Voir l'entraînement actif", "View detail": "Voir le détail", +"View sources": "Voir les sources", "View the active workout": "Voir l'entraînement actif", "View the full plan": "Voir le plan complet", "Visual only": "Visuel uniquement", @@ -2525,6 +2571,7 @@ "Watch for early-illness signs": "Surveiller les premiers signes de maladie", "Watch your heart, live": "Observez votre cœur, en direct", "Watches your resting HR, HRV, skin temperature and respiration against your own 28-day baseline. On-device and approximate: informational only, not a diagnosis.": "Surveille votre FC au repos, votre VFC, votre température cutanée et votre respiration par rapport à votre propre référence de 28 jours. Sur l'appareil et approximatif : à titre informatif uniquement, pas un diagnostic.", +"We compare today's resting heart rate and HRV to your own 30-day baseline. A higher-than-usual resting HR and a lower-than-usual HRV both push the score up, classic signs the body is activated. The combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at your baseline, 3 is highly activated.": "Nous comparons ta fréquence cardiaque au repos et ta VFC du jour à ta référence sur 30 jours. Une FC au repos plus haute que d'habitude et une VFC plus basse que d'habitude poussent toutes deux le score vers le haut — signes classiques d'un corps activé. L'écart combiné est ramené sur une échelle de 0 à 3 : 0 = calme, 1,5 = ta référence, 3 = très activé.", "We could not take over this ring.": "Nous n'avons pas pu prendre le contrôle de cet anneau.", "We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's banked motion history, so your strap needs to sync that history before NOOP has anything to count.": "Nous ne voyons encore aucun mouvement provenant de votre bracelet. Les pas sont estimés à partir de l'historique de mouvement stocké par votre WHOOP, donc votre bracelet doit d'abord synchroniser cet historique avant que NOOP ait quoi que ce soit à compter.", "Wear & presence": "Port et présence", @@ -2532,6 +2579,7 @@ "Wear state": "État de port", "Wear the strap for scoring": "Portez le bracelet pour obtenir un score", "Wear the strap overnight and these read from your nightly skin temperature.": "Portez le bracelet pendant la nuit et ces valeurs seront lues à partir de votre température cutanée nocturne.", +"Wear the strap overnight to score a night first.": "Porte le bracelet la nuit pour qu'une nuit soit d'abord évaluée.", "Wear the strap, tap once, then let it sync and share your strap log.": "Portez le bracelet, touchez une fois, laissez-le se synchroniser, puis partagez le journal du bracelet.", "Wed": "Mer", "Wednesday": "Mercredi", @@ -2610,6 +2658,7 @@ "Wrist temperature needs Series 8 or later, and the newest US units dropped the blood-oxygen sensor. Where a sensor isn't there, NOOP reads “not available” instead of zero.": "La température au poignet nécessite une Series 8 ou ultérieure, et les derniers modèles américains ont perdu le capteur d'oxygène sanguin. Là où un capteur est absent, NOOP affiche « non disponible » plutôt que zéro.", "Writes a timestamped copy of your strap log to NOOP's folder in the Files app, once a day. Handy for a bug report without remembering to grab it. On iPhone it fires when iOS next wakes NOOP near your chosen time, not guaranteed to the minute (keep NOOP open overnight for the best chance). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans le dossier de NOOP de l'app Fichiers, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur iPhone, cela se déclenche au prochain réveil de NOOP par iOS proche de l'heure choisie, sans garantie à la minute près (laissez NOOP ouvert la nuit pour de meilleures chances). Tout reste sur %@ ; rien n'est envoyé en ligne.", "Writes a timestamped copy of your strap log to your Documents folder, once a day. Handy for a bug report without remembering to grab it. On Mac it runs while NOOP is open (and catches up on launch if the time passed while it was closed). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans votre dossier Documents, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur Mac, cela s'exécute pendant que NOOP est ouvert (et rattrape au lancement si l'heure est passée pendant sa fermeture). Tout reste sur %@ ; rien n'est envoyé en ligne.", +"YOUR CARDS": "TES CARTES", "YOUR DAILY SCORES": "VOS SCORES QUOTIDIENS", "YOUR READING": "VOTRE MESURE", "Yes": "Oui", @@ -2750,6 +2799,7 @@ "low": "faible", "low-stress days · %lldd": "jours à faible stress · %lldj", "mid": "moyen", +"minus": "moins", "missing": "manquant", "moderate": "modéré", "month": "mois", @@ -2798,6 +2848,7 @@ "p < 0.05": "p < 0,05", "partial": "partiel", "peak %@ · %@": "pic %1$@ · %2$@", +"plus": "plus", "positive": "positif", "projected · %@": "prévu · %@", "quarter": "trimestre", From c2e6e5f3be12df4ac7eebbbf38a60e292669bd84 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:54:50 +0200 Subject: [PATCH 20/28] Android: 100% French coverage for the wrapped UI strings Translated the 885 tr() strings not already covered by the shared iOS dictionary (settings, device pairing, data sources, sleep/health/vitals, trends/insights, workouts, live sessions, onboarding). Every distinct tr() call site on the ~50 wrapped screens now resolves to French on a French device; English devices are unaffected. Co-Authored-By: Claude Sonnet 5 --- android/app/src/main/res/raw/fr_strings.json | 891 ++++++++++++++++++- 1 file changed, 888 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/res/raw/fr_strings.json b/android/app/src/main/res/raw/fr_strings.json index 9e2e29f96..70558c631 100644 --- a/android/app/src/main/res/raw/fr_strings.json +++ b/android/app/src/main/res/raw/fr_strings.json @@ -1,4 +1,6 @@ { +" drinks": " boissons", +" · Charging": " · En charge", "%@ %@ relationship (r = %@, n = %lld).": "Relation %1$@ %2$@ (r = %3$@, n = %4$lld).", "%@ (Beta)": "%@ (bêta)", "%@ (experimental)": "%@ (expérimental)", @@ -179,9 +181,18 @@ "%llds left · %lld beats": "%1$llds restantes · %2$lld battements", "%lld–%lld%@ · step %lld": "%lld–%lld%@ · étape %lld", "%lld′ %lld″": "%1$lld′ %2$lld″", +"(Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then ": "(Oura : Account → Export Data ; Fitbit : Google Takeout ; Garmin : Export Your Data), puis ", +"(and keep wearing your strap).": "(et continue à porter ton bracelet).", +"(deep + REM sleep), and how consistent your sleep and wake timing is.": "(sommeil profond + paradoxal), et à quel point tes heures de coucher et de lever sont régulières.", "(nothing to share yet)": "(rien à partager pour l'instant)", +"(range and ectopic-beat filtering) before computing RMSSD the same way your ": "(filtrage de la plage et des battements ectopiques) avant de calculer le RMSSD de la même façon que ton ", +"(those are WHOOP-only).": "(réservés à WHOOP).", +"* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); ": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (dépend du firmware) ; ", "* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); no steps over BLE on a 4.0. No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (selon le firmware) ; pas de pas via BLE sur un 4.0. Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", +"* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw ": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut ", "* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw motion count (#78). No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut de mouvement (#78). Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", +"+ drinks": "+ boissons", +", in use": ", utilisé", "0 pts": "0 pt", "0–%@ scale": "Échelle 0–%@", "1 app on": "1 application active", @@ -221,6 +232,7 @@ "14d": "14j", "180 DAYS": "180 JOURS", "1Y": "1A", +"2 days ago": "il y a 2 jours", "28d": "28j", "2pm+": "à partir de 14h", "3 months": "3 mois", @@ -229,6 +241,7 @@ "30D": "30J", "30d": "30j", "365 DAYS": "365 JOURS", +"4.0 vs 5.0/MG": "4.0 vs 5.0/MG", "5-minute average · selected day": "Moyenne sur 5 minutes · jour sélectionné", "5-minute average · since midnight": "Moyenne sur 5 minutes · depuis minuit", "6 months": "6 mois", @@ -243,27 +256,36 @@ "A %@ tonight tends to line up with about %@ higher on tomorrow’s %@ for you, based on typical patterns.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de plus sur votre %3$@ de demain, sur la base de schémas typiques.", "A %@ tonight tends to line up with about %@ lower on tomorrow’s %@ for you, based on %lld of your %@ days.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de %4$lld de vos %5$@ jours.", "A %@ tonight tends to line up with about %@ lower on tomorrow’s %@ for you, based on typical patterns.": "Un %1$@ ce soir tend à correspondre chez vous à environ %2$@ de moins sur votre %3$@ de demain, sur la base de schémas typiques.", +"A 60-second snapshot of your beat-to-beat (R-R) intervals from the strap, cleaned ": "Un instantané de 60 secondes de tes intervalles battement à battement (R-R) issus du bracelet, nettoyés ", "A 60-second snapshot of your beat-to-beat (R-R) intervals from the strap, cleaned (range and ectopic-beat filtering) before computing RMSSD the same way your overnight HRV is computed.": "Un instantané de 60 secondes de vos intervalles battement à battement (R-R) provenant du bracelet, nettoyés (filtrage de plage et des battements ectopiques) avant de calculer le RMSSD de la même façon que votre VFC nocturne.", "A WHOOP export backfills recovery, strain, sleep and workouts. Apple Health can add HR, HRV, sleep, SpO₂, steps, workouts and weight.": "Un export WHOOP complète rétroactivement récupération, effort, sommeil et entraînements. Apple Santé peut ajouter FC, VFC, sommeil, SpO₂, pas, entraînements et poids.", +"A WHOOP export backfills recovery, strain, sleep and workouts. Health Connect can add steps, HR, HRV, sleep and weight from Android sources.": "Un export WHOOP recharge la récupération, l'effort, le sommeil et les entraînements. Health Connect peut ajouter les pas, la FC, la VFC, le sommeil et le poids depuis des sources Android.", "A WHOOP membership runs $300–480 a year, for life. NOOP does this for free: one person, no servers, no subscription.": "Un abonnement WHOOP coûte 300 à 480 $ par an, à vie. NOOP fait cela gratuitement : une personne, aucun serveur, aucun abonnement.", +"A WHOOP strap bonds to a single device. While it's connected to NOOP it won't stream ": "Un bracelet WHOOP se lie à un seul appareil. Tant qu'il est connecté à NOOP, il ne diffuse pas ", +"A badge shows whether a number was scored on-device by NOOP, or imported ": "Un badge indique si une valeur a été calculée sur l'appareil par NOOP, ou importée ", "A badge shows whether a number was scored on-device by NOOP, or imported from Whoop or Apple Health.": "Un badge indique si un chiffre a été calculé sur l'appareil par NOOP, ou importé depuis Whoop ou Apple Santé.", "A baseline-normalized recovery score: your resting heart rate, sleep quality and night-to-night consistency, weighted against your own baseline, with heart-rate variability (rMSSD) leading wherever the strap gives us a clean reading.": "Un score de récupération normalisé par rapport à une référence : votre fréquence cardiaque au repos, la qualité de votre sommeil et la régularité nuit après nuit, pondérées par rapport à votre propre référence, la variabilité de la fréquence cardiaque (rMSSD) prenant le dessus partout où le bracelet fournit une mesure propre.", +"A blend of how long you slept versus your personal need (the biggest ": "Un mélange entre la durée de ton sommeil et ton besoin personnel (le principal ", "A blend of how long you slept versus your personal need (the biggest factor), how efficiently (asleep versus in bed), how much was restorative (deep + REM sleep), and how consistent your sleep and wake timing is.": "Un mélange de la durée de votre sommeil par rapport à votre besoin personnel (le facteur le plus important), de son efficacité (endormi par rapport au temps au lit), de sa part réparatrice (sommeil profond + REM), et de la régularité de vos horaires de sommeil et de réveil.", "A bulk markers CSV import (date, marker, value, unit) lands with the file importers in Data Sources, same as nutrition and lifting. For now, add readings one at a time above. Everything you import stays on %@.": "Un import CSV groupé de marqueurs (date, marqueur, valeur, unité) arrive avec les importateurs de fichiers dans Sources de données, comme pour la nutrition et la musculation. Pour l'instant, ajoutez les mesures une par une ci-dessus. Tout ce que vous importez reste sur %@.", "A buzz, not a banner": "Une vibration, pas une bannière", "A calm evening reminder, timed from your wake time and usual sleep need. It's a suggestion, not an alarm.": "Un rappel calme le soir, calé sur votre heure de réveil et votre besoin de sommeil habituel. C'est une suggestion, pas une alarme.", "A calm hour now helps you hit your wake time well-rested.": "Une heure calme maintenant vous aide à atteindre votre heure de réveil bien reposé.", "A calm nudge %lldh %lldm before your wake time.": "Un rappel discret %1$lldh %2$lldm avant votre heure de réveil.", +"A calm ring rolls HRV, resting heart rate and sleep into one read on whether to push or rest.": "Un anneau sobre réunit la VFC, la fréquence cardiaque au repos et le sommeil en une seule lecture pour savoir s'il faut pousser ou récupérer.", "A cardiovascular load in the Banister TRIMP family: time spent in each heart-rate zone, weighted so harder zones count for more, summed into one daily figure.": "Une charge cardiovasculaire de la famille Banister TRIMP : le temps passé dans chaque zone de fréquence cardiaque, pondéré pour que les zones plus intenses comptent davantage, additionné en un seul chiffre quotidien.", "A classic one-glance read of NOOP's own scores. Same data, different lens.": "Une lecture claire, en un coup d'œil, des scores de NOOP. Mêmes données, autre angle.", "A clean, shareable one-page PDF of your recovery, sleep, HRV, resting heart rate and strain over a date range. Saved on your %@. Nothing leaves the device.": "Un PDF d'une page, clair et partageable, de votre récupération, sommeil, VFC, fréquence cardiaque au repos et effort sur une période donnée. Enregistré sur votre %@. Rien ne quitte l'appareil.", "A couple of metrics depend on which Apple Watch you wear. Wrist temperature, which feeds skin temp, arrived with Series 8, so older watches don't report it. Blood oxygen is the bigger one: Apple removed the SpO₂ sensor from the newest US units over a patent dispute, so those simply don't measure it.": "Quelques métriques dépendent de l'Apple Watch que vous portez. La température du poignet, qui alimente la température cutanée, est arrivée avec la Series 8 ; les montres plus anciennes ne la fournissent donc pas. L'oxygène sanguin est le cas le plus notable : Apple a retiré le capteur SpO₂ des derniers modèles américains à la suite d'un litige de brevet, ces derniers ne le mesurent donc tout simplement pas.", "A couple of metrics depend on your model": "Quelques métriques dépendent de votre modèle", +"A few honest words, so nothing is a surprise.": "Quelques mots honnêtes, pour que rien ne te surprenne.", "A few honest words, so nothing's a surprise.": "Quelques mots honnêtes, pour qu'il n'y ait pas de surprise.", +"A few more days and we can show your Fitness Age": "Encore quelques jours et ton Âge de forme s'affichera", "A few more days and we can show your Fitness Age.": "Encore quelques jours et nous pourrons afficher votre Âge de forme physique.", "A few more days and we can show your Vitality.": "Encore quelques jours et nous pourrons afficher votre Vitalité.", "A few more days of wear, plus the basics below, and we can show your Fitness Age.": "Encore quelques jours de port, plus les informations de base ci-dessous, et nous pourrons afficher votre Âge de forme physique.", "A few signals are up": "Quelques signaux sont en hausse", +"A flat or off-wrist strap won't advertise, so nothing shows up. A real phone is ": "Un bracelet déchargé ou retiré du poignet n'émet rien, donc rien n'apparaît. Un vrai téléphone est ", "A gentle buzz when your HRV drops while your heart rate is calm, a cue to take a paced breath. Rate-limited to once every 15 minutes; off by default.": "Une vibration douce quand votre VFC chute alors que votre fréquence cardiaque est calme, une invitation à respirer à un rythme régulier. Limité à une fois toutes les 15 minutes ; désactivé par défaut.", "A gentle wrist buzz when you've been sitting too long, a nudge to get up and move. Inferred from the strap's motion on each history sync, so it lags real time by a sync or two.": "Une légère vibration au poignet quand vous êtes assis depuis trop longtemps, une invitation à vous lever et à bouger. Déduite du mouvement du bracelet à chaque synchronisation d'historique, elle accuse donc un retard d'une ou deux synchronisations par rapport au temps réel.", "A logged caffeine is past your bedtime cutoff. It may still be on board and keep you up. Just a timing heads-up.": "Une prise de caféine consignée dépasse votre limite avant le coucher. Elle peut encore agir et vous empêcher de dormir. Juste un repère de timing.", @@ -273,7 +295,10 @@ "A private notebook, not a medical service.": "Un carnet privé, pas un service médical.", "A private window into your recovery, sleep and strain, read straight from your strap, kept only on this Mac.": "Une fenêtre privée sur votre récupération, sommeil et effort, lu directement depuis votre bracelet, conservé uniquement sur ce Mac.", "A private window into your recovery, sleep and strain. Read straight from your strap, kept only on %@.": "Une fenêtre privée sur votre récupération, votre sommeil et votre effort. Lue directement depuis votre bracelet, conservée uniquement sur %@.", +"A private window into your recovery, sleep and strain. Read straight from your strap, kept only on this phone.": "Une fenêtre privée sur ta récupération, ton sommeil et ton effort. Lu directement depuis ton bracelet, conservé uniquement sur ce téléphone.", "A quick word before we connect": "Un mot rapide avant de nous connecter", +"A quick word before you connect": "Un petit mot avant de connecter", +"A quiet, ongoing status": "Un état discret et continu", "A read-only export of the decoded sensor streams NOOP already stores. Works on any strap. Nothing is written to your device, and nothing is uploaded.": "Un export en lecture seule des flux de capteurs décodés que NOOP stocke déjà. Fonctionne avec n'importe quel bracelet. Rien n'est écrit sur votre appareil, rien n'est envoyé en ligne.", "A relaxation rhythm, not cardiac control. It never paces below a safe rate and you can stop anytime. If your heart rate doesn't settle, we'll say so plainly.": "Un rythme de relaxation, pas un contrôle cardiaque. Il ne descend jamais sous une fréquence sûre et vous pouvez arrêter à tout moment. Si votre fréquence cardiaque ne se stabilise pas, nous vous le dirons clairement.", "A reminder to recharge before bed when the strap drops to 15%, and a heads-up when it reaches 100%, each at most once per charge cycle.": "Un rappel de recharger avant le coucher quand le bracelet descend à 15 %, et un avis quand il atteint 100 %, chacun au plus une fois par cycle de charge.", @@ -283,8 +308,10 @@ "A simple goal that adjusts to your effort. General wellness guidance, not medical advice.": "Un objectif simple qui s'adapte à votre effort. Orientation générale de bien-être, pas un avis médical.", "A standalone companion for your WHOOP. Everything stays on this device, your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil, votre historique, votre flux en direct, vos chiffres. Rien n'est téléchargé. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", "A standalone companion for your WHOOP. Everything stays on this device: your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil : votre historique, votre flux en direct, vos chiffres. Rien n'est envoyé en ligne. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", +"A standalone companion for your WHOOP. Everything stays on this phone: your history, your live stream, your numbers. Nothing is uploaded.": "Un compagnon autonome pour ton WHOOP. Tout reste sur ce téléphone : ton historique, ton flux en direct, tes chiffres. Rien n'est envoyé en ligne.", "A still, seated snapshot of your heart-rate variability": "Un instantané, assis et immobile, de votre variabilité de fréquence cardiaque", "A sync is already in progress.": "Une synchronisation est déjà en cours.", +"A transparent cardiorespiratory recipe that recovers deep and REM better than the ": "Une recette cardiorespiratoire transparente qui récupère mieux le sommeil profond et paradoxal que ", "A transparent cardiorespiratory recipe that recovers deep and REM better than the default staging. Opt-in and experimental: it only changes how already-detected nights are split into stages (detection and scores are unchanged), and the default staging stays in place if you leave this off. Takes effect on the next nights staged.": "Une recette cardiorespiratoire transparente qui restitue mieux le sommeil profond et REM que la répartition par défaut. Facultatif et expérimental : cela change seulement la manière dont les nuits déjà détectées sont réparties en phases (la détection et les scores restent inchangés), et la répartition par défaut reste en place si vous laissez cette option désactivée. Prend effet sur les prochaines nuits traitées.", "A typical recent day": "Une journée récente typique", "A watch samples your heart-rate variability rather than streaming it all night, so your Charge score needs roughly seven nights to calibrate. Until then NOOP shows “needs more data”, never a guessed number.": "Une montre échantillonne votre variabilité de fréquence cardiaque au lieu de la diffuser toute la nuit ; votre score de Charge a donc besoin d'environ sept nuits pour se calibrer. En attendant, NOOP affiche « données insuffisantes », jamais un chiffre deviné.", @@ -295,7 +322,10 @@ "ALL TIME": "DEPUIS TOUJOURS", "ALL YOUR DATA · NONE OF THE CLOUD": "TOUTES VOS DONNÉES · SANS LE CLOUD", "ALL-OUT": "MAXIMAL", +"API Key": "Clé API", +"API Key (optional)": "Clé API (facultatif)", "API key": "Clé API", +"API key (hidden)": "Clé API (masquée)", "API key (optional)": "Clé API (facultatif)", "AVG": "MOY", "AVG HR": "FC MOY", @@ -310,11 +340,14 @@ "About the same as your age": "À peu près comme votre âge", "About this nap": "À propos de cette sieste", "About you": "À propos de vous", +"About your age": "À propos de ton âge", "About your main sleep": "À propos de votre sommeil principal", +"Above": "Au-dessus", "Accept & Continue": "Accepter et continuer", "Across the quiet windows we could read, your beat-to-beat timing held a tight, even shape.": "Sur les fenêtres calmes que nous avons pu lire, le rythme battement à battement est resté serré et régulier.", "Active": "Actif", "Active Energy": "Énergie active", +"Active band": "Bracelet actif", "Active energy": "Énergie active", "Active hours end": "Fin des heures actives", "Active hours start": "Début des heures actives", @@ -323,10 +356,12 @@ "Activity & Energy": "Activité et énergie", "Activity Breakdown": "Répartition de l'activité", "Activity Cost": "Coût de l'activité", +"Acts as a standard Bluetooth heart-rate strap. Pair NOOP from your treadmill, ": "Fonctionne comme un bracelet de fréquence cardiaque Bluetooth standard. Associe NOOP depuis ton tapis de course, ", "Acts as a standard Bluetooth heart-rate strap. Pair NOOP from your treadmill, bike or app to see your strap's heart rate there.": "Fonctionne comme un ceinture cardio Bluetooth standard. Appairez NOOP depuis votre tapis, vélo ou application pour y voir la fréquence cardiaque de votre bracelet.", "Acute (7-day) vs chronic (28-day) training load. 0.8–1.3 is the sweet spot.": "Charge d'entraînement aiguë (7 jours) vs chronique (28 jours). 0,8–1,3 est la zone idéale.", "Add": "Ajouter", "Add Workout": "Ajouter un entraînement", +"Add a custom item…": "Ajouter un élément personnalisé…", "Add a custom marker": "Ajouter un marqueur personnalisé", "Add a custom question…": "Ajouter une question personnalisée…", "Add a device": "Ajouter un appareil", @@ -339,9 +374,12 @@ "Add nap": "Ajouter une sieste", "Add workout": "Ajouter un entraînement", "Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On %@ only. Nothing is synced.": "Ajoute un journal d'hydratation simple avec un objectif quotidien qui s'ajuste à votre effort. Touchez pour ajouter une gorgée, une tasse ou une bouteille et regardez l'anneau de progression se remplir. Sur %@ uniquement. Rien n'est synchronisé.", +"Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On this phone only. Nothing is synced.": "Ajoute un suivi d'hydratation simple avec un objectif quotidien qui s'adapte à ton effort. Appuie pour ajouter une gorgée, un verre ou une bouteille et regarde l'anneau de progression se remplir. Sur ce téléphone uniquement. Rien n'est synchronisé.", "Adds a water-log card to your dashboard": "Ajoute une carte de suivi de l'eau à votre tableau de bord", +"Adds your VO₂max estimate": "Ajoute ton estimation de VO₂max", "Adjust amount": "Ajuster la quantité", "Adjust manually": "Ajuster manuellement", +"Adjust sleep times": "Ajuster les heures de sommeil", "Adjust this session's details.": "Ajustez les détails de cette séance.", "Advanced": "Avancé", "Advanced HRV": "VFC avancée", @@ -350,6 +388,8 @@ "Advanced: use your own key": "Avancé : utiliser votre propre clé", "Aerobic": "Aérobie", "After a sync, NOOP looks over your recent heart rate for a sustained, raised stretch that looks like exercise and offers to save it. It only ever suggests. Nothing is saved until you tap Save, and you can dismiss any suggestion. Deliberately conservative, so the odd workout may be missed. On %@ only.": "Après une synchronisation, NOOP examine votre fréquence cardiaque récente à la recherche d'une période soutenue et élevée qui ressemble à de l'exercice, et propose de l'enregistrer. Il ne fait que suggérer. Rien n'est enregistré tant que vous n'appuyez pas sur Enregistrer, et vous pouvez ignorer toute suggestion. Volontairement prudent, il se peut donc qu'un entraînement occasionnel passe inaperçu. Sur %@ uniquement.", +"After a sync, NOOP looks over your recent heart rate for a sustained, raised stretch that looks like exercise and offers to save it. It only ever suggests. Nothing is saved until you tap Save, and you can dismiss any suggestion. Deliberately conservative, so the odd workout may be missed. On this phone only.": "Après une synchro, NOOP examine ta fréquence cardiaque récente à la recherche d'une période prolongée et élevée qui ressemble à de l'exercice, et propose de l'enregistrer. Il ne fait que suggérer. Rien n'est enregistré tant que tu n'appuies pas sur Enregistrer, et tu peux ignorer n'importe quelle suggestion. Volontairement prudent, donc un entraînement occasionnel peut passer inaperçu. Sur ce téléphone uniquement.", +"After last night is processed, a notification with your Charge and Rest. Posts ": "Une fois la nuit dernière traitée, une notification avec ta Charge et ton Repos. Publiée ", "After your iPhone reboots, unlock it once. Until you do, iOS keeps NOOP's files locked (Data Protection), so new history can't be written or synced.": "Après le redémarrage de votre iPhone, déverrouillez-le une fois. Jusque-là, iOS garde les fichiers de NOOP verrouillés (protection des données), donc aucun nouvel historique ne peut être écrit ni synchronisé.", "Age": "Âge", "Age, %lld years": "Âge, %lld ans", @@ -358,27 +398,45 @@ "Alert": "Alerte", "Alerts": "Alertes", "All": "Tout", +"All Health Data, then import the .zip here in Data Sources.": "Toutes les données de santé, puis importe le .zip ici dans Sources de données.", "All Sessions": "Toutes les séances", "All caught up": "Tout est à jour", "All history": "Tout l'historique", "All of this is computed on your own device from data you already have. No heartbeat data leaves it.": "Tout ceci est calculé sur votre propre appareil à partir de données que vous avez déjà. Aucune donnée de battement cardiaque n'en sort.", +"All other apps": "Toutes les autres applis", +"All sources": "Toutes les sources", +"All sports": "Tous les sports", "All stays on %@. Nothing is sent anywhere.": "Tout reste sur %@. Rien n'est envoyé nulle part.", +"All stays on this phone. Nothing is sent anywhere.": "Tout reste sur ce téléphone. Rien n'est envoyé où que ce soit.", +"Allow": "Autoriser", "Allow Apple Health access": "Autoriser l'accès à Apple Santé", +"Allow Bluetooth and tap Scan to find your strap, or keep going and connect later.": "Autorise le Bluetooth et appuie sur Scanner pour trouver ton bracelet, ou continue et connecte-le plus tard.", +"Allow Nearby devices": "Autoriser les appareils à proximité", "Allow access": "Autoriser l'accès", +"Allow background running": "Autoriser l'exécution en arrière-plan", +"Allow installing apps, then tap Update again.": "Autorise l'installation d'applis, puis appuie de nouveau sur Mettre à jour.", "Already clean.": "Déjà propre.", +"Also buzz for apps that aren't in the lists above (e.g. BeReal). Android ": "Vibre aussi pour les applis qui ne sont pas dans les listes ci-dessus (p. ex. BeReal). Android ", "Also share my patterns & Lab Book": "Partager aussi mes tendances et mon carnet de laboratoire", "Also share my patterns and Lab Book with the coach": "Partager aussi mes tendances et mon carnet de laboratoire avec le Coach", +"Also write the strap log to the system log (logcat) for development over adb. Off by default; the in-app log and “Share strap log” below work either way.": "Écrit aussi le journal du bracelet dans le journal système (logcat) pour le développement via adb. Désactivé par défaut ; le journal intégré et « Partager le journal du bracelet » ci-dessous fonctionnent dans tous les cas.", "Always rely on your doctor, pharmacist, or a qualified professional to interpret results and make decisions. If a number worries you, talk to them, not to an app.": "Fiez-vous toujours à votre médecin, pharmacien ou un professionnel qualifié pour interpréter les résultats et prendre des décisions. Si un chiffre vous inquiète, parlez-en à eux, pas à une application.", +"Amazfit": "Amazfit", +"Amazfit / Zepp": "Amazfit / Zepp", "Amount": "Quantité", "Amount in mg (optional)": "Quantité en mg (facultatif)", "Amount in millilitres": "Quantité en millilitres", +"An HRV reading needs the live R-R stream. Open the Live screen and connect your strap, ": "Une lecture de VFC nécessite le flux R-R en direct. Ouvre l'écran Live et connecte ton bracelet, ", "An HRV reading needs the live R-R stream. Open the Live screen and connect your strap, then come back.": "Une mesure de VFC nécessite le flux R-R en direct. Ouvrez l'écran En direct et connectez votre bracelet, puis revenez.", "An estimated VO2max from the Nes / HUNT Fitness Study model (resting heart rate, age and activity), read against population norms to express it as a fitness age.": "Un VO2max estimé selon le modèle de l'étude Nes / HUNT Fitness (fréquence cardiaque au repos, âge et activité), comparé aux normes de population pour l'exprimer sous forme d'âge physique.", "An experimental picture of your beat-to-beat heart timing. Not an ECG and not a diagnosis. You'll read and accept an experimental note before it shows anything.": "Un aperçu expérimental du rythme battement par battement de votre cœur. Ce n'est ni un ECG ni un diagnostic. Vous lirez et accepterez une note expérimentale avant qu'il n'affiche quoi que ce soit.", "An experimental picture of your beat-to-beat timing": "Un aperçu expérimental de votre rythme battement par battement", "An experimental picture of your beat-to-beat timing. Please read these first.": "Un aperçu expérimental de votre rythme battement par battement. Veuillez d'abord lire ceci.", +"An experimental picture of your beat-to-beat timing: a Poincaré scatter and plain regularity stats from quiet resting windows. Not an ECG and not a diagnosis; you'll read a short disclaimer and accept before it turns on.": "Un aperçu expérimental de ton rythme battement à battement : un nuage de points de Poincaré et de simples statistiques de régularité issues de fenêtres de repos calmes. Ce n'est ni un ECG ni un diagnostic ; tu liras une courte clause de non-responsabilité et l'accepteras avant l'activation.", "Analyse my sleep": "Analyser mon sommeil", "And make sure it's charged.": "Et assurez-vous qu'il est chargé.", +"Android is limiting NOOP in the background, which stops overnight syncing — your scores can stay stuck on “calibrating”. Tap Allow so it can keep the strap connected.": "Android limite NOOP en arrière-plan, ce qui interrompt la synchro nocturne — tes scores peuvent rester bloqués sur « calibrage ». Appuie sur Autoriser pour qu'il garde le bracelet connecté.", +"Android's Health Connect. No file needed. On-device; it never overwrites richer ": "Le Health Connect d'Android. Aucun fichier nécessaire. Sur l'appareil ; il n'écrase jamais des données plus riches ", "Answers are about the night and day leading into this morning, the same attribution a WHOOP export uses, so logged and imported days line up.": "Les réponses portent sur la nuit et la journée précédant ce matin, la même attribution qu'utilise un export WHOOP, afin que les jours enregistrés et importés correspondent.", "Any OpenAI-compatible server: Ollama, LM Studio, llama.cpp, or your own gateway. Stays on your network; nothing leaves %@.": "Tout serveur compatible OpenAI : Ollama, LM Studio, llama.cpp, ou votre propre passerelle. Reste sur votre réseau ; rien ne quitte %@.", "Any amount helps. Thank you, genuinely.": "Tout montant est utile. Merci, sincèrement.", @@ -404,7 +462,10 @@ "Arms the strap to buzz at your wake time, even if NOOP is closed. Still experimental on WHOOP 4.0, so keep a backup alarm until you've confirmed it wakes you.": "Arme le bracelet pour vibrer à votre heure de réveil, même si NOOP est fermé. Encore expérimental sur WHOOP 4.0, gardez donc une alarme de secours jusqu'à ce que vous ayez confirmé qu'il vous réveille.", "Ask Coach about your data…": "Demandez à Coach à propos de vos données…", "Ask about your charge, effort, rest and workouts, grounded in your own numbers.": "Posez des questions sur votre charge, votre effort, votre repos et vos entraînements, ancrées dans vos propres chiffres.", +"Ask about your recovery, strain, sleep and HRV, grounded in your own numbers.": "Pose des questions sur ta récupération, ton effort, ton sommeil et ta VFC, à partir de tes propres chiffres.", "Ask about your recovery, strain, sleep and workouts, grounded in your own numbers.": "Posez des questions sur votre récupération, effort, sommeil et entraînements, basées sur vos propres chiffres.", +"Ask anything about your recent recovery, strain, sleep or HRV.": "Demande ce que tu veux sur ta récupération, ton effort, ton sommeil ou ta VFC récents.", +"Ask your coach…": "Demande à ton coach…", "Ask your first question": "Posez votre première question", "Asleep": "Endormi", "Asleep Time": "Temps de sommeil", @@ -414,9 +475,12 @@ "Attach this to a bug report.": "Joignez ceci à un rapport de bug.", "Audio cues": "Signaux sonores", "Aug": "Août", +"Auto": "Auto", "Auto · %@ confidence": "Auto · confiance %@", "Auto-detect workouts": "Détecter automatiquement les entraînements", "Auto-nudge": "Rappel automatique", +"Auto-sync Health Connect periodically": "Synchroniser Health Connect automatiquement et régulièrement", +"Auto-sync periodically": "Synchro automatique régulière", "Automatic": "Automatique", "Automation": "Automatisation", "Automations": "Automatisations", @@ -433,8 +497,10 @@ "Average heart rate in beats per minute, optional": "Fréquence cardiaque moyenne en battements par minute, facultatif", "Avg": "Moy.", "Avg HR": "FC moy.", +"Avg HR (bpm, optional)": "FC moy. (bpm, facultatif)", "Avg HR is shown as typed. The HR graph, zones and Effort stay from the recorded session.": "La FC moy. est affichée telle que saisie. Le graphique de FC, les zones et l'Effort restent ceux de la séance enregistrée.", "Avg pace": "Allure moy.", +"Avg/sess": "Moy./séance", "Awaiting strap": "En attente du bracelet", "Awake": "Éveillé", "BANISTER TRIMP / HR ZONES": "BANISTER TRIMP / ZONES DE FC", @@ -442,23 +508,29 @@ "BEAT-TO-BEAT SCATTER": "NUAGE BATTEMENT PAR BATTEMENT", "BEATS PER MINUTE": "BATTEMENTS PAR MINUTE", "BEATS READ": "BATTEMENTS LUS", +"BLE protocol reverse-engineering": "Rétro-ingénierie du protocole BLE", "BMI": "IMC", "Back": "Retour", "Back to standard setup": "Retour à la configuration standard", "Back to the marker list": "Retour à la liste des marqueurs", "Back up now": "Sauvegarder maintenant", "Backed up": "Sauvegardé", +"Backed up to your folder.": "Sauvegardé dans ton dossier.", "Background Bluetooth has OS limits: iOS may pause NOOP when it's not in the foreground, so keep it open while syncing a fresh strap.": "Le Bluetooth en arrière-plan a des limites au niveau du système : iOS peut mettre NOOP en pause lorsqu'il n'est pas au premier plan, alors gardez-le ouvert pendant la synchronisation d'un nouveau bracelet.", "Backs up to your folder about once a day and keeps the latest %lld. On this platform it runs when you next open NOOP.": "Sauvegarde dans votre dossier environ une fois par jour et conserve les %lld plus récentes. Sur cette plateforme, cela s'exécute à la prochaine ouverture de NOOP.", +"Backup & Move": "Sauvegarde et transfert", "Backup & Sync": "Sauvegarde et synchronisation", "Backup & Sync to a folder…": "Sauvegarde et synchronisation vers un dossier…", "Backup & restore": "Sauvegarde et restauration", "Backup exported": "Sauvegarde exportée", +"Backup exported. Copy this file to your new phone and use Import there to restore everything.": "Sauvegarde exportée. Copie ce fichier sur ton nouveau téléphone et utilise Importer là-bas pour tout restaurer.", "Backup failed - re-pick the folder and try again.": "Échec de la sauvegarde, resélectionnez le dossier et réessayez.", "Backup failed: %@": "Échec de la sauvegarde : %@", "Backup folder": "Dossier de sauvegarde", "Backup imported": "Sauvegarde importée", +"Backup imported. Fully close and reopen NOOP for it to take effect.": "Sauvegarde importée. Ferme complètement puis rouvre NOOP pour l'appliquer.", "Backup problem": "Problème de sauvegarde", +"Backup saved.": "Sauvegarde enregistrée.", "Backup wake: your smart alarm time is here.": "Réveil de secours : votre heure d'alarme intelligente est arrivée.", "Baevsky Stress Index": "Indice de stress de Baevsky", "Balance": "Équilibre", @@ -467,25 +539,35 @@ "Based mostly on typical patterns, not yet yours. Log a few more %@ days and this becomes yours.": "Basé principalement sur des tendances types, pas encore les vôtres. Enregistrez encore quelques jours de %@ et cela deviendra le vôtre.", "Baseline": "Référence", "Baselines are learned on-device over your first 14 days. Until then, typical ranges apply.": "Les références sont apprises sur l'appareil au cours de vos 14 premiers jours. D'ici là, des plages types s'appliquent.", +"Baselines learned on-device over 14 days. Bars are an approximate read of each ": "Références apprises sur l'appareil sur 14 jours. Les barres sont une lecture approximative de chaque ", +"Baselines learned on-device over 14 days. Bars read each signal against a ": "Références apprises sur l'appareil sur 14 jours. Les barres évaluent chaque signal par rapport à une ", "Battery": "Batterie", "Battery %lld percent": "Batterie à %lld pour cent", "Battery %lld%%": "Batterie %lld%%", "Battery %lld%% · Charging": "Batterie %lld%% · En charge", "Battery alerts": "Alertes de batterie", +"Beat-to-beat": "Battement à battement", +"Beat-to-beat scatter": "Nuage de points battement à battement", "Beat-to-beat timing varies for many ordinary reasons: breathing, movement, an imperfect optical reading, or the occasional extra or skipped beat that most healthy people have.": "Le rythme battement par battement varie pour de nombreuses raisons ordinaires : la respiration, le mouvement, une lecture optique imparfaite, ou le battement occasionnel en trop ou manquant que la plupart des personnes en bonne santé ont.", "Beats": "Battements", "Beats per minute": "Battements par minute", "Beats per minute across the session": "Battements par minute au cours de la séance", +"Beats read": "Battements lus", "Bedtime": "Heure du coucher", +"Bedtime & wake time": "Heure de coucher et de lever", "Before you turn on Rhythm": "Avant d'activer Rhythm", "Before you use NOOP": "Avant d'utiliser NOOP", +"Begin": "Commencer", "Behaviour": "Comportement", "Behaviour Effects": "Effets comportementaux", +"Below": "En dessous", "Below Zone 1": "Sous la zone 1", "Below need": "Sous les besoins", "Beta": "Bêta", "Beta feature": "Fonction bêta", +"Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, ": "Bêta. * correspond à une estimation sur l'appareil. La température cutanée est une tendance par rapport à ta propre référence, ", "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, and HRV needs you to be still. No Oura Readiness or SpO₂ percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO₂ ne provient de la bague (importez un fichier Oura pour cela).", +"Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps ": "Bêta. * correspond à une estimation sur l'appareil. La température cutanée est une tendance par rapport à ta propre référence, les pas ", "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, les pas sont un simple comptage de mouvement, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO2 ne provient de la bague (importez un fichier Oura pour cela).", "Beta. Read this first.": "Bêta. Lisez ceci d'abord.", "Blood Oxygen": "Oxygène sanguin", @@ -495,6 +577,10 @@ "Blood oxygen (SpO₂)": "Oxygène sanguin (SpO₂)", "Blood panel": "Bilan sanguin", "Blood pressure": "Tension artérielle", +"Blue Titanium": "Titane bleu", +"Bluetooth is currently off.": "Le Bluetooth est actuellement désactivé.", +"Bluetooth is on.": "Le Bluetooth est activé.", +"Bluetooth link at a time.": "liaison Bluetooth à la fois.", "Body": "Corps", "Body Age": "Âge corporel", "Body Age %lld": "Âge corporel %lld", @@ -503,10 +589,16 @@ "Body clock": "Horloge biologique", "Body fat": "Graisse corporelle", "Bond your strap on the Live screen to feel the transitions hands-free.": "Associez votre bracelet sur l'écran En direct pour ressentir les transitions sans les mains.", +"Bonded": "Lié", +"Bonded · idle": "Lié · inactif", "Bonded · streaming": "Associé · flux actif", +"Bonded · syncing": "Lié · synchro en cours", "Bonded, streaming.": "Associé, flux actif.", +"Bonded. You can keep going.": "Lié. Tu peux continuer.", "Bonded. You're set.": "Associé. Vous êtes prêt.", +"Both NOOP and the Oura app can use a ring you own by key, but only one can hold the ": "NOOP et l'appli Oura peuvent tous les deux utiliser une bague que tu possèdes via sa clé, mais un seul peut garder la ", "Both NOOP and the Oura app can use a ring you own by key, but only one can hold the Bluetooth link at a time.": "NOOP et l'application Oura peuvent tous deux utiliser une bague que vous possédez via une clé, mais un seul peut détenir la liaison Bluetooth à la fois.", +"Both straps are supported": "Les deux bracelets sont pris en charge", "Bottle": "Bouteille", "Bounce back": "Récupération", "Box": "Carré", @@ -522,7 +614,10 @@ "Breathe out for %lld seconds": "Expirez pendant %lld secondes", "Breathe out…": "Expirez…", "Breaths per minute": "Respirations par minute", +"Bring in a markers CSV (date, marker, value, unit). Names that match the catalog ": "Importe un CSV de marqueurs (date, marqueur, valeur, unité). Les noms qui correspondent au catalogue ", "Bring your history": "Apportez votre historique", +"Bring your own API key. It is stored encrypted on this device and only used to ": "Utilise ta propre clé API. Elle est stockée chiffrée sur cet appareil et sert uniquement à ", +"Broadcast HR is ON. Your strap is advertising its heart rate continuously, ": "La diffusion de la FC est ACTIVÉE. Ton bracelet émet sa fréquence cardiaque en continu, ", "Broadcast HR is ON. Your strap is advertising its heart rate continuously, which keeps its radio hot and drains the battery faster. Turn it off when you're not using it with another device.": "La diffusion de la FC est ACTIVÉE. Votre bracelet diffuse en continu sa fréquence cardiaque, ce qui maintient sa radio active et vide la batterie plus vite. Désactivez-la lorsque vous ne l'utilisez pas avec un autre appareil.", "Broadcast heart rate": "Diffuser la fréquence cardiaque", "Broadcast heart rate (Garmin/ANT)": "Diffuser la fréquence cardiaque (Garmin/ANT)", @@ -540,6 +635,7 @@ "Building your baseline. About %lld more nights until your scores are personal.": "Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés.", "Building your baseline. About 1 more night until your scores are personal.": "Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés.", "Building your best-sourced record…": "Construction de votre relevé le mieux documenté…", +"Building · 1 window": "En construction · 1 fenêtre", "Building, moves as you do": "Construction, évolue avec vous", "Building, wear it tonight": "Construction, portez-le ce soir", "Building. Keep logging.": "En construction. Continuez à enregistrer.", @@ -548,16 +644,20 @@ "Built from published methods (Nes/HUNT) on %@. It's a fitness comparison against an average peer your age, not a biological or medical age.": "Calculé selon des méthodes publiées (Nes/HUNT) sur %@. C'est une comparaison de forme physique avec une personne moyenne de votre âge, pas un âge biologique ou médical.", "Built from the watch's cardio-fitness VO₂ max, the same number the Fitness app shows.": "Calculé à partir du VO₂ max cardio de la montre, le même chiffre qu'affiche l'application Fitness.", "Built on": "Construit le", +"Buzz": "Vibration", "Buzz Strap": "Faire vibrer le bracelet", "Buzz back (confirm)": "Vibration en retour (confirmer)", "Buzz cues on": "Signaux haptiques activés", +"Buzz on incoming calls": "Vibrer pour les appels entrants", "Buzz strap": "Vibrer le bracelet", "Buzz strength": "Intensité de la vibration", "Buzz the time": "Vibrer l'heure", +"Buzz the time on your strap": "Faire vibrer l'heure sur ton bracelet", "Buzz when you hit your top zone (ease off) and again when you recover. Uses your max HR from Settings.": "Vibre lorsque vous atteignez votre zone maximale (ralentissez) et à nouveau lorsque vous récupérez. Utilise votre FC max des Paramètres.", "Buzz your strap or mark a moment from Siri, Spotlight, the Shortcuts app, or a Back-Tap / automation. No setup needed.": "Faites vibrer votre bracelet ou marquez un instant depuis Siri, Spotlight, l'application Raccourcis, ou un Tapback / une automatisation. Aucune configuration nécessaire.", "Buzz your strap when these apps notify you. Everything runs on %@.": "Faites vibrer votre bracelet lorsque ces applications vous notifient. Tout s'exécute sur %@.", "Buzz your strap when these apps notify you. Everything runs on this Mac.": "Vibrez votre bracelet quand ces apps vous notifient. Tout fonctionne sur ce Mac.", +"Buzz your strap when these apps notify you. Everything runs on this device.": "Fais vibrer ton bracelet quand ces applis t'envoient une notification. Tout fonctionne sur cet appareil.", "Buzzes after you've been sitting past your threshold.": "Vibre après que vous êtes resté assis au-delà de votre seuil.", "Buzzes your wrist": "Vibre à votre poignet", "By Day": "Par jour", @@ -571,7 +671,9 @@ "CONTROL CENTER": "CENTRE DE CONTRÔLE", "CSV exported": "CSV exporté", "CUSTOMISE": "PERSONNALISER", +"Cadence": "Cadence", "Caffeine": "Caféine", +"Caffeine logged.": "Caféine enregistrée.", "Caffeine may still be active": "La caféine pourrait encore être active", "Calendar": "Calendrier", "Calibrate steps estimate": "Calibrer l'estimation des pas", @@ -579,6 +681,7 @@ "Calibrate your steps": "Calibrer vos pas", "Calibrating": "Calibration en cours", "Calibrating (%lld of %lld)": "Calibrage, %1$lld sur %2$lld", +"Calibrating , no heart rate banked yet today. Your curve fills in as the strap offloads.": "Calibrage , aucune fréquence cardiaque enregistrée aujourd'hui pour l'instant. Ta courbe se remplit à mesure que le bracelet transfère ses données.", "Calibrating, %lld of %lld": "Calibration, %lld de %lld", "Calibrating, %lld of %lld nights": "Calibrage, %lld nuits sur %lld", "Calibrating, no data yet": "Calibrage, aucune donnée pour le moment", @@ -587,20 +690,27 @@ "Calibrating. Building your baseline. About 1 more night until your scores are personal.": "Calibrage. Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés.", "Calibrating. Too thin to read yet.": "Calibrage. Encore trop peu de données pour une lecture.", "Calibration": "Calibrage", +"Calls": "Appels", "Calm": "Calme", "Calm me": "Calmez-moi", "Calm me · 3 min": "Calmez-moi · 3 min", "Calm time": "Temps calme", +"Calories": "Calories", +"Calories (kcal, optional)": "Calories (kcal, facultatif)", "Calories In": "Calories consommées", "Calories in kilocalories, optional": "Calories en kilocalories, facultatif", "Calories must be 0–20,000.": "Les calories doivent être comprises entre 0 et 20 000.", +"Can be chatty; quiet hours and \"only when worn\" still apply.": "Peut être bavard ; les heures calmes et \"uniquement lorsqu'il est porté\" s'appliquent toujours.", +"Can't connect - your strap's pairing was reset": "Connexion impossible - l'association de ton bracelet a été réinitialisée", "Can't connect: your strap's pairing was reset": "Connexion impossible : l'appairage de votre bracelet a été réinitialisé", "Cancel": "Annuler", "Capturing": "Capture en cours", "Capturing. %lld seconds remaining, %lld beats collected.": "Capture en cours. %1$lld secondes restantes, %2$lld battements collectés.", "Carbs": "Glucides", "Cardiac": "Cardiaque", +"Cardiovascular load for the day, on a 0-100 scale (was 0-21).": "Charge cardiovasculaire de la journée, sur une échelle de 0-100 (auparavant 0-21).", "Cardiovascular load for the day, on a 0–100 scale (was 0–21).": "Charge cardiovasculaire de la journée, sur une échelle de 0 à 100 (auparavant 0 à 21).", +"Cards with no value yet show a dash.": "Les cartes sans valeur pour l'instant affichent un tiret.", "Category": "Catégorie", "Change": "Modifier", "Change folder": "Changer de dossier", @@ -609,13 +719,18 @@ "Change to Number": "Passer en nombre", "Change to Yes/No": "Passer en Oui/Non", "Changes the Bluetooth name your WHOOP 4.0 advertises (what you see when pairing). The strap reboots to apply, so the new name appears the next time it connects. WHOOP 4.0 only.": "Modifie le nom Bluetooth que diffuse votre WHOOP 4.0 (celui que vous voyez lors de l'appairage). Le bracelet redémarre pour appliquer le changement, le nouveau nom apparaît donc à la prochaine connexion. WHOOP 4.0 uniquement.", +"Charge": "Charge", "Charge %lld out of 100": "Charge %lld sur 100", "Charge (all history)": "Charge (tout l'historique)", "Charge (past year)": "Charge (année écoulée)", +"Charge , all history": "Charge , tout l'historique", +"Charge , past year": "Charge , année écoulée", "Charge baseline calibrating. %@, %@. %@.": "Calibrage de la référence de charge. %1$@, %2$@. %3$@.", "Charge baseline recalibrating": "Recalibrage de la référence de charge", +"Charge baseline reset. NOOP will re-learn it from tonight. Your history stays, and it takes a few nights to settle.": "Référence de Charge réinitialisée. NOOP la réapprendra à partir de ce soir. Ton historique est conservé, et il faut quelques nuits pour qu'elle se stabilise.", "Charge calibrating": "Charge en calibration", "Charge calibrating, needs more data": "Charge en calibration, besoin de plus de données", +"Charge is NOOP's daily readiness score, learned from your own HRV, resting heart rate and more over time. Your history stays.": "La Charge est le score de préparation quotidien de NOOP, appris au fil du temps à partir de ta propre VFC, ta FC au repos et d'autres signaux. Ton historique est conservé.", "Charge is low and sleep was consistent.": "La charge est basse et le sommeil a été régulier.", "Charge is low but sleep ran short.": "La charge est basse et le sommeil a été insuffisant.", "Charge is low.": "La charge est basse.", @@ -625,44 +740,56 @@ "Charge is strong and sleep was consistent.": "La charge est élevée et le sommeil a été régulier.", "Charge is strong but sleep ran short.": "La charge est élevée mais le sommeil a été insuffisant.", "Charge is strong.": "La charge est élevée.", +"Charge it and put it on": "Mets-le en charge et enfile-le", "Charge model": "Modèle de charge", +"Charge needs about four nights of sleep to learn your baseline (that's \"Calibrating\", ": "La Charge a besoin d'environ quatre nuits de sommeil pour apprendre ta référence (c'est le \"Calibrage\", ", "Charge out of date, last synced %@. Open NOOP on iPhone.": "Charge non à jour, dernière synchro %@. Ouvrez NOOP sur iPhone.", "Charge stale · %@": "Charge obsolète · %@", "Charge trend": "Tendance de la charge", "Charge unavailable": "Charge indisponible", +"Charge weighs five signals against your own baseline: your overnight HRV matters most, ": "La Charge évalue cinq signaux par rapport à ta propre référence : ta VFC nocturne compte le plus, ", "Charge weighs your HRV against your personal baseline (~55%%), resting heart rate (~20%%), rest quality (~15%%), respiration (~5%%) and skin-temperature deviation (~5%%). Effort is a 0–%@ cardiovascular load from time in heart-rate zones. Rest is staged from movement and heart rate. Everything is computed here from the strap's raw data. It works for any day NOOP collected raw streams.": "La charge pondère votre VFC par rapport à votre référence personnelle (~55 %%), votre fréquence cardiaque au repos (~20 %%), la qualité du repos (~15 %%), la respiration (~5 %%) et l'écart de température cutanée (~5 %%). L'effort est une charge cardiovasculaire de 0 à %@ calculée à partir du temps passé dans les zones de fréquence cardiaque. Le repos est décomposé en phases à partir du mouvement et de la fréquence cardiaque. Tout est calculé ici à partir des données brutes du bracelet. Cela fonctionne pour tout jour où NOOP a collecté des flux bruts.", +"Charge weighs your heart-rate variability against your personal baseline ": "La Charge évalue ta variabilité de fréquence cardiaque par rapport à ta référence personnelle ", "Charge · Effort · Rest": "Charge · Effort · Repos", "Charge → Next-day charge": "Charge → Charge du lendemain", "Charge, Effort and Rest (and how they differ from WHOOP).": "Charge, Effort et Repos (et en quoi ils diffèrent de WHOOP).", +"Charge, Effort and Rest are scored on your own device from your strap data. ": "La Charge, l'Effort et le Repos sont calculés sur ton propre appareil à partir des données de ton bracelet. ", "Charge, Effort and Rest are scored on your own device from your strap data. Charge needs about four nights of sleep to learn your baseline (that's \"Calibrating\", counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. On a WHOOP 5 or MG the strap banks little history, so that count can sit at 0 of 4 until you have worn it across a few nights. That's the strap's sync limit, not a fault. Before there's a number, NOOP shows what it can without faking one.": "Charge, Effort et Repos sont calculés sur votre propre appareil à partir des données de votre bracelet. La charge nécessite environ quatre nuits de sommeil pour apprendre votre référence (c'est le « Calibrage », compté en nuits sur 4 sur la bague), et continue de s'affiner au cours de vos premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, ce compteur peut donc rester à 0 sur 4 jusqu'à ce que vous l'ayez porté quelques nuits. C'est la limite de synchronisation du bracelet, pas un défaut. Tant qu'il n'y a pas de chiffre, NOOP montre ce qu'il peut sans en inventer un.", "Charge, Effort and Rest become personal after a few nights of wear.": "Charge, Effort et Repos deviennent personnalisés après quelques nuits de port.", "Charge, Effort and Rest build over your next few nights of wear.": "Charge, Effort et Repos se construisent au cours de vos prochaines nuits de port.", +"Charge, Effort and Rest, and how they differ from WHOOP": "La Charge, l'Effort et le Repos, et en quoi ils diffèrent de WHOOP", "Charge, Effort, Rest, HRV, resting and live heart rate, and strap battery at a glance.": "Charge, Effort, Repos, VFC, fréquence cardiaque au repos et en direct, et batterie du bracelet en un coup d'œil.", +"Charge, effort and rest - scored with the model, explained in plain terms.": "Charge, effort et repos - calculés par le modèle, expliqués simplement.", "Charge: how recovered are you?": "Charge : à quel point êtes-vous récupéré ?", "Charging": "En charge", "Chart colours": "Couleurs des graphiques", "Check for updates": "Rechercher des mises à jour", "Check the values and try again.": "Vérifiez les valeurs et réessayez.", "Checking…": "Vérification…", +"Checks GitHub for the latest version when you tap. Nothing else is sent.": "Vérifie la dernière version sur GitHub quand tu appuies dessus. Rien d'autre n'est envoyé.", "Checks the project's home (GitHub) for the latest version when you tap. Nothing else is sent.": "Vérifie la page du projet (GitHub) pour la dernière version lorsque vous touchez. Rien d'autre n'est envoyé.", "Choose": "Choisir", "Choose .csv…": "Choisir .csv…", "Choose .gpx / .tcx / .fit…": "Choisir .gpx / .tcx / .fit…", +"Choose CSV…": "Choisir un CSV…", "Choose Light, Dark, or follow your system. Dark is the signature near-black; Light keeps the same clean look on a bright canvas.": "Choisissez Clair, Sombre, ou suivez votre système. Sombre est le noir profond caractéristique ; Clair conserve le même look épuré sur un fond lumineux.", "Choose Mi Fitness export…": "Choisir l'export Mi Fitness…", "Choose a backup": "Choisir une sauvegarde", "Choose a folder for NOOP backups (for example a Google Drive or iCloud folder).": "Choisissez un dossier pour les sauvegardes NOOP (par exemple un dossier Google Drive ou iCloud).", "Choose a markers CSV file to import": "Choisir un fichier CSV de marqueurs à importer", "Choose a wearable signal to compare": "Choisissez un signal d'appareil connecté à comparer", +"Choose double-tap action": "Choisir l'action du double appui", "Choose export.zip…": "Choisir export.zip…", "Choose export…": "Choisir l'export…", "Choose folder": "Choisir un dossier", +"Choose how NOOP looks on your home screen. The launcher may take a moment to refresh the icon after you change it.": "Choisis l'apparence de NOOP sur ton écran d'accueil. Le lanceur peut mettre un moment à actualiser l'icône après la modification.", "Choose how NOOP looks. The whole app updates as you tap. You can change this any time in Settings → Appearance.": "Choisissez l'apparence de NOOP. Toute l'application se met à jour au fur et à mesure. Vous pouvez changer cela à tout moment dans Paramètres → Apparence.", "Choose how distances, weights, heights, temperatures and Effort are shown. Your data is always stored the same way. This only changes the display.": "Choisissez comment les distances, poids, tailles, températures et l'Effort sont affichés. Vos données sont toujours stockées de la même manière. Cela ne change que l'affichage.", "Choose photo": "Choisir une photo", "Choose the buzz pattern for %@": "Choisir le schéma de vibration pour %@", "Choose which Key Metrics show and reorder them": "Choisissez les indicateurs clés à afficher et réorganisez-les", "Choose which cards show and reorder them": "Choisissez les cartes à afficher et réorganisez-les", +"Choose which cards show on Today and reorder them with the arrows. ": "Choisis les cartes affichées sur Aujourd'hui et réorganise-les avec les flèches. ", "Choose which tiles show on your Control Center and reorder them with the arrows.": "Choisissez les tuiles à afficher dans votre Centre de contrôle et réorganisez-les avec les flèches.", "Classic": "Classique", "Clean up": "Nettoyer", @@ -672,10 +799,16 @@ "Clear": "Effacer", "Clear %@ override, use the default wake time": "Effacer la substitution %@, utiliser l'heure de réveil par défaut", "Clear all": "Tout effacer", +"Clear filters": "Effacer les filtres", +"Clear search": "Effacer la recherche", +"Clear today": "Effacer aujourd'hui", "Close": "Fermer", "Close HRV reading": "Fermer la mesure de VFC", "Close Rhythm": "Fermer Rhythm", "Close Support": "Fermer l'assistance", +"Close the official WHOOP app": "Ferme l'appli officielle WHOOP", +"Cloud shape": "Forme du nuage", +"Coach": "Coach", "Coach instructions": "Instructions du Coach", "Coach instructions editor": "Éditeur des instructions du Coach", "Coach is thinking": "Coach réfléchit", @@ -685,6 +818,7 @@ "Coach talks only to the server URL you set. Point it at a local model (Ollama, LM Studio, llama.cpp) to keep everything on your own machine. Nothing is sent until you ask.": "Coach ne parle qu'à l'URL de serveur que vous définissez. Pointez-le vers un modèle local (Ollama, LM Studio, llama.cpp) pour tout garder sur votre propre machine. Rien n'est envoyé tant que vous ne le demandez pas.", "Coach uses your own API key. Pick a provider, paste a key, and choose a model. Your key is stored securely in the Keychain and never leaves %@ except as the request you make.": "Coach utilise votre propre clé API. Choisissez un fournisseur, collez une clé, et choisissez un modèle. Votre clé est stockée en sécurité dans le Trousseau et ne quitte jamais %@ sauf sous la forme de la requête que vous effectuez.", "Coach uses your own API key. Pick a provider, paste a key, and choose a model. Your key is stored securely in the macOS Keychain and never leaves your Mac except as the request you make.": "Coach utilise votre propre clé API. Choisissez un fournisseur, collez une clé et sélectionnez un modèle. Votre clé est stockée de façon sécurisée dans le trousseau macOS et ne quitte jamais votre Mac sauf pour la requête que vous effectuez.", +"Code, releases, and where to report an issue: github.com/MatB57/noop": "Code, versions et où signaler un problème : github.com/MatB57/noop", "Coherence": "Cohérence", "Coherence 5.5": "Cohérence 5,5", "Coherence estimate": "Estimation de cohérence", @@ -696,6 +830,8 @@ "Coming soon": "Bientôt disponible", "Coming together": "En cours d'assemblage", "Compare": "Comparer", +"Compare needs at least two metrics with history": "La comparaison nécessite au moins deux métriques avec un historique", +"Compare needs at least two metrics with history. Import your ": "La comparaison nécessite au moins deux métriques avec un historique. Importe tes ", "Compare needs at least two metrics with history. Import your WHOOP export in Data Sources first.": "Comparer nécessite au moins deux métriques avec un historique. Importez d'abord votre export WHOOP dans Sources de données.", "Compare sources": "Comparer les sources", "Compare with a signal": "Comparer avec un signal", @@ -710,6 +846,7 @@ "Configure": "Configurer", "Connect": "Connecter", "Connect Apple Health": "Connecter Apple Santé", +"Connect a WHOOP, a heart-rate strap or a gym machine and watch each beat in real time, with zones that match your profile. Already have history elsewhere? Import it from WHOOP, Apple Health, Oura, Fitbit or Garmin.": "Connecte un WHOOP, un bracelet de fréquence cardiaque ou une machine de salle de sport et observe chaque battement en temps réel, avec des zones adaptées à ton profil. Tu as déjà un historique ailleurs ? Importe-le depuis WHOOP, Apple Santé, Oura, Fitbit ou Garmin.", "Connect a WHOOP, a heart-rate strap or a gym machine and watch each beat in real time: heart rate, variability and zones as they happen. Already have history elsewhere? Import it from WHOOP, Apple Health, Oura, Fitbit or Garmin.": "Connectez un WHOOP, un bracelet cardio ou un appareil de salle de sport et regardez chaque battement en temps réel : fréquence cardiaque, variabilité et zones au fur et à mesure. Vous avez déjà un historique ailleurs ? Importez-le depuis WHOOP, Apple Santé, Oura, Fitbit ou Garmin.", "Connect a provider": "Connecter un fournisseur", "Connect strap for buzz cues": "Connecter le bracelet pour les signaux haptiques", @@ -723,13 +860,18 @@ "Connect your strap for haptic guidance. You'll feel one pulse on the inhale, two on the exhale, so you can breathe with your eyes closed.": "Connectez votre bracelet pour un guidage haptique. Vous sentirez une pulsation à l'inspiration, deux à l'expiration, afin de pouvoir respirer les yeux fermés.", "Connect your strap for the felt cue. The sweep paces you with one buzz on the inhale, two on the exhale.": "Connectez votre bracelet pour le signal ressenti. Le balayage vous guide avec une vibration à l'inspiration, deux à l'expiration.", "Connect your strap on the Live screen to take a reading.": "Connectez votre bracelet dans l'écran En direct pour effectuer une mesure.", +"Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss.": "Connecte ton bracelet pour activer ceci ; ça repose sur l'alarme du firmware du bracelet lui-même. Confirmé fonctionnel sur WHOOP 4.0 ; encore expérimental sur 5.0 et MG. Garde une alarme de secours pour tout ce que tu ne peux vraiment pas manquer.", "Connect your strap to enable": "Connectez votre bracelet pour activer", +"Connect your strap to sync its stored history. Until then, only imported data ": "Connecte ton bracelet pour synchroniser l'historique qu'il a stocké. En attendant, seules les données importées ", "Connect your strap to sync its stored history. Until then, only imported data shows here.": "Connectez votre bracelet pour synchroniser son historique enregistré. En attendant, seules les données importées s'affichent ici.", "Connect your strap to test": "Connectez votre bracelet pour tester", +"Connect your strap to use this. It arms the strap to buzz at your earliest wake time as a gentler first wake-up.": "Connecte ton bracelet pour utiliser cette fonction. Elle règle le bracelet pour qu'il vibre à ton heure de réveil la plus tôt, comme un premier réveil plus doux.", "Connect your strap. Calm me is a felt rhythm on the wrist, so it needs a bonded connection.": "Connectez votre bracelet. « Calmez-moi » est un rythme ressenti au poignet, il nécessite donc une connexion appairée.", "Connected": "Connecté", "Connected now": "Connecté maintenant", "Connected to %@. Pair or switch bands in Devices.": "Connecté à %@. Appairez ou changez de bracelet dans Appareils.", +"Connected · pairing": "Connecté · association", +"Connected, pairing…": "Connecté, association…", "Connected, waiting for a streaming state.": "Connecté, en attente d'un état de diffusion.", "Connected.": "Connecté.", "Connected. Finishing the secure pairing handshake…": "Connecté. Finalisation de la poignée de main d'appairage sécurisée…", @@ -768,19 +910,26 @@ "Couldn't safely back up right now. Recent changes are still in the write-ahead log.": "Sauvegarde impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées.", "Couldn't safely export right now. Recent changes are still in the database's write-ahead log. Close any in-flight sync, then try again.": "Export impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées de la base de données. Fermez toute synchronisation en cours, puis réessayez.", "Couldn't start. Needs a connected strap and a resting heart rate.": "Démarrage impossible. Un bracelet connecté et une fréquence cardiaque au repos sont nécessaires.", +"Couldn't use that photo. Try another.": "Impossible d'utiliser cette photo. Essaie-en une autre.", +"Couldn't write the debug export.": "Impossible d'écrire l'export de débogage.", "Couldn't write the strap log right now.": "Impossible d'écrire le journal du bracelet pour le moment.", "Counter ticks per step. Leave at 1.0 unless your steps run high. On a WHOOP 5/MG they can run very high (10× or more), so this goes up to 30. Walk a known 1,000 steps and divide NOOP's count by the real count to get your value.": "Impulsions du compteur par pas. Laissez à 1,0 sauf si votre nombre de pas est trop élevé. Sur un WHOOP 5/MG, il peut être très élevé (10× ou plus), d'où une plage allant jusqu'à 30. Marchez 1 000 pas connus et divisez le comptage de NOOP par le nombre réel pour obtenir votre valeur.", +"Counting…": "Comptage…", +"CrossFit": "CrossFit", "Crunching your raw streams…": "Traitement de vos flux bruts…", "Cues sent": "Signaux envoyés", "Cup": "Tasse", "Current calibration": "Calibration actuelle", "Current charge": "Charge actuelle", +"Current physiology, strap trust, and session controls": "Physiologie actuelle, fiabilité du bracelet et commandes de session", "Current physiology, strap trust, and session controls in one working view.": "Physiologie actuelle, fiabilité du bracelet et commandes de séance dans une seule vue de travail.", "Current: %@": "Actuel : %@", "Custom": "Personnalisé", "Custom %lld ml": "Personnalisé %lld ml", +"Custom amount": "Quantité personnalisée", "Custom marker name": "Nom du marqueur personnalisé", "Custom marker unit": "Unité du marqueur personnalisé", +"Custom model": "Modèle personnalisé", "Custom model id": "ID de modèle personnalisé", "Custom size": "Taille personnalisée", "Customise your cards": "Personnalisez vos cartes", @@ -788,24 +937,32 @@ "Custom…": "Personnalisé…", "Cutoff before bed": "Limite avant le coucher", "Cycle awareness": "Suivi du cycle", +"Cycle phase, body-clock and illness heads-up are approximations computed on your device from ": "La phase du cycle, l'horloge biologique et l'alerte maladie sont des approximations calculées sur ton appareil à partir de ", "Cycle phase: %@.": "Phase du cycle : %@.", "Cycle phase: %@. About day %lld to %lld.": "Phase du cycle : %1$@. Environ du jour %2$lld au %3$lld.", "Cycle phase: %@. About day %lld.": "Phase du cycle : %1$@. Environ jour %2$lld.", +"Cycling": "Vélo", "DAY %lld/%lld": "JOUR %1$lld/%2$lld", +"DAYTIME SLEEP": "SOMMEIL DE JOUR", "DEPLETED": "ÉPUISÉ", "DESCRIPTIVE STATS": "STATISTIQUES DESCRIPTIVES", "DIAGNOSTIC TOOLS": "OUTILS DE DIAGNOSTIC", "DONE": "TERMINÉ", "DURATION + EFFICIENCY + STAGES": "DURÉE + EFFICACITÉ + PHASES", +"Daily 0-3 proxy": "Approximation quotidienne 0-3", "Daily 0–3 proxy": "Proxy quotidien 0–3", "Daily auto-backup": "Sauvegarde automatique quotidienne", +"Daily auto-export": "Export automatique quotidien", "Daily auto-export of the strap log": "Export automatique quotidien du journal du bracelet", "Daily auto-export time": "Heure d'export automatique quotidien", +"Daily export time": "Heure de l'export quotidien", +"Daily reports": "Rapports quotidiens", "Daily signals": "Signaux quotidiens", "Dark": "Sombre", "Data & App": "Données et application", "Data Sources": "Sources de données", "Data sources": "Sources de données", +"Date": "Date", "Date and time taken": "Date et heure de la mesure", "Date taken": "Date de la mesure", "Day": "Jour", @@ -819,11 +976,15 @@ "Days with higher %@ tend to be your better-mood days.": "Les jours avec un %@ plus élevé ont tendance à être vos jours de meilleure humeur.", "Days with higher %@ tend to be your lower-mood days.": "Les jours avec un %@ plus élevé ont tendance à être vos jours de moins bonne humeur.", "Daytime sleep": "Sommeil diurne", +"Debt": "Dette", +"Debug logging": "Journalisation de débogage", "Dec": "Déc", "Deep": "Profond", "Deep (SWS) Sleep": "Sommeil profond (SWS)", "Deep Timeline": "Chronologie détaillée", "Deep Timeline, every second of your day, zoomable": "Chronologie détaillée, chaque seconde de votre journée, avec zoom", +"Deep blue accent on a dark blue-grey canvas.": "Accent bleu profond sur un fond gris-bleu foncé.", +"Deep blue accent on warm paper.": "Accent bleu profond sur papier chaud.", "Deep calm": "Calme profond", "Deep data (R22) needs an iPhone or Android. A Mac can't form the encrypted bond a 5/MG requires.": "Les données approfondies (R22) nécessitent un iPhone ou un Android. Un Mac ne peut pas établir l'appairage chiffré requis par un 5/MG.", "Deep sleep": "Sommeil profond", @@ -835,11 +996,16 @@ "Delete the %lld millilitre drink logged at %@": "Supprimer la boisson de %1$lld millilitres enregistrée à %2$@", "Delete this custom question": "Supprimer cette question personnalisée", "Delete this device's data…": "Supprimer les données de cet appareil…", +"Delete this nap": "Supprimer cette sieste", "Delete this reading": "Supprimer cette mesure", "Delete this sleep": "Supprimer ce sommeil", +"Delete this sleep session": "Supprimer cette session de sommeil", "Delete this sleep session?": "Supprimer cette séance de sommeil ?", "Depleted": "Épuisé", +"Descriptive stats": "Statistiques descriptives", +"Detect short naps": "Détecter les siestes courtes", "Detected": "Détecté", +"Detects call-style notifications from known calling apps.": "Détecte les notifications de type appel provenant d'applis d'appel connues.", "Device": "Appareil", "Device actions for %@": "Actions sur l'appareil pour %@", "Device metrics": "Métriques de l'appareil", @@ -858,6 +1024,8 @@ "Dismiss (not a workout)": "Ignorer (pas un entraînement)", "Dismiss this workout suggestion": "Ignorer cette suggestion d'entraînement", "Dismiss to Updates": "Ignorer et classer dans Mises à jour", +"Display name": "Nom affiché", +"Distance": "Distance", "Don't ask again": "Ne plus demander", "Don't buzz when the strap is off your wrist.": "Ne pas vibrer quand le bracelet n'est pas au poignet.", "Don't see it?": "Vous ne le voyez pas ?", @@ -871,14 +1039,18 @@ "Dose-response curve. Each extra %@ lines up with about %@ on %@, your own data.": "Courbe dose-réponse. Chaque %@ supplémentaire correspond à environ %@ sur %@, vos propres données.", "Double tap to collapse": "Double-tapez pour réduire", "Double tap to expand": "Double-tapez pour développer", +"Double-tap": "Double appui", "Double-tap the strap to trigger an action on %@. (The strap exposes a single double-tap gesture.)": "Double-tapez sur le bracelet pour déclencher une action sur %@. (Le bracelet ne propose qu'un seul geste de double-tap.)", "Download": "Télécharger", +"Download failed — opening the release page.": "Échec du téléchargement — ouverture de la page de version.", "Drag to pan · double-tap to reset": "Glissez pour déplacer · double-tap pour réinitialiser", "Drag to reorder. Toggle a card on or off. Cards with no value yet show a dash.": "Glissez pour réorganiser. Activez ou désactivez une carte. Les cartes sans valeur affichent un tiret.", -"Drives your Fitness Age": "Détermine votre âge de forme physique", +"Drives your Fitness Age": "Alimente ton Âge de forme", "Dumps the last 24 hours of decoded per-sample sensor streams (heart rate, R-R, motion, steps, SpO₂, skin temperature, respiration, events) to a single CSV. All on %@, nothing uploaded. Share it to help prototype and test sleep, activity and strength algorithms.": "Exporte les 24 dernières heures des flux de capteurs décodés échantillon par échantillon (fréquence cardiaque, R-R, mouvement, pas, SpO₂, température cutanée, respiration, événements) dans un seul fichier CSV. Tout sur %@, rien n'est envoyé. Partagez-le pour aider à prototyper et tester les algorithmes de sommeil, d'activité et de force.", "Duplicate as manual…": "Dupliquer en manuel…", +"Dur": "Dur.", "Duration": "Durée", +"Duration (minutes)": "Durée (minutes)", "Duration in minutes": "Durée en minutes", "During quiet, still, resting windows, NOOP looks at the timing between your heartbeats (R-R intervals) and draws their Poincaré scatter. From the cloud it computes its short and long axes (SD1, SD2) and a few plain regularity numbers. Movement and noisy windows are skipped, not shown. These are transparent, published descriptive statistics: a picture of your timing, never a clinical measurement.": "Pendant les périodes calmes, immobiles et au repos, NOOP examine le rythme entre vos battements cardiaques (intervalles R-R) et trace leur nuage de Poincaré. À partir du nuage, il calcule ses axes court et long (SD1, SD2) et quelques chiffres de régularité simples. Les périodes avec mouvement ou bruit sont ignorées, non affichées. Ce sont des statistiques descriptives transparentes et publiées : une image de votre rythme, jamais une mesure clinique.", "EARLIER": "PLUS TÔT", @@ -887,11 +1059,15 @@ "EXPLORATORY": "EXPLORATOIRE", "EXPORT": "EXPORTER", "EXTRA / SKIPPED": "EXTRA / IGNORÉ", +"Each bar is one day's Charge score, low to high. The 53-week calendar ": "Chaque barre représente le score de Charge d'un jour, du plus bas au plus haut. Le calendrier de 53 semaines ", +"Each day uses the time above unless you set a different one here.": "Chaque jour utilise l'heure ci-dessus, sauf si tu en définis une différente ici.", "Each dot pairs one heartbeat interval with the next. A tight line along the diagonal means a steady beat; a rounder, more spread-out cloud means the timing varied more.": "Chaque point associe un intervalle de battement cardiaque au suivant. Une ligne serrée le long de la diagonale indique un battement régulier ; un nuage plus rond et plus étalé indique une variation plus importante du rythme.", +"Each line is how many points that signal moved Charge versus sitting at your ": "Chaque ligne correspond au nombre de points dont ce signal a fait bouger la Charge par rapport à un maintien à ta ", "Each line min–max normalized within %@ · %@": "Chaque ligne normalisée min–max dans %1$@ · %2$@", "Each metric, where your Apple Watch sits on it, and why.": "Chaque métrique, où se situe votre Apple Watch, et pourquoi.", "Each score follows a published method, computed on your device. We name the method family so you can read up on it, and we never claim to reproduce another company's number exactly.": "Chaque score suit une méthode publiée, calculée sur votre appareil. Nous indiquons la famille de méthode pour que vous puissiez vous documenter, et nous ne prétendons jamais reproduire exactement le chiffre d'une autre entreprise.", "Each test logs extra detail for one part of the app while you wear the strap, then bundles it for a bug report.": "Chaque test consigne des détails supplémentaires pour une partie de l'application pendant que vous portez le bracelet, puis les regroupe pour un rapport de bug.", +"Earlier": "Plus tôt", "Early warning: take it easy": "Alerte précoce : allez-y doucement", "Edit": "Modifier", "Edit Key Metrics": "Modifier les indicateurs clés", @@ -908,6 +1084,10 @@ "Edit…": "Modifier…", "Effect size": "Taille d'effet", "Efficiency": "Efficacité", +"Effort": "Effort", +"Effort (0-100)": "Effort (0-100)", +"Effort building": "Effort en construction", +"Effort needs the machine's heart rate; without it the session logs the machine metrics only.": "L'effort a besoin de la fréquence cardiaque de la machine ; sans elle, la séance n'enregistre que les données de la machine.", "Effort scale": "Échelle d'effort", "Effort this %@": "Effort sur ce %@", "Effort: how hard did your heart work?": "Effort : à quel point votre cœur a-t-il travaillé ?", @@ -918,18 +1098,23 @@ "Enable inactivity reminder": "Activer le rappel d'inactivité", "Enable wrist alerts": "Activer les alertes au poignet", "Encrypted": "Chiffré", +"Encrypted stream - deep controls and history sync available.": "Flux chiffré - contrôles avancés et synchro de l'historique disponibles.", "Encrypted stream: deep controls and history sync available.": "Flux chiffré : commandes avancées et synchronisation de l'historique disponibles.", "End": "Terminer", +"End experiment": "Terminer l'expérience", "End session": "Terminer la séance", "End the experiment plan. Journal and metric history stay untouched.": "Terminer le plan d'expérience. Le journal et l'historique des métriques restent intacts.", "End workout": "Terminer l'entraînement", "Enjoying NOOP?": "Vous appréciez NOOP ?", "Enter NOOP": "Entrer dans NOOP", "Enter a model id": "Entrez un ID de modèle", +"Enter a sport, a positive duration (≤ 24h), and valid HR (25-250) / calories (0-20,000).": "Saisis un sport, une durée positive (≤ 24h) et une FC valide (25-250) / des calories (0-20,000).", "Enter a sport.": "Saisissez un sport.", +"Enter any model id the provider accepts.": "Saisis n'importe quel identifiant de modèle accepté par le fournisseur.", "Entered together; stored as two markers so each lines up cleanly against your signals.": "Saisis ensemble ; enregistrés comme deux marqueurs afin que chacun s'aligne précisément avec vos signaux.", "Equal breath · ~5.5 br/min coherence": "Respiration égale · cohérence ~5,5 resp/min", "Error: %@": "Erreur : %@", +"Est.": "Est.", "Estimate": "Estimation", "Estimate (partial data)": "Estimation (données partielles)", "Estimate from PPG-derived R-R: relaxation guidance, not a clinical reading. Your pace drifts, so we date it and you can re-measure anytime.": "Estimation à partir des R-R dérivés du PPG : guide de relaxation, pas une mesure clinique. Votre rythme dérive, nous la datons donc et vous pouvez remesurer à tout moment.", @@ -947,21 +1132,28 @@ "Evening": "Soir", "Evening forecast": "Prévision du soir", "Event": "Événement", +"Every": "Tous les", "Every beat, every night, every day, woven into one quiet picture of you. Welcome to NOOP.": "Chaque battement, chaque nuit, chaque jour, tissé en un portrait silencieux de vous. Bienvenue dans NOOP.", "Every day": "Chaque jour", "Every metric is an unvalidated approximation. Don't use NOOP to diagnose, treat, or make any health decision. Always consult a qualified professional.": "Chaque métrique est une approximation non validée. N'utilisez pas NOOP pour diagnostiquer, traiter ou prendre une décision de santé. Consultez toujours un professionnel qualifié.", "Every release, mirrored. A fallback if GitHub is ever down.": "Chaque version, mise en miroir. Une solution de repli si GitHub venait à être indisponible.", +"Every score carries a small honesty label. Calibrating means NOOP is still ": "Chaque score porte une petite étiquette d'honnêteté. « Calibrage » signifie que NOOP est encore ", "Every score carries a small honesty label. Calibrating means NOOP is still learning your baseline, or doesn't have enough data yet. Building means there's enough to show, but it's thin. Solid means full inputs are present. When NOOP can't compute a score honestly, it shows nothing rather than a fake number.": "Chaque score porte une petite étiquette de transparence. « Calibration » signifie que NOOP apprend encore votre référence ou n'a pas encore assez de données. « En construction » signifie qu'il y en a assez pour afficher quelque chose, mais c'est mince. « Solide » signifie que toutes les données sont présentes. Quand NOOP ne peut pas calculer un score honnêtement, il n'affiche rien plutôt qu'un chiffre inventé.", "Every second of your day, zoomable.": "Chaque seconde de votre journée, avec zoom.", +"Every second of your day. Drag back up to 3 days.": "Chaque seconde de ta journée. Fais glisser jusqu'à 3 jours en arrière.", "Every session, threaded together.": "Chaque séance, reliée ensemble.", "Every signal, one tap deep.": "Chaque signal, à un tap.", "Everyone has a breathing pace (usually between 4.5 and 7 breaths a minute) where the heart's rhythm swings the most with each breath. We pace you through a few candidate paces, measure how your HRV responds, and lock the one that resonates best for you.": "Chacun a un rythme respiratoire (généralement entre 4,5 et 7 respirations par minute) où le rythme cardiaque oscille le plus à chaque respiration. Nous vous guidons à travers quelques rythmes candidats, mesurons la réponse de votre VFC, et fixons celui qui résonne le mieux pour vous.", "Everything else, one tap away": "Tout le reste, à portée d'un tap", +"Everything here is a pattern in your own logged days: an association with an ": "Tout ici est une tendance dans tes propres journées enregistrées : une association avec un ", "Everything here is a pattern in your own logged days: an association with an effect size and confidence, never a cause or a diagnosis. Population patterns are shown as “typical” and are always overridden by your own data once you have enough of it. Approximations, not WHOOP’s scores; not a medical device.": "Tout ici est un schéma issu de vos propres jours enregistrés : une association avec une taille d'effet et un niveau de confiance, jamais une cause ou un diagnostic. Les schémas de population sont indiqués comme « typiques » et sont toujours remplacés par vos propres données dès que vous en avez suffisamment. Des approximations, pas les scores de WHOOP ; ce n'est pas un dispositif médical.", "Everything lives on %@. No account, no sync, no cloud. Your thread is yours alone.": "Tout réside sur %@. Aucun compte, aucune synchronisation, aucun cloud. Votre conversation n'appartient qu'à vous.", +"Everything lives on this phone. No account, no sync, no cloud.": "Tout vit sur ce téléphone. Pas de compte, pas de synchro, pas de cloud.", "Everything stays on %@. Bring your history in once, then it's yours.": "Tout reste sur %@. Importez votre historique une seule fois, il est ensuite à vous.", "Everything stays on this Mac. Bring your history in once, then it's yours.": "Tout reste sur ce Mac. Importez votre historique une fois, il est à vous.", +"Everything stays on this phone. Bring your history in once, then it's yours.": "Tout reste sur ce téléphone. Importe ton historique une fois, et il est à toi.", "Everything stays on your device": "Tout reste sur votre appareil", +"Exact model unknown. Shows what every WHOOP can do. * on-device estimate · ": "Modèle exact inconnu. Montre ce que chaque WHOOP peut faire. * estimation sur l'appareil · ", "Exact model unknown. Shows what every WHOOP can do. * on-device estimate · no SpO₂ % off the strap (import a WHOOP CSV for that).": "Modèle exact inconnu. Montre ce que chaque WHOOP peut faire. * estimation sur l'appareil · pas de % SpO₂ depuis le bracelet (importez un CSV WHOOP pour cela).", "Expand for the full read": "Développer pour la lecture complète", "Expanded": "Développé", @@ -970,19 +1162,25 @@ "Experiment progress": "Progression de l'expérience", "Experiment window length": "Durée de la fenêtre de l'expérience", "Experimental": "Expérimental", +"Experimental probes, diagnostics, raw-sensor export, and the Trends report. Tucked away to keep the everyday screen tidy.": "Sondes expérimentales, diagnostics, export brut des capteurs et le rapport Tendances. Rangés à l'écart pour garder l'écran de tous les jours épuré.", "Experimental sleep staging (V2)": "Détection des phases de sommeil expérimentale (V2)", +"Experimental sleep staging V2": "Découpage expérimental du sommeil V2", "Experimental wellness visualization: not a diagnosis, not an ECG, and not a medical device. It cannot detect any heart condition. Beat-to-beat variation has many ordinary, benign causes. If you feel unwell or are worried, contact a qualified professional; in an emergency, your local emergency service. Everything is computed on your device.": "Visualisation expérimentale du bien-être : ce n'est pas un diagnostic, pas un ECG, et pas un dispositif médical. Elle ne peut détecter aucune affection cardiaque. La variation battement à battement a de nombreuses causes courantes et bénignes. Si vous ne vous sentez pas bien ou êtes inquiet, contactez un professionnel qualifié ; en cas d'urgence, votre service d'urgence local. Tout est calculé sur votre appareil.", "Experimental · Sleep staging": "Expérimental · Phases de sommeil", "Experimental · WHOOP 5 / MG": "Expérimental · WHOOP 5 / MG", +"Experimental). Right now your wake time is saved but the strap is NOT armed.": "Expérimental). Pour l'instant, ton heure de réveil est enregistrée mais le bracelet n'est PAS armé.", "Experimental, and not a medical device": "Expérimental, et ce n'est pas un dispositif médical", +"Experimental, best-effort support. We're still testing these, so they might not connect on ": "Prise en charge expérimentale, au mieux. On teste encore ces appareils, donc ils peuvent ne pas se connecter sur ", "Experimental, best-effort support. We're still testing these, so they might not connect on every device. They never make up data, and they'll tell you honestly when live isn't possible.": "Prise en charge expérimentale, au mieux des capacités. Nous testons encore ces appareils, ils pourraient donc ne pas se connecter sur tous les modèles. Ils n'inventent jamais de données et vous diront honnêtement quand le direct n'est pas possible.", "Experimental: at your own risk": "Expérimental : à vos risques et périls", "Experimental: if your band needs pairing, we'll tell you honestly rather than show a fake reading.": "Expérimental : si votre bracelet nécessite un appairage, nous vous le dirons honnêtement plutôt que d'afficher une mesure fictive.", +"Experimental: live heart rate where the band exposes it. Some bands need a pairing ": "Expérimental : fréquence cardiaque en direct quand le bracelet l'expose. Certains bracelets ont besoin d'un ", "Experimental: live heart rate where the band exposes it. Some bands need a pairing we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, recovery, skin temp, SpO₂ or steps.": "Expérimental : fréquence cardiaque en direct là où le bracelet l'expose. Certains bracelets nécessitent un appairage que nous ne pouvons pas encore effectuer. NOOP le dira honnêtement et n'affichera jamais de chiffre inventé. Pas de sommeil, récupération, température cutanée, SpO₂ ni pas.", "Experimental: this is best-effort. If live doesn't work, you can export from Zepp and import the file.": "Expérimental : ceci est au mieux des capacités. Si le direct ne fonctionne pas, vous pouvez exporter depuis Zepp et importer le fichier.", "Explained by": "Expliqué par", "Explore": "Explorer", "Export": "Exporter", +"Export CSV writes a WHOOP-format zip of your days, sleeps, workouts and journal that re-imports into NOOP on Android or Mac. On-device computed rows are marked APPROXIMATE in its Source column; the .noopbak backup stays the lossless restore path.": "Export CSV crée un zip au format WHOOP de tes journées, sommeils, entraînements et journal, réimportable dans NOOP sur Android ou Mac. Les lignes calculées sur l'appareil sont marquées APPROXIMATE dans sa colonne Source ; la sauvegarde .noopbak reste la voie de restauration sans perte.", "Export CSV…": "Exporter en CSV…", "Export NOOP backup": "Exporter la sauvegarde NOOP", "Export NOOP data as CSV": "Exporter les données NOOP en CSV", @@ -991,27 +1189,40 @@ "Export failed: %@": "Échec de l'export : %@", "Export for Shortcuts (Apple Health)": "Exporter pour Raccourcis (Apple Santé)", "Export frames…": "Exporter les trames…", +"Export now": "Exporter maintenant", "Export problem": "Problème d'export", "Export raw + log": "Exporter les données brutes + le journal", +"Export raw + log (matched pair)": "Exporter brut + journal (paire associée)", "Export raw sensor data (CSV)": "Exporter les données brutes du capteur (CSV)", +"Export time": "Heure d'export", "Export trends report": "Exporter le rapport de tendances", "Exporting…": "Export en cours…", "Export…": "Exporter…", +"Extra / skipped": "En trop / ignoré", "FULL BOND": "APPAIRAGE COMPLET", "Fair": "Correct", "Fallback": "Solution de repli", "Fat": "Graisses", "Fat burn": "Combustion des graisses", +"Feature": "Fonction", +"Feature by strap": "Fonction par bracelet", "Features": "Fonctionnalités", "Feb": "Fév", +"Feel the current time as a sequence of buzzes (#460). Does nothing unless your strap is connected.": "Ressens l'heure actuelle sous forme d'une série de vibrations (#460). Sans effet si ton bracelet n'est pas connecté.", "Female": "Féminin", +"Fetch models from provider": "Récupérer les modèles du fournisseur", "Fetch the available models from %@ using your saved key": "Récupérer les modèles disponibles depuis %@ avec votre clé enregistrée", +"Fetching this metric's history.": "Récupération de l'historique de cette mesure.", +"Fetching…": "Récupération…", "File": "Fichier", +"Filter by source": "Filtrer par source", +"Filter by sport": "Filtrer par sport", "Find it in the sidebar under Data Sources.": "Retrouvez-le dans la barre latérale sous Sources de données.", "Find your resonance pace": "Trouvez votre rythme de résonance", "Find your strap": "Trouvez votre bracelet", "Fine-tune when alerts reach your wrist.": "Réglez précisément quand les alertes atteignent votre poignet.", "Finish": "Terminer", +"Finish selecting": "Terminer la sélection", "Finishing the pairing handshake. Sync now becomes available once the strap is paired.": "Finalisation de la poignée de main d'appairage. La synchronisation immédiate devient disponible une fois le bracelet appairé.", "Fire a single haptic pulse on the strap (requires a bonded connection)": "Envoyer une impulsion haptique sur le bracelet (nécessite une connexion associée)", "Fire a test buzz now": "Envoyer une vibration test maintenant", @@ -1019,14 +1230,18 @@ "Fires a test buzz on your strap": "Envoie une vibration test sur votre bracelet", "Fires the moment the strap leaves your wrist.": "Se déclenche dès que le bracelet quitte votre poignet.", "Firmware version %@": "Version du firmware %@", -"Fitness Age": "Âge fitness", +"Fitness Age": "Âge de forme", "Fitness Age %lld, %@. Tap to see the trend.": "Âge de forme physique %1$lld, %2$@. Tapez pour voir la tendance.", "Fitness Age %lld, %lld year%@ %@ than your age. Tap to see the trend.": "Âge de forme physique %1$lld, %2$lld an%3$@ %4$@ que votre âge. Tapez pour voir la tendance.", +"Fitness folder (or its .db / a .zip of it) from your phone and choose it here.": "Dossier Fitness (ou son .db / un .zip de celui-ci) depuis ton téléphone et choisis-le ici.", "Fitted from": "Ajusté à partir de", "Fix in Settings": "Corriger dans les Paramètres", +"Flag drinks late enough to still be active at bedtime.": "Signale les boissons prises assez tard pour être encore actives au coucher.", "Flags accepted, but the enable sequence doesn't start a separate live stream. The deep records arrive as part of the normal history sync (#494).": "Indicateurs acceptés, mais la séquence d'activation ne démarre pas de flux en direct séparé. Les enregistrements détaillés arrivent dans le cadre de la synchronisation normale de l'historique (#494).", "Follicular": "Folliculaire", +"Following your phone's light/dark setting.": "Suit le réglage clair/sombre de ton téléphone.", "For a WHOOP 4.0, which sends no step count: NOOP estimates steps from motion, calibrated to your phone. Tap to see how close it is and adjust it.": "Pour un WHOOP 4.0, qui n'envoie aucun nombre de pas : NOOP estime les pas à partir du mouvement, calibré sur votre téléphone. Tapez pour voir sa précision et l'ajuster.", +"For power users.": "Pour les utilisateurs avancés.", "Force-quit the official WHOOP app first, or pairing may fail.": "Forcez d'abord la fermeture de l'application officielle WHOOP, sinon l'appairage pourrait échouer.", "Forget the saved key and disconnect": "Oublier la clé enregistrée et déconnecter", "Frame": "Trame", @@ -1034,8 +1249,10 @@ "Fri": "Ven", "Friday": "Vendredi", "From": "De", +"From Health Connect this is derived from your weight and profile height.": "Depuis Health Connect, c'est déduit de ton poids et de la taille de ton profil.", "From strap HR": "Depuis la FC du bracelet", "From the watch's wrist-temperature sensor during sleep, on Series 8 and later. Older models don't have the sensor, so it reads “not available” rather than zero.": "Depuis le capteur de température au poignet de la montre pendant le sommeil, sur Series 8 et ultérieures. Les modèles plus anciens n'ont pas le capteur, il affiche donc « non disponible » plutôt que zéro.", +"From your nightly readings": "À partir de tes mesures nocturnes", "From your nightly sensor": "Depuis votre capteur nocturne", "From your nightly temperature": "Depuis votre température nocturne", "Full day": "Journée entière", @@ -1044,6 +1261,8 @@ "Fully quit and reopen NOOP to load it.": "Fermez complètement puis rouvrez NOOP pour le charger.", "Functional strength": "Force fonctionnelle", "Fused on %@. Nothing leaves it: no account, no cloud.": "Fusionné sur %@. Rien n'en sort : aucun compte, aucun cloud.", +"GPX · TCX · FIT (one workout per file)": "GPX · TCX · FIT (un entraînement par fichier)", +"Garmin": "Garmin", "Garmin watch": "Montre Garmin", "Generated by NOOP on %@ · all on-device, no account, no cloud.": "Généré par NOOP sur %@ · tout sur l'appareil, aucun compte, aucun cloud.", "Get Started": "Commencer", @@ -1062,18 +1281,25 @@ "Good afternoon": "Bon après-midi", "Good evening": "Bonsoir", "Good morning": "Bonjour", +"Good morning!": "Bonjour !", "Good morning. Your smart alarm just woke you.": "Bonjour. Votre alarme intelligente vient de vous réveiller.", "Got it": "Compris", "Grab this when you report a bug. It tells me what the app saw.": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé.", "Grab this when you report a bug. It tells me what the app saw. (The full live log is also on the Live screen.)": "Récupérez ceci pour signaler un bug. Cela m'indique ce que l'application a observé. (Le journal complet en direct est aussi disponible dans l'écran En direct.)", "Grant Health access": "Autoriser l'accès à Santé", +"Grant permission": "Accorder l'autorisation", "Gravity coverage": "Couverture de gravité", "Great": "Excellent", +"Group…": "Grouper…", +"Guaranteed wake": "Réveil garanti", +"Guarding your session. Silence means you're on track.": "Surveille ta séance. Le silence veut dire que tout va bien.", "Gym equipment": "Équipement de salle", "Gym equipment (FTMS)": "Équipement de salle (FTMS)", "HEART RATE": "FRÉQUENCE CARDIAQUE", "HF power": "Puissance HF", "HIGH": "ÉLEVÉ", +"HIIT": "HIIT", +"HR": "FC", "HR %@ · Effort %@": "FC %@ · Effort %@", "HR ZONE": "ZONE FC", "HR Zones": "Zones FC", @@ -1083,14 +1309,19 @@ "HR density (per min)": "Densité FC (par min)", "HR eased %lld → %lld over %@.": "FC réduite de %1$lld à %2$lld sur %3$@.", "HR held steady (%lld → %lld). Try a paced breath instead.": "FC restée stable (%1$lld → %2$lld). Essayez plutôt une respiration guidée.", +"HR may or may not appear, and deeper metrics (recovery, strain, sleep) aren't ": "La FC peut apparaître ou non, et les mesures plus poussées (récupération, effort, sommeil) ne sont pas ", "HR settled %lld → %lld over %@.": "FC stabilisée de %1$lld à %2$lld sur %3$@.", "HR settled over %@.": "FC stabilisée sur %@.", "HR unavailable": "FC indisponible", +"HR zone": "Zone de FC", +"HR zones": "Zones de FC", "HR-zone coaching": "Coaching par zone de FC", "HRV": "VFC", "HRV %@ · RHR %@": "VFC %@ · FCR %@", "HRV %lld%% over baseline": "VFC %lld %% au-dessus de la référence", "HRV %lld%% under baseline": "VFC %lld %% en dessous de la référence", +"HRV (RMSSD)": "VFC (RMSSD)", +"HRV (rMSSD)": "VFC (rMSSD)", "HRV Reading": "Mesure de la VFC", "HRV has dropped well below your baseline, pointing to elevated stress or fatigue. Ease off and give your body time to recover.": "La VFC est tombée bien en dessous de votre référence, signe de stress ou de fatigue élevés. Levez le pied et laissez à votre corps le temps de récupérer.", "HRV is above baseline, a sign of a relaxed, well-recovered nervous system. Stress is low.": "La VFC est au-dessus de la référence, signe d'un système nerveux détendu et bien récupéré. Le stress est faible.", @@ -1108,9 +1339,17 @@ "Heads-up": "Attention", "Heads-up. Your WHOOP only talks to one phone at a time. Force-quit the official WHOOP app first, or pairing may fail.": "Attention. Votre WHOOP ne communique qu'avec un seul téléphone à la fois. Forcez d'abord la fermeture de l'application WHOOP officielle, sinon le jumelage risque d'échouer.", "Health": "Santé", +"Health & wellness": "Santé et bien-être", +"Health Connect": "Health Connect", +"Health Connect access not granted.": "Accès à Health Connect non accordé.", +"Health Connect is not available on this device.": "Health Connect n'est pas disponible sur cet appareil.", +"Health Connect isn't set up on this device. Install it from Google Play, then return here to import.": "Health Connect n'est pas configuré sur cet appareil. Installe-le depuis Google Play, puis reviens ici pour importer.", +"Health Connect so other apps can use them. Only NOOP's own values are ": "Health Connect pour que d'autres applis puissent les utiliser. Seules les valeurs propres à NOOP sont ", +"Health Connect write access not granted.": "Accès en écriture à Health Connect non accordé.", "Health Monitor": "Moniteur de santé", "Health database": "Base de données Santé", "Health insights": "Perspectives sur la santé", +"Healthy Min": "Min. sain", "Heart & Vitals": "Cœur et constantes", "Heart Rate": "Fréquence cardiaque", "Heart Rate Variability": "Variabilité de la fréquence cardiaque", @@ -1139,10 +1378,14 @@ "Help, contact, and attribution.": "Aide, contact et crédits.", "Helping most: %@": "Aide le plus : %@", "Hidden": "Masqué", +"Hide": "Masquer", "Hide %@": "Masquer %@", "Hide data source detail": "Masquer le détail de la source de données", "Hide this question": "Masquer cette question", "High": "Élevé", +"Hiking": "Randonnée", +"Historical trend from cached daily metrics.": "Tendance historique à partir des mesures quotidiennes en cache.", +"Historical vitals from your cached daily metrics.": "Constantes historiques à partir de tes mesures quotidiennes en cache.", "History": "Historique", "History fills the dashboard immediately": "L'historique remplit immédiatement le tableau de bord", "History stays under the original question so WHOOP imports still line up.": "L'historique reste sous la question d'origine pour que les imports WHOOP restent alignés.", @@ -1152,8 +1395,11 @@ "History synced %@": "Historique synchronisé %@", "Holding you back: %@": "Vous freine : %@", "Holds the screen awake while you're recording a workout, so your live heart rate stays visible without the device dimming. Only applies during a recording. The screen sleeps normally the rest of the time. Leaving it on does use a bit more battery, and means your unlocked screen stays visible for the whole workout, so flip it off if that's a concern.": "Garde l'écran allumé pendant l'enregistrement d'un entraînement, pour que votre fréquence cardiaque en direct reste visible sans que l'appareil s'assombrisse. Ne s'applique que pendant un enregistrement. Le reste du temps, l'écran se met en veille normalement. Le laisser activé consomme un peu plus de batterie et signifie que votre écran déverrouillé reste visible pendant tout l'entraînement, alors désactivez-le si cela vous préoccupe.", +"Holds the screen awake while you're recording a workout, so your live heart rate stays visible without the phone dimming. Only applies during a recording. The screen sleeps normally the rest of the time. Leaving it on does use a bit more battery, and means your unlocked screen stays visible for the whole workout, so flip it off if that's a concern.": "Garde l'écran allumé pendant que tu enregistres un entraînement, pour que ta fréquence cardiaque en direct reste visible sans que le téléphone ne s'assombrisse. Ne s'applique que pendant un enregistrement. Le reste du temps, l'écran se met en veille normalement. Le laisser activé consomme effectivement un peu plus de batterie et fait que ton écran déverrouillé reste visible pendant tout l'entraînement, donc désactive-le si ça te pose problème.", +"Hourly stress today: ": "Stress par heure aujourd'hui : ", "Hours asleep": "Heures de sommeil", "Hours asleep trend": "Tendance des heures de sommeil", +"Hours of sleep debt per day": "Heures de dette de sommeil par jour", "Hours vs Needed": "Heures vs nécessaires", "How %@ is calculated": "Comment %@ est calculé", "How Charge is calculated": "Comment la charge est calculée", @@ -1165,16 +1411,21 @@ "How Rest is calculated": "Comment le repos est calculé", "How Stress is calculated": "Comment le stress est calculé", "How They Move Together": "Comment ils évoluent ensemble", +"How accurate is this Fitness Age?": "Quelle est la fiabilité de cet Âge de forme ?", "How accurate is this?": "Quelle est la précision de ces données ?", "How accurate is this? %@ the data behind your Fitness Age": "Quelle est la précision de ces données ? %@ les données derrière votre âge fitness", "How accurate is this? Hide the data behind your Fitness Age": "Quelle est la précision de ces données ? Masquer les données derrière votre âge fitness", "How accurate is this? Show the data behind your Fitness Age": "Quelle est la précision de ces données ? Afficher les données derrière votre âge fitness", +"How are you feeling today?": "Comment te sens-tu aujourd'hui ?", "How closely a good night tracks next-morning charge.": "À quel point une bonne nuit reflète la charge du lendemain matin.", "How much one day's charge carries into the next.": "Dans quelle mesure la charge d'un jour se répercute sur le suivant.", +"How recovered you are , led by HRV versus your personal baseline.": "À quel point tu es récupéré , guidé par la VFC comparée à ta référence personnelle.", "How recovered you are, led by HRV versus your personal baseline.": "Votre niveau de récupération, déterminé par la VFC par rapport à votre référence personnelle.", +"How restorative your sleep was , duration, efficiency, deep+REM, timing.": "À quel point ton sommeil a été réparateur , durée, efficacité, profond+REM, horaires.", "How restorative your sleep was: duration, efficiency, deep+REM, timing.": "Le caractère réparateur de votre sommeil : durée, efficacité, profond+paradoxal, horaires.", "How strong the buzz is.": "L'intensité de la vibration.", "How sure is NOOP? · Solid · Building · Calibrating": "Quel est le niveau de confiance de NOOP ? · Solide · En construction · Calibrage", +"How the smart wake works": "Comment fonctionne le réveil intelligent", "How this is computed": "Comment c'est calculé", "How this is measured": "Comment cela est mesuré", "How this score is calculated": "Comment ce score est calculé", @@ -1194,83 +1445,127 @@ "I have read and accept these terms, and I'm using NOOP with my own device and my own data, at my own risk.": "J'ai lu et j'accepte ces conditions, et j'utilise NOOP avec mon propre appareil et mes propres données, à mes risques et périls.", "I understand": "J'ai compris", "I understand this disconnects the ring from Oura and that NOOP cannot undo it for me.": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi.", +"I understand this disconnects the ring from Oura and that NOOP cannot undo it for me. To ": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas l'annuler pour moi. Pour ", "I understand this disconnects the ring from Oura and that NOOP cannot undo it for me. To go back to Oura I would factory-reset the ring again and set it up in the Oura app.": "Je comprends que cela déconnecte la bague d'Oura et que NOOP ne peut pas annuler cette action pour moi. Pour revenir à Oura, je devrai réinitialiser la bague aux paramètres d'usine et la configurer à nouveau dans l'application Oura.", "I understand this is an experimental wellness feature, not a medical device or a diagnosis.": "Je comprends qu'il s'agit d'une fonctionnalité de bien-être expérimentale, et non d'un dispositif médical ni d'un diagnostic.", "I'm awake": "Je suis réveillé", "I'm wearing it": "Je le porte", "IDLE": "INACTIF", "INBOX": "BOÎTE DE RÉCEPTION", +"Icon": "Icône", "Idle": "Inactif", +"If the Oura app is still running it will hold the ring and adoption will fail. Force-quit ": "Si l'appli Oura tourne encore, elle va garder la bague et l'adoption échouera. Ferme de force ", "If the Oura app is still running it will hold the ring and adoption will fail. Force-quit Oura, then scan.": "Si l'application Oura est encore en cours d'exécution, elle retiendra la bague et l'adoption échouera. Forcez la fermeture d'Oura, puis lancez le scan.", +"If the strap is nearby, NOOP will keep the BLE link alive in the background. You can continue through profile and import while it bonds.": "Si le bracelet est à proximité, NOOP maintiendra la liaison BLE active en arrière-plan. Tu peux continuer avec le profil et l'import pendant l'association.", "If you don't see the prompt, enable NOOP under Settings › Health › Data Access & Devices.": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils.", "If you don't see the prompt, turn NOOP on under Settings › Health › Data Access & Devices.": "Si vous ne voyez pas la demande, activez NOOP dans Réglages › Santé › Accès aux données et appareils.", +"If you extracted your ring's 16-byte key from a previous Oura setup, NOOP can talk to the ": "Si tu as extrait la clé de 16 octets de ta bague lors d'une configuration Oura précédente, NOOP peut communiquer avec la ", "If you extracted your ring's 16-byte key from a previous Oura setup, NOOP can talk to the ring with that key without resetting it, so the Oura app keeps working too. NOOP does not extract keys for you and cannot help you find one. If you do not know what this means, go back and use the standard setup or file import.": "Si vous avez extrait la clé de 16 octets de votre bague depuis une configuration Oura précédente, NOOP peut communiquer avec la bague grâce à cette clé sans la réinitialiser, si bien que l'application Oura continue elle aussi de fonctionner. NOOP n'extrait pas de clé pour vous et ne peut pas vous aider à en trouver une. Si vous ne savez pas ce que cela signifie, revenez en arrière et utilisez la configuration standard ou l'import de fichier.", "If you feel unwell or are worried about your heart, contact a qualified professional; in an emergency, your local emergency service. Do not rely on NOOP.": "Si vous vous sentez mal ou si vous êtes inquiet pour votre cœur, contactez un professionnel qualifié ; en cas d'urgence, appelez les services d'urgence locaux. Ne comptez pas sur NOOP.", "If you're still seated, buzz again this often.": "Si vous êtes toujours assis, vibrer à nouveau à cette fréquence.", "Illness early-warning": "Alerte précoce de maladie", +"Illness heads-up": "Alerte maladie", "Imaging": "Imagerie", "Imperial": "Impérial", "Import": "Importer", "Import Apple Health export": "Importer un export Apple Santé", +"Import Apple Health export…": "Importer un export Apple Santé…", +"Import HR, HRV, sleep, SpO₂ and steps from an Apple Health export. On ": "Importe la FC, la VFC, le sommeil, la SpO₂ et les pas depuis un export Apple Santé. Sur ", +"Import Mi Band export…": "Importer un export Mi Band…", "Import NOOP backup": "Importer une sauvegarde NOOP", "Import WHOOP export": "Importer un export WHOOP", +"Import WHOOP export (.zip)": "Importer un export WHOOP (.zip)", +"Import a Mi Band / Smart Band 8, 9 or 10's full history (steps, heart rate, ": "Importe tout l'historique d'un Mi Band / Smart Band 8, 9 ou 10 (pas, fréquence cardiaque, ", "Import a WHOOP export, Apple Health or a second band and your best-sourced record builds here, on this device.": "Importez un export WHOOP, Apple Santé ou un second bracelet, et votre historique le plus complet se construit ici, sur cet appareil.", +"Import a WHOOP export, Health Connect or a second band and your best-sourced record builds here, on this device.": "Importe un export WHOOP, Health Connect ou un deuxième bracelet et ton relevé le mieux sourcé se construit ici, sur cet appareil.", +"Import a single exported workout file from any brand (Garmin, Coros, Suunto, ": "Importe un seul fichier d'entraînement exporté, de n'importe quelle marque (Garmin, Coros, Suunto, ", "Import a single exported workout file from any brand (Garmin, Coros, Suunto, Wahoo, Polar, Strava, Apple) straight off your device. GPS route, distance, heart rate and calories come in where the file has them. Fully offline; nothing leaves %@.": "Importez un seul fichier d'entraînement exporté depuis n'importe quelle marque (Garmin, Coros, Suunto, Wahoo, Polar, Strava, Apple) directement depuis votre appareil. Le tracé GPS, la distance, la fréquence cardiaque et les calories sont importés lorsque le fichier les contient. Entièrement hors ligne ; rien ne quitte %@.", "Import an Apple Health export (Health app → profile → Export All Health Data → export.zip). 7 years of HR, HRV, sleep, SpO₂, steps and more, streamed locally. Large exports take a minute or two.": "Importez un export Apple Santé (application Santé → profil → Exporter toutes les données de santé → export.zip). 7 ans de FC, VFC, sommeil, SpO₂, pas et plus, traités localement. Les exports volumineux prennent une minute ou deux.", +"Import daily calories, protein, carbs, fat and body weight from a ": "Importe les calories, protéines, glucides, lipides et le poids corporel quotidiens depuis un ", "Import daily nutrition totals from a Cronometer or MacroFactor CSV export: calories in, protein, carbs, fat (and weight if present). Other trackers work too if the file has a date column and daily totals.": "Importez les totaux nutritionnels quotidiens depuis un export CSV Cronometer ou MacroFactor : calories consommées, protéines, glucides, lipides (et poids si présent). D'autres applications de suivi fonctionnent aussi si le fichier contient une colonne date et des totaux quotidiens.", "Import failed. Your existing data was kept. %@": "Échec de l'import. Vos données existantes ont été conservées. %@", "Import failed: %@": "Échec de l'import : %@", +"Import from Health Connect": "Importer depuis Health Connect", +"Import lifting log…": "Importer un journal de musculation…", +"Import nutrition CSV…": "Importer un CSV de nutrition…", "Import readings": "Importer des mesures", +"Import staged — NOOP is reloading. If it doesn't, fully close and reopen it.": "Import préparé — NOOP se recharge. Si ce n'est pas le cas, ferme-le complètement et rouvre-le.", "Import temp files": "Importer des fichiers temporaires", +"Import wearable export…": "Importer un export d'appareil connecté…", +"Import workout file…": "Importer un fichier d'entraînement…", "Import your Mi Band history (steps, heart rate, resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness app. On your iPhone: Files → On My iPhone → Mi Fitness, long-press the folder → Compress, then choose the .zip here. Fully offline; no Xiaomi account or Bluetooth needed. Smart Band 8/9/10.": "Importez l'historique de votre Mi Band (pas, fréquence cardiaque, FC au repos, phases de sommeil, SpO₂, stress et score de sommeil) directement depuis l'application Mi Fitness. Sur votre iPhone : Fichiers → Sur mon iPhone → Mi Fitness, appui long sur le dossier → Compresser, puis choisissez le fichier .zip ici. Entièrement hors ligne ; aucun compte Xiaomi ni Bluetooth requis. Smart Band 8/9/10.", "Import your full WHOOP history (recovery, strain, sleep, workouts) from a data export (.zip). Works for WHOOP 4.0, 5.0 and MG. Get one at app.whoop.com → Data Management.": "Importez tout votre historique WHOOP (récupération, effort, sommeil, entraînements) depuis un export de données (.zip). Fonctionne pour WHOOP 4.0, 5.0 et MG. Obtenez-en un sur app.whoop.com → Data Management.", +"Import your history first": "Importe d'abord ton historique", +"Import your history first. A WHOOP export in Data Sources fills ": "Importe d'abord ton historique. Un export WHOOP dans Sources de données remplit ", "Import your history first. A WHOOP export in Data Sources fills every metric you can explore here in about a minute.": "Importez d'abord votre historique. Un export WHOOP dans Sources de données remplit toutes les métriques que vous pouvez explorer ici en environ une minute.", +"Import your own data export from Oura, Fitbit or Garmin: sleep, resting heart ": "Importe ton propre export de données depuis Oura, Fitbit ou Garmin : sommeil, fréquence cardiaque au ", "Import your own data export from Oura, Fitbit or Garmin: sleep, resting heart rate, HRV, steps and more, where the export has them. Download it from the brand's app (Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then choose the file here. Fully offline; nothing leaves %@. Each brand's own readiness or sleep score is kept for reference only. Your scores stay yours.": "Importez votre propre export de données depuis Oura, Fitbit ou Garmin : sommeil, fréquence cardiaque au repos, VFC, pas et plus, lorsque l'export les contient. Téléchargez-le depuis l'application de la marque (Oura : Compte → Export Data ; Fitbit : Google Takeout ; Garmin : Export Your Data), puis choisissez le fichier ici. Entièrement hors ligne ; rien ne quitte %@. Le score de forme ou de sommeil propre à chaque marque est conservé à titre de référence uniquement. Vos scores restent les vôtres.", +"Import your strength-training history from a Hevy CSV export or a Liftosaur ": "Importe ton historique de musculation depuis un export CSV Hevy ou un Liftosaur ", "Import your strength-training history from a Hevy CSV export or a Liftosaur JSON export. Each workout becomes a Strength session with a training-volume estimate (weight × reps). It's a volume figure, not a measured strain. It never changes your Effort.": "Importez votre historique de musculation depuis un export CSV Hevy ou un export JSON Liftosaur. Chaque entraînement devient une séance de Force avec une estimation du volume d'entraînement (poids × répétitions). C'est un chiffre de volume, pas un effort mesuré. Cela ne modifie jamais votre Effort.", "Imported": "Importé", "Imported %lld days (%lld values)": "%1$lld jours importés (%2$lld valeurs)", "Imported %lld workouts": "%lld entraînements importés", "Imported 1 workout": "1 entraînement importé", +"Imported — reloading NOOP…": "Importé — rechargement de NOOP…", "Importing overwrites everything currently on %@. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect. Export CSV writes a WHOOP-format zip of your days, sleeps, workouts and journal that re-imports into NOOP on Mac, iPhone, or Android. On-device computed rows are marked APPROXIMATE in its Source column; the full backup stays the lossless restore path.": "L'import écrase tout ce qui se trouve actuellement sur %@. Vos anciennes données sont conservées dans un fichier annexe par précaution. NOOP doit être relancé pour que l'import prenne effet. « Exporter en CSV » écrit un zip au format WHOOP de vos jours, sommeils, entraînements et journal, qui peut être réimporté dans NOOP sur Mac, iPhone ou Android. Les lignes calculées sur l'appareil sont marquées APPROXIMATIVE dans la colonne Source ; la sauvegarde complète reste le moyen de restauration sans perte.", "Importing overwrites everything currently on this Mac. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect.": "L'importation écrase tout ce qui se trouve actuellement sur ce Mac. Vos anciennes données sont conservées dans un fichier annexe au cas où. NOOP doit être relancé pour qu'une importation prenne effet.", +"Importing overwrites everything currently on this phone. Your old data is kept in a side file just in case. NOOP needs a relaunch for an import to take effect. ": "L'import écrase tout ce qui se trouve actuellement sur ce téléphone. Tes anciennes données sont conservées dans un fichier annexe au cas où. NOOP doit redémarrer pour qu'un import prenne effet. ", "Importing…": "Importation…", "Import…": "Importer…", +"In band": "Dans la zone", +"In typical range": "Dans la plage habituelle", "In your data so far, this doesn’t move your %@ the way it typically does.": "Dans vos données actuelles, cela ne fait pas varier votre %@ comme d'habitude.", "In your range": "Dans votre plage", "Inactivity reminder": "Rappel d'inactivité", +"Inbox": "Boîte de réception", "Incl. Helio. Live heart rate where the band exposes it. Help us test.": "Incl. Helio. Fréquence cardiaque en direct lorsque le bracelet la fournit. Aidez-nous à tester.", "Independent, and experimental": "Indépendant, et expérimental", "Independent: not affiliated with WHOOP": "Indépendant : non affilié à WHOOP", "Informational only, not medical advice.": "À titre informatif uniquement, pas un avis médical.", "Insights": "Perspectives", +"Insights read your journal and outcomes": "Les analyses lisent ton journal et tes résultats", "Insights read your journal and outcomes. Import your WHOOP export, which includes your journal, in Data Sources to unlock them.": "Les perspectives lisent votre journal et vos résultats. Importez votre export WHOOP, qui inclut votre journal, dans Sources de données pour les débloquer.", "Installed outside the App Store": "Installé en dehors de l'App Store", +"Installing NOOP's key and confirming the ring answers only to NOOP. Keep the ring close and ": "Installation de la clé de NOOP et vérification que la bague ne répond qu'à NOOP. Garde la bague à proximité et ", "Installing NOOP's key and confirming the ring answers only to NOOP. Keep the ring close and do not open the Oura app.": "Installation de la clé de NOOP et vérification que la bague ne répond qu'à NOOP. Gardez la bague à proximité et n'ouvrez pas l'application Oura.", +"Intelligence": "Intelligence", "Intensity Minutes": "Minutes d'intensité", "Intensity minutes": "Minutes d'intensité", "Interrogate what affects what.": "Interrogez ce qui affecte quoi.", "Interval Timer": "Minuteur d'intervalle", "Intervals": "Intervalles", +"Intervention": "Intervention", "Intraday": "Intrajournalier", "It all stays on your strap and %@: no account, no cloud.": "Tout reste sur votre bracelet et %@ : pas de compte, pas de cloud.", +"It compares your resting heart rate and recent activity against people your age. ": "Il compare ta FC au repos et ton activité récente à celles des personnes de ton âge. ", "It is a picture, not a verdict": "C'est un instantané, pas un verdict", "It is not a substitute for a professional": "Cela ne remplace pas un professionnel", "It isn't held by the WHOOP phone app. Only one host at a time: close the app or turn off its Bluetooth.": "Il n'est pas retenu par l'application WHOOP du téléphone. Un seul hôte à la fois : fermez l'application ou désactivez son Bluetooth.", "It shows the shape of your heartbeat timing and a plain-language description of how steady it looked. It does not tell you whether anything is right or wrong.": "Cela montre la forme du rythme de vos battements et une description en langage simple de sa régularité apparente. Cela ne vous dit pas si quelque chose est normal ou anormal.", +"It stays on this phone, and over time you'll see how it lines up with your sleep, ": "Ça reste sur ce téléphone, et avec le temps tu verras comment ça colle avec ton sommeil, ", +"It's a notebook, not a lab. NOOP lines up the numbers you enter. It doesn't test, ": "C'est un carnet, pas un labo. NOOP aligne les chiffres que tu saisis. Il ne teste pas, ", "It's a notebook, not a lab. NOOP lines up the numbers you enter. It doesn't test, read, or judge them. Not medical advice.": "C'est un carnet, pas un laboratoire. NOOP aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse ni ne les juge. Ce n'est pas un avis médical.", "It's charged and worn. The sensor needs skin contact to wake.": "Il est chargé et porté. Le capteur a besoin d'un contact avec la peau pour se réveiller.", "It's within about a metre of %@.": "Il se trouve à environ un mètre de %@.", +"JSON export. Each workout becomes a Strength session with a training-volume ": "Export JSON. Chaque entraînement devient une séance Musculation avec un volume d'entraînement ", "Jan": "Janv.", +"Journal": "Journal", "Jul": "Juil.", "Jun": "Juin", +"Kcal": "Kcal", +"Keep connected in the background": "Rester connecté en arrière-plan", +"Keep it near this phone while pairing and during the first sync.": "Garde-le près de ce téléphone pendant l'appairage et la première synchro.", "Keep it within about a metre of %@.": "Gardez-le à environ un mètre de %@.", +"Keep logging. Effects need days both with and without each behaviour.": "Continue à enregistrer. Les effets ont besoin de jours avec et sans chaque comportement.", "Keep screen on during a workout": "Garder l'écran allumé pendant un entraînement", "Keep the Oura app instead (import a file)": "Conserver l'application Oura à la place (importer un fichier)", "Keep the Oura app instead, import a file": "Conserver l'application Oura à la place, importer un fichier", "Keep the ring on the charger or on your finger so it stays awake.": "Gardez la bague sur le chargeur ou à votre doigt pour qu'elle reste éveillée.", "Keep your bloods, BP and body numbers private, on %@.": "Gardez vos résultats sanguins, votre tension et vos données corporelles privés, sur %@.", "Keep your own numbers here": "Conservez vos propres données ici", +"Keeps streaming from your strap with an ongoing notification, even after you close NOOP. Turn off to disconnect when the app is closed.": "Continue de recevoir le flux de ton bracelet avec une notification persistante, même après avoir fermé NOOP. Désactive pour te déconnecter quand l'appli est fermée.", "Keeps the detailed beat-to-beat heart-rate stream running all day and night, not just while a live screen is open, so NOOP captures much more for overnight HRV, recovery and sleep. Uses more battery: your strap streams heart rate continuously while connected.": "Maintient le flux détaillé battement par battement de la fréquence cardiaque actif jour et nuit, pas seulement lorsqu'un écran en direct est ouvert, afin que NOOP capture bien plus de données pour la VFC nocturne, la récupération et le sommeil. Utilise plus de batterie : votre bracelet transmet en continu la fréquence cardiaque tant qu'il est connecté.", +"Keeps the detailed beat-to-beat stream running all day and night, not just while a live screen is open, so NOOP captures much more for overnight HRV, recovery and sleep. Uses more battery (your strap streams heart rate continuously). Needs \"Keep connected in the background\" on.": "Maintient le flux détaillé battement par battement actif jour et nuit, pas seulement quand un écran en direct est ouvert, pour que NOOP capture bien plus pour la VFC nocturne, la récupération et le sommeil. Consomme plus de batterie (ton bracelet diffuse la fréquence cardiaque en continu). Nécessite l'activation de \"Rester connecté en arrière-plan\".", "Key Metrics": "Métriques clés", "LAST NIGHT": "NUIT DERNIÈRE", "LIGHT": "LÉGER", @@ -1282,9 +1577,12 @@ "LOW": "FAIBLE", "LOW SIGNAL": "SIGNAL FAIBLE", "Lab Book": "Carnet de labo", +"Lab Book is a private notebook, not a medical service. NOOP stores and lines up the numbers ": "Lab Book est un carnet privé, pas un service médical. NOOP stocke et aligne les chiffres ", "Lab Book is a private notebook, not a medical service. NOOP stores and lines up the numbers you enter. It doesn't test, read, diagnose, or advise. Your records never leave %@; there's no account or cloud, so it isn't \"HIPAA-covered.\" Always rely on your doctor or pharmacist to interpret results.": "Le Carnet de labo est un carnet privé, pas un service médical. NOOP stocke et aligne les chiffres que vous saisissez. Il ne les teste, ne les analyse, ne les diagnostique ni ne les commente. Vos données ne quittent jamais %@ ; il n'y a ni compte ni cloud, donc ce n'est pas « couvert par HIPAA ». Fiez-vous toujours à votre médecin ou pharmacien pour interpréter les résultats.", +"Lab Book keeps your own numbers - it doesn't test, read, or judge them, and it's not medical ": "Lab Book garde tes propres chiffres - il ne les teste pas, ne les lit pas et ne les juge pas, et ce n'est pas un avis médical ", "Lab Book keeps your own numbers. It doesn't test, read, or judge them, and it's not medical advice. Everything stays on %@.": "Le Carnet de labo conserve vos propres données. Il ne les teste, ne les analyse ni ne les juge, et ce n'est pas un avis médical. Tout reste sur %@.", "Last %lld beats": "%lld derniers battements", +"Last 14 days": "14 derniers jours", "Last 14 nights": "14 dernières nuits", "Last 30 days": "30 derniers jours", "Last 6 months": "6 derniers mois", @@ -1308,15 +1606,18 @@ "Last session: %@": "Dernière séance : %@", "Last sleep": "Dernier sommeil", "Last sync": "Dernière synchronisation", +"Last sync: ": "Dernière synchro : ", "Last synced %@.": "Dernière synchronisation %@.", "Last synced %lld minutes ago. Reconnect to pull the latest.": "Dernière synchronisation il y a %lld minutes. Reconnectez-vous pour récupérer les dernières données.", "Last synced %lldm ago": "Dernière synchronisation il y a %lldmin", "Last week": "La semaine dernière", "Last year": "L'année dernière", +"Late-caffeine nudge": "Rappel caféine tardive", "Later": "Plus tard", "Latest": "Dernier", "Latest entry: %@.": "Dernière entrée : %@.", "Latest reading": "Dernière lecture", +"Latest readings": "Dernières mesures", "Latest sleep · %@": "Dernier sommeil · %@", "Latest sleep, %@. This is your last scored session. Wear the strap overnight for a fresh score.": "Dernier sommeil, %@. C'est votre dernière séance notée. Portez le bracelet pendant la nuit pour obtenir un nouveau score.", "Lean Body Mass": "Masse maigre", @@ -1326,17 +1627,21 @@ "Learning your baseline, %lld of %lld nights.": "Apprentissage de votre référence, %lld de %lld nuits.", "Learning your pattern": "Apprentissage de votre schéma", "Leave none active": "Ne laisser aucune active", +"Led by your heart-rate variability (HRV) measured against your own ": "Guidé par ta variabilité de la fréquence cardiaque (VFC) mesurée par rapport à ta propre ", "Led by your heart-rate variability (HRV) measured against your own personal baseline, plus resting heart rate, last night's Rest, breathing rate, and a skin-temperature signal (an early illness or overreach flag). Higher HRV versus your baseline means more Charge. NOOP needs a few nights to learn your baseline first. Until then you'll see “Calibrating”.": "Déterminée par votre variabilité de fréquence cardiaque (VFC) mesurée par rapport à votre référence personnelle, ainsi que par la fréquence cardiaque au repos, le Repos de la nuit dernière, la fréquence respiratoire et un signal de température cutanée (un indicateur précoce de maladie ou de surcharge). Une VFC plus élevée par rapport à votre référence signifie plus de Charge. NOOP a d'abord besoin de quelques nuits pour apprendre votre référence. Jusque-là, vous verrez « Calibrage ».", "Led by your heart-rate variability versus your own baseline. The watch samples HRV rather than streaming it, so this needs about a week of nights to calibrate. Until then NOOP shows “needs more data”, never a guessed number.": "Déterminée par votre variabilité de fréquence cardiaque par rapport à votre propre référence. La montre échantillonne la VFC au lieu de la transmettre en continu, ce qui nécessite environ une semaine de nuits pour se calibrer. D'ici là, NOOP affiche « besoin de plus de données », jamais un chiffre estimé.", "Leftover import copies": "Copies d'import restantes", +"Lengthen window": "Allonger la fenêtre", "Let NOOP tap your wrist for the things you turn on below, so you can leave your phone and still feel what matters.": "Laissez NOOP tapoter votre poignet pour les fonctions que vous activez ci-dessous, afin que vous puissiez laisser votre téléphone tout en ressentant ce qui compte.", "Let the check-in fire on its own. Off keeps it manual: you start a breath from Breathe yourself.": "Laisser le check-in se déclencher tout seul. Désactivé, cela reste manuel : vous démarrez vous-même une respiration depuis Respirer.", "Let the coach use my data": "Laisser le coach utiliser mes données", +"Lets NOOP notice a fresh HRV dip while you're still and offer a minute to breathe. \"Stress\" here is an autonomic proxy from your own baseline, never a diagnosis. The strap gives one light confirming buzz; no push notification.": "Permet à NOOP de repérer une nouvelle baisse de VFC quand tu es immobile et de te proposer une minute pour respirer. Ici, le \"Stress\" est un indicateur indirect du système autonome basé sur ta propre référence, jamais un diagnostic. Le bracelet émet une seule légère vibration de confirmation ; pas de notification push.", "Lifting": "Musculation", "Lifting log (Hevy / Liftosaur)": "Journal de musculation (Hevy / Liftosaur)", "Light": "Léger", "Light + sleep timing only": "Lumière + horaires de sommeil uniquement", "Light Sleep": "Sommeil léger", +"Likely your Oura warranty and support, because the ring is no longer paired to Oura. ": "Probablement ta garantie et ton support Oura, parce que la bague n'est plus appairée à Oura. ", "Likely your Oura warranty and support, because the ring is no longer paired to Oura. Treat this as permanent.": "Probablement votre garantie et le support Oura, car la bague n'est plus jumelée à Oura. Considérez ceci comme permanent.", "Lining them up…": "Alignement en cours…", "Liquid Today (prototype)": "Liquid Today (prototype)", @@ -1344,8 +1649,11 @@ "Live Body Console": "Console corporelle en direct", "Live HR": "FC en direct", "Live HR (not fully paired)": "FC en direct (jumelage incomplet)", +"Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery ": "FC + R-R en direct uniquement · ni sommeil, ni récupération, ni température cutanée, ni SpO₂, ni pas, ni batterie ", "Live HR + R-R only · no sleep, recovery, skin temp, SpO₂, steps or battery (those are WHOOP-only).": "FC en direct + R-R uniquement · pas de sommeil, récupération, température cutanée, SpO₂, pas ou batterie (réservés à WHOOP).", "Live HR works. Free the strap to unlock buzz, alarms & sync": "La FC en direct fonctionne. Libérez le bracelet pour débloquer vibrations, alarmes et synchro", +"Live HR · not fully paired": "FC en direct · pas entièrement appairé", +"Live Physiology": "Physiologie en direct", "Live Session": "Séance en direct", "Live Sessions (beta)": "Séances en direct (bêta)", "Live heart rate": "Fréquence cardiaque en direct", @@ -1358,7 +1666,9 @@ "Live heart rate is instant. Recovery, strain and sleep sharpen as NOOP learns your baseline over your first nights of wear. Want your history now? Import your WHOOP export in Data Sources and it backfills in about a minute.": "La fréquence cardiaque en direct est instantanée. Récupération, effort et sommeil s'affinent à mesure que NOOP apprend votre référence pendant vos premières nuits de port. Vous voulez votre historique tout de suite ? Importez votre export WHOOP dans Sources de données, il se recharge en environ une minute.", "Live heart rate on bands that don't need pairing. Help us test.": "Fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de jumelage. Aidez-nous à tester.", "Live heart rate over time": "Fréquence cardiaque en direct dans le temps", +"Live heart rate over time, no data": "Fréquence cardiaque en direct au fil du temps, aucune donnée", "Live heart rate, and HRV when the ring can measure it.": "Fréquence cardiaque en direct, et VFC quand la bague peut la mesurer.", +"Live machine data over Bluetooth FTMS. No sleep, recovery, skin temp or SpO₂. ": "Données machine en direct via Bluetooth FTMS. Pas de sommeil, de récupération, de température cutanée ni de SpO₂. ", "Live machine data over Bluetooth FTMS. No sleep, recovery, skin temp or SpO₂. Effort needs the machine's heart rate; without it the session logs the machine metrics only.": "Données de machine en direct via Bluetooth FTMS. Pas de sommeil, récupération, température cutanée ou SpO₂. L'Effort nécessite la fréquence cardiaque de la machine ; sans elle, la séance n'enregistre que les métriques de la machine.", "Live now. Your scores are building.": "En direct maintenant. Vos scores se construisent.", "Live vitals, streamed from the strap.": "Constantes en direct, diffusées depuis le bracelet.", @@ -1366,19 +1676,26 @@ "Loading your history…": "Chargement de votre historique…", "Loading your sessions…": "Chargement de vos séances…", "Loading your sleep history…": "Chargement de votre historique de sommeil…", +"Loading…": "Chargement…", +"Local Bluetooth only. Nothing leaves your phone. Off by default.": "Bluetooth local uniquement. Rien ne quitte ton téléphone. Désactivé par défaut.", +"Local Oura support is new and we cannot test every ring here. It may not connect on your ": "La prise en charge locale d'Oura est récente et nous ne pouvons pas tester chaque bague ici. Elle peut ne pas se connecter sur ta ", "Local Oura support is new and we cannot test every ring here. It may not connect on your ring, and it can change between updates. NOOP never makes up a number. If something does not work, it will tell you plainly.": "Le support Oura local est récent et nous ne pouvons pas tester chaque bague ici. Il se peut qu'il ne se connecte pas à votre bague, et cela peut changer d'une mise à jour à l'autre. NOOP n'invente jamais de chiffre. Si quelque chose ne fonctionne pas, il vous le dira clairement.", "Lock %@": "Verrouiller %@", "Lock the Mac when I take the strap off": "Verrouiller le Mac quand je retire le bracelet", +"Lock the device when I take the strap off": "Verrouiller l'appareil quand je retire le bracelet", "Locked": "Verrouillé", "Locked %@ · paces drift, re-measure anytime.": "Verrouillé %@ · les rythmes évoluent, remesurez à tout moment.", "Locked %@. Switch to Breathe to use it, or re-measure above.": "Verrouillé %@. Passez à Respirer pour l'utiliser, ou remesurez ci-dessus.", "Log": "Journal", "Log %@": "Enregistrer %@", +"Log a coffee, tea, or energy drink and NOOP shows a rough estimate of how much may ": "Enregistre un café, un thé ou une boisson énergisante et NOOP affiche une estimation approximative de la quantité qui peut ", "Log a coffee, tea, or energy drink and NOOP shows a rough estimate of how much may still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement.": "Enregistrez un café, un thé ou une boisson énergisante, et NOOP affiche une estimation approximative de ce qui pourrait encore être actif. C'est un repère basé sur une demi-vie type de 5 à 6 heures, pas une mesure.", "Log a session you tracked elsewhere.": "Enregistrez une séance suivie ailleurs.", "Log a sleep mark": "Enregistrer un repère de sommeil", +"Log alcohol or late caffeine with an amount and NOOP fits a personal dose curve: ": "Enregistre l'alcool ou la caféine tardive avec une quantité et NOOP ajuste une courbe de dose personnelle : ", "Log alcohol or late caffeine with an amount and NOOP fits a personal dose curve: how much each extra unit tends to move your numbers. Until then it shows typical patterns, clearly labelled as not yet yours.": "Enregistrez de l'alcool ou de la caféine tardive avec une quantité, et NOOP ajuste une courbe de dose personnelle : dans quelle mesure chaque unité supplémentaire a tendance à faire varier vos chiffres. En attendant, il affiche des schémas types, clairement indiqués comme n'étant pas encore les vôtres.", "Log at least one behaviour above before starting an experiment.": "Enregistrez au moins un comportement ci-dessus avant de lancer une expérience.", +"Log behaviours above. After a few days of answers, NOOP ranks how each ": "Enregistre les comportements ci-dessus. Après quelques jours de réponses, NOOP classe à quel point chaque ", "Log behaviours above. After a few days of answers, NOOP ranks how each one moves your charge, HRV and rest. Importing a WHOOP export (which includes its journal) backfills history instantly.": "Enregistrez des comportements ci-dessus. Après quelques jours de réponses, NOOP classe l'influence de chacun sur votre charge, votre VFC et votre repos. Importer un export WHOOP (qui inclut son journal) recharge instantanément l'historique.", "Log custom %lld millilitres": "Enregistrer %lld millilitres personnalisés", "Log going to sleep": "Enregistrer l'endormissement", @@ -1392,6 +1709,7 @@ "Logged today": "Enregistré aujourd'hui", "Logged wake-up at %@.": "Réveil enregistré à %@.", "Logging ahead for tomorrow: today's activities inform tomorrow's recovery, just as yesterday's are reflected in today's. Tomorrow's answers line up with tomorrow's morning.": "Enregistrement anticipé pour demain : les activités d'aujourd'hui influencent la récupération de demain, tout comme celles d'hier se reflètent dans celle d'aujourd'hui. Les réponses de demain correspondront au matin de demain.", +"Long axis": "Grand axe", "Long exhale · downshift to rest": "Expiration longue · bascule vers le repos", "Long press to show or hide your heart rate.": "Appui long pour afficher ou masquer ta fréquence cardiaque.", "Longer ranges unlock as more history builds.": "Les plages plus longues se débloquent à mesure que l'historique s'étoffe.", @@ -1408,6 +1726,7 @@ "MEDIUM": "MOYEN", "METHOD": "MÉTHODE", "MODERATE": "MODÉRÉ", +"MS RMSSD": "MS RMSSD", "Main sleep": "Sommeil principal", "Maintain": "Maintenir", "Make %@ your active device now? It will provide your live data. You can change this any time.": "Faire de %@ votre appareil actif maintenant ? Il fournira vos données en direct. Vous pouvez changer cela à tout moment.", @@ -1423,6 +1742,7 @@ "Make the strap do things: tap to act, walk away to lock, train by feel.": "Faites faire des choses à votre bracelet : touchez pour agir, éloignez-vous pour verrouiller, entraînez-vous au feeling.", "Make this your active device?": "Faire de cet appareil votre appareil actif ?", "Make this your active strap?": "Faire de ce bracelet votre bracelet actif ?", +"Makes your WHOOP 5.0/MG advertise its heart rate as a standard Bluetooth HR sensor, so a Garmin (Edge/watch), Zwift or gym equipment can use it during a workout. Applied on the next connection (and immediately if connected); writes the strap's whoop_live_hr_in_adv_ind_pkt flag. Reversible. 5/MG only.": "Fait que ta WHOOP 5.0/MG diffuse sa fréquence cardiaque comme un capteur de FC Bluetooth standard, pour qu'un Garmin (Edge/montre), Zwift ou un équipement de salle puisse l'utiliser pendant un entraînement. Appliqué à la prochaine connexion (et immédiatement si connecté) ; écrit le flag whoop_live_hr_in_adv_ind_pkt du bracelet. Réversible. 5/MG uniquement.", "Makes your WHOOP 5.0/MG advertise its heart rate as a standard Bluetooth HR sensor, so a Garmin (Edge/watch), Zwift or gym equipment can use it during a workout. Applied on the next connection (and immediately if connected); writes the strap's whoop_live_hr_in_adv_ind_pkt flag. Reversible. iPhone-side only. A Mac can't write to a 5/MG.": "Fait en sorte que votre WHOOP 5.0/MG diffuse sa fréquence cardiaque comme un capteur FC Bluetooth standard, afin qu'un Garmin (Edge/montre), Zwift ou un équipement de salle puisse l'utiliser pendant un entraînement. Appliqué à la prochaine connexion (et immédiatement si déjà connecté) ; modifie l'indicateur whoop_live_hr_in_adv_ind_pkt du bracelet. Réversible. Côté iPhone uniquement. Un Mac ne peut pas écrire sur un 5/MG.", "Male": "Masculin", "Manage devices": "Gérer les appareils", @@ -1430,21 +1750,29 @@ "Manual (you set this by hand)": "Manuel (défini manuellement)", "Manual override": "Remplacement manuel", "Manual steps coefficient": "Coefficient manuel des pas", +"Manual steps coefficient, automatic": "Coefficient de pas manuel, automatique", "Map of your %@ route, %@.": "Carte de votre parcours de %1$@, %2$@.", "Mar": "Mars", "Mark a Moment": "Marquer un moment", "Mark a moment": "Marquer un moment", "Mark all read": "Tout marquer comme lu", +"Mark done": "Marquer comme fait", "Mark done today": "Marquer comme fait aujourd'hui", +"Mark read": "Marquer comme lu", "Marker": "Repère", "Markers": "Marqueurs", "Match": "Correspondance", +"Max": "Max", +"Max 4": "Max 4", "Max HR": "FC max", "Max Heart Rate": "Fréquence cardiaque maximale", "Max heart rate": "Fréquence cardiaque maximale", "Max heart rate override": "Remplacement FC max", "Max heart rate override, %@": "Remplacement FC max, %@", +"Max heart rate override, automatic": "Remplacement de la fréquence cardiaque max, automatique", +"Maximum": "Maximum", "May": "Mai", +"Maybe later": "Plus tard", "Mean": "Moyenne", "Mean %@": "Moyenne %@", "Mean HR": "FC moyenne", @@ -1452,10 +1780,15 @@ "Measuring…": "Mesure en cours…", "Medium": "Moyen", "Menu and settings": "Menu et paramètres", +"Merge": "Fusionner", "Metric": "Métrique", "Metric Relationships": "Relations entre métriques", "Metrics": "Métriques", +"Mi Band": "Mi Band", +"Mi Band / Smart Band 8 · 9 · 10": "Mi Band / Smart Band 8 · 9 · 10", "Mid-cycle shift": "Décalage en milieu de cycle", +"Millilitres (ml)": "Millilitres (ml)", +"Min": "Min", "Mind": "Esprit", "Minutes seated before the first nudge.": "Minutes assis avant la première relance.", "Min–max normalized · sparse series widened past %@ · %@": "Normalisé min-max · séries clairsemées élargies au-delà de %1$@ · %2$@", @@ -1468,9 +1801,11 @@ "Mon": "Lun", "Monday": "Lundi", "Mood": "Humeur", +"Mood check-in": "Point sur l'humeur", "Mood, alongside your body's signals": "Humeur, aux côtés des signaux de votre corps", "More": "Plus", "Morning": "Matin", +"Morning recap": "Récap du matin", "Most Active": "Le plus actif", "Mostly %@ (%@).": "Principalement %1$@ (%2$@).", "Mostly steady, with a few isolated extra or skipped beats. Very common and usually nothing.": "Globalement régulier, avec quelques battements supplémentaires ou manqués isolés. Très courant et généralement sans gravité.", @@ -1480,8 +1815,10 @@ "Move %@ up": "Déplacer %@ vers le haut", "Move all your NOOP data to another machine. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces %@'s data with a backup.": "Déplacez toutes vos données NOOP vers un autre appareil. L'export enregistre tout (historique, sommeils, entraînements, paramètres) dans un seul fichier que vous pouvez copier ; l'import remplace les données de %@ par une sauvegarde.", "Move all your NOOP data to another machine. Export saves everything, history, sleeps, workouts, settings, to a single file you can copy across; import replaces this Mac's data with a backup.": "Transférez toutes vos données NOOP vers une autre machine. L'export sauvegarde tout, historique, nuits, entraînements, paramètres, dans un seul fichier à copier ; l'import remplace les données de ce Mac par une sauvegarde.", +"Move all your NOOP data to another phone. Export saves everything (history, sleeps, workouts, settings) to a single file you can copy across; import replaces this phone's data with a backup.": "Déplace toutes tes données NOOP vers un autre téléphone. L'export enregistre tout (historique, sommeils, entraînements, réglages) dans un seul fichier que tu peux copier ; l'import remplace les données de ce téléphone par une sauvegarde.", "Move anyway": "Déplacer quand même", "Move reminder": "Rappel de mouvement", +"Move this sleep?": "Déplacer ce sommeil ?", "Movement": "Mouvement", "Mute wrist alerts overnight.": "Couper les alertes au poignet pendant la nuit.", "My Dashboard": "Mon tableau de bord", @@ -1489,29 +1826,43 @@ "N-of-1 protocol": "Protocole N-de-1", "NEW": "NOUVEAU", "NO DATA": "PAS DE DONNÉES", +"NOOP": "NOOP", "NOOP %@ is here. Tap to read what's new.": "NOOP %@ est arrivé. Appuyez pour découvrir les nouveautés.", "NOOP Charge": "Charge NOOP", +"NOOP can read a coarse menstrual-cycle phase from your nightly skin temperature, ": "NOOP peut déduire une phase approximative du cycle menstruel à partir de ta température cutanée nocturne, ", "NOOP can read a coarse menstrual-cycle phase from your nightly skin temperature, entirely on your device. It is awareness only: not contraception, not a fertility predictor, not a medical service.": "NOOP peut estimer une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur votre appareil. C'est uniquement informatif : ni contraception, ni prédicteur de fertilité, ni service médical.", "NOOP can run off only an Apple Watch, no chest strap needed. The watch is the sensor; NOOP does the thinking on your phone, computing Charge, Rest, Effort and your Fitness Age from your Health data, all on-device.": "NOOP peut fonctionner avec une simple Apple Watch, sans ceinture pectorale. La montre est le capteur ; NOOP fait les calculs sur votre téléphone, en calculant Charge, Repos, Effort et votre âge de forme physique à partir de vos données Santé, le tout sur l'appareil.", "NOOP can tap your wrist when your %@ needs you. No glance at the screen required.": "NOOP peut tapoter votre poignet quand votre %@ a besoin de vous. Pas besoin de regarder l'écran.", +"NOOP can't reach the strap. Force stop it (swiping it out of recents isn't enough).": "NOOP n'arrive pas à joindre le bracelet. Force son arrêt (le balayer hors des applis récentes ne suffit pas).", "NOOP computed": "Calculé par NOOP", +"NOOP doesn't have permission to set exact alarms, so your wake isn't guaranteed. ": "NOOP n'a pas l'autorisation de définir des alarmes exactes, donc ton réveil n'est pas garanti. ", +"NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step ": "NOOP estime tes pas à partir des mouvements de ta WHOOP, calibrés sur le comptage de pas de ton téléphone ", "NOOP estimates your steps from your WHOOP's motion, calibrated to your phone's step count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.": "NOOP estime vos pas à partir du mouvement de votre WHOOP, calibré sur le nombre de pas de votre téléphone. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas.", +"NOOP follows your system by default, or pick Light or Dark. You can change this any time in Settings → Appearance.": "NOOP suit ton système par défaut, ou choisis Clair ou Sombre. Tu peux changer ça à tout moment dans Réglages → Apparence.", +"NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 ": "NOOP te donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0-100 ", "NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0-100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": "NOOP te donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de ton bracelet avec des méthodes de science du sport publiées et revues par les pairs, et calculés entièrement sur ton appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les trois mêmes questions avec de la science ouverte, donc ils suivront généralement la même direction que ceux de WHOOP, sans coïncider chiffre pour chiffre. Et c'est justement l'idée.", "NOOP gives you three daily scores (Charge, Effort and Rest), each on a 0–100 scale. They're built from your strap's raw signals using published, peer-reviewed sport science, and computed entirely on your device. They are NOT WHOOP's scores: we don't have WHOOP's private algorithms and don't pretend to. They aim at the same three questions using open science, so they'll usually track WHOOP's in direction, but won't match number-for-number. And that's the point.": "NOOP vous donne trois scores quotidiens (Charge, Effort et Repos), chacun sur une échelle de 0 à 100. Ils sont construits à partir des signaux bruts de votre bracelet en utilisant une science du sport publiée et évaluée par des pairs, et calculés entièrement sur votre appareil. Ce ne sont PAS les scores de WHOOP : nous n'avons pas les algorithmes privés de WHOOP et ne prétendons pas les avoir. Ils visent les mêmes trois questions avec une science ouverte, donc ils suivront généralement la direction de ceux de WHOOP, mais ne correspondront pas chiffre pour chiffre. Et c'est bien le but.", +"NOOP holds the Bluetooth link open in the background so your data stays current. One low-priority notification shows it's connected. Nothing noisy.": "NOOP garde la liaison Bluetooth ouverte en arrière-plan pour que tes données restent à jour. Une notification de faible priorité indique qu'il est connecté. Rien de bruyant.", "NOOP is a personal, open project: not the WHOOP app, and not affiliated with WHOOP. It reads a strap you own, on your own device. Treat it as a capable work-in-progress rather than a finished product.": "NOOP est un projet personnel et ouvert : ce n'est pas l'application WHOOP et il n'est pas affilié à WHOOP. Il lit un bracelet que vous possédez, sur votre propre appareil. Considérez-le comme un travail en cours capable plutôt qu'un produit fini.", "NOOP is an unofficial project: not affiliated with, endorsed by, or sponsored by WHOOP, Inc. \"WHOOP\" is their trademark, used only to name the hardware NOOP works with.": "NOOP est un projet non officiel : il n'est ni affilié, ni approuvé, ni sponsorisé par WHOOP, Inc. « WHOOP » est leur marque déposée, utilisée uniquement pour nommer le matériel avec lequel NOOP fonctionne.", "NOOP is free and provided \"as is\", with no warranty. Liability is limited to the maximum extent the law that applies to you allows, and nothing here removes protections your local law won't let us remove.": "NOOP est gratuit et fourni « tel quel », sans garantie. La responsabilité est limitée dans la mesure maximale permise par la loi qui vous est applicable, et rien ici ne supprime les protections que votre législation locale ne nous permet pas de supprimer.", "NOOP is not a medical device. It is for informational and personal-insight purposes only and is not intended to diagnose, treat, cure or prevent any condition. Talk to a clinician for medical advice.": "NOOP n'est pas un dispositif médical. Il est destiné uniquement à des fins d'information et d'introspection personnelle et n'est pas conçu pour diagnostiquer, traiter, guérir ou prévenir une condition. Consultez un clinicien pour des conseils médicaux.", "NOOP is opening your on-device data. Your paired bands will appear here in a moment.": "NOOP ouvre vos données sur l'appareil. Vos bracelets couplés apparaîtront ici dans un instant.", +"NOOP keeps your strap connected in the background. When you continue, allow notifications so it can show that link and reach your wrist.": "NOOP garde ton bracelet connecté en arrière-plan. Quand tu continues, autorise les notifications pour qu'il puisse afficher cette liaison et atteindre ton poignet.", "NOOP looks for known notification apps on %@: Mail, Outlook, WhatsApp, Teams, Messages, Slack and similar. Install one and it'll appear here automatically.": "NOOP recherche les applications de notification connues sur %@ : Mail, Outlook, WhatsApp, Teams, Messages, Slack et similaires. Installez-en une et elle apparaîtra ici automatiquement.", "NOOP looks for machines that broadcast the standard Bluetooth Fitness Machine service.": "NOOP recherche les appareils qui diffusent le service Bluetooth Fitness Machine standard.", "NOOP never decides whether a value is \"normal,\" \"high,\" or \"low.\" Any reference range shown is exactly what you typed from your own report.": "NOOP ne décide jamais si une valeur est « normale », « élevée » ou « basse ». Toute plage de référence affichée est exactement ce que vous avez saisi depuis votre propre rapport.", +"NOOP never fills this in - it only shows back exactly what you type from your own report.": "NOOP ne remplit jamais ça - il ne fait que te réafficher exactement ce que tu saisis d'après ton propre rapport.", "NOOP never fills this in. It only shows back exactly what you type from your own report.": "NOOP ne remplit jamais ce champ lui-même. Il affiche uniquement ce que vous saisissez depuis votre propre rapport.", +"NOOP never makes up a number. When it can't compute one honestly it tells you ": "NOOP n'invente jamais un chiffre. Quand il ne peut pas en calculer un honnêtement, il te le ", "NOOP never makes up a number. When it can't compute one honestly it tells you what's missing and what to do, rather than showing a fake value.": "NOOP n'invente jamais de chiffre. Quand il ne peut pas en calculer un honnêtement, il vous dit ce qui manque et quoi faire, plutôt que d'afficher une fausse valeur.", +"NOOP never shows you a number it had to make up. If a score isn't ready, ": "NOOP ne te montre jamais un chiffre qu'il a dû inventer. Si un score n'est pas prêt, ", "NOOP never shows you a number it had to make up. If a score isn't ready, it tells you why and what to do next. Everything here runs on your device, from your strap.": "NOOP ne vous montre jamais un chiffre qu'il a dû inventer. Si un score n'est pas prêt, il vous explique pourquoi et quoi faire ensuite. Tout ici fonctionne sur votre appareil, à partir de votre bracelet.", "NOOP pairs directly with your WHOOP over Bluetooth, no WHOOP app, no cloud.": "NOOP se connecte directement à votre WHOOP via Bluetooth, sans app WHOOP, sans cloud.", "NOOP pairs directly with your WHOOP over Bluetooth: no WHOOP app, no cloud.": "NOOP se couple directement à votre WHOOP via Bluetooth : pas d'application WHOOP, pas de cloud.", +"NOOP pairs with the WHOOP 4.0 and the WHOOP 5.0/MG. They share most of what matters (live ": "NOOP s'appaire avec la WHOOP 4.0 et la WHOOP 5.0/MG. Elles partagent l'essentiel (en direct ", "NOOP picks the best-sourced number and shows you where each came from. It's for wellness and curiosity. It doesn't diagnose or replace medical advice.": "NOOP choisit le chiffre le mieux sourcé et vous montre d'où vient chacun. C'est pour le bien-être et la curiosité. Il ne diagnostique pas et ne remplace pas un avis médical.", +"NOOP picks your main sleep as your longest real block, and (once it has ": "NOOP choisit ton sommeil principal comme ton plus long bloc réel, et (une fois qu'il a ", "NOOP picks your main sleep as your longest real block, and (once it has learned your usual hours) the one nearest your normal sleep time. Everything else that day is a nap. You can always edit bed and wake times.": "NOOP choisit comme sommeil principal votre plus long bloc réel, et (une fois qu'il a appris vos horaires habituels) celui le plus proche de votre heure de sommeil normale. Tout le reste ce jour-là est une sieste. Vous pouvez toujours modifier les heures de coucher et de réveil.", "NOOP reads live heart rate on bands that don't require pairing. Newer bands need an auth handshake we can't do yet.": "NOOP lit la fréquence cardiaque en direct sur les bracelets qui ne nécessitent pas de couplage. Les bracelets plus récents nécessitent une authentification que nous ne pouvons pas encore effectuer.", "NOOP reads live heart rate when the band exposes it. Some bands need a pairing we can't do yet. If so, we'll say so honestly.": "NOOP lit la fréquence cardiaque en direct quand le bracelet l'expose. Certains bracelets nécessitent un couplage que nous ne pouvons pas encore effectuer. Si c'est le cas, nous vous le dirons honnêtement.", @@ -1520,11 +1871,14 @@ "NOOP scores your charge, effort and rest itself: on-device, no cloud.": "NOOP calcule lui-même votre charge, votre effort et votre repos : sur l'appareil, sans cloud.", "NOOP scores your recovery, strain and sleep itself, on-device, no cloud.": "NOOP calcule lui-même votre récupération, effort et sommeil, sur l'appareil, sans cloud.", "NOOP shows the %@ reading because it %@ for this metric: a higher-trust source here, not a verdict that the others are wrong.": "NOOP affiche la mesure de %1$@ car il %2$@ pour cette métrique : une source plus fiable ici, pas un verdict que les autres se trompent.", +"NOOP starts looking as soon as this step appears. You can keep going while it bonds.": "NOOP commence à chercher dès que cette étape apparaît. Tu peux continuer pendant l'association.", "NOOP stores and lines up the numbers you enter yourself. It does not test you, read your results, give medical advice, or diagnose anything.": "NOOP stocke et aligne les chiffres que vous saisissez vous-même. Il ne vous teste pas, ne lit pas vos résultats, ne donne pas d'avis médical et ne diagnostique rien.", "NOOP stores this key only on this device, in the same place it stores your paired bands.": "NOOP stocke cette clé uniquement sur cet appareil, au même endroit que vos bracelets couplés.", "NOOP talks to your strap directly over Bluetooth Low Energy. There's no server in the middle. The connection is local, and so is every reading it pulls in.": "NOOP communique directement avec votre bracelet via Bluetooth Low Energy. Il n'y a aucun serveur intermédiaire. La connexion est locale, tout comme chaque mesure qu'il récupère.", "NOOP talks to your strap's firmware over an unofficial, independently-mapped protocol. There is a residual risk to the device, its data, and its connection to official services. You assume that risk.": "NOOP communique avec le micrologiciel de votre bracelet via un protocole non officiel, cartographié de manière indépendante. Il subsiste un risque résiduel pour l'appareil, ses données et sa connexion aux services officiels. Vous assumez ce risque.", "NOOP taps your strap so an alert lands on your wrist instead of your screen. No need to reach for it. Everything stays on %@.": "NOOP tapote votre bracelet pour qu'une alerte arrive sur votre poignet plutôt que sur votre écran. Pas besoin de le prendre en main. Tout reste sur %@.", +"NOOP uses Bluetooth to find your strap. When you continue, allow the permission so it can scan.": "NOOP utilise le Bluetooth pour trouver ton bracelet. Quand tu continues, autorise l'accès pour qu'il puisse scanner.", +"NOOP will install its own key on the ring and become its owner. The Oura app will no ": "NOOP va installer sa propre clé sur la bague et en devenir le propriétaire. L'appli Oura ne ", "NOOP will install its own key on the ring and become its owner. The Oura app will no longer control this ring. This is intended and it cannot be undone from NOOP.": "NOOP installera sa propre clé sur la bague et en deviendra le propriétaire. L'application Oura ne contrôlera plus cette bague. C'est voulu et cela ne peut pas être annulé depuis NOOP.", "NOOP will look for it nearby.": "NOOP le recherchera à proximité.", "NOOP will re-learn your baseline from tonight's data onward. Your history is kept, and it takes a few nights to settle.": "NOOP réapprendra votre référence à partir des données de cette nuit. Votre historique est conservé, et il faut quelques nuits pour se stabiliser.", @@ -1538,11 +1892,15 @@ "NOOP: all your data, none of the cloud.": "NOOP : toutes vos données, aucun cloud.", "Name": "Nom", "Name & confirm": "Nommer et confirmer", +"Name the merged session": "Nomme la session fusionnée", +"Nap detection": "Détection des siestes", "Nap ended": "Sieste terminée", "Nap started": "Sieste commencée", "Nap(s)": "Sieste(s)", "Naps": "Siestes", "Navy": "Marine", +"Needed": "Requis", +"Needs Phone permission; NOOP never reads numbers or call logs.": "Nécessite l'autorisation Téléphone ; NOOP ne lit jamais tes numéros ni ton journal d'appels.", "Needs at least 14 days of history. When two or more signals drift together you get a banner on the dashboard and a notification, at most once a day.": "Nécessite au moins 14 jours d'historique. Quand deux signaux ou plus dérivent ensemble, vous recevez une bannière sur le tableau de bord et une notification, au maximum une fois par jour.", "Needs interval frames": "Nécessite des trames d'intervalle", "Needs more data": "Nécessite plus de données", @@ -1551,12 +1909,15 @@ "Needs the strap. No data for today. Was your strap worn and connected overnight?": "Nécessite le bracelet. Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?", "Neutral": "Neutre", "Never": "Jamais", +"New": "Nouveau", "New data added": "Nouvelles données ajoutées", "New here?": "Nouveau ici ?", "New item group": "Nouveau groupe d'éléments", "New release notes and fresh data will land here.": "Les nouvelles notes de version et les données récentes arriveront ici.", "New strap name": "Nouveau nom du bracelet", "Newer WHOOP band, experimental in NOOP": "Bracelet WHOOP plus récent, expérimental dans NOOP", +"Newer WHOOP band. Experimental in NOOP": "Bracelet WHOOP plus récent. Expérimental dans NOOP", +"Newest first. Restoring replaces this device's data.": "Le plus récent d'abord. Une restauration remplace les données de cet appareil.", "Next day": "Jour suivant", "Next morning": "Matin suivant", "Next night": "Nuit suivante", @@ -1570,13 +1931,17 @@ "No HRV value": "Aucune valeur de VFC", "No RMSSD · not enough R-R data": "Aucun RMSSD · données R-R insuffisantes", "No SpO₂ import or Health value": "Aucun import SpO₂ ni valeur Santé", +"No WHOOP? You can still continue. Pair a heart-rate strap (Polar, Wahoo, Coospo, Garmin HRM…) ": "Pas de WHOOP ? Tu peux quand même continuer. Associe un bracelet de fréquence cardiaque (Polar, Wahoo, Coospo, Garmin HRM…) ", "No WHOOP? You can still continue. Pair a heart-rate strap (Polar, Wahoo, Coospo, Garmin HRM…) or a gym machine under Devices, or import from WHOOP, Apple Health, Oura, Fitbit, Garmin and more under Data Sources. You can do either any time.": "Pas de WHOOP ? Vous pouvez quand même continuer. Couplez une ceinture cardio (Polar, Wahoo, Coospo, Garmin HRM…) ou une machine de salle dans Appareils, ou importez depuis WHOOP, Apple Santé, Oura, Fitbit, Garmin et plus dans Sources de données. Vous pouvez faire l'un ou l'autre à tout moment.", "No account, no cloud, no sync. NOOP talks only to your strap and keeps everything local. Your data is yours alone.": "Aucun compte, aucun cloud, aucune synchronisation. NOOP ne communique qu'avec votre bracelet et garde tout en local. Vos données n'appartiennent qu'à vous.", "No active stream": "Aucun flux actif", "No backup yet.": "Aucune sauvegarde pour l'instant.", "No backups found": "Aucune sauvegarde trouvée", +"No biometrics yet": "Pas encore de données biométriques", +"No biometrics yet. Import your WHOOP export (and Apple Health if you ": "Pas encore de données biométriques. Importe ton export WHOOP (et Apple Santé si tu ", "No biometrics yet. Import your WHOOP export (and Apple Health if you have it) in Data Sources to fill this in.": "Pas encore de données biométriques. Importez votre export WHOOP (et Apple Santé si vous l'avez) dans Sources de données pour les compléter.", "No caffeine logged. Log an intake to see an estimate.": "Aucune caféine enregistrée. Enregistrez une prise pour voir une estimation.", +"No cardio load yet. Effort builds once your heart rate climbs into your effort ": "Pas encore de charge cardio. L'effort augmente une fois que ta fréquence cardiaque monte dans ta zone d'effort ", "No cardio load yet. Effort builds once your heart rate climbs into your effort zone (around 50% of your heart-rate reserve). A calm day honestly reads near zero.": "Aucune charge cardio pour l'instant. L'Effort augmente quand votre fréquence cardiaque atteint votre zone d'effort (environ 50 % de votre réserve de fréquence cardiaque). Une journée calme affiche honnêtement une valeur proche de zéro.", "No chest strap? No problem. NOOP can run off only your Apple Watch. It reads your watch's data through Apple Health and works out your Charge, Rest, Effort and Fitness Age right here on your phone. Everything stays on the device.": "Pas de ceinture pectorale ? Aucun problème. NOOP peut fonctionner avec votre seule Apple Watch. Il lit les données de votre montre via Apple Santé et calcule votre Charge, Repos, Effort et âge de forme physique directement sur votre téléphone. Tout reste sur l'appareil.", "No clear pattern": "Aucun schéma clair", @@ -1586,25 +1951,33 @@ "No data for these metrics in %@. Widen the range or pick metrics you've logged.": "Pas de données pour ces métriques dans %@. Élargissez la plage ou choisissez des métriques que vous avez enregistrées.", "No data for today. Was your strap worn and connected overnight?": "Aucune donnée pour aujourd'hui. Votre bracelet était-il porté et connecté pendant la nuit ?", "No data, open NOOP on iPhone": "Pas de données, ouvrez NOOP sur iPhone", +"No days yet where both NOOP and your phone counted steps. Once your phone logs a ": "Aucun jour où NOOP et ton téléphone ont compté des pas en même temps. Dès que ton téléphone enregistre un ", "No days yet where both NOOP and your phone counted steps. Once your phone logs a few days alongside the strap, they'll appear here so you can see how close the estimate is.": "Aucun jour pour l'instant où NOOP et votre téléphone ont tous deux compté les pas. Une fois que votre téléphone enregistre quelques jours en parallèle du bracelet, ils apparaîtront ici pour voir la précision de l'estimation.", "No drinks logged yet. Tap Sip, Cup or Bottle to start.": "Aucune boisson enregistrée pour l'instant. Appuyez sur Gorgée, Tasse ou Bouteille pour commencer.", "No earlier night stored yet. Earlier nights sync in the morning.": "Aucune nuit antérieure stockée pour l'instant. Les nuits précédentes se synchronisent le matin.", +"No effort yet": "Pas encore d'effort", "No extra tonight. Your %@ forecast stays where it is.": "Rien d'extra ce soir. Votre prévision de %@ reste inchangée.", "No folder chosen yet. Pick one your cloud app already syncs, or any local folder.": "Aucun dossier choisi pour l'instant. Choisissez-en un que votre application cloud synchronise déjà, ou n'importe quel dossier local.", "No heart rate for this day": "Aucune fréquence cardiaque pour ce jour", +"No heart rate for this day. Step back to a day the strap was worn.": "Aucune fréquence cardiaque pour ce jour. Reviens à un jour où le bracelet était porté.", "No heart-rate data": "Aucune donnée de fréquence cardiaque", "No history yet.": "Aucun historique pour l'instant.", "No iOS diagnostics available.": "Aucun diagnostic iOS disponible.", "No intraday stress data yet today.": "Aucune donnée de stress intrajournalière aujourd'hui pour l'instant.", +"No live heart rate yet · open Live to pair your strap": "Pas encore de fréquence cardiaque en direct · ouvre Direct pour associer ton bracelet", "No live heart rate yet. Open Live to pair your strap.": "Aucune fréquence cardiaque en direct pour l'instant. Ouvrez En direct pour coupler votre bracelet.", "No live stream.": "Aucun flux en direct.", "No match. Add it as a custom marker below.": "Aucune correspondance. Ajoutez-le comme marqueur personnalisé ci-dessous.", "No metrics yet. Import your Whoop export or wear the strap to begin.": "Aucune métrique pour l'instant. Importez votre export Whoop ou portez le bracelet pour commencer.", "No motion synced yet": "Aucun mouvement synchronisé pour l'instant", "No movement detail for this night": "Aucun détail de mouvement pour cette nuit", +"No movement detail for this night.": "Aucun détail de mouvement pour cette nuit.", "No movement detail recorded for this night": "Aucun détail de mouvement enregistré pour cette nuit", "No naps recorded for this day.": "Aucune sieste enregistrée pour ce jour.", +"No naps to review. Detected naps show up here after a history sync.": "Aucune sieste à examiner. Les siestes détectées apparaissent ici après une synchro de l'historique.", "No nightly skin-temp value": "Aucune valeur de température cutanée nocturne", +"No nights here yet": "Pas encore de nuits ici", +"No nights here yet. Import your WHOOP export in Data Sources to see ": "Pas encore de nuits ici. Importe ton export WHOOP dans Sources de données pour voir ", "No nights here yet. Import your WHOOP export in Data Sources to see every night, your sleep stages and trends straight away. Or open Intelligence to see last night computed from the strap after you wear it to bed.": "Pas encore de nuits ici. Importez votre export WHOOP dans Sources de données pour voir chaque nuit, vos stades de sommeil et tendances immédiatement. Ou ouvrez Intelligence pour voir la nuit dernière calculée depuis le bracelet après l'avoir porté au lit.", "No nights with sleep data yet. Your ledger fills in as you wear the strap to bed.": "Aucune nuit avec données de sommeil pour l'instant. Votre registre se remplit à mesure que vous portez le bracelet pour dormir.", "No numeric readings yet.": "Aucune mesure numérique pour l'instant.", @@ -1617,12 +1990,16 @@ "No readings this week yet": "Aucune mesure cette semaine pour l'instant", "No respiratory-rate value": "Aucune valeur de fréquence respiratoire", "No resting HR value": "Aucune valeur de fréquence cardiaque au repos", +"No samples yet": "Pas encore d'échantillons", "No scored days in this window. Widen the range or import more history.": "Aucun jour avec score dans cette période. Élargissez la plage ou importez plus d'historique.", +"No scored days yet. Sync your strap to collect raw streams. Effort and Rest are ": "Pas encore de jours notés. Synchronise ton bracelet pour collecter les flux bruts. L'Effort et le Repos sont ", "No sleep tracked last night": "Aucun sommeil enregistré la nuit dernière", "No sources yet": "Aucune source pour l'instant", +"No stage breakdown for this night.": "Aucune répartition des phases pour cette nuit.", "No stage data recorded for this night.": "Aucune donnée de phase enregistrée pour cette nuit.", "No strap connected": "Aucun bracelet connecté", "No strap connected. Put your WHOOP nearby and tap Re-scan to pair.": "Aucun bracelet connecté. Placez votre WHOOP à proximité et appuyez sur Rescanner pour le coupler.", +"No stress history yet": "Pas encore d'historique de stress", "No stress history yet. Import your WHOOP export in Data Sources to see it.": "Pas encore d'historique de stress. Importez votre export WHOOP dans Sources de données pour le voir.", "No supported apps found": "Aucune application prise en charge trouvée", "No usable activity found. Point at a .gpx, .tcx or .fit workout file.": "Aucune activité utilisable trouvée. Indiquez un fichier d'entraînement .gpx, .tcx ou .fit.", @@ -1634,72 +2011,97 @@ "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in, or add one you tracked elsewhere.": "Aucun entraînement pour l'instant. Ils proviennent de l'historique de votre WHOOP et d'Apple Santé. Importez dans Sources de données pour les récupérer, ou ajoutez-en un que vous avez suivi ailleurs.", "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in.": "Pas encore d'entraînements. Ils proviennent de votre historique WHOOP et Apple Santé. Importez dans Sources de données pour les intégrer.", "Non-binary": "Non-binaire", +"None": "Aucun", "Noon": "Midi", "Normalized": "Normalisé", "Normalized overlay": "Superposition normalisée", "Not a medical device, not medical advice": "Pas un dispositif médical, pas un avis médical", "Not a workout": "Pas un entraînement", "Not affiliated with, endorsed by, or connected to WHOOP. Interoperability software for hardware you own and your own data. Use it only with a device you own, and not in breach of any agreement that applies to you. Not a medical device.": "Non affilié à, approuvé par ou connecté à WHOOP. Logiciel d'interopérabilité pour le matériel que vous possédez et vos propres données. Utilisez-le uniquement avec un appareil que vous possédez et sans enfreindre aucun accord qui vous est applicable. Pas un dispositif médical.", +"Not affiliated with, endorsed by, or connected to WHOOP. Interoperability software for your own device and data. Not a medical device.": "Sans affiliation, approbation ni lien avec WHOOP. Logiciel d'interopérabilité pour ton propre appareil et tes propres données. Ce n'est pas un dispositif médical.", "Not available": "Non disponible", "Not calibrated": "Non calibré", "Not calibrated yet": "Pas encore calibré", "Not connected": "Non connecté", "Not connected. Open Live to pair.": "Non connecté. Ouvrez En direct pour coupler.", "Not enough clean beat data to lock a pace today. Try again rested, sitting still with the strap snug. For now we'll pace you at 5.5 br/min (coherence).": "Pas assez de données de battements propres pour fixer un rythme aujourd'hui. Réessayez reposé, assis immobile avec le bracelet bien ajusté. Pour l'instant, nous vous guiderons à 5,5 resp/min (cohérence).", +"Not enough clean beats - sit still and try again.": "Pas assez de battements exploitables - reste immobile et réessaie.", "Not enough clean beats. Sit still and try again.": "Pas assez de battements propres. Restez immobile et réessayez.", "Not enough clean beats. Sit still and try again. %lld of %lld beats survived filtering (need %lld).": "Pas assez de battements propres. Restez immobile et réessayez. %1$lld battements sur %2$lld ont survécu au filtrage (il en faut %3$lld).", "Not enough data": "Données insuffisantes", "Not enough data for this window.": "Pas assez de données pour cette fenêtre.", "Not enough data in this range yet": "Données insuffisantes dans cette plage pour l'instant", "Not enough data yet": "Données insuffisantes pour l'instant", +"Not enough history in this range": "Pas assez d'historique sur cette période", +"Not enough history in this range. Try 3M, 6M, or ALL.": "Pas assez d'historique sur cette période. Essaie 3M, 6M ou TOUT.", +"Not enough history yet": "Pas encore assez d'historique", "Not enough nights yet.": "Pas encore assez de nuits.", +"Not enough overlap between your journal answers and ": "Pas assez de recoupement entre tes réponses au journal et ", "Not enough overlap between your journal answers and %@ to measure an effect yet. Keep logging. Effects need days both with and without each behaviour.": "Pas assez de recoupement entre vos réponses au journal et %@ pour mesurer un effet pour l'instant. Continuez à enregistrer. Les effets nécessitent des jours avec et sans chaque comportement.", "Not enough overlap between your journal answers and %@ yet. Keep logging. Each behaviour needs days both with and without it before NOOP can read its effect.": "Pas encore assez de recoupement entre vos réponses au journal et %@. Continuez à enregistrer. Chaque comportement nécessite des jours avec et sans avant que NOOP puisse en lire l'effet.", "Not enough overlapping days between these metrics in %@. Widen the range.": "Pas assez de jours chevauchants entre ces métriques dans %@. Élargissez la plage.", +"Not enough overlapping history to correlate mood with your ": "Pas assez d'historique commun pour corréler l'humeur avec tes ", "Not enough overlapping history to correlate your metrics yet.": "Pas encore assez d'historique chevauchant pour corréler vos métriques.", "Not enough recent days to chart a trend yet. Import a history or keep wearing your strap.": "Pas encore assez de jours récents pour tracer une tendance. Importez un historique ou continuez à porter votre bracelet.", +"Not enough recent days to chart a trend yet. Keep wearing your strap to populate it.": "Pas encore assez de jours récents pour tracer une tendance. Continue à porter ton bracelet pour la remplir.", "Not now": "Pas maintenant", "Not recording": "Aucun enregistrement", "Not recording. Strap not connected. Tap to connect.": "Aucun enregistrement. Bracelet non connecté. Appuyez pour connecter.", "Not set": "Non défini", +"Not showing up? Make sure you reset the ring in the Oura app and force-quit it, then ": "Rien n'apparaît ? Vérifie que tu as réinitialisé la bague dans l'app Oura et que tu l'as forcée à quitter, puis ", "Not showing up? Make sure you reset the ring in the Oura app and force-quit it, then tap Rescan. A ring still owned by Oura will not list here.": "Elle n'apparaît pas ? Assurez-vous d'avoir réinitialisé la bague dans l'application Oura et de l'avoir forcée à quitter, puis appuyez sur Rescanner. Une bague encore possédée par Oura n'apparaîtra pas ici.", "Not showing up? The official WHOOP app may still be holding it. Force-quit that app, then tap Rescan.": "Il n'apparaît pas ? L'application WHOOP officielle le retient peut-être encore. Forcez la fermeture de cette application, puis appuyez sur Rescanner.", "Not synced yet": "Pas encore synchronisé", "Note (optional)": "Note (facultatif)", +"Notes": "Notes", "Nothing": "Rien", "Nothing here yet. Tap Enable Apple Health above to read your data live, or import a Health export .zip in Data Sources.": "Rien pour l'instant. Appuyez sur Activer Apple Santé ci-dessus pour lire vos données en direct, ou importez un fichier .zip d'export Santé dans Sources de données.", "Nothing here yet. This sideloaded install can't read Apple Health directly. Import a Health export .zip in Data Sources, or turn on Shortcuts Export to bring your strap data into Health.": "Rien pour l'instant. Cette installation sideloadée ne peut pas lire Apple Santé directement. Importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export via Raccourcis pour faire entrer les données de votre bracelet dans Santé.", "Nothing imported": "Rien d'importé", +"Nothing imported yet": "Rien d'importé pour l'instant", "Nothing imported yet. In Data Sources, choose your Mi Fitness export (a .zip of the Mi Fitness app folder from the Files app) to bring in your steps, heart rate, sleep stages, SpO₂ and stress.": "Rien d'importé pour l'instant. Dans Sources de données, choisissez votre export Mi Fitness (un .zip du dossier de l'application Mi Fitness depuis l'application Fichiers) pour récupérer vos pas, votre fréquence cardiaque, vos phases de sommeil, votre SpO₂ et votre stress.", +"Nothing imported yet. On an iPhone: Health app, tap your photo, Export ": "Rien d'importé pour l'instant. Sur un iPhone : app Santé, touche ta photo, Exporter ", "Nothing imported yet. On an iPhone: Health app, tap your photo, Export All Health Data, then import the .zip here in Data Sources.": "Rien d'importé encore. Sur iPhone : app Santé, tapez votre photo, Exporter toutes les données de santé, puis importez le .zip ici dans Sources de données.", "Nothing in the catalog moves clearly with %@ over this window. Widen the range to surface relationships.": "Rien dans le catalogue n'évolue clairement avec %@ sur cette fenêtre. Élargissez la plage pour faire apparaître des relations.", "Nothing leaves your %@": "Rien ne quitte votre %@", +"Nothing leaves your phone": "Rien ne quitte ton téléphone", "Nothing notable": "Rien de notable", "Nothing offloaded for this window yet.": "Rien de déchargé pour cette période pour l'instant.", "Nothing selected yet.": "Rien de sélectionné encore.", "Nothing to export": "Rien à exporter", "Nothing to fuse yet": "Rien à fusionner pour l'instant", "Nothing to reclaim right now. NOOP already cleans up import scratch space automatically.": "Rien à récupérer pour l'instant. NOOP nettoie déjà automatiquement l'espace temporaire d'import.", +"Notifications": "Notifications", +"Notifications are off, so this can't buzz yet. Turn on the master switch in ": "Les notifications sont désactivées, donc ça ne peut pas encore vibrer. Active l'interrupteur principal dans ", "Notifications are off, so this can't buzz yet. Turn on the master switch in Notifications to let it through.": "Les notifications sont désactivées, donc cela ne peut pas encore vibrer. Activez l'interrupteur principal dans Notifications pour le laisser passer.", "Notify on low and full battery": "Notifier en cas de batterie faible ou pleine", "Now": "Maintenant", +"Number": "Nombre", +"Nutrition (CSV)": "Nutrition (CSV)", "OFFLINE": "HORS LIGNE", "Off": "Désactivé", "Off baseline": "Hors référence", "Off wrist": "Hors du poignet", +"Off your baseline": "Écart avec ta référence", "Off, the coach answers generally and sends none of your metrics.": "Désactivé, le coach répond de manière générale et n'envoie aucune de vos métriques.", "Off: only your core metrics are shared, not your patterns or Lab Book.": "Désactivé : seules vos métriques principales sont partagées, pas vos tendances ni votre Carnet de labo.", "Off: the coach answers generally and sends none of your metrics.": "Désactivé : le Coach répond de manière générale et n'envoie aucune de vos métriques.", +"Offer a breath automatically": "Proposer une respiration automatiquement", "Offers to save a workout when it spots sustained elevated heart rate": "Propose d'enregistrer un entraînement quand il détecte une fréquence cardiaque élevée soutenue", "Offline": "Hors ligne", +"Offload in progress": "Déchargement en cours", "Okay": "OK", "On %@": "Sur %@", +"On Android 12+, \"Nearby devices\" is the Bluetooth permission. NOOP needs it to find your strap.": "Sur Android 12+, \"Appareils à proximité\" est l'autorisation Bluetooth. NOOP en a besoin pour trouver ton bracelet.", "On a 5/MG connection NOOP will send a puffin realtime-stream request after the handshake, and log what comes back. If you have a 5/MG strap, turning this on and sharing your strap log helps map the protocol. No effect on WHOOP 4.0.": "Sur une connexion 5/MG, NOOP enverra une requête de flux temps réel puffin après la poignée de main et journalisera ce qui revient. Si vous avez un bracelet 5/MG, activer ceci et partager votre journal de bracelet aide à cartographier le protocole. Aucun effet sur le WHOOP 4.0.", +"On a WHOOP 4.0?": "Sur un WHOOP 4.0 ?", "On a beta version of iOS, things can break that work on the release build.": "Sur une version bêta d'iOS, des choses qui fonctionnent sur la version finale peuvent casser.", "On iPhone this is a sideloaded build. Re-sign it about every 7 days on a free Apple ID (longer on a paid account). After your phone reboots, unlock it once so NOOP can read and sync its data.": "Sur iPhone, il s'agit d'une version sideloadée. Resignez-la environ tous les 7 jours avec un identifiant Apple gratuit (plus longtemps avec un compte payant). Après le redémarrage de votre téléphone, déverrouillez-le une fois pour que NOOP puisse lire et synchroniser ses données.", "On iPhone this is best-effort (iOS decides when background tasks run). Everything stays on %@.": "Sur iPhone, cela se fait au mieux (c'est iOS qui décide quand les tâches en arrière-plan s'exécutent). Tout reste sur %@.", "On target": "Dans l'objectif", +"On the days your phone also counted steps, NOOP learns how much your motion maps to ": "Les jours où ton téléphone a aussi compté des pas, NOOP apprend à quel point ton mouvement correspond à ", "On the days your phone also counted steps, NOOP learns how much your motion maps to steps, then applies that to the strap-only days. The more matching days it has, the more it trusts the estimate.": "Les jours où votre téléphone comptait aussi les pas, NOOP apprend comment votre mouvement se traduit en pas, puis applique cela aux jours avec seulement le bracelet. Plus il a de jours correspondants, plus il fait confiance à l'estimation.", +"On this phone": "Sur ce téléphone", "On wrist": "Au poignet", "On, your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching.": "Activé, votre récupération, sommeil, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", "On-device": "Sur l'appareil", @@ -1707,55 +2109,80 @@ "On-device footprint": "Empreinte sur l'appareil", "On: a short summary of your strongest patterns and logged health numbers is added. Summaries only, never raw readings.": "Activé : un court résumé de vos tendances les plus marquées et de vos données de santé enregistrées est ajouté. Uniquement des résumés, jamais de mesures brutes.", "On: your charge, rest, HRV and workouts are shared with the provider for tailored coaching.": "Activé : votre charge, repos, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", +"On: your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching.": "Activé : ta récupération, ton sommeil, ta VFC et tes entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", +"Once NOOP has 14 nights of history, in-range compares each vital to your own ": "Une fois que NOOP a 14 nuits d'historique, \"dans la norme\" compare chaque constante à ta propre ", "Once NOOP has 14 nights of history, in-range compares each vital to your own baseline (approximate, not medical advice); until then, typical adult ranges apply.": "Une fois que NOOP a 14 nuits d'historique, « dans la norme » compare chaque signe vital à votre propre référence (approximatif, pas un avis médical) ; jusque-là, les plages typiques pour un adulte s'appliquent.", +"Once a day at a time you choose, NOOP writes a timestamped strap log (plus the raw 5/MG capture, if you have one) to its export folder. No sharing, nothing leaves the phone. Useful for chasing an intermittent overnight fault. Off by default.": "Une fois par jour à l'heure de ton choix, NOOP écrit un journal du bracelet horodaté (plus la capture brute 5/MG, si tu en as une) dans son dossier d'export. Aucun partage, rien ne quitte le téléphone. Utile pour traquer un défaut nocturne intermittent. Désactivé par défaut.", "Once this week has a day or two of data, your week-in-review appears here.": "Une fois que cette semaine a un ou deux jours de données, votre bilan de la semaine apparaît ici.", +"One check-in per day; picking another face overwrites today's.": "Un check-in par jour ; choisir un autre visage remplace celui d'aujourd'hui.", +"One phone at a time": "Un seul téléphone à la fois", "One reading so far: %@ %@. Log a few more to see a trend.": "Une seule mesure pour l'instant : %@ %@. Enregistrez-en quelques-unes de plus pour voir une tendance.", "Only buzz when worn": "Vibrer uniquement quand porté", "Only during active hours": "Uniquement pendant les heures actives", "Only if your server requires one": "Uniquement si votre serveur en exige une", "Only nudge during your active hours.": "Ne relancer que pendant vos heures actives.", +"Only one reading in range , widen the window to see a trend.": "Une seule mesure sur la période , élargis la fenêtre pour voir une tendance.", "Only when worn": "Uniquement quand porté", "Open": "Ouvrir", "Open Backup and Sync to a folder": "Ouvrir Sauvegarde et synchronisation vers un dossier", +"Open Journal": "Ouvrir le journal", "Open NOOP": "Ouvrir NOOP", "Open NOOP first so it can reach your strap.": "Ouvrez d'abord NOOP pour qu'il puisse atteindre votre bracelet.", +"Open NOOP near your strap and let it catch up (a full history sync can take a while on ": "Ouvre NOOP près de ton bracelet et laisse-le se mettre à jour (une synchro complète de l'historique peut prendre du temps sur ", "Open NOOP near your strap and let it catch up (a full history sync can take a while on first run). Once a day or two of motion lands, your step estimate and the calibration below will start to fill in.": "Ouvrez NOOP près de votre bracelet et laissez-le se mettre à jour (une synchronisation complète de l'historique peut prendre du temps au premier lancement). Une fois qu'un ou deux jours de mouvement sont arrivés, votre estimation de pas et la calibration ci-dessous commenceront à se remplir.", "Open NOOP on your iPhone to sync": "Ouvrez NOOP sur votre iPhone pour synchroniser", +"Open Notification Access": "Ouvrir l'accès aux notifications", +"Open Notification Access settings": "Ouvrir les réglages d'accès aux notifications", "Open Rhythm": "Ouvrir Rhythm", "Open Test Centre": "Ouvrir le Centre de test", "Open Test Centre to report a bug": "Ouvrez le Centre de test pour signaler un bug", +"Open WHOOP app, then Force stop": "Ouvre l'app WHOOP, puis Forcer l'arrêt", "Open live view": "Ouvrir la vue en direct", +"Open the official Oura app and remove this ring (Oura calls it \"factory reset\" or \"unpair and ": "Ouvre l'app officielle Oura et retire cette bague (Oura appelle ça \"réinitialisation d'usine\" ou \"dissocier et ", "Open the official Oura app and remove this ring (Oura calls it \"factory reset\" or \"unpair and reset\"). This wipes the ring's owner so NOOP can take it over.": "Ouvrez l'application Oura officielle et retirez cette bague (Oura appelle cela « réinitialisation d'usine » ou « désassocier et réinitialiser »). Cela efface le propriétaire de la bague pour que NOOP puisse en prendre le contrôle.", "Open-source BLE reverse-engineering work. Thank you.": "Travail de rétro-ingénierie BLE open-source. Merci.", "Opens the Apple Watch setup and Health permission": "Ouvre la configuration de l'Apple Watch et l'autorisation Santé", "Opens the Devices screen, where you pair and switch bands.": "Ouvre l'écran Appareils, où vous couplez et changez de bracelets.", "Opens the GitHub releases page in your browser. NOOP itself makes no network connections, the macOS sandbox has no network entitlement.": "Ouvre la page des versions GitHub dans votre navigateur. NOOP lui-même n'effectue aucune connexion réseau, le bac à sable macOS n'a pas de droit réseau.", "Opens workout detail": "Ouvre le détail de l'entraînement", +"Optimal": "Optimal", "Optional": "Facultatif", "Optional note": "Note facultative", +"Optional phone notifications, off by default. These arrive after your strap syncs ": "Notifications téléphone facultatives, désactivées par défaut. Elles arrivent après la synchro de ton bracelet ", "Optional trackers, off by default. Turn them on to add their cards. Everything stays on %@.": "Trackers facultatifs, désactivés par défaut. Activez-les pour ajouter leurs cartes. Tout reste sur %@.", +"Optional · adds your VO₂max estimate": "Facultatif · ajoute ton estimation de VO₂max", "Optional, on-device reads from your nightly signals. Each is off by default: for awareness only, never a diagnosis.": "Lectures facultatives, sur l'appareil, à partir de vos signaux nocturnes. Chacune est désactivée par défaut : uniquement à titre informatif, jamais un diagnostic.", +"Optional, on-device wellness signals. Each is off by default, computed only on this phone from data you already have, and never a medical diagnosis.": "Signaux de bien-être facultatifs, sur l'appareil. Chacun est désactivé par défaut, calculé uniquement sur ce téléphone à partir de données que tu as déjà, et n'est jamais un diagnostic médical.", "Optional. Add a photo for the avatar in the top-left. Stored only on %@. NOOP is offline, so it's never uploaded.": "Facultatif. Ajoutez une photo pour l'avatar en haut à gauche. Stockée uniquement sur %@. NOOP est hors ligne, elle n'est donc jamais téléversée.", +"Optional. Add a photo for the avatar in the top-left. Stored only on this phone. NOOP is offline, so it's never uploaded.": "Facultatif. Ajoute une photo pour l'avatar en haut à gauche. Stockée uniquement sur ce téléphone. NOOP est hors ligne, donc elle n'est jamais envoyée.", "Optional. Stored only on this device.": "Facultatif. Stocké uniquement sur cet appareil.", "Optional: adds your VO₂max estimate. The Fitness Age itself doesn't need it. Measure around your middle, at the navel.": "Facultatif : ajoute votre estimation de VO₂max. L'âge de forme physique lui-même n'en a pas besoin. Mesurez autour de votre taille, au niveau du nombril.", "Optional: import now, or continue and return to Data Sources later.": "Facultatif : importez maintenant, ou continuez et revenez à Sources de données plus tard.", +"Optional: import now, or skip and return to Data Sources later.": "Facultatif : importe maintenant, ou passe et reviens à Sources de données plus tard.", "Other": "Autre", "Other metrics (R-R, frames, battery, history) need a full sync.": "Les autres métriques (R-R, trames, batterie, historique) nécessitent une synchronisation complète.", "Other sources don’t offload raw per-second data on-device.": "Les autres sources ne déchargent pas de données brutes à la seconde sur l'appareil.", "Oura / Fitbit / Garmin export": "Export Oura / Fitbit / Garmin", +"Oura JSON · Fitbit Takeout · Garmin GDPR (daily metrics + sleep)": "Oura JSON · Fitbit Takeout · Garmin GDPR (métriques quotidiennes + sommeil)", "Oura Readiness / Sleep score": "Score Préparation / Sommeil d'Oura", "Oura ring": "Bague Oura", "Oura's own Readiness and Sleep scores. NOOP does not copy them. It computes its own.": "Les scores Préparation et Sommeil propres à Oura. NOOP ne les copie pas. Il calcule les siens.", +"Oura, then scan.": "Oura, puis scanne.", "Outcome": "Résultat", "Outcome metric": "Métrique de résultat", "Outside range": "Hors plage", +"Outside typical range": "Hors de la plage habituelle", +"Over the visible window": "Sur la fenêtre visible", "Over your readings, %@ and %@ move largely independently. No clear relationship.": "Sur vos mesures, %1$@ et %2$@ évoluent largement de manière indépendante. Aucune relation claire.", "Overlay": "Superposition", +"Overlay 2-4 signals": "Superpose 2-4 signaux", "Overlay 2–4 signals": "Superposer 2–4 signaux", "Overlay signals, draw conclusions.": "Superposez les signaux, tirez des conclusions.", "Overnight computed": "Calculé pendant la nuit", "Overnight only": "Nuit uniquement", +"Overnight sleep staging, resting heart rate, skin-temperature trend, motion and ": "Phases de sommeil nocturne, FC au repos, tendance de température cutanée, mouvement et ", "Overnight sleep staging, resting heart rate, skin-temperature trend, motion and battery, read straight off the ring.": "Phases de sommeil nocturne, fréquence cardiaque au repos, tendance de température cutanée, mouvement et batterie, lus directement depuis la bague.", +"Override the automatic fit with your own steps-per-motion value. Useful if your phone ": "Remplace l'ajustement automatique par ta propre valeur de pas par mouvement. Utile si ton téléphone ", "Override the automatic fit with your own steps-per-motion value. Useful if your phone has no step history to learn from, or the estimate runs consistently high or low. Set it back to auto by dragging to the far left.": "Remplacez l'ajustement automatique par votre propre valeur de pas par mouvement. Utile si votre téléphone n'a pas d'historique de pas pour apprendre, ou si l'estimation est systématiquement trop haute ou trop basse. Remettez sur auto en faisant glisser complètement à gauche.", "Own your data, offline": "Possédez vos données, hors ligne", "Owned": "Possédé", @@ -1767,6 +2194,7 @@ "Pace": "Rythme", "Pair and manage the bands NOOP reads from.": "Couplez et gérez les bracelets que NOOP lit.", "Paired": "Couplé", +"Paired locally. NOOP owns this ring while it holds the key. If you reset it again or set it ": "Associée localement. NOOP contrôle cette bague tant qu'il détient la clé. Si tu la réinitialises encore ou que tu la ", "Paired locally. NOOP owns this ring while it holds the key. If you reset it again or set it up in the Oura app, NOOP no longer owns it and you would re-add it to take it over.": "Couplée localement. NOOP possède cette bague tant qu'il détient la clé. Si vous la réinitialisez à nouveau ou la configurez dans l'application Oura, NOOP ne la possède plus et vous devrez la rajouter pour en reprendre le contrôle.", "Paired · idle": "Couplé · inactif", "Pairing help: %@": "Aide au couplage : %@", @@ -1777,22 +2205,36 @@ "Passive": "Passif", "Paste your %@ API key": "Collez votre clé API %@", "Patterns in your own data: association, not cause.": "Tendances dans vos propres données : association, pas cause.", +"Pause": "Pause", +"Pause to change work, rest, or rounds.": "Mets en pause pour changer le travail, le repos ou les rounds.", "Paused": "En pause", "Peak": "Pic", "Peak heart rate": "Fréquence cardiaque de pointe", "Peaked": "Au sommet", +"Pearson r": "r de Pearson", "Pearson r over the visible window · |r| ≥ 0.30, n ≥ 10": "Pearson r sur la fenêtre visible · |r| ≥ 0,30, n ≥ 10", "Per extra %@": "Par %@ supplémentaire", +"Per night, trailing 14 days": "Par nuit, 14 derniers jours", "Per night, trailing 30 days": "Par nuit, sur les 30 derniers jours", +"Per-day wake time": "Heure de réveil par jour", "Per-night need": "Besoin par nuit", "Per-night sleep balance: %lld nights, net %@": "Bilan de sommeil par nuit : %1$lld nuits, net %2$@", +"Permission granted.": "Autorisation accordée.", "Personal Experiment": "Expérience personnelle", "Personal curve · prior-shrunk": "Courbe personnelle · réduite par prior", +"Phase 1": "Phase 1", "Phone": "Téléphone", +"Phone calls": "Appels téléphoniques", +"Phone permission was denied, so phone-call buzzing is off.": "L'autorisation Téléphone a été refusée, donc la vibration pour les appels est désactivée.", "Pick %@": "Choisir %@", "Pick a new active strap": "Choisir un nouveau bracelet actif", "Pick a sport. NOOP records HR, peak, average and effort from the live feed.": "Choisissez un sport. NOOP enregistre la FC, le pic, la moyenne et l'effort à partir du flux en direct.", +"Pick a wearable signal (resting HR, HRV, sleep, Charge, weight…) to line it up against this ": "Choisis un signal du wearable (FC au repos, VFC, sommeil, Charge, poids…) pour le comparer à ce ", "Pick a wearable signal (resting HR, HRV, sleep, Charge, weight…) to line it up against this marker. NOOP averages the signal over the %@ before each reading.": "Choisissez un signal du dispositif porté (FC au repos, VFC, sommeil, Charge, poids…) à comparer avec ce marqueur. NOOP fait la moyenne du signal sur les %@ précédant chaque mesure.", +"Pick at least two metrics above to overlay them and read how they move together.": "Choisis au moins deux métriques ci-dessus pour les superposer et voir comment elles évoluent ensemble.", +"Pick metric": "Choisir une métrique", +"Pick night date": "Choisir la date de la nuit", +"Pick one behaviour you log, one outcome, and a short window. NOOP ": "Choisis un comportement que tu notes, un résultat, et une courte période. NOOP ", "Pick one behaviour you log, one outcome, and a short window. NOOP compares the days you log the behaviour against your behaviour-free days before the start.": "Choisissez un comportement que vous enregistrez, un résultat et une courte période. NOOP compare les jours où vous enregistrez le comportement aux jours sans ce comportement avant le début.", "Pick the ring": "Choisir la bague", "Pick when the nap started and ended. NOOP stages it from your data as its own session, separate from the night's sleep.": "Choisissez quand la sieste a commencé et fini. NOOP la traite à partir de vos données comme une session à part, séparée du sommeil nocturne.", @@ -1806,14 +2248,19 @@ "Plan a trip or shift": "Planifier un voyage ou un décalage", "Plan · %lld-day shift": "Plan · décalage de %lld jours", "Please read and accept the points below.": "Veuillez lire et accepter les points ci-dessous.", +"Point the coach at any OpenAI-compatible server: a local model (Ollama, LM ": "Dirige le coach vers n'importe quel serveur compatible OpenAI : un modèle local (Ollama, LM ", +"Points": "Points", "Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio broadcast": "Diffusion Polar, Wahoo, Coospo, Garmin HRM, Amazfit Helio", "Poor": "Faible", +"Post-workout summary": "Résumé post-entraînement", +"Power": "Puissance", "Power users only.": "Réservé aux utilisateurs avancés.", "Powers Charge, Effort, Rest and Sleep": "Alimente Charge, Effort, Repos et Sommeil", "Powers Charge, Effort, Rest, Sleep + Health Monitor": "Alimente Charge, Effort, Repos, Sommeil + Moniteur de santé", "Powers Effort now; Charge and Rest once enough nights and decode are confirmed": "Alimente l'Effort dès maintenant ; Charge et Repos une fois assez de nuits et le décodage confirmés", "Powers Rest, Effort, Fitness Age and steps, plus Charge once recovery calibrates": "Alimente Repos, Effort, âge de forme physique et pas, plus Charge une fois la récupération calibrée", "Powers the live console + Effort. No Charge, Rest or Sleep": "Alimente la console en direct + Effort. Aucune Charge, Repos ou Sommeil", +"Pre-filled from last night. Tap to confirm or change.": "Pré-rempli d'après la nuit dernière. Touche pour confirmer ou modifier.", "Preparing…": "Préparation en cours…", "Presence automation: set a Focus, pause media, set away…": "Automatisation de présence : définir un mode Focus, mettre en pause les contenus, se marquer absent…", "Preview": "Aperçu", @@ -1823,16 +2270,23 @@ "Previous week": "Semaine précédente", "Previously paired but not currently connected. Re-scan to reconnect.": "Précédemment couplé mais non connecté actuellement. Rescannez pour reconnecter.", "Primed": "Prêt", +"Private by default: only your question and a short metrics summary are sent, ": "Privé par défaut : seuls ta question et un bref résumé de tes métriques sont envoyés, ", "Probably not illness": "Probablement pas une maladie", "Profile": "Profil", "Profile and settings": "Profil et réglages", "Profile photo": "Photo de profil", "Progress toward today's goal": "Progression vers l'objectif du jour", +"Project home": "Accueil du projet", "Project home & source": "Page et code source du projet", "Project home and source code on GitHub": "Page et code source du projet sur GitHub", +"Project home and source on GitHub": "Accueil du projet et code source sur GitHub", +"Project information, attribution, and contact details.": "Informations sur le projet, attribution et coordonnées.", "Proof that the console is current": "Preuve que la console est à jour", "Protein": "Protéines", +"Provenance": "Provenance", "Provider": "Fournisseur", +"Pull steps, heart rate, HRV, sleep, SpO₂, weight and workouts straight from ": "Récupère les pas, la fréquence cardiaque, la VFC, le sommeil, la SpO₂, le poids et les entraînements directement depuis ", +"Pulling your strap's stored history. This drains oldest-first; a deep backlog ": "Récupération de l'historique stocké sur ton bracelet. Ça se vide du plus ancien au plus récent ; un gros retard ", "Pulling your strap's stored history. This drains oldest-first; a deep backlog now continues automatically across passes instead of waiting between syncs.": "Récupération de l'historique stocké sur votre bracelet. Il se vide en commençant par le plus ancien ; un arriéré important se poursuit désormais automatiquement sur plusieurs passages au lieu d'attendre entre les synchronisations.", "Pulls your strap's stored history immediately, without waiting for the next automatic sync.": "Récupère immédiatement l'historique stocké sur votre bracelet, sans attendre la prochaine synchronisation automatique.", "Put the band into pairing mode, on your wrist and awake.": "Mettez le bracelet en mode couplage, au poignet et actif.", @@ -1844,37 +2298,47 @@ "Questions, feedback, bugs: %@": "Questions, retours, bugs : %@", "Quick actions": "Actions rapides", "Quick sweep · ~7 min": "Balayage rapide · ~7 min", +"Quiet and settled.": "Calme et posé.", "Quiet hours": "Heures de silence", "Quiet hours end": "Fin des heures silencieuses", "Quiet hours start": "Début des heures silencieuses", +"R-R": "R-R", "R-R intervals": "Intervalles R-R", "RECORDING": "ENREGISTREMENT", "RECORDING WORKOUT": "ENREGISTREMENT DE L'ENTRAÎNEMENT", "RECOVERY VITALS": "CONSTANTES DE RÉCUPÉRATION", +"REM": "REM", "REM Sleep": "Sommeil paradoxal", "REM sleep": "Sommeil paradoxal", "REST": "REPOS", "RESTING HR + SLEEP + HRV": "FC AU REPOS + SOMMEIL + VFC", +"RMSSD": "RMSSD", "RMSSD %.0f ms now vs your ~%.0f ms baseline (estimate from PPG-derived R-R).": "RMSSD %.0f ms maintenant contre votre référence de ~%.0f ms (estimation à partir des R-R dérivés du PPG).", "RMSSD %lld milliseconds": "RMSSD %lld millisecondes", +"RMSSD - · not enough R-R data": "RMSSD - · pas assez de données R-R", "ROUND": "CYCLE", "RSA RESPONSE BY PACE": "RÉPONSE RSA PAR ALLURE", "RSA response by pace": "Réponse RSA par allure", "Radio connected, stream not yet trusted.": "Radio connectée, flux pas encore fiable.", "Range": "Plage", "Ranked · your data": "Classé · vos données", +"Ranked, lag-aware: which of your habits actually move your Charge, plus your ": "Classé, tenant compte du décalage : quelles habitudes influencent vraiment ta Charge, plus ton ", "Ranked, lag-aware: which of your habits actually move your Charge, plus your personal alcohol/caffeine dose-response.": "Classé, avec prise en compte du délai : quelles habitudes influencent réellement votre Charge, plus votre réponse personnelle à la dose d'alcool/caféine.", "Raw · per second": "Brut · par seconde", "Re-anchors every baseline that feeds Charge to your recent nights. No stored day is deleted.": "Réancre chaque référence qui alimente la Charge sur vos nuits récentes. Aucun jour enregistré n'est supprimé.", "Re-label as": "Réétiqueter comme", +"Re-label as…": "Renommer en…", "Re-nudge every": "Relancer toutes les", +"Re-pull new Health Connect data (e.g. Samsung Health → Health Connect) each ": "Récupère à nouveau les nouvelles données Health Connect (p. ex. Samsung Health → Health Connect) à chaque ", "Re-scan": "Nouveau scan", +"Re-share your live strap heart rate over Bluetooth as a standard heart-rate ": "Repartage la fréquence cardiaque en direct de ton bracelet via Bluetooth comme un moniteur de fréquence cardiaque standard ", "Re-share your live strap heart rate over Bluetooth as a standard heart-rate sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read it. Local Bluetooth only. Nothing leaves %@. Off by default.": "Repartagez la fréquence cardiaque en direct de votre bracelet via Bluetooth comme un capteur de fréquence cardiaque standard, afin qu'un tapis de course, un vélo, Zwift, Peloton ou toute application fitness à proximité puisse la lire. Bluetooth local uniquement. Rien ne quitte %@. Désactivé par défaut.", "React when the strap comes off or goes on. Note: macOS reserves true auto-UNLOCK for Apple Watch, so this can lock, not unlock.": "Réagissez lorsque le bracelet est retiré ou remis. Remarque : macOS réserve le vrai déverrouillage automatique à l'Apple Watch, ceci peut donc verrouiller, pas déverrouiller.", "React when the strap comes off or goes on. Run a Shortcut to set a Focus, pause media, mark yourself away.": "Réagissez lorsque le bracelet est retiré ou remis. Exécutez un raccourci pour activer un mode Focus, mettre en pause les médias ou vous marquer absent.", "Read the full Lab Book note": "Lire la note complète du Cahier de labo", "Read the full note": "Lire la note complète", "Read your heart rate, HRV, blood oxygen, respiratory rate, sleep, steps and energy straight from Apple Health, and write NOOP's strap-derived metrics back. Everything stays on %@.": "Lisez votre fréquence cardiaque, VFC, oxygène sanguin, fréquence respiratoire, sommeil, pas et énergie directement depuis Apple Santé, et réécrivez les métriques de NOOP issues du bracelet. Tout reste sur %@.", +"Read-only; never overwrites strap data.": "Lecture seule ; n'écrase jamais les données du bracelet.", "Readiness": "Préparation", "Readiness: %@": "Préparation : %@", "Readiness: %@. %@": "Préparation : %1$@. %2$@", @@ -1886,12 +2350,14 @@ "Reading your Fitness Age…": "Lecture de votre âge de forme…", "Reading your Mi Band history…": "Lecture de l'historique de votre Mi Band…", "Reading your Vitality…": "Lecture de votre Vitality…", +"Reading your check-ins…": "Lecture de tes check-ins…", "Reading your heart-rate variability and resting heart rate…": "Lecture de votre variabilité de la fréquence cardiaque et FC au repos…", "Reading your history…": "Lecture de votre historique…", "Reading your journal and outcomes…": "Lecture de votre journal et résultats…", "Reading your logbook…": "Lecture de votre journal…", "Reading your sources…": "Lecture de vos sources…", "Reads a coarse menstrual-cycle phase from your nightly skin temperature, entirely on %@. Awareness only: not contraception, not a fertility predictor, not a medical service. The card appears in Health.": "Lit une phase approximative du cycle menstruel à partir de votre température cutanée nocturne, entièrement sur %@. À titre informatif uniquement : ni contraception, ni prédicteur de fertilité, ni service médical. La carte apparaît dans Santé.", +"Reads a coarse menstrual-cycle phase from your nightly skin-temperature shift, on this device only. Awareness only: not contraception, not a fertility predictor, not a medical service.": "Déduit une phase approximative du cycle menstruel à partir de la variation nocturne de ta température cutanée, sur cet appareil uniquement. À titre informatif seulement : ce n'est ni un moyen de contraception, ni un prédicteur de fertilité, ni un service médical.", "Ready": "Prêt", "Ready for a marked effort.": "Prêt pour un effort marqué.", "Ready to breathe": "Prêt à respirer", @@ -1903,6 +2369,8 @@ "Recalibrate Charge baseline": "Recalibrer la référence de Charge", "Recalibrate your Charge baseline?": "Recalibrer votre référence de Charge ?", "Recent": "Récent", +"Recent changes and what to expect": "Changements récents et ce à quoi t'attendre", +"Recent intervals: ": "Intervalles récents : ", "Recent intervals: %@ ms": "Intervalles récents : %@ ms", "Recent moments": "Moments récents", "Recharge your WHOOP before tonight.": "Rechargez votre WHOOP avant ce soir.", @@ -1912,6 +2380,8 @@ "Reconnect help: %@": "Aide à la reconnexion : %@", "Reconnect to pull the latest.": "Reconnectez-vous pour récupérer les dernières données.", "Reconnects this run": "Reconnexions de cette session", +"Record 5/MG raw capture": "Enregistrer la capture brute 5/MG", +"Record 5/MG raw capture (research)": "Enregistrer la capture brute 5/MG (recherche)", "Record a timestamped moment (and buzz the strap if it's connected).": "Enregistrez un moment horodaté (et faites vibrer le bracelet s'il est connecté).", "Record a timestamped moment in NOOP.": "Enregistrez un moment horodaté dans NOOP.", "Record or inspect the current stream": "Enregistrer ou inspecter le flux actuel", @@ -1919,17 +2389,22 @@ "Recorded on device": "Enregistré sur l'appareil", "Recording": "Enregistrement", "Recording status, not shown for a past day": "État d'enregistrement, non affiché pour un jour passé", +"Recording workout": "Enregistrement de l'entraînement", "Recording. Your strap is connected and saving data.": "Enregistrement en cours. Votre bracelet est connecté et enregistre les données.", "Records & sources": "Enregistrements et sources", "Records a live machine workout, Effort-scored from HR when the machine reports it": "Enregistre un entraînement sur machine en direct, l'Effort étant calculé à partir de la FC lorsque la machine la transmet", +"Records the raw frames of each 5/MG history sync to a file on this phone, so you can share them and help NOOP learn to decode 5/MG sleep, recovery and strain. The file contains raw biometric frames (heart rate, R-R, skin temperature, motion) and the strap's own diagnostic text. Nothing leaves the phone unless you share it. Off by default.": "Enregistre les trames brutes de chaque synchro d'historique 5/MG dans un fichier sur ce téléphone, pour que tu puisses les partager et aider NOOP à apprendre à décoder le sommeil, la récupération et l'effort en 5/MG. Le fichier contient des trames biométriques brutes (fréquence cardiaque, R-R, température cutanée, mouvement) et le texte de diagnostic du bracelet lui-même. Rien ne quitte le téléphone sauf si tu le partages. Désactivé par défaut.", "Recovery": "Récupération", "Recovery / Charge": "Récupération / Charge", +"Recovery buzz": "Vibration de récupération", "Recovery calendar, %lld days, average %lld, low %lld, high %lld": "Calendrier de récupération, %lld jours, moyenne %lld, minimum %lld, maximum %lld", "Recovery calendar, no data": "Calendrier de récupération, aucune donnée", "Recovery takes about a week": "La récupération prend environ une semaine", +"Recovery vitals": "Constantes de récupération", "Recovery weighs your HRV against your personal baseline (~60%), resting heart rate (~20%), sleep performance (~15%) and respiration (~5%). Day strain is a 0–21 cardiovascular load from time in heart-rate zones. Sleep is staged from movement and heart rate. Everything is computed here from the strap's raw data, it works for any day NOOP collected raw streams.": "La récupération pondère votre VFC par rapport à votre référence personnelle (~60%), fréquence cardiaque au repos (~20%), performance du sommeil (~15%) et respiration (~5%). L'effort du jour est une charge cardiovasculaire de 0 à 21 basée sur le temps dans les zones de fréquence cardiaque. Le sommeil est stagé à partir du mouvement et de la fréquence cardiaque. Tout est calculé ici à partir des données brutes du bracelet, cela fonctionne pour chaque jour où NOOP a collecté des flux bruts.", "Recovery, NOOP's Charge score, is led by your heart-rate variability measured against your own personal baseline. A chest strap streams beat-to-beat data densely all night, so it can learn that baseline fast. An Apple Watch instead samples HRV, a handful of readings through the day plus overnight, so the signal is real but sparser.": "La Récupération, le score de Charge de NOOP, est principalement déterminée par votre variabilité de fréquence cardiaque mesurée par rapport à votre référence personnelle. Un bracelet pectoral transmet des données battement par battement de manière dense toute la nuit, il peut donc apprendre cette référence rapidement. Une Apple Watch échantillonne quant à elle la VFC, avec quelques mesures dans la journée et pendant la nuit, le signal est donc réel mais plus épars.", "Recovery, Test Centre, experimental probes, and backup. Tucked away to keep the everyday screen tidy.": "Récupération, Centre de test, sondes expérimentales et sauvegarde. Rangés pour garder l'écran du quotidien épuré.", +"Recovery, strain, sleep and workouts, stored locally. Import a full ": "Récupération, effort, sommeil et entraînements, stockés localement. Importe un export complet ", "Reference range from my report (optional)": "Plage de référence de mon bilan (facultatif)", "Reference range from your own report, optional": "Plage de référence de votre propre bilan, facultatif", "Refresh": "Actualiser", @@ -1954,18 +2429,25 @@ "Remove photo": "Supprimer la photo", "Remove this device?": "Supprimer cet appareil ?", "Removed": "Supprimé", +"Removed Apple Health imported data.": "Données importées d'Apple Santé supprimées.", "Removed all Apple Health imported data.": "Toutes les données importées d'Apple Santé ont été supprimées.", "Removed · data kept": "Supprimé · données conservées", +"Removes this recorded sleep and recomputes the day without it. NOOP won't re-detect sleep in this window. You can undo for a few seconds after.": "Supprime ce sommeil enregistré et recalcule la journée sans lui. NOOP ne redétectera pas de sommeil dans cette fenêtre. Tu peux annuler pendant quelques secondes après.", "Removes this recorded sleep and recomputes the day without it. This can't be undone.": "Supprime ce sommeil enregistré et recalcule le jour sans lui. Cette action est irréversible.", +"Removes this sleep and recomputes the day without it. You can undo for a few seconds after.": "Supprime ce sommeil et recalcule la journée sans lui. Tu peux annuler pendant quelques secondes après.", "Removing…": "Suppression…", "Rename": "Renommer", "Rename device": "Renommer l'appareil", "Rename item": "Renommer l'élément", +"Rename your strap's Bluetooth name, useful for a second-hand band. The strap ": "Renomme le nom Bluetooth de ton bracelet, utile pour un bracelet d'occasion. Le bracelet ", "Rename…": "Renommer…", +"Replace": "Remplacer", "Replace all current data with the backup %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde %@ ? Cette action est irréversible.", "Replace all current data with the backup from %@? This cannot be undone.": "Remplacer toutes les données actuelles par la sauvegarde du %@ ? Cette action est irréversible.", +"Replace all current data?": "Remplacer toutes les données actuelles ?", "Replace all data": "Remplacer toutes les données", "Replace this device's data with one of the backups in your folder. This overwrites current data, so back up first if you're unsure.": "Remplacez les données de cet appareil par l'une des sauvegardes de votre dossier. Cela écrase les données actuelles, faites donc d'abord une sauvegarde en cas de doute.", +"Replace this device's data with one of your backups. This overwrites current data, ": "Remplace les données de cet appareil par l'une de tes sauvegardes. Cela écrase les données actuelles, ", "Replaces the Today tab with the prototype redesign. Turn it off any time to return to the classic dashboard. Reads the same live data from your strap.": "Remplace l'onglet Aujourd'hui par la refonte prototype. Désactive-la quand tu veux pour revenir au tableau de bord classique. Elle lit les mêmes données en direct de ton bracelet.", "Report": "Signaler", "Report a %@ bug": "Signaler un bug de %@", @@ -1987,6 +2469,7 @@ "Respiratory Rate": "Fréquence respiratoire", "Respiratory rate": "Fréquence respiratoire", "Rest": "Repos", +"Rest HR": "FC au repos", "Rest baseline": "Référence de repos", "Rest quality": "Qualité du repos", "Rest up": "Reposez-vous", @@ -2013,6 +2496,7 @@ "Restore this backup?": "Restaurer cette sauvegarde ?", "Restore to Today": "Restaurer dans Aujourd'hui", "Restored": "Restauré", +"Restored — reloading NOOP…": "Restauré — rechargement de NOOP…", "Resume": "Reprendre", "Return to workout": "Retour à l'entraînement", "Reveal in Finder": "Afficher dans le Finder", @@ -2020,15 +2504,20 @@ "Reverts to the default profile icon": "Revient à l'icône de profil par défaut", "Review before sharing": "Vérifier avant de partager", "Rhythm": "Rythme", +"Rhythm (experimental)": "Rythme (expérimental)", "Rhythm only looks during quiet, still, resting windows, so it needs a calm night's worth of steady beats. Once there's a clean window, the scatter and its description show here.": "Rythme n'observe que pendant les fenêtres calmes, immobiles et de repos, il a donc besoin d'une nuit calme aux battements réguliers. Une fois qu'une fenêtre propre existe, le nuage de points et sa description s'affichent ici.", "Rhythm visualization (experimental)": "Visualisation du rythme (expérimental)", "Ring key (32 hex characters)": "Clé de la bague (32 caractères hexadécimaux)", +"Ring key, 32 hex characters": "Clé de la bague, 32 caractères hexadécimaux", "Ring key, 32 hexadecimal characters": "Clé de la bague, 32 caractères hexadécimaux", "Rolling RMSSD %lld milliseconds": "RMSSD glissant %lld millisecondes", "Rough": "Difficile", "Rough guide only, based on what you logged.": "Simple indication approximative, basée sur ce que vous avez consigné.", +"Round": "Round", "Round %lld of %lld": "Round %1$lld sur %2$lld", +"Rounds": "Rounds", "Route": "Itinéraire", +"Rowing": "Aviron", "Run NOOP actions hands-free.": "Exécutez des actions NOOP en mains libres.", "Run a Shortcut when put back on": "Exécuter un raccourci lorsqu'il est remis", "Run a Shortcut when taken off": "Exécuter un raccourci lorsqu'il est retiré", @@ -2038,7 +2527,10 @@ "Run now": "Exécuter maintenant", "Running": "En cours", "Runs the stream only during your quiet hours window (22:00 to 07:00 by default), roughly halving the battery cost. Daytime Stress readings will be sparser, since Stress reads this live stream.": "N'exécute le flux que pendant votre fenêtre d'heures calmes (de 22h00 à 07h00 par défaut), ce qui réduit environ de moitié le coût en batterie. Les mesures de Stress en journée seront plus rares, car Stress lit ce flux en direct.", +"SD1 · ms": "SD1 · ms", "SD1:SD2 ratio": "Rapport SD1:SD2", +"SD2 · ms": "SD2 · ms", +"SDNN": "SDNN", "SECONDS": "SECONDES", "SENSOR": "CAPTEUR", "SESSION DONE": "SÉANCE TERMINÉE", @@ -2047,6 +2539,7 @@ "SIGNIFICANT": "SIGNIFICATIF", "SILENT GUARDIAN": "GARDIEN SILENCIEUX", "SLEEP": "SOMMEIL", +"SOLID": "FIABLE", "SPEED": "VITESSE", "STEPS ESTIMATE": "ESTIMATION DES PAS", "STRAP LOG": "JOURNAL DU BRACELET", @@ -2054,12 +2547,15 @@ "STRENUOUS": "INTENSE", "STRONGER SIGNAL": "SIGNAL PLUS FORT", "SYNCING %lld": "SYNCHRONISATION %lld", +"SYNTHESIS": "SYNTHÈSE", "Same cardiovascular-load idea as WHOOP's Day Strain (0–21). We rescaled the top of the ladder from 21 to 100 so all three scores share one scale. The rungs didn't move, so a 100 is as rare as a 21.0 was.": "Même principe de charge cardiovasculaire que l'Effort du jour de WHOOP (0-21). Nous avons redimensionné le haut de l'échelle de 21 à 100 pour que les trois scores partagent une seule échelle. Les échelons n'ont pas bougé, un 100 est donc aussi rare que l'était un 21,0.", "Same core idea as WHOOP's Recovery % (HRV-led recovery), but our weighting and baseline maths are our own, and openly documented.": "Même principe de base que le % de Récupération de WHOOP (récupération pilotée par la VFC), mais notre pondération et nos calculs de référence sont les nôtres, et documentés ouvertement.", "Sat": "Sam", "Saturday": "Samedi", "Save": "Enregistrer", "Save & Continue": "Enregistrer et continuer", +"Save & continue": "Enregistrer et continuer", +"Save a full backup to a folder you choose - point it at Google Drive / Dropbox for off-device sync.": "Enregistre une sauvegarde complète dans un dossier de ton choix - pointe-le vers Google Drive / Dropbox pour une synchro hors appareil.", "Save a full backup to a folder you choose - point it at Google Drive, iCloud or Dropbox for off-device sync.": "Enregistrez une sauvegarde complète dans un dossier de votre choix - pointez-le vers Google Drive, iCloud ou Dropbox pour une synchronisation hors appareil.", "Save it": "Enregistrer", "Save key": "Enregistrer la clé", @@ -2073,6 +2569,7 @@ "Saved to %@. The zip re-imports into NOOP (Data Sources → WHOOP Export) on any Mac, iPhone, or Android device.": "Enregistré dans %@. Le zip se réimporte dans NOOP (Sources de données → Export WHOOP) sur n'importe quel Mac, iPhone ou appareil Android.", "Saves every raw 5/MG frame (with a timestamp and the live heart rate) to a JSON file you can share to help map the biometric layout. This only records frames the strap already sent (it never writes to your strap), so it is safe to leave on. Export the file and attach it to a protocol-mapping issue.": "Enregistre chaque trame brute 5/MG (avec un horodatage et la fréquence cardiaque en direct) dans un fichier JSON que vous pouvez partager pour aider à cartographier la structure biométrique. Cela n'enregistre que les trames déjà envoyées par le bracelet (rien n'est jamais écrit sur votre bracelet), il est donc sans risque de le laisser activé. Exportez le fichier et joignez-le à un signalement de cartographie de protocole.", "Saves every raw 5/MG frame (with a timestamp and the live heart rate) to a JSON file you can share to help map the biometric layout. This only records frames the strap already sent, it never writes to your strap, so it is safe to leave on. Export the file and attach it to a protocol-mapping issue.": "Enregistre chaque trame brute 5/MG (avec un horodatage et la fréquence cardiaque en direct) dans un fichier JSON que vous pouvez partager pour aider à cartographier la disposition biométrique. Cela n'enregistre que les trames que le bracelet a déjà envoyées, il n'écrit jamais sur votre bracelet, il est donc sans danger de le laisser activé. Exportez le fichier et joignez-le à une issue de cartographie de protocole.", +"Saves the last 24h of decoded sensor samples (heart rate, R-R, motion, steps and any 5/MG deep streams you've unlocked) as one CSV you can share, for tinkering with your own data. Nothing leaves the phone unless you share it.": "Enregistre les dernières 24 h d'échantillons de capteurs décodés (fréquence cardiaque, R-R, mouvement, pas et tous les flux détaillés 5/MG que tu as débloqués) dans un seul CSV que tu peux partager, pour bricoler avec tes propres données. Rien ne quitte le téléphone sauf si tu le partages.", "Saves the raw capture and the strap log together as a matched pair. Attach both to a protocol-mapping issue.": "Enregistre la capture brute et le journal du bracelet ensemble, comme une paire assortie. Joignez les deux à un signalement de cartographie de protocole.", "Save…": "Enregistrer…", "Saving to: %@": "Enregistrement dans : %@", @@ -2080,12 +2577,17 @@ "Scan": "Scanner", "Scan & Connect": "Scanner et connecter", "Scan & connect": "Scanner et connecter", +"Scan again": "Scanner à nouveau", +"Scan and connect to start a live stream.": "Scanne et connecte-toi pour démarrer un flux en direct.", "Scan connects to %@. To pair or switch bands, open Devices.": "« Scanner » se connecte à %@. Pour appairer ou changer de bracelet, ouvrez Appareils.", "Scan for %@": "Rechercher %@", "Scan for your Oura ring": "Rechercher votre bague Oura", "Scan for your ring": "Rechercher votre bague", "Scanning your data…": "Analyse de vos données…", "Scanning…": "Scan en cours…", +"Schedule": "Planning", +"Scheduled debug export (#510)": "Export de débogage planifié (#510)", +"Score": "Score", "Scores still calibrating, no single day-owner yet": "Scores encore en calibration, pas encore de propriétaire unique du jour", "Scroll to zoom · drag to pan": "Défiler pour zoomer · glisser pour déplacer", "Search": "Rechercher", @@ -2093,15 +2595,23 @@ "Search markers (e.g. LDL, ferritin)": "Rechercher des marqueurs (ex. LDL, ferritine)", "Search sport": "Rechercher un sport", "Searching": "Recherche en cours", +"Searching for your WHOOP… make sure it's charged, on your wrist, and the official WHOOP app isn't connected to it.": "Recherche de ton WHOOP… vérifie qu'il est chargé, à ton poignet, et que l'app officielle WHOOP n'y est pas connectée.", "Searching…": "Recherche…", "See how Charge, Effort and Rest are calculated, and how they differ from WHOOP.": "Découvrez comment la Charge, l'Effort et le Repos sont calculés, et en quoi ils diffèrent de WHOOP.", +"See how it works": "Voir comment ça marche", "See recovery, beautifully": "Visualisez la récupération, avec élégance", +"See recovery, clearly": "Vois ta récupération, clairement", "See what shaped your Charge": "Découvrez ce qui a façonné votre Charge", "See your full readiness": "Voir votre préparation complète", +"Select": "Sélectionner", +"Select sessions to merge or delete": "Sélectionne les sessions à fusionner ou supprimer", +"Selected night": "Nuit sélectionnée", "Self-tracking, not a clinical assessment. If low mood persists, talk to a professional. You deserve support.": "Auto-suivi, pas une évaluation clinique. Si une humeur basse persiste, parlez-en à un professionnel. Vous méritez du soutien.", "Send": "Envoyer", "Send a haptic buzz to your WHOOP strap.": "Envoyez une vibration haptique à votre bracelet WHOOP.", "Send enable sequence to strap": "Envoyer la séquence d'activation au bracelet", +"Send message": "Envoyer le message", +"Sensor": "Capteur", "Sep": "Sept", "Server URL": "URL du serveur", "Session": "Séance", @@ -2111,21 +2621,31 @@ "Session progress": "Progression de la séance", "Session summary": "Résumé de la séance", "Sessions": "Séances", +"Set": "Définir", "Set a wake time for specific days (a lie-in at the weekend, say). Days you leave alone use the time above.": "Définissez une heure de réveil pour des jours précis (une grasse matinée le week-end, par exemple). Les jours non modifiés utilisent l'heure ci-dessus.", "Set custom container size": "Définir une taille de récipient personnalisée", +"Set it back to auto by dragging to the far left.": "Remets-le sur auto en faisant glisser tout à gauche.", "Set this up on your iPhone": "Configurez ceci sur votre iPhone", "Set up Apple Watch": "Configurer l'Apple Watch", "Settings": "Paramètres", +"Settings → Notifications to let it through.": "Réglages → Notifications pour l'autoriser.", "Settling": "Stabilisation", "Setup": "Configuration", "Sex": "Sexe", "Share": "Partager", +"Share 5/MG capture (for the decode effort)": "Partager la capture 5/MG (pour l'effort de décodage)", +"Share back to Health Connect": "Repartager vers Health Connect", +"Share computed metrics back to Health Connect": "Repartager les métriques calculées vers Health Connect", "Share of imported zone time, duration-weighted across sessions (approximate).": "Part du temps en zone importé, pondérée par la durée sur l'ensemble des séances (approximative).", "Share of imported zone time, duration-weighted across sessions, approximate.": "Part du temps de zone importé, pondéré par la durée entre les séances, approximatif.", +"Share on-device signals with the Coach": "Partager les signaux sur l'appareil avec le Coach", +"Share strap log (for bug reports)": "Partager le journal du bracelet (pour les rapports de bug)", "Sharing %lld bpm. Waiting for a device to pair.": "Partage de %lld bpm. En attente d'un appareil à appairer.", +"Short axis": "Petit axe", "Shortcut name": "Nom du raccourci", "Shortcuts Export": "Export Raccourcis", "Shortcuts file export": "Export de fichier Raccourcis", +"Shorten window": "Raccourcir la fenêtre", "Should you push today?": "Devriez-vous forcer aujourd'hui ?", "Show %@": "Afficher %@", "Show & reorder": "Afficher et réorganiser", @@ -2139,8 +2659,10 @@ "Shows the advanced settings sections": "Affiche les sections des réglages avancés", "Shows your live heart rate on the Lock Screen and in the Dynamic Island while the strap is connected. Turn it off to keep your live HR out of the Dynamic Island. (Any one already showing clears within a moment.)": "Affiche votre fréquence cardiaque en direct sur l'écran verrouillé et dans la Dynamic Island tant que le bracelet est connecté. Désactivez-le pour garder votre FC en direct hors de la Dynamic Island. (Une FC déjà affichée disparaît sous peu.)", "Signal Trust": "Fiabilité du signal", +"Signal lost — coaching paused.": "Signal perdu — coaching en pause.", "Signals up": "Signaux en hausse", "Silence-first strap coaching during workouts.": "Coaching du bracelet, silencieux d'abord, pendant les entraînements.", +"Silent haptic HIIT - the strap buzzes the transitions": "HIIT haptique silencieux - le bracelet vibre aux transitions", "Silent haptic HIIT, the strap buzzes the transitions": "HIIT haptique silencieux, le bracelet vibre aux transitions", "Silent haptic HIIT: the strap buzzes the transitions": "HIIT haptique silencieux : le bracelet vibre aux transitions", "Similar in spirit to WHOOP's Sleep Performance %; our composite is our own.": "Similaire dans l'esprit au % de Performance de sommeil de WHOOP ; notre composite nous est propre.", @@ -2159,6 +2681,7 @@ "Skin temperature %@. %@. Reliable.": "Température cutanée %1$@. %2$@. Fiable.", "Skin-temperature deviation": "Écart de température cutanée", "Skin-temperature trend": "Tendance de la température cutanée", +"Skip": "Passer", "Skip alerts when the strap is off your wrist.": "Ignorer les alertes lorsque le bracelet n'est pas au poignet.", "Skip for now": "Passer pour l'instant", "Skip today": "Ignorer aujourd'hui", @@ -2172,6 +2695,10 @@ "Sleep Performance": "Performance du sommeil", "Sleep Score": "Score de sommeil", "Sleep avg": "Sommeil moy.", +"Sleep consistency nightly bed and wake chart": "Graphique de régularité du sommeil : heures de coucher et de réveil par nuit", +"Sleep debt trend chart": "Graphique de tendance de la dette de sommeil", +"Sleep deleted.": "Sommeil supprimé.", +"Sleep hours trend chart": "Graphique de tendance des heures de sommeil", "Sleep marks": "Marqueurs de sommeil", "Sleep performance": "Performance de sommeil", "Sleep performance %lld of 100": "Performance de sommeil %lld sur 100", @@ -2184,18 +2711,23 @@ "Sleep stages, no data": "Stades de sommeil, aucune donnée", "Sleep staging": "Répartition des phases de sommeil", "Sleep steadiness: Rest varied ±%@ pts night to night.": "Stabilité du sommeil : le Repos a varié de ±%@ pts d'une nuit à l'autre.", +"Sleep window over recent nights": "Fenêtre de sommeil sur les nuits récentes", "Sleep · scores · recording · where your numbers come from": "Sommeil · scores · enregistrement · provenance de vos chiffres", "Sleep-debt ledger": "Registre de dette de sommeil", +"Slept": "Dormi", "Slightly off baseline (HRV is a little low), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la VFC est un peu basse), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", "Slightly off baseline (resting HR is a touch high), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la FC au repos est un peu élevée), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", "Slope source": "Source de la pente", "Slow threads": "Fils lents", "Smart alarm": "Réveil intelligent", "So your zones, calories and baselines are accurate.": "Pour que vos zones, calories et références soient exactes.", +"So your zones, calories and on-device scoring start from the right numbers.": "Pour que tes zones, tes calories et le calcul des scores sur l'appareil partent des bons chiffres.", +"Soft tone on each phase · honours silent mode": "Son doux à chaque phase · respecte le mode silencieux", "Soft tone on each phase · respects silent mode": "Tonalité douce à chaque phase · respecte le mode silencieux", "Solid": "Solide", "Solid signal.": "Signal solide.", "Some occasional extra or skipped beats": "Quelques battements supplémentaires ou manqués occasionnels", +"Source": "Source", "Source Apple Health": "Source Apple Santé", "Source imported activity file": "Source : fichier d'activité importé", "Source imported lifting log": "Source : journal de musculation importé", @@ -2204,14 +2736,19 @@ "Source: %@": "Source : %@", "Source: Apple Watch": "Source : Apple Watch", "Sources differ": "Les sources diffèrent", +"SpO₂, respiratory rate and skin temperature are sleep-window ": "SpO₂, fréquence respiratoire et température cutanée sont propres à la fenêtre de sommeil ", "Sparse, widened to %@ · %lld readings": "Épars, élargi à %1$@ · %2$lld mesures", +"Speed": "Vitesse", "Speed · Cadence · Power · Distance · Energy · Heart rate (if the machine sends it)": "Vitesse · Cadence · Puissance · Distance · Énergie · Fréquence cardiaque (si la machine l'envoie)", +"Sport": "Sport", "Square breath · steady focus": "Respiration carrée · concentration stable", +"Src": "Src", "Stage breakdown": "Répartition des stades", "Stages vs typical": "Stades vs typique", "Standard HR is not a full bond": "La FC standard n'est pas un appairage complet", "Standard HR mode (low bandwidth)": "Mode FC standard (faible bande passante)", "Standard HR mode, low bandwidth. %@": "Mode FC standard, faible bande passante. %@", +"Standard HR sensor (0x180D)": "Capteur FC standard (0x180D)", "Start": "Démarrer", "Start %@": "Démarrer %@", "Start a Breathe session": "Démarrer une séance de Respiration", @@ -2226,6 +2763,7 @@ "Start experiment": "Démarrer l'expérience", "Start session": "Démarrer la séance", "Start workout": "Démarrer l'entraînement", +"Started": "Démarré", "Started %@ · testing %@": "Commencé %@ · test %@", "Started at %lld bpm · the rhythm trails your heart down.": "Commencé à %lld bpm · le rythme accompagne votre cœur vers le bas.", "Starting…": "Démarrage…", @@ -2246,6 +2784,7 @@ "Steps, active energy and logged workouts feed Effort. Dense and reliable.": "Les pas, l'énergie active et les entraînements enregistrés alimentent l'Effort. Dense et fiable.", "Steps, active energy and logged workouts feed your Effort. Dense and reliable.": "Les pas, l'énergie active et les entraînements enregistrés alimentent votre Effort. Dense et fiable.", "Steps, heart rate, sleep, SpO₂ and stress, imported from Mi Fitness, read locally on %@.": "Pas, fréquence cardiaque, sommeil, SpO₂ et stress, importés depuis Mi Fitness, lus localement sur %@.", +"Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app.": "Pas, cœur, sommeil, composition corporelle et VO₂ max - synchronisés depuis l'app de bureau.", "Steps, heart, sleep, body composition and VO₂ max, read locally on %@.": "Pas, cœur, sommeil, composition corporelle et VO₂ max, lus localement sur %@.", "Still": "Immobile", "Stop": "Arrêter", @@ -2257,6 +2796,7 @@ "Storage": "Stockage", "Storage unavailable": "Stockage indisponible", "Stored as %@. %@": "Enregistré sous %1$@. %2$@", +"Strain": "Effort", "Strain nudges and your smart alarm tap your wrist the moment they fire.": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet dès qu'ils se déclenchent.", "Strain nudges and your smart alarm tap your wrist the same way.": "Les alertes d'Effort et votre réveil intelligent tapotent votre poignet de la même manière.", "Strained": "Sous tension", @@ -2281,8 +2821,11 @@ "Strap sync": "Sync du bracelet", "Strap wake-alarm": "Réveil du bracelet", "Streaming live": "Diffusion en direct", +"Streaming locally": "Diffusion en local", "Streaming now": "En cours de diffusion", "Strength Activity Time": "Temps d'activité de force", +"Strength Training": "Musculation", +"Stress": "Stress", "Stress %@ of 3, %@": "Stress %@ de 3, %@", "Stress Trend": "Tendance du stress", "Stress avg": "Stress moy.", @@ -2290,13 +2833,16 @@ "Stress check-ins (haptic)": "Bilans de stress (haptiques)", "Stress is derived from two autonomic signals.": "Le stress est dérivé de deux signaux autonomes.", "Stress monitor": "Moniteur de stress", +"Stress through the day": "Stress au fil de la journée", "Stress trend": "Tendance du stress", "Strong": "Fort", +"Studio, llama.cpp) keeps everything on your device; an API key is optional.": "Studio, llama.cpp) garde tout sur ton appareil ; une clé API est facultative.", "Suggested prompt: %@": "Suggestion : %@", "Summary": "Résumé", "Sun": "Dim", "Sunday": "Dimanche", "Support": "Assistance", +"Support & project home": "Support et page du projet", "Support NOOP": "Soutenir NOOP", "Support NOOP: help and contact": "Soutenir NOOP : aide et contact", "Support NOOP: help and contact.": "Soutenir NOOP : aide et contact.", @@ -2305,13 +2851,18 @@ "Sustained high stress": "Stress élevé prolongé", "Sweep progress": "Progression du balayage", "Sweeping…": "Balayage en cours…", +"Swimming": "Natation", "Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward).": "Tonus sympathique contre parasympathique d'après la VFC dans le domaine fréquentiel. Plus élevé penche vers le sympathique (côté stress).", "Sync": "Synchroniser", "Sync now": "Synchroniser maintenant", +"Sync now. A sync is already in progress.": "Synchroniser maintenant. Une synchro est déjà en cours.", +"Sync now. Connect your strap first.": "Synchroniser maintenant. Connecte d'abord ton bracelet.", +"Sync now. Pulls your strap's stored history immediately, without waiting ": "Synchroniser maintenant. Récupère aussitôt l'historique stocké sur ton bracelet, sans attendre ", "Synced from": "Synchronisé depuis", "Synced from: %@": "Synchronisé depuis : %@", "Syncing": "Synchronisation", "Syncing strap history…": "Synchronisation de l'historique du bracelet…", +"Syncing your strap history…": "Synchronisation de l'historique de ton bracelet…", "Syncing…": "Synchronisation…", "Syncs your strap's stored history right away, instead of waiting for the next automatic sync.": "Synchronise immédiatement l'historique enregistré de votre bracelet, au lieu d'attendre la prochaine synchronisation automatique.", "Synthesis": "Synthèse", @@ -2340,12 +2891,15 @@ "Tap a drink to edit it, or use the trash to delete.": "Touchez une boisson pour la modifier, ou utilisez la corbeille pour la supprimer.", "Tap a workout for its detail · tap ••• to re-label, edit or delete it.": "Touchez un entraînement pour ses détails · touchez ••• pour le renommer, le modifier ou le supprimer.", "Tap the one that's yours.": "Touchez celui qui vous appartient.", +"Tap to allow it in system settings.": "Appuie pour l'autoriser dans les réglages système.", "Tap to compare": "Touchez pour comparer", "Tap to edit the amount": "Touchez pour modifier la quantité", "Tap to log": "Touchez pour enregistrer", "Tap when you're heading to bed or when you wake. Each tap is logged with the time. It doesn't change tonight's detected sleep.": "Touchez quand vous allez vous coucher ou quand vous vous réveillez. Chaque appui est enregistré avec l'heure. Cela ne modifie pas le sommeil détecté cette nuit.", +"Tap your wrist for incoming phone calls and strict best-effort VoIP calls.": "Vibration au poignet pour les appels entrants et, au mieux, les appels VoIP.", "Temperature": "Température", "Temperature unit": "Unité de température", +"Tennis": "Tennis", "Test %@ buzz": "Test de vibration %@", "Test Centre": "Centre de test", "Test action": "Action test", @@ -2354,24 +2908,39 @@ "That file is too large to import.": "Ce fichier est trop volumineux pour être importé.", "That file isn't a NOOP backup. It doesn't look like a SQLite database.": "Ce fichier n'est pas une sauvegarde NOOP. Il ne ressemble pas à une base de données SQLite.", "That is not a 32-character hex key.": "Ce n'est pas une clé hexadécimale de 32 caractères.", +"That time can't be saved (it lands in the future or ends before it starts).": "Cet horaire ne peut pas être enregistré (il tombe dans le futur ou se termine avant de commencer).", "That's normal. A paced breath often settles things when a metronome alone doesn't.": "C'est normal. Une respiration cadencée apaise souvent les choses là où un métronome seul n'y parvient pas.", "That's why a watch-only Charge starts out “Calibrating”. NOOP needs about seven nights of your HRV to learn what normal looks like for you. Until it has them it withholds the score rather than guess. Once the baseline is set, your Charge appears with its confidence, on the same 0–100 scale as a strap's.": "C'est pourquoi une Charge basée uniquement sur la montre commence par « Étalonnage ». NOOP a besoin d'environ sept nuits de votre VFC pour apprendre ce qui est normal pour vous. Tant qu'il ne les a pas, il préfère ne pas afficher de score plutôt que de deviner. Une fois la référence établie, votre Charge apparaît avec son niveau de confiance, sur la même échelle de 0 à 100 qu'un bracelet.", +"The Oura app and your Oura account stop working with this ring. This is the point. ": "L'app Oura et ton compte Oura cessent de fonctionner avec cette bague. C'est justement le but. ", "The Oura app and your Oura account stop working with this ring. This is the point. You are replacing Oura.": "L'application Oura et votre compte Oura cessent de fonctionner avec cet anneau. C'est le but recherché. Vous remplacez Oura.", "The average above was edited. The graph, zones and Effort stay from the recorded session.": "La moyenne ci-dessus a été modifiée. Le graphique, les zones et l'Effort restent ceux de la séance enregistrée.", "The backup archive doesn't contain a database file.": "L'archive de sauvegarde ne contient pas de fichier de base de données.", +"The basics": "Les bases", "The best-sourced number per metric across every band you use.": "Le chiffre le mieux documenté pour chaque métrique, parmi tous les bracelets que vous utilisez.", +"The best-sourced number per metric, across your bands.": "Le chiffre le mieux sourcé pour chaque métrique, tous tes bracelets confondus.", +"The coach talks only to the server URL you set. Point it at a local model to ": "Le coach communique uniquement avec l'URL du serveur que tu définis. Pointe-le vers un modèle local pour ", +"The daily export runs at this time.": "L'export quotidien s'exécute à cette heure.", +"The earliest NOOP will wake you.": "L'heure la plus tôt à laquelle NOOP te réveillera.", "The full terms are in TERMS.md, shipped with NOOP. This is not legal advice.": "Les conditions complètes se trouvent dans TERMS.md, fourni avec NOOP. Ceci n'est pas un avis juridique.", +"The guaranteed alarm fires this long after your earliest time.": "L'alarme garantie se déclenche ce délai après ton heure la plus tôt.", "The honest catch: a watch isn't a chest strap. It's brilliant at sleep, steps, workouts and fitness, and lighter on the dense heart-rate-variability a strap measures all night. So recovery takes about a week to calibrate, and a couple of metrics depend on your watch model. NOOP is upfront about all of it. Every watch-derived number carries a confidence, and where the watch can't be honest, NOOP shows nothing instead of a made-up figure.": "Le hic honnête : une montre n'est pas un bracelet pectoral. Elle excelle pour le sommeil, les pas, les entraînements et la forme physique, et est plus légère sur la VFC dense qu'un bracelet mesure toute la nuit. La récupération met donc environ une semaine à s'étalonner, et quelques métriques dépendent du modèle de votre montre. NOOP est transparent sur tout cela. Chaque chiffre dérivé de la montre porte un niveau de confiance, et là où la montre ne peut pas être honnête, NOOP n'affiche rien plutôt qu'un chiffre inventé.", "The line is each waking hour's 0-3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": "La courbe est l'indice 0-3 de chaque heure d'éveil, évalué par rapport à tes heures calmes du jour. La barre en dessous répartit ta journée en temps de stress calme, modéré et élevé.", "The line is each waking hour's 0–3 proxy, scored against your own calm hours today. The bar below splits your day into calm, moderate and high stress time.": "La ligne représente l'indicateur 0 à 3 de chaque heure éveillée, évalué par rapport à vos propres heures calmes du jour. La barre ci-dessous répartit votre journée en temps de stress calme, modéré et élevé.", +"The line traces your autonomic load across the waking day, scored ": "La courbe retrace ta charge autonome sur la journée d'éveil, notée ", "The master switch for every wrist buzz (inactivity, stress, alerts). Off keeps the strap quiet no matter what else is on.": "L'interrupteur principal de chaque vibration au poignet (inactivité, stress, alertes). Désactivé, il garde le bracelet silencieux quoi qu'il arrive d'autre.", "The method behind the score, not today's values.": "La méthode derrière le score, pas les valeurs du jour.", +"The most common cause is the ring was not fully reset in the Oura app, or the Oura ": "La cause la plus fréquente : la bague n'a pas été entièrement réinitialisée dans l'app Oura, ou l'app Oura ", "The nudge fires %lldh %lldm before this.": "L'alerte se déclenche %1$lldh %2$lldm avant cela.", "The numbers": "Les chiffres", +"The one rule": "La règle unique", +"The ring is not bricked. To go back to where you started, factory-reset it again and set it ": "La bague n'est pas HS. Pour revenir au point de départ, refais une réinitialisation d'usine et configure-la ", "The scatter looked rounder and more spread out than a tight, steady beat. This has many ordinary causes and is not a diagnosis.": "La dispersion semblait plus ronde et plus étalée qu'un battement compact et régulier. Cela a de nombreuses causes ordinaires et n'est pas un diagnostic.", +"The sensor needs skin contact before data starts to mean anything.": "Le capteur a besoin d'un contact avec la peau pour que les données commencent à avoir un sens.", "The strap alarm is a silent buzz, not a sound": "L'alarme du bracelet est une vibration silencieuse, pas un son", "The strap buzzes a gentle rhythm just below your current heart rate, a felt metronome to relax toward. It trails your heart down rather than yanking it, and stops on its own.": "Le bracelet vibre selon un rythme doux juste en dessous de votre fréquence cardiaque actuelle, un métronome ressenti vers lequel vous détendre. Il accompagne votre cœur vers le bas plutôt que de le tirer brusquement, et s'arrête de lui-même.", +"The strap signal was gone for 10 minutes, so the session ended itself.": "Le signal du bracelet a disparu pendant 10 minutes, donc la séance s'est arrêtée d'elle-même.", "The thread of you over time.": "Le fil de vous au fil du temps.", +"The three scores": "Les trois scores", "The wake-alarm above buzzes your wrist from the strap's own firmware. It can't sound a loud alarm, and a sideloaded app can't sound a reliable background wake on this device either (that needs a critical-alert permission this build doesn't have). Keep your phone's built-in Clock alarm as a backup. NOOP's phone-based smart wake (light-sleep detection) is available on the Android app.": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore, et une application en sideload ne peut pas non plus déclencher un réveil en arrière-plan fiable sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas). Gardez le réveil intégré de l'application Horloge de votre téléphone en secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android.", "The wake-alarm above buzzes your wrist from the strap's own firmware. It can't sound a loud alarm. We also schedule a backup notification at your wake time, but a sideloaded app can't sound a guaranteed wake on this device (that needs a critical-alert permission this build doesn't have), so Focus or silent mode can still mute it. Keep your phone's built-in Clock alarm as your real backup. NOOP's phone-based smart wake (light-sleep detection) is available on the Android app.": "Le réveil ci-dessus fait vibrer votre poignet grâce au micrologiciel du bracelet lui-même. Il ne peut pas émettre une alarme sonore. Nous programmons aussi une notification de secours à l'heure de votre réveil, mais une application en sideload ne peut pas garantir un réveil sonore sur cet appareil (cela nécessite une autorisation d'alerte critique que cette version n'a pas), donc le mode Ne pas déranger ou le mode silencieux peuvent quand même la couper. Gardez le réveil intégré de l'application Horloge de votre téléphone comme véritable secours. Le réveil intelligent de NOOP basé sur le téléphone (détection du sommeil léger) est disponible dans l'application Android.", "Theme": "Thème", @@ -2381,21 +2950,31 @@ "There's no NOOP data to export yet.": "Il n'y a pas encore de données NOOP à exporter.", "There's no NOOP data to export yet. Import or record some first.": "Il n'y a pas encore de données NOOP à exporter. Importez ou enregistrez-en d'abord.", "These are experimental probes, off by default. The fuller WHOOP 5/MG controls and the raw-sensor CSV export still live in Settings under Diagnostics.": "Ce sont des sondes expérimentales, désactivées par défaut. Les commandes WHOOP 5/MG plus complètes et l'export CSV des capteurs bruts se trouvent toujours dans Paramètres, sous Diagnostics.", +"These are extra, on-demand HRV lenses computed from today's R-R intervals. They ": "Ce sont des angles VFC supplémentaires, à la demande, calculés à partir des intervalles R-R d'aujourd'hui. Ils ", "These are extra, on-demand HRV lenses computed from today's R-R intervals. They are informational and do not change the stress score above.": "Ce sont des perspectives VFC supplémentaires, à la demande, calculées à partir des intervalles R-R du jour. Elles sont informatives et ne modifient pas le score de stress ci-dessus.", +"These are independent approximations from a consumer strap, built on open science: not ": "Ce sont des approximations indépendantes issues d'un bracelet grand public, fondées sur la science ouverte : pas ", "These are independent approximations from a consumer strap, built on open science: not medical advice, and not WHOOP's official scores.": "Ce sont des approximations indépendantes issues d'un bracelet grand public, basées sur la science ouverte : ni un avis médical, ni les scores officiels de WHOOP.", "These are independent estimates computed on your device from your Apple Health data, not medical advice. Confidence labels are honest about how much NOOP knows so far.": "Ce sont des estimations indépendantes calculées sur votre appareil à partir de vos données Apple Santé, pas un avis médical. Les niveaux de confiance indiquent honnêtement ce que NOOP sait pour l'instant.", +"These are the days where your phone also counted steps, so NOOP can learn how your ": "Ce sont les jours où ton téléphone a lui aussi compté les pas, pour que NOOP puisse apprendre comment ton ", "These are the days where your phone also counted steps, so NOOP can learn how your motion maps to steps. Or set the coefficient manually below.": "Ce sont les jours où votre téléphone a aussi compté les pas, afin que NOOP puisse apprendre comment votre mouvement se traduit en pas. Ou réglez le coefficient manuellement ci-dessous.", +"These are your own numbers shown back to you. NOOP doesn't decide whether any value is ": "Ce sont tes propres chiffres qui te sont présentés. NOOP ne décide pas si une valeur est ", "These are your own numbers shown back to you. NOOP doesn't decide whether any value is normal, high or low.": "Ce sont vos propres chiffres qui vous sont montrés. NOOP ne décide pas si une valeur est normale, élevée ou basse.", +"These days are excluded from the estimate (your phone's real count is shown instead). ": "Ces jours sont exclus de l'estimation (le vrai décompte de ton téléphone est affiché à la place). ", "These days are excluded from the estimate (your phone's real count is shown instead). They're here only so you can judge the estimate's accuracy.": "Ces jours sont exclus de l'estimation (le comptage réel de votre téléphone est affiché à la place). Ils sont là uniquement pour que vous puissiez juger de la précision de l'estimation.", "These power your heart-rate zones, calorie estimates and recovery baselines. Keep them accurate.": "Ces éléments alimentent vos zones de fréquence cardiaque, estimations de calories et références de récupération. Gardez-les précis.", +"These sessions have no sport label yet. Pick one for the merged session.": "Ces séances n'ont pas encore d'étiquette de sport. Choisis-en une pour la séance fusionnée.", "These vitals are from %@": "Ces signes vitaux proviennent de %@", +"They're here only so you can judge the estimate's accuracy.": "Elles ne sont là que pour te permettre de juger la précision de l'estimation.", "Thinking": "Réflexion en cours", +"Thinking…": "Réflexion…", "This builds from the strap as it syncs. Effort and rest appear after you have worn it and slept a night. Charge needs about four nights of sleep to learn your baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first couple of weeks. On a WHOOP 5 or MG the strap banks little history, so the night count can climb slowly or sit at 0 of 4 until you have worn it across a few nights. That's its sync limit, not a fault. Import your WHOOP export to skip the wait.": "Cela se construit à partir du bracelet au fil de sa synchronisation. L'Effort et le Repos apparaissent après l'avoir porté et dormi une nuit. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre votre référence (vous verrez \"Étalonnage\" jusque-là), et continue de s'affiner pendant vos deux premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, donc le compteur de nuits peut progresser lentement ou rester à 0 sur 4 tant que vous ne l'avez pas porté quelques nuits. C'est sa limite de synchronisation, pas un défaut. Importez votre export WHOOP pour éviter l'attente.", "This builds from the strap as it syncs. Strain and sleep appear after you have worn it and slept a night. Recovery needs about a week of nights to learn your baseline, or import your WHOOP export to skip the wait.": "Ceci se construit depuis le bracelet au fil de la synchronisation. L'effort et le sommeil apparaissent après que vous l'ayez porté et dormi une nuit. La récupération nécessite environ une semaine de nuits pour apprendre votre référence, ou importez votre export WHOOP pour éviter l'attente.", +"This can run while you finish setup": "Ça peut tourner pendant que tu termines la configuration", "This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", "This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to grant here.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici.", "This install can't connect to Apple Health directly. It was signed with a profile that doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", "This install can't connect to Apple Health directly. It was signed with a profile that doesn't include Apple's Health permission, so there's nothing to grant here.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici.", +"This is a coarse cue from heart rate, not a clinical sleep-stage reading. If the strap ": "C'est un indice approximatif tiré de la fréquence cardiaque, pas une lecture clinique des phases de sommeil. Si le bracelet ", "This is a sideloaded build, installed outside the App Store. It needs re-signing periodically: roughly every 7 days on a free Apple ID, about a year on a paid developer account.": "Ceci est une version en sideload, installée en dehors de l'App Store. Elle doit être resignée périodiquement : environ tous les 7 jours avec un identifiant Apple gratuit, environ un an avec un compte développeur payant.", "This is a wellness visualization, not a screening test. It does not tell you to see a clinician and it names no condition. This is not legal or medical advice.": "Ceci est une visualisation de bien-être, pas un test de dépistage. Elle ne vous dit pas de consulter un médecin et ne nomme aucune affection. Ceci n'est pas un avis juridique ou médical.", "This is an experimental wellness visualization of your beat-to-beat timing. It is NOT an ECG, and it cannot diagnose, detect, or rule out any heart condition.": "Ceci est une visualisation expérimentale de bien-être de la cadence battement par battement. Ce n'est PAS un ECG, et cela ne peut ni diagnostiquer, ni détecter, ni écarter une affection cardiaque.", @@ -2405,12 +2984,15 @@ "This is your last scored session. Wear the strap overnight for a fresh score.": "Voici votre dernière séance notée. Portez le bracelet une nuit pour obtenir un nouveau score.", "This is your only sleep block today.": "C'est votre seul bloc de sommeil aujourd'hui.", "This isn't a NOOP backup from this app. It's missing the migration bookkeeping a NOOP backup carries (it looks like an Android backup or another app's database), and restoring it would strand your store. To move your history across platforms, export the WHOOP-format CSV on the other device (Settings → Export data) and import that here, or import your original WHOOP / Apple Health export.": "Ce n'est pas une sauvegarde NOOP provenant de cette application. Il lui manque les informations de migration qu'une sauvegarde NOOP contient (elle ressemble à une sauvegarde Android ou à la base de données d'une autre application), et la restaurer bloquerait votre stockage. Pour transférer votre historique entre plateformes, exportez le CSV au format WHOOP sur l'autre appareil (Paramètres → Exporter les données) et importez-le ici, ou importez votre export WHOOP / Apple Santé d'origine.", +"This metric needs at least two nights of data.": "Cette métrique a besoin d'au moins deux nuits de données.", "This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it.": "Cela déplace la nuit vers une période sans données enregistrées. Les stades ne peuvent pas y être calculés, la nuit peut donc apparaître vide tant que des données ne la couvrent pas.", "This night scored high efficiency but very little deep or REM, more likely a staging estimate miss than a real restorative shortfall. The totals are kept as-is; read the split with care.": "Cette nuit a obtenu une efficacité élevée mais très peu de sommeil profond ou paradoxal, ce qui ressemble plus à une erreur d'estimation des phases qu'à un réel déficit de récupération. Les totaux sont conservés tels quels ; interprétez la répartition avec prudence.", "This permanently deletes all data recorded from %@. This can't be undone.": "Cela supprime définitivement toutes les données enregistrées depuis %@. Cette action est irréversible.", +"This permanently deletes everything imported from Apple Health: heart rate, HRV, ": "Ceci supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, ", "This permanently deletes everything imported from Apple Health: heart rate, HRV, sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "Cela supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, sommeil, pas, entraînements et plus encore. Les données en direct de votre bracelet ne sont pas concernées. Cette action est irréversible.", "This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays.": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé.", "This restarts the roughly 4-night build-up for Charge and your HRV baseline. Your history stays. Use it if a bad first week, like wearing it while sick, set your baseline off.": "Cela relance la constitution d'environ 4 nuits pour la Charge et votre référence de VFC. Votre historique est conservé. Utilisez cette option si une mauvaise première semaine, par exemple portée pendant une maladie, a faussé votre référence.", +"This section is on the way.": "Cette section arrive bientôt.", "This session": "Cette séance", "This session's contribution to the day's Effort, as captured during the workout.": "La contribution de cette séance à l'Effort du jour, telle qu'enregistrée pendant l'entraînement.", "This sideloaded build expired %lld days ago. Re-sign it to keep it running.": "Cette version en sideload a expiré il y a %lld jours. Resignez-la pour continuer à l'utiliser.", @@ -2419,6 +3001,7 @@ "This sideloaded build expires in 1 day. Re-sign to keep it running.": "Cette version en sideload expire dans 1 jour. Resignez-la pour continuer à l'utiliser.", "This stands on community reverse-engineering. Huge thanks:": "Ceci repose sur la rétro-ingénierie communautaire. Un immense merci :", "This stays on your device. It is never uploaded, never synced, never shared.": "Cela reste sur votre appareil. Ce n'est jamais téléversé, jamais synchronisé, jamais partagé.", +"This vital needs at least two historical readings before NOOP can chart it.": "Cette constante a besoin d'au moins deux relevés historiques avant que NOOP puisse la représenter.", "This week": "Cette semaine", "Three quiet promises.": "Trois promesses discrètes.", "Threshold": "Seuil", @@ -2427,14 +3010,17 @@ "Time": "Heure", "Time asleep": "Temps de sommeil", "Time in Bed": "Temps au lit", +"Time in band": "Temps dans la zone", "Time in each %HRmax zone, derived from the strap's heart rate over this window (approximate).": "Temps dans chaque zone %HRmax, calculé à partir de la fréquence cardiaque du bracelet sur cette période (approximatif).", "Time of day": "Moment de la journée", "Time range": "Plage de temps", "Time to move. You've been seated a while.": "Il est temps de bouger. Vous êtes assis depuis un moment.", "Time to wind down": "Il est temps de se détendre", "Timing forecast": "Prévision des horaires", +"Tip: a desktop Drive / Dropbox app auto-syncs a chosen folder. On the phone, save to a ": "Astuce : une app Drive / Dropbox sur ordinateur synchronise automatiquement un dossier choisi. Sur le téléphone, enregistre dans un ", "Tip: choose a folder in iCloud Drive and your backups sync to all your Apple devices automatically, no account setup needed.": "Astuce : choisissez un dossier dans iCloud Drive et vos sauvegardes se synchroniseront automatiquement sur tous vos appareils Apple, sans configuration de compte.", "Tip: the share sheet can save the PDF to Files, AirDrop it, or send it on.": "Astuce : le menu de partage permet d'enregistrer le PDF dans Fichiers, de l'envoyer par AirDrop ou de le transférer.", +"Titanium": "Titane", "To get your Apple Health data in anyway: import a Health export .zip in Data Sources, or turn on Shortcuts Export to feed your strap data into Health without the entitlement. (A build installed from the App Store or signed with a paid Apple Developer account connects directly.)": "Pour intégrer quand même vos données Apple Santé : importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export Raccourcis pour envoyer les données de votre bracelet vers Santé sans l'entitlement. (Une version installée depuis l'App Store ou signée avec un compte Apple Developer payant se connecte directement.)", "Today": "Aujourd'hui", "Today's Timeline": "Chronologie du jour", @@ -2443,6 +3029,7 @@ "Today's mood: %@, %lld of 5": "Humeur du jour : %1$@, %2$lld sur 5", "Today's scores owned by %@": "Scores du jour gérés par %@", "Today's stress split: calm %@, moderate %@, high %@.": "Répartition du stress du jour : calme %1$@, modéré %2$@, élevé %3$@.", +"Today's value is your recorded daily stress score (0-3).": "La valeur du jour est ton score de stress quotidien enregistré (0-3).", "Today’s Synthesis": "Synthèse du jour", "Tomorrow": "Demain", "Tomorrow's Charge": "Charge de demain", @@ -2452,34 +3039,51 @@ "Tonight's lands after you sleep with the strap on.": "Le score de cette nuit arrivera après avoir dormi avec le bracelet.", "Tonight’s forecast": "Prévision pour cette nuit", "Top sport": "Sport principal", +"Top zone": "Zone la plus haute", +"Total": "Total", "Total Calories": "Calories totales", "Total Distance": "Distance totale", "Total Time": "Temps total", "Total Workouts": "Total des entraînements", +"Track GPS route": "Suivre le tracé GPS", "Track a workout manually. Records heart rate and effort until you end it.": "Suivez un entraînement manuellement. Enregistre la fréquence cardiaque et l'effort jusqu'à ce que vous le terminiez.", "Trailing %lld days": "%lld derniers jours", "Trailing window": "Fenêtre glissante", "Train by feel. The strap buzzes so you don't have to watch a screen.": "Entraînez-vous au ressenti. Le bracelet vibre pour que vous n'ayez pas à regarder un écran.", "Treadmill, indoor bike, rower or cross-trainer (Bluetooth FTMS)": "Tapis de course, vélo d'intérieur, rameur ou vélo elliptique (Bluetooth FTMS)", +"Treat this as permanent.": "Considère ça comme définitif.", "Trend": "Tendance", "Trend only where supported, and Apple removed the SpO₂ sensor from the newest US units, so on those it simply isn't there. NOOP shows nothing rather than a fake reading.": "Tendance uniquement là où c'est pris en charge, et Apple a retiré le capteur SpO₂ des tout derniers modèles américains, donc il est tout simplement absent sur ceux-ci. NOOP n'affiche rien plutôt qu'une mesure factice.", "Trend, %lld points, latest %@, low %@, high %@": "Tendance, %lld points, dernier %@, minimum %@, maximum %@", "Trends": "Tendances", +"Trends need history to draw": "Les tendances ont besoin d'historique pour s'afficher", +"Trends need history to draw. Import your WHOOP export in Data Sources ": "Les tendances ont besoin d'historique pour s'afficher. Importe ton export WHOOP dans Sources de données ", "Trends need history to draw. Import your WHOOP export in Data Sources to see weeks, months and years instantly.": "Les tendances ont besoin d'historique pour s'afficher. Importez votre export WHOOP dans Sources de données pour voir semaines, mois et années instantanément.", "Trends report": "Rapport de tendances", "Try WHOOP 5/MG protocol probes": "Essayer les sondes de protocole WHOOP 5/MG", +"Try Yesterday or 2 days ago from the bar above if the strap or import did not produce a daily vitals snapshot yet.": "Essaie Hier ou Il y a 2 jours dans la barre ci-dessus si le bracelet ou l'import n'a pas encore produit d'instantané quotidien des constantes.", +"Try a longer interval like 3M, 6M, 1Y, or ALL to see this vital’s trend.": "Essaie un intervalle plus long comme 3M, 6M, 1Y ou ALL pour voir la tendance de cette constante.", "Try again": "Réessayer", +"Try asking": "Essaie de demander", +"Try connecting now": "Essayer de se connecter maintenant", "Tue": "Mar", "Tuesday": "Mardi", +"Turn Bluetooth on": "Activer le Bluetooth", "Turn off": "Désactiver", "Turn off cycle awareness": "Désactiver le suivi du cycle", +"Turn on Bluetooth": "Active le Bluetooth", "Turn on Rhythm": "Activer Rythme", "Turn on a test for the thing that's wrong, wear the strap, then tap Report. All on %@.": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Tout sur %@.", "Turn on a test for the thing that's wrong, wear the strap, then tap Report. Your strap log, recalibrate, scheduled export and experimental probes all live here too.": "Activez un test pour ce qui ne va pas, portez le bracelet, puis touchez Rapport. Le journal du bracelet, le réétalonnage, l'export planifié et les sondes expérimentales se trouvent aussi ici.", "Turn on cycle awareness": "Activer le suivi du cycle", +"Turn on the smart alarm to wake inside a window you choose.": "Active l'alarme intelligente pour te réveiller dans une plage que tu choisis.", "Turn on the wind-down reminder below to land at your wake time rested.": "Activez le rappel de détente ci-dessous pour arriver reposé à l'heure de votre réveil.", +"Type in a blood-pressure reading or a cholesterol value from your last appointment. ": "Saisis une mesure de tension ou une valeur de cholestérol de ton dernier rendez-vous. ", "Type in a blood-pressure reading or a cholesterol value from your last appointment. It stays on %@, and over time you'll see how it lines up with your sleep, heart rate and recovery.": "Saisissez une mesure de tension artérielle ou une valeur de cholestérol issue de votre dernière consultation. Elle reste sur %@, et avec le temps vous verrez comment elle s'accorde avec votre sommeil, votre fréquence cardiaque et votre récupération.", "Type in a number from your own report. It stays on %@.": "Saisissez un chiffre issu de votre propre rapport. Il reste sur %@.", +"Type in a number from your own report. It stays on this phone.": "Saisis un chiffre issu de ton propre compte rendu. Il reste sur ce téléphone.", +"Typical": "Habituel", +"Typical effort": "Effort habituel", "Typical for you": "Typique pour vous", "Typical range": "Plage typique", "Undo": "Annuler", @@ -2487,9 +3091,11 @@ "Unit": "Unité", "Units": "Unités", "Unknown": "Inconnu", +"Unlock WHOOP 5/MG deep data": "Débloque les données avancées du WHOOP 5/MG", "Unlock WHOOP 5/MG deep data (R22)": "Débloquer les données approfondies WHOOP 5/MG (R22)", "Unlocks your VO₂max": "Débloque votre VO₂max", "Unread. %@. %@": "Non lu. %1$@. %2$@", +"Update": "Mettre à jour", "Updated weekly": "Mis à jour hebdomadairement", "Updates": "Mises à jour", "Updates, %lld unread": "Mises à jour, %lld non lues", @@ -2500,11 +3106,14 @@ "Use file import": "Utiliser l'import de fichier", "Use file import for Oura": "Utiliser l'import de fichier pour Oura", "Use it only with a device you own, to read your own data. Whether to use it (and any effect on your WHOOP account, subscription, device, or warranty) is your decision, and your risk alone.": "Utilisez-le uniquement avec un appareil qui vous appartient, pour lire vos propres données. L'utiliser (et tout effet sur votre compte WHOOP, votre abonnement, votre appareil ou votre garantie) relève de votre seule décision et de votre seul risque.", +"Use model": "Utiliser le modèle", "Use my resonance pace": "Utiliser mon rythme de résonance", +"Uses the same quiet-hours and worn-only rules.": "Utilise les mêmes règles d'heures calmes et de port du bracelet uniquement.", "Uses the watch's Broadcast Heart Rate. We'll show you how.": "Utilise la diffusion de la fréquence cardiaque de la montre. Nous vous montrerons comment faire.", "Using": "Utilisation", "Using NOOP may breach WHOOP's Terms of Service": "L'utilisation de NOOP peut enfreindre les conditions d'utilisation de WHOOP", "Using NOOP on iPhone": "Utiliser NOOP sur iPhone", +"VO₂ Max": "VO₂ Max", "VO₂ Max (estimated)": "VO₂ Max (estimé)", "Value": "Valeur", "Variation is normal and often benign": "La variation est normale et souvent bénigne", @@ -2519,51 +3128,76 @@ "Visual only": "Visuel uniquement", "Vital Signs": "Signes vitaux", "Vitality": "Vitalité", +"VoIP calls": "Appels VoIP", "WHAT IT'S GREAT AT": "CE EN QUOI ELLE EXCELLE", "WHAT MOVES YOU ›": "CE QUI VOUS FAIT BOUGER ›", "WHAT THE WATCH CAN DO": "CE QUE LA MONTRE PEUT FAIRE", "WHAT TO EXPECT": "À QUOI S'ATTENDRE", "WHAT'S NEW": "NOUVEAUTÉS", "WHERE IT'S LIGHTER THAN A STRAP": "OÙ ELLE EST PLUS LIMITÉE QU'UN BRACELET", +"WHOOP": "WHOOP", +"WHOOP 4.0": "WHOOP 4.0", "WHOOP 4.0 is tested and works end to end. WHOOP 5.0/MG is newer: live heart rate works today, but deeper metrics (recovery, strain, sleep) for 5/MG are still being figured out. NOOP always tells you what's live versus still building.": "WHOOP 4.0 est testé et fonctionne de bout en bout. WHOOP 5.0/MG est plus récent : la fréquence cardiaque en direct fonctionne déjà, mais les métriques plus approfondies (récupération, effort, sommeil) pour 5/MG sont encore en cours d'étude. NOOP vous indique toujours ce qui est actif et ce qui est encore en construction.", "WHOOP 4.0 is the supported path": "WHOOP 4.0 est la voie prise en charge", "WHOOP 4.0 protocol": "Protocole WHOOP 4.0", +"WHOOP 4.0 versus 5.0: what each can read and why": "WHOOP 4.0 contre 5.0 : ce que chacun peut lire et pourquoi", +"WHOOP 4.0 vs 5.0/MG": "WHOOP 4.0 vs 5.0/MG", "WHOOP 4.0 · motion → steps": "WHOOP 4.0 · mouvement → pas", +"WHOOP 5 / MG (experimental)": "WHOOP 5 / MG (expérimental)", +"WHOOP 5.0 / MG": "WHOOP 5.0 / MG", +"WHOOP 5.0 / MG bonds to one device at a time, so unpair it from the official WHOOP app first.": "Le WHOOP 5.0 / MG se lie à un seul appareil à la fois : dissocie-le d'abord de l'app officielle WHOOP.", "WHOOP 5.0 / MG bonds to one device at a time. Unpair it from the official WHOOP app first.": "WHOOP 5.0 / MG se couple à un seul appareil à la fois. Dissociez-le d'abord de l'application officielle WHOOP.", "WHOOP 5.0 / MG support is newer and still experimental in NOOP.": "La prise en charge de WHOOP 5.0 / MG est plus récente et encore expérimentale dans NOOP.", "WHOOP 5.0 protocol": "Protocole WHOOP 5.0", +"WHOOP 5.0/MG may need pairing mode the first time, with the official WHOOP app closed.": "Le WHOOP 5.0/MG peut avoir besoin du mode d'appairage la première fois, avec l'app officielle WHOOP fermée.", +"WHOOP 5.0/MG pairs with one app at a time. If a scan finds nothing, unpair it in ": "Le WHOOP 5.0/MG s'appaire avec une seule app à la fois. Si un scan ne trouve rien, dissocie-le dans ", "WHOOP 5.0/MG pairs with one app at a time. If a scan finds nothing, unpair it in the official WHOOP app and fully close that app, then Scan again.": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si un scan ne trouve rien, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis relancez le scan.", "WHOOP 5.0/MG pairs with one app at a time. If nothing's found, unpair it in the official WHOOP app and fully close that app, then Scan.": "WHOOP 5.0/MG s'associe à une seule application à la fois. Si rien n'est trouvé, dissociez-le dans l'application officielle WHOOP et fermez complètement cette application, puis lancez le scan.", "WHOOP 5/MG straps hand a fresh app only live heart rate. The official app switches on the deeper streams (high-rate HR + motion + history) by writing a set of feature flags, a sequence two independent projects have documented. With this on, the button below sends that exact sequence to your strap. Unlike everything else here it does write to the strap, but it's reversible (it only changes which data the strap chooses to emit) and is the same thing the official app does. Experimental: it may do nothing on your firmware. iPhone/Android only. A Mac can't write to a 5/MG.": "Les bracelets WHOOP 5/MG ne fournissent à une nouvelle application que la fréquence cardiaque en direct. L'application officielle active les flux plus approfondis (FC haute fréquence + mouvement + historique) en écrivant un ensemble d'indicateurs de fonctionnalités, une séquence documentée par deux projets indépendants. Une fois cette option activée, le bouton ci-dessous envoie exactement cette séquence à votre bracelet. Contrairement à tout le reste ici, cela écrit bien sur le bracelet, mais c'est réversible (cela ne change que les données que le bracelet choisit d'émettre) et c'est exactement ce que fait l'application officielle. Expérimental : cela peut n'avoir aucun effet sur votre micrologiciel. iPhone/Android uniquement. Un Mac ne peut pas écrire sur un 5/MG.", +"WHOOP 5/MG straps hand a fresh app only live heart rate. The official app switches on the deeper streams (high-rate HR + motion + history) by writing a set of feature flags, a sequence two independent projects have documented. With this on, the button below sends that exact sequence to your strap. Unlike everything else here it does write to the strap, but it's reversible (it only changes which data the strap emits) and is the same thing the official app does. Experimental: it may do nothing on your firmware.": "Les bracelets WHOOP 5/MG ne fournissent à une nouvelle app que la fréquence cardiaque en direct. L'app officielle active les flux plus avancés (FC haute fréquence + mouvement + historique) en écrivant un ensemble d'indicateurs de fonctionnalités, une séquence que deux projets indépendants ont documentée. Une fois ceci activé, le bouton ci-dessous envoie exactement cette séquence à ton bracelet. Contrairement à tout le reste ici, il écrit bel et bien sur le bracelet, mais c'est réversible (ça change seulement quelles données le bracelet émet) et c'est exactement ce que fait l'app officielle. Expérimental : ça peut ne rien faire selon ton firmware.", "WHOOP Export": "Export WHOOP", +"WHOOP History": "Historique WHOOP", "WHOOP Strap (Live BLE)": "Bracelet WHOOP (BLE en direct)", +"WHOOP app isn't installed": "L'app WHOOP n'est pas installée", +"WHOOP data export (.zip) from app.whoop.com → Data Management and it ": "Export de données WHOOP (.zip) depuis app.whoop.com → Data Management et il ", +"WHOOP data, and writes nothing unless you opt in to sharing back below.": "les données WHOOP, et n'écrit rien sauf si tu choisis de partager en retour ci-dessous.", +"WHOOP export in Data Sources first.": "ton export WHOOP dans Sources de données d'abord.", "WHOOP import": "Import WHOOP", +"WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps are an early, ": "WHOOP est le bracelet principal de NOOP, entièrement pris en charge. Les autres bracelets cardio bénéficient d'une prise en charge récente et ", "WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps are an early, in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper sleep and recovery data.": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque sont un ajout récent, encore en développement : ils diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP.", +"WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps stream live heart rate ": "WHOOP est le bracelet principal de NOOP, entièrement pris en charge. Les autres bracelets cardio diffusent la fréquence cardiaque en direct ", "WHOOP is NOOP's primary, fully-supported band. Other heart-rate straps stream live heart rate and HRV, but not WHOOP's deeper sleep and recovery data.": "WHOOP est le bracelet principal et pleinement pris en charge par NOOP. Les autres bracelets de fréquence cardiaque diffusent la fréquence cardiaque et la VFC en direct, mais pas les données plus approfondies de sommeil et de récupération de WHOOP.", "WHOOP straps don't appear in your %@'s Bluetooth settings. They advertise on a custom profile that only apps like NOOP can find, so there's nothing to pair there, and you shouldn't try.": "Les bracelets WHOOP n'apparaissent pas dans les réglages Bluetooth de votre %@. Ils s'annoncent sur un profil personnalisé que seules des applications comme NOOP peuvent détecter : il n'y a donc rien à associer là-bas, et vous ne devriez pas essayer.", "WHOOP's imported per-zone split for this session.": "Répartition par zone importée de WHOOP pour cette séance.", "WORK": "TRAVAIL", "WORKOUT IN PROGRESS": "ENTRAÎNEMENT EN COURS", +"Wahoo, Polar, Strava, Apple) straight off your phone. GPS route, distance, heart rate ": "Wahoo, Polar, Strava, Apple) directement depuis ton téléphone. Tracé GPS, distance, fréquence cardiaque ", "Waist (optional)": "Tour de taille (facultatif)", "Waist in centimetres": "Tour de taille en centimètres", "Waist in inches": "Tour de taille en pouces", "Waist not set": "Tour de taille non défini", "Waist, %lld centimetres": "Tour de taille, %lld centimètres", "Waist, %lld inches": "Tour de taille, %lld pouces", +"Waist, not set. Optional: adds your VO₂max estimate": "Tour de taille, non défini. Facultatif : ajoute ton estimation de VO₂max", "Waiting for R-R": "En attente de R-R", "Waiting for R-R intervals.": "En attente des intervalles R-R.", "Waiting for a resting heart rate. Start a live reading first, or come back when you're still.": "En attente d'une fréquence cardiaque au repos. Démarrez d'abord une mesure en direct, ou revenez lorsque vous êtes immobile.", "Wake": "Réveil", +"Wake alarm": "Alarme de réveil", "Wake at": "Réveil à", +"Wake me no earlier than": "Ne me réveille pas avant", +"Wake me with a smart alarm": "Réveille-moi avec une alarme intelligente", "Wake me with a strap buzz": "Me réveiller avec une vibration du bracelet", "Wake the machine. Start pedalling, walking or rowing so it powers on its Bluetooth.": "Réveillez l'appareil. Commencez à pédaler, marcher ou ramer pour qu'il active son Bluetooth.", "Wake time": "Heure de réveil", "Wake your Amazfit / Zepp band and make sure it isn't connected to the Zepp app right now.": "Réveillez votre bracelet Amazfit / Zepp et assurez-vous qu'il n'est pas connecté à l'application Zepp en ce moment.", "Wake your Mi Band and make sure it isn't connected to the Mi Fitness / Zepp Life app right now.": "Réveillez votre Mi Band et assurez-vous qu'elle n'est pas connectée à l'application Mi Fitness / Zepp Life en ce moment.", "Wake your strap. Put it on, or dampen the contacts.": "Réveillez votre bracelet. Portez-le, ou humidifiez les contacts.", +"Wake-up": "Réveil", "Walking": "Marche", "Want the full breakdown of every metric and how sure NOOP is about each one? The “About Apple Watch data” page in Settings has the honest table.": "Vous voulez le détail complet de chaque métrique et le degré de certitude de NOOP pour chacune ? La page « À propos des données Apple Watch » dans Paramètres contient le tableau honnête.", "Warmer than your baseline": "Plus chaude que votre référence", +"Warming up - keep moving to climb into Zone 1.": "Échauffement - continue de bouger pour monter en Zone 1.", "Warming up. Keep moving to climb into Zone 1.": "Échauffement. Continuez à bouger pour atteindre la Zone 1.", "Warn me about caffeine close to bedtime": "M'avertir en cas de caféine proche du coucher", "Warn me when I log caffeine too close to bedtime. A timing guide from your own bedtime, not a measurement.": "M'avertir lorsque j'enregistre de la caféine trop près de l'heure du coucher. Un repère basé sur votre propre heure de coucher, pas une mesure.", @@ -2571,8 +3205,14 @@ "Watch for early-illness signs": "Surveiller les premiers signes de maladie", "Watch your heart, live": "Observez votre cœur, en direct", "Watches your resting HR, HRV, skin temperature and respiration against your own 28-day baseline. On-device and approximate: informational only, not a diagnosis.": "Surveille votre FC au repos, votre VFC, votre température cutanée et votre respiration par rapport à votre propre référence de 28 jours. Sur l'appareil et approximatif : à titre informatif uniquement, pas un diagnostic.", +"Watches your resting heart rate, HRV and skin temperature for the pattern that often shows up before you feel unwell, and surfaces a gentle heads-up. An observation about your own numbers, not a diagnosis.": "Surveille ta FC au repos, ta VFC et ta température cutanée pour repérer le schéma qui apparaît souvent avant que tu te sentes patraque, et affiche un petit avertissement bienveillant. Une observation sur tes propres chiffres, pas un diagnostic.", +"We compare today's resting heart rate and HRV to your own 30-day ": "On compare ta FC au repos et ta VFC d'aujourd'hui à ta propre référence sur 30 jours ", "We compare today's resting heart rate and HRV to your own 30-day baseline. A higher-than-usual resting HR and a lower-than-usual HRV both push the score up, classic signs the body is activated. The combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at your baseline, 3 is highly activated.": "Nous comparons ta fréquence cardiaque au repos et ta VFC du jour à ta référence sur 30 jours. Une FC au repos plus haute que d'habitude et une VFC plus basse que d'habitude poussent toutes deux le score vers le haut — signes classiques d'un corps activé. L'écart combiné est ramené sur une échelle de 0 à 3 : 0 = calme, 1,5 = ta référence, 3 = très activé.", "We could not take over this ring.": "Nous n'avons pas pu prendre le contrôle de cet anneau.", +"We didn't get data from 2 days ago": "On n'a pas reçu les données d'il y a 2 jours", +"We didn't get today's data": "On n'a pas reçu les données d'aujourd'hui", +"We didn't get yesterday's data": "On n'a pas reçu les données d'hier", +"We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's ": "On ne détecte encore aucun mouvement de ton bracelet. Les pas sont estimés à partir des relevés de l'accéléromètre de ton WHOOP ", "We're not seeing any motion from your strap yet. Steps are estimated from your WHOOP's banked motion history, so your strap needs to sync that history before NOOP has anything to count.": "Nous ne voyons encore aucun mouvement provenant de votre bracelet. Les pas sont estimés à partir de l'historique de mouvement stocké par votre WHOOP, donc votre bracelet doit d'abord synchroniser cet historique avant que NOOP ait quoi que ce soit à compter.", "Wear & presence": "Port et présence", "Wear it snug on your wrist or bicep, sensor against skin.": "Portez-le ajusté au poignet ou au biceps, capteur contre la peau.", @@ -2581,6 +3221,8 @@ "Wear the strap overnight and these read from your nightly skin temperature.": "Portez le bracelet pendant la nuit et ces valeurs seront lues à partir de votre température cutanée nocturne.", "Wear the strap overnight to score a night first.": "Porte le bracelet la nuit pour qu'une nuit soit d'abord évaluée.", "Wear the strap, tap once, then let it sync and share your strap log.": "Portez le bracelet, touchez une fois, laissez-le se synchroniser, puis partagez le journal du bracelet.", +"Wear your strap for a full week and it appears here.": "Porte ton bracelet pendant une semaine complète et ça apparaît ici.", +"Wear your strap or import your WHOOP export in Data Sources. Once this week has a ": "Porte ton bracelet ou importe ton export WHOOP dans Sources de données. Une fois que cette semaine aura ", "Wed": "Mer", "Wednesday": "Mercredi", "Week in review": "Bilan de la semaine", @@ -2591,6 +3233,7 @@ "Weight in kilograms": "Poids en kilogrammes", "Weight in pounds": "Poids en livres", "Weight, %lld pounds": "Poids, %lld livres", +"Weight, height and waist add a VO₂max estimate. They don't change the Fitness Age itself.": "Le poids, la taille et le tour de taille ajoutent une estimation de VO₂max. Ils ne changent pas l'Âge de forme lui-même.", "Wellbeing": "Bien-être", "Wellness score": "Score de bien-être", "What \"recording\" means": "Ce que signifie « enregistrement »", @@ -2600,7 +3243,11 @@ "What are you adding?": "Qu'ajoutez-vous ?", "What changed": "Ce qui a changé", "What correlates": "Ce qui corrèle", +"What drove Charge": "Ce qui a influencé la Charge", "What each activity costs your recovery": "Ce que chaque activité coûte à votre récupération", +"What each can read, and why": "Ce que chacun peut lire, et pourquoi", +"What each strap can read, and why some features differ.": "Ce que chaque bracelet peut lire, et pourquoi certaines fonctionnalités diffèrent.", +"What moves you ›": "Ce qui te fait bouger ›", "What moves you. Ranked patterns in your own data, and your dose-response.": "Ce qui vous fait bouger. Des tendances classées dans vos propres données, et votre réponse à la dose.", "What moves your %@": "Ce qui fait évoluer votre %@", "What shaped it": "Ce qui l'a façonné", @@ -2611,24 +3258,34 @@ "What you get": "Ce que vous obtenez", "What you lose": "Ce que vous perdez", "What your watch is great at, where it's lighter than a chest strap, and how sure NOOP is.": "Ce en quoi votre montre excelle, où elle est plus légère qu'une ceinture pectorale, et le niveau de confiance de NOOP.", +"What's new": "Nouveautés", "What's new in NOOP %@": "Nouveautés de NOOP %@", "What's new in the app and your data": "Nouveautés dans l'application et vos données", "When %@ is higher, %@ tends to be higher.": "Quand %1$@ est plus élevé, %2$@ a tendance à l'être aussi.", "When %@ is higher, %@ tends to be lower.": "Quand %1$@ est plus élevé, %2$@ a tendance à être plus bas.", +"When Android asks, allow Bluetooth so NOOP can scan and connect.": "Quand Android le demande, autorise le Bluetooth pour que NOOP puisse scanner et se connecter.", +"When Android asks, allow notifications so NOOP can keep you informed.": "Quand Android le demande, autorise les notifications pour que NOOP puisse te tenir au courant.", "When I double-tap": "Quand je double-tape", +"When a dip is detected, surface the check-in card on its own (rate-limited, quiet-hours aware). Off keeps it manual.": "Quand une baisse est détectée, afficher la carte de check-in automatiquement (fréquence limitée, tient compte des heures calmes). Désactivé : ça reste manuel.", "When a fresh, non-exercise HRV dip is detected while you're still, NOOP offers a one-minute guided breath: a single confirming buzz and a dismissible card. Never an alarm, never a diagnosis.": "Quand une chute récente de la VFC hors exercice est détectée pendant que vous êtes immobile, NOOP propose une respiration guidée d'une minute : une seule vibration de confirmation et une carte que vous pouvez ignorer. Jamais une alarme, jamais un diagnostic.", +"When a new workout syncs in, a notification with its Effort, duration and average ": "Quand un nouvel entraînement se synchronise, une notification avec son Effort, sa durée et sa moyenne ", +"When on, NOOP taps your wrist for the apps you pick below, so you can leave ": "Quand c'est activé, NOOP te tape le poignet pour les apps que tu choisis ci-dessous, pour que tu puisses laisser ", "When on, NOOP taps your wrist for the apps you pick below, so you can leave the %@ and still feel what matters.": "Une fois activé, NOOP fait vibrer votre poignet pour les applications que vous choisissez ci-dessous, afin que vous puissiez laisser %@ de côté tout en ressentant ce qui compte.", "When the %@ apps you choose send a notification, NOOP taps your strap: Slack, Calendar, Messages, whatever matters. Everything stays on %@.": "Quand les applications de %1$@ que vous choisissez envoient une notification, NOOP fait vibrer votre bracelet : Slack, Calendrier, Messages, tout ce qui compte. Tout reste sur %2$@.", +"When the opt-in Coach is set up with your own key, also include a short summary of your strongest on-device patterns and Lab Book markers in its context. Summary only; no raw data leaves your phone. Requires the Coach's own data consent first.": "Quand le Coach optionnel est configuré avec ta propre clé, inclure aussi dans son contexte un bref résumé de tes tendances les plus marquées sur l'appareil et des marqueurs du Carnet de labo. Résumé uniquement ; aucune donnée brute ne quitte ton téléphone. Nécessite d'abord le consentement aux données propre au Coach.", "When the ring is reset and waking, tap Scan below.": "Quand l'anneau est réinitialisé et se réveille, appuyez sur « Scanner » ci-dessous.", "When the system prompt appears, choose Allow so NOOP can find your strap.": "Quand la demande système apparaît, choisissez Autoriser pour que NOOP puisse trouver votre bracelet.", "When this is on, NOOP rewrites a plain-text file (On My iPhone › NOOP › noop_sync.txt) each time you leave the app: one line per 15 minutes of heart rate, HRV and steps, read straight from your strap. Pair it with the Siri Shortcut that reads the file and logs everything into Apple Health (no HealthKit entitlement needed), so it works on sideloaded installs. The setup guide and the pre-built Shortcut live in the project wiki on GitHub.": "Quand c'est activé, NOOP réécrit un fichier texte brut (Sur mon iPhone › NOOP › noop_sync.txt) chaque fois que vous quittez l'application : une ligne par tranche de 15 minutes de fréquence cardiaque, VFC et pas, lue directement depuis votre bracelet. Associez-le au raccourci Siri qui lit le fichier et enregistre tout dans Apple Santé (aucune autorisation HealthKit requise), pour que cela fonctionne aussi sur les installations en sideload. Le guide de configuration et le raccourci prêt à l'emploi se trouvent dans le wiki du projet sur GitHub.", "When you import an Apple Health or WHOOP export, iOS hands NOOP a private copy of the file. NOOP reads it, saves your data into the health database, then deletes the copy. Older builds didn't delete every copy. This screen reclaims any that were left behind.": "Quand vous importez un export Apple Santé ou WHOOP, iOS remet à NOOP une copie privée du fichier. NOOP la lit, enregistre vos données dans la base de santé, puis supprime la copie. Les anciennes versions ne supprimaient pas toutes les copies. Cet écran récupère celles qui sont restées en place.", +"When your strap is connected NOOP is saving data live. \"Last synced\" tells ": "Quand ton bracelet est connecté, NOOP enregistre les données en direct. \"Dernière synchro\" indique ", "When your strap is connected NOOP is saving data live. \"Last synced\" tells you how fresh it is. If it says \"Not recording\", reconnect.": "Quand votre bracelet est connecté, NOOP enregistre les données en direct. « Dernière synchro » indique leur fraîcheur. S'il indique « Aucun enregistrement », reconnectez-vous.", "Where NOOP's on-device space is going, and a one-tap clean-up.": "Où va l'espace de NOOP sur l'appareil, et un nettoyage en un geste.", "Where a sensor isn't on your watch, NOOP reads “not available” for that metric, never a zero, never an invented number. Everything else keeps working.": "Là où un capteur est absent de votre montre, NOOP affiche « non disponible » pour cette mesure, jamais un zéro, jamais un chiffre inventé. Tout le reste continue de fonctionner.", "Where your numbers come from": "D'où viennent vos chiffres", "Which strap are you pairing?": "Quel bracelet associez-vous ?", "Which strap?": "Quel bracelet ?", +"While you're inside the window, NOOP watches your live heart rate from the strap. Deep ": "Pendant que tu es dans la plage, NOOP surveille ta fréquence cardiaque en direct depuis le bracelet. Le sommeil profond ", +"Whoop": "Whoop", "Whoop import": "Importation Whoop", "Why am I run down?": "Pourquoi suis-je épuisé ?", "Why does this grow?": "Pourquoi cela augmente-t-il ?", @@ -2639,41 +3296,56 @@ "Wind down": "Se détendre", "Wind-down nudge": "Rappel de détente", "Window": "Fenêtre", +"Window length": "Durée de la plage", "Windowed rMSSD": "rMSSD par fenêtre", "Wire NOOP's actions into a Back-Tap, a focus automation, or a longer Shortcut. For example, double-tap the back of your iPhone to buzz the strap.": "Reliez les actions de NOOP à un Toucher au dos, une automatisation de Focus ou un raccourci plus élaboré. Par exemple, appuyez deux fois à l'arrière de votre iPhone pour faire vibrer le bracelet.", "With": "Avec", "Without": "Sans", "Woke": "Réveillé", +"Won't connect? Run through these": "Ça ne se connecte pas ? Passe en revue ces points", "Work": "Travail", "Working…": "Traitement en cours…", "Workout": "Entraînement", +"Workout actions": "Actions d'entraînement", "Workout file (GPX / TCX / FIT)": "Fichier d'entraînement (GPX / TCX / FIT)", "Workout saved": "Entraînement enregistré", "Workout saved · %@": "Entraînement enregistré · %@", "Workouts": "Entraînements", "Worn": "Porté", "Wrist alerts": "Alertes au poignet", +"Wrist alerts (strain nudges and your smart alarm) arrive as notifications too.": "Les alertes au poignet (rappels d'effort et ton alarme intelligente) arrivent aussi sous forme de notifications.", "Wrist delivery isn't live yet. It needs a small on-device watcher (coming in an update) to read macOS notifications. Everything stays on this Mac. Your choices are saved now and will apply automatically once delivery ships.": "La diffusion au poignet n'est pas encore active. Elle nécessite un petit observateur sur l'appareil (à venir dans une mise à jour) pour lire les notifications macOS. Tout reste sur ce Mac. Vos choix sont enregistrés dès maintenant et s'appliqueront automatiquement une fois la fonction disponible.", +"Wrist delivery needs Notification Access so NOOP can read which apps notify ": "L'envoi au poignet nécessite l'accès aux notifications pour que NOOP puisse voir quelles apps notifient ", "Wrist temp": "Temp. poignet", "Wrist temperature needs Series 8 or later, and the newest US units dropped the blood-oxygen sensor. Where a sensor isn't there, NOOP reads “not available” instead of zero.": "La température au poignet nécessite une Series 8 ou ultérieure, et les derniers modèles américains ont perdu le capteur d'oxygène sanguin. Là où un capteur est absent, NOOP affiche « non disponible » plutôt que zéro.", +"Write the metrics NOOP computes from your strap (resting HR, HRV, SpO₂, ": "Écrire les métriques que NOOP calcule à partir de ton bracelet (FC au repos, VFC, SpO₂, ", +"Writes a fresh backup to your folder about once a day (keeps the latest ": "Écrit une nouvelle sauvegarde dans ton dossier environ une fois par jour (conserve la plus récente ", "Writes a timestamped copy of your strap log to NOOP's folder in the Files app, once a day. Handy for a bug report without remembering to grab it. On iPhone it fires when iOS next wakes NOOP near your chosen time, not guaranteed to the minute (keep NOOP open overnight for the best chance). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans le dossier de NOOP de l'app Fichiers, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur iPhone, cela se déclenche au prochain réveil de NOOP par iOS proche de l'heure choisie, sans garantie à la minute près (laissez NOOP ouvert la nuit pour de meilleures chances). Tout reste sur %@ ; rien n'est envoyé en ligne.", "Writes a timestamped copy of your strap log to your Documents folder, once a day. Handy for a bug report without remembering to grab it. On Mac it runs while NOOP is open (and catches up on launch if the time passed while it was closed). Everything stays on %@; nothing is uploaded.": "Écrit une copie horodatée du journal de votre bracelet dans votre dossier Documents, une fois par jour. Pratique pour un rapport de bug sans avoir à y penser. Sur Mac, cela s'exécute pendant que NOOP est ouvert (et rattrape au lancement si l'heure est passée pendant sa fermeture). Tout reste sur %@ ; rien n'est envoyé en ligne.", +"Writes a timestamped strap log (and the raw .bin if a 5/MG capture exists) to the app's export folder once a day at the time below.": "Écrit un journal horodaté du bracelet (et le .bin brut si une capture 5/MG existe) dans le dossier d'export de l'app une fois par jour à l'heure ci-dessous.", +"Xiaomi Mi Band": "Xiaomi Mi Band", "YOUR CARDS": "TES CARTES", "YOUR DAILY SCORES": "VOS SCORES QUOTIDIENS", "YOUR READING": "VOTRE MESURE", "Yes": "Oui", "Yesterday": "Hier", "Yesterday’s Synthesis": "Synthèse d'hier", +"Yoga": "Yoga", +"You": "Toi", +"You are replacing Oura.": "Tu remplaces Oura.", "You can change what you share any time in Settings › Health › Data Access & Devices.": "Vous pouvez modifier ce que vous partagez à tout moment dans Paramètres › Santé › Accès aux données et appareils.", "You can still bring your data in by importing a Health export from Data Sources. A build from the App Store, or one signed with a paid Apple Developer account, connects directly.": "Vous pouvez tout de même importer vos données via un export Santé depuis les Sources de données. Une version issue de l'App Store, ou signée avec un compte Apple Developer payant, se connecte directement.", +"You removed your active strap. Choose which paired band provides your live data, or ": "Tu as retiré ton bracelet actif. Choisis quel bracelet appairé fournit tes données en direct, ou ", "You removed your active strap. Choose which paired band provides your live data, or leave none active and pair one later.": "Vous avez retiré votre bracelet actif. Choisissez quel bracelet associé fournira vos données en direct, ou n'en laissez aucun actif et associez-en un plus tard.", "You said: %@": "Vous avez dit : %@", "You'll be reminded around %@.": "Vous recevrez un rappel vers %@.", "You'll likely wake around %lld ± %lld Charge if you sleep about %@ tonight.": "Vous vous réveillerez probablement avec une Charge d'environ %1$lld ± %2$lld si vous dormez environ %3$@ cette nuit.", "You're all caught up.": "Vous êtes à jour.", "You're carrying about %@ of surplus over %@. You've slept past your need on balance. Nicely ahead.": "Vous portez un surplus d'environ %@ sur %@. Vous avez dormi au-delà de vos besoins, en solde. Bien en avance.", +"You're connected.": "Tu es connecté.", "You're connected. NOOP is reading your Apple Watch data now. Your Charge score will spend its first week or so calibrating, then settle in.": "Vous êtes connecté. NOOP lit désormais les données de votre Apple Watch. Votre score de Charge passera sa première semaine environ à se calibrer, puis se stabilisera.", "You're in balance with your baseline, so moderate strain is well-judged": "Vous êtes en équilibre avec votre référence, un effort modéré est donc bien choisi", +"You're not missing the broadcast feature. To share your heart rate with a gym machine, ": "Tu ne rates rien avec la fonction de diffusion. Pour partager ta fréquence cardiaque avec une machine de salle, ", "You're on the latest (%@).": "Vous avez la dernière version (%@).", "You're roughly on top of your sleep across %@. Slept minutes balance out against your need.": "Vous êtes globalement à jour sur votre sommeil sur %@. Les minutes dormies compensent vos besoins.", "You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day.": "Vous êtes proche de votre référence autonome habituelle : stress modéré, une journée normale et équilibrée.", @@ -2686,30 +3358,40 @@ "Your GPS route for this session, recorded and stored on your device. Nothing leaves your phone.": "Votre trajet GPS pour cette séance, enregistré et stocké sur votre appareil. Rien ne quitte votre téléphone.", "Your HRV dipped while you were still. Want a minute to breathe?": "Votre VFC a chuté pendant que vous étiez immobile. Envie d'une minute pour respirer ?", "Your Monday-to-Sunday, read in one glance.": "Votre semaine du lundi au dimanche, lue en un coup d'œil.", +"Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings → ": "Ton WHOOP 5/MG n'activera pas ça tant que le mode Expérimental n'est pas activé (Réglages → ", "Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings, Experimental). Right now your wake time is saved but the strap is NOT armed. Even with Experimental on, a 5/MG strap-driven wake is still unconfirmed on our side, so keep a backup alarm.": "Votre WHOOP 5/MG n'activera pas cette fonction tant que le mode Expérimental n'est pas activé (Paramètres, Expérimental). Pour l'instant, votre heure de réveil est enregistrée mais le bracelet n'est PAS armé. Même avec le mode Expérimental activé, un réveil piloté par le bracelet 5/MG reste non confirmé de notre côté, gardez donc un réveil de secours.", "Your WHOOP is at 100%.": "Votre WHOOP est à 100 %.", "Your WHOOP only talks to one phone at a time.": "Votre WHOOP ne communique qu'avec un seul téléphone à la fois.", "Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day.": "Vos marqueurs autonomes penchent vers le stress aujourd'hui. Considérez-la comme une journée axée sur la récupération.", "Your bands report different numbers. Here's every source, and the one NOOP is using.": "Vos bracelets rapportent des chiffres différents. Voici chaque source, et celle que NOOP utilise.", +"Your bloods, BP and body numbers. Kept private here.": "Tes analyses sanguines, ta tension et tes mensurations. Gardées privées ici.", "Your bloods, BP and body numbers. Kept private, on %@.": "Vos analyses sanguines, votre tension et vos données corporelles. Gardées privées, sur %@.", +"Your bloods, BP and body numbers. Kept private, on this phone.": "Tes analyses sanguines, ta tension et tes mensurations. Gardées privées, sur ce téléphone.", +"Your cardiovascular load. NOOP turns every second of heart rate into a ": "Ta charge cardiovasculaire. NOOP transforme chaque seconde de fréquence cardiaque en ", "Your cardiovascular load. NOOP turns every second of heart rate into a training-impulse using heart-rate-reserve zones (Karvonen), weights time in harder zones more heavily (Edwards / Banister), and places it on a logarithmic 0–100 scale, so easy days sit low and an all-out day approaches 100, which stays genuinely rare. A long walk with little cardio still counts, through a steps / active-energy floor.": "Votre charge cardiovasculaire. NOOP transforme chaque seconde de fréquence cardiaque en impulsion d'entraînement à l'aide des zones de réserve de fréquence cardiaque (Karvonen), pondère plus fortement le temps passé dans les zones difficiles (Edwards / Banister), et place le résultat sur une échelle logarithmique de 0 à 100, si bien que les jours faciles restent bas et qu'une journée à fond approche 100, ce qui reste vraiment rare. Une longue marche avec peu de cardio compte tout de même, grâce à un seuil minimal basé sur les pas et l'énergie active.", "Your cards": "Vos cartes", "Your current strap": "Votre bracelet actuel", "Your curve fills in as the strap offloads its history.": "Votre courbe se remplit à mesure que le bracelet transfère son historique.", +"Your daily scores": "Tes scores quotidiens", "Your data has been restored. Quit and reopen NOOP for it to take effect.": "Vos données ont été restaurées. Quittez et rouvrez NOOP pour que cela prenne effet.", "Your fluid intake today, on %@ only.": "Votre apport en liquides aujourd'hui, sur %@ uniquement.", +"Your fluid intake today, on this phone only.": "Ton apport en liquides aujourd'hui, sur ce téléphone uniquement.", "Your last %lld hours have stayed in the high band. A few minutes of paced breathing can help downshift your nervous system.": "Vos dernières %lld heures sont restées dans la bande haute. Quelques minutes de respiration rythmée peuvent aider à apaiser votre système nerveux.", "Your last %lld readings: %@ %@, holding steady.": "Vos %1$lld dernières mesures : %2$@ %3$@, stables.", "Your last %lld readings: %@ %@, trending down.": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la baisse.", "Your last %lld readings: %@ %@, trending up.": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la hausse.", "Your live heart rate is working from the strap, and charge, effort and rest build from it over your next few nights of wear, sharpening as it learns your baseline. Want your full history instantly? Import your WHOOP export in Data Sources and it backfills in about a minute.": "Votre fréquence cardiaque en direct fonctionne déjà depuis le bracelet, et la charge, l'effort et le repos s'en construisent au fil de vos prochaines nuits de port, s'affinant à mesure qu'ils apprennent votre référence. Vous voulez tout votre historique instantanément ? Importez votre export WHOOP dans les Sources de données, il se complète en une minute environ.", +"Your live heart rate is working from the strap, and recovery, strain ": "Ta fréquence cardiaque en direct fonctionne depuis le bracelet, et la récupération, l'effort ", "Your live heart rate is working from the strap, and recovery, strain and sleep build from it over your next few nights of wear, sharpening as it learns your baseline. Want your full history instantly? Import your WHOOP export in Data Sources and it backfills in about a minute.": "Votre fréquence cardiaque en direct fonctionne depuis le bracelet, et la récupération, l'effort et le sommeil se construisent à partir de là sur vos prochaines nuits de port, s'affinant au fur et à mesure qu'il apprend votre référence. Vous voulez votre historique complet instantanément ? Importez votre export WHOOP dans Sources de données et il se remplit en environ une minute.", "Your locked pace": "Votre rythme verrouillé", "Your locked pace · %@ br/min": "Votre rythme verrouillé · %@ resp/min", "Your nervous system is well-recovered, so you're primed to push": "Votre système nerveux est bien récupéré, vous êtes donc prêt à pousser", +"Your night data is in. Logging how you felt helps NOOP learn what drives your best recovery.": "Tes données de nuit sont arrivées. Noter comment tu t'es senti aide NOOP à comprendre ce qui favorise ta meilleure récupération.", "Your numbers, your strap, and how NOOP works. All on %@.": "Vos chiffres, votre bracelet et le fonctionnement de NOOP. Tout sur %@.", "Your numbers, your strap, and how NOOP works. All on this Mac.": "Vos chiffres, votre bracelet et comment NOOP fonctionne. Tout sur ce Mac.", +"Your numbers, your strap, and how NOOP works. All on this phone.": "Tes chiffres, ton bracelet et le fonctionnement de NOOP. Tout sur ce téléphone.", "Your profile photo": "Votre photo de profil", +"Your reading": "Ton relevé", "Your records never leave %@. There's no account, no cloud, no NOOP server. Because NOOP is an independent app you run yourself (not a healthcare provider), it isn't \"HIPAA-covered,\" and that protection doesn't apply here; the safety comes from the data being local-only and yours.": "Vos données ne quittent jamais %@. Il n'y a ni compte, ni cloud, ni serveur NOOP. Comme NOOP est une application indépendante que vous exécutez vous-même (pas un prestataire de santé), elle n'est pas « couverte par HIPAA », et cette protection ne s'applique pas ici ; la sécurité vient du fait que les données restent uniquement locales et vous appartiennent.", "Your resonance pace": "Votre rythme de résonance", "Your rhythm looked steady": "Votre rythme semblait stable", @@ -2717,19 +3399,27 @@ "Your ring": "Votre anneau", "Your ring talks to NOOP only, fully offline, no Oura account.": "Votre anneau ne communique qu'avec NOOP, entièrement hors ligne, sans compte Oura.", "Your scores build over a few nights": "Vos scores se construisent en quelques nuits", +"Your strap": "Ton bracelet", "Your strap in real time, heart rate and frames as they arrive.": "Votre bracelet en temps réel, fréquence cardiaque et trames à mesure qu'ils arrivent.", "Your strap is bonded and ready to stream.": "Votre bracelet est appairé et prêt à transmettre.", "Your strap is bonded · %lld%% battery.": "Votre bracelet est appairé · %lld %% de batterie.", "Your strap is connected and saving data.": "Votre bracelet est connecté et enregistre les données.", +"Your strap is connected and we're trying an experimental handshake to bring up live ": "Ton bracelet est connecté et on tente une poignée de main expérimentale pour activer les données en direct ", "Your strap is paired and sending data. Open Live for a real-time heart rate.": "Votre bracelet est associé et envoie des données. Ouvrez En direct pour une fréquence cardiaque en temps réel.", +"Your strap only pairs with ONE app at a time. If the WHOOP app is connected, ": "Ton bracelet ne s'appaire qu'avec UNE seule app à la fois. Si l'app WHOOP est connectée, ", "Your strap wake-alarm and the evening wind-down reminder, in one place.": "Votre alarme de réveil par bracelet et le rappel de détente du soir, au même endroit.", "Your thread starts here.": "Votre fil commence ici.", +"Your wake window, the strap wake-alarm, and the evening wind-down reminder, in one place.": "Ta plage de réveil, l'alarme de réveil du bracelet et le rappel de relaxation du soir, au même endroit.", "Your watch, NOOP's brain": "Votre montre, le cerveau de NOOP", +"Your whole history is one file on this phone. Export it to keep a copy ": "Tout ton historique tient dans un seul fichier sur ce téléphone. Exporte-le pour en garder une copie ", +"Zone": "Zone", "Zone %lld · %@": "Zone %1$lld · %2$@", "Zone %lld · %lld%%": "Zone %1$lld · %2$lld %%", "Zone %lld: %lld–%lld bpm (%lld–%lld%% max HR)": "Zone %1$lld : %2$lld–%3$lld bpm (%4$lld–%5$lld %% FC max)", +"Zoomed in - drag to pan": "Zoom avant - fais glisser pour te déplacer", "Zoomed in · drag to pan": "Zoom activé · glissez pour déplacer", "Zoomed in. Drag to pan": "Zoom activé. Glissez pour déplacer", +"Zwift, Peloton or a Garmin, open Data Sources and turn on \"Broadcast heart rate\": ": "Zwift, Peloton ou un Garmin, ouvre Sources de données et active \"Diffuser la fréquence cardiaque\" : ", "a moderate": "un modéré", "a strong": "un fort", "a strong night, supporting recovery": "une nuit forte, favorise la récupération", @@ -2740,28 +3430,76 @@ "about your age": "environ votre âge", "above baseline, limiting recovery": "au-dessus de la référence, limite la récupération", "above baseline, supporting recovery": "au-dessus de la référence, favorise la récupération", +"above, read hour by hour). Hours without enough data are skipped.": "ci-dessus, lu heure par heure). Les heures sans assez de données sont ignorées.", "active": "actif", +"advice. Everything stays on this phone.": "conseil. Tout reste sur ce téléphone.", +"against your own calm hours today (the same 0-3 proxy as the score ": "par rapport à tes propres heures calmes d'aujourd'hui (le même indicateur 0-3 que le score ", +"aggregates from your most recent imported day; resting HR and HRV update daily. ": "s'agrègent à partir de ton jour importé le plus récent ; la FC au repos et la VFC se mettent à jour chaque jour. ", "all history": "tout l'historique", "all time": "depuis toujours", "all your data, none of the cloud": "toutes vos données, sans le cloud", +"an iPhone: Health app → tap your photo → Export All Health Data, then ": "un iPhone : app Santé → appuie sur ta photo → Exporter toutes les données de santé, puis ", +"and HRV needs you to be still. No Oura Readiness or SpO₂ ": "et la VFC exige que tu restes immobile. Pas de Oura Readiness ni de SpO₂ ", +"and HRV, but not WHOOP's deeper sleep and recovery data.": "et la VFC, mais pas les données de sommeil et de récupération plus poussées de WHOOP.", +"and NOOP scores the data, so they land soon after, not the exact second you wake or ": "et NOOP note les données, donc elles arrivent peu après, pas à la seconde exacte où tu te réveilles ou ", +"and calories come in where the file has them. Fully offline; nothing leaves your phone.": "et les calories sont reprises là où le fichier les contient. Entièrement hors ligne ; rien ne quitte ton téléphone.", +"and more under Data Sources. You can do either any time.": "et plus encore dans Sources de données. Tu peux faire l'un ou l'autre à tout moment.", +"and only after you set a key.": "et seulement après avoir défini une clé.", +"and sleep build from it over your next few nights of wear, sharpening as it ": "et le sommeil se construisent à partir de là au fil de tes prochaines nuits de port, en s'affinant à mesure que ça ", +"anything to count.": "quoi que ce soit à comptabiliser.", +"app is still running. Reset the ring again, force-quit Oura, then try once more. If it keeps ": "l'app tourne encore. Réinitialise à nouveau la bague, force la fermeture d'Oura, puis réessaie. Si ça persiste ", +"app's on-device database. Fully offline; no Xiaomi account or Bluetooth. Export the Mi ": "base de données de l'app sur l'appareil. Entièrement hors ligne ; pas de compte Xiaomi ni de Bluetooth. Exporte le Mi ", +"are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 ": "sont un décompte de mouvement brut, et la VFC exige que tu restes immobile. Pas de Oura Readiness ni de SpO2 ", +"are informational and do not change the stress score above.": "sont informatifs et ne modifient pas le score de stress ci-dessus.", +"are shown as “typical” and are always overridden by your own data once ": "sont affichées comme “typiques” et sont toujours remplacées par tes propres données une fois ", "as of %@": "au %@", +"as of today": "aujourd'hui", +"as of yesterday": "hier", "asleep %@": "endormi %@", "asleep last night": "endormi la nuit dernière", "at baseline": "au niveau de référence", +"automatically once access is granted.": "automatiquement une fois l'accès accordé.", "average %@ bpm": "moyenne %@ bpm", "avg %@": "moy. %@", "avg %@ · %lldh": "moy. %1$@ · %2$lldh", "avg %lld": "moy. %lld", "avg · %lldd": "moy. · %lldj", +"back and use the standard setup or file import.": "en arrière et utilise la configuration standard ou l'import de fichier.", +"backfills your whole history in about a minute. Working now on Android.": "récupère tout ton historique en une minute environ. Fonctionne désormais sur Android.", "balanced": "équilibré", +"banked motion history, so your strap needs to sync that history before NOOP has ": "l'historique de mouvement accumulé, donc ton bracelet doit synchroniser cet historique avant que NOOP ait ", +"based on typical patterns": "d'après des tendances types", +"baseline (approximate, not medical advice); until then typical adult ranges apply.": "référence (approximatif, pas un avis médical) ; d'ici là, les plages adultes types s'appliquent.", +"baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first ": "référence (tu verras \"Calibrage\" jusque-là), et continue de s'affiner pendant tes premiers ", +"baseline. A higher-than-usual resting HR and a lower-than-usual HRV ": "référence. Une FC au repos plus élevée que d'habitude et une VFC plus basse que d'habitude ", +"battery, read straight off the ring.": "batterie, lu directement depuis la bague.", "below a good night, limiting recovery": "en dessous d'une bonne nuit, limite la récupération", "below baseline, limiting recovery": "en dessous de la référence, limite la récupération", "below baseline, supporting recovery": "en dessous de la référence, favorise la récupération", +"below will start to fill in.": "ci-dessous vont commencer à se remplir.", +"bike or app to see your strap's heart rate there.": "vélo ou appli pour y voir la fréquence cardiaque de ton bracelet.", +"body metrics yet.": "de métriques corporelles pour l'instant.", +"both push the score up, classic signs the body is activated. The ": "font toutes deux monter le score, signes classiques d'un corps activé. Le ", +"bpm": "bpm", "br/min": "resp/min", "building": "en construction", +"calls": "appels", +"changes your Effort.": "change jamais ton Effort.", +"choose the file here. Fully offline; nothing leaves your phone. Each brand's own ": "choisis le fichier ici. Entièrement hors ligne ; rien ne quitte ton téléphone. Le format propre à chaque marque ", "clean intervals": "intervalles propres", +"combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at ": "le décalage combiné est reporté sur une échelle 0-3 : 0 correspond au calme, 1,5 se situe à ", +"compares the days you log the behaviour against your behaviour-free ": "compare les jours où tu notes le comportement à tes jours sans ce comportement ", "cooler than baseline, limiting recovery": "plus fraîche que la référence, limite la récupération", +"count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.": "compte. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas.", +"counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. ": "comptées comme des nuits de 4 sur la bague), et continue de s'affiner pendant tes deux premières semaines. ", "covered": "couvert", +"day or two of data, your week-in-review appears here.": "jour ou deux de données, ton bilan de la semaine apparaît ici.", +"days before the start.": "jours avant le début.", +"decoded for 5/MG yet. Nothing's wrong with your strap - WHOOP 4.0 is fully supported.": "décodé pour le 5/MG pour l'instant. Rien ne cloche avec ton bracelet - le WHOOP 4.0 est entièrement pris en charge.", +"default staging. Opt-in and experimental: it only changes how already-detected ": "le découpage par défaut. Optionnel et expérimental : ça ne change que la façon dont les nuits déjà détectées ", +"device, from your strap.": "appareil, depuis ton bracelet.", +"do not open the Oura app.": "n'ouvre pas l'appli Oura.", +"doesn't let NOOP see every installed app, so this is how you cover the rest. ": "ne laisse pas NOOP voir toutes les applis installées, c'est donc comme ça que tu couvres le reste. ", "down %lld points": "en baisse de %lld points", "down 1 point": "en baisse de 1 point", "drink": "boisson", @@ -2772,18 +3510,59 @@ "e.g. Magnesium": "ex. Magnésium", "e.g. Running": "ex. Course à pied", "e.g. fasting, morning draw": "ex. à jeun, prélèvement matinal", +"e.g. gpt-4o": "ex. gpt-4o", "e.g. mmol/L": "ex. mmol/L", +"effect size and confidence, never a cause or a diagnosis. Population patterns ": "la taille d'effet et la confiance, jamais une cause ni un diagnostic. Les tendances de population ", +"else that day is a nap. You can always edit bed and wake times.": "reste ce jour-là est une sieste. Tu peux toujours modifier les heures de coucher et de lever.", +"enough to show, but it's thin. Solid means full inputs are present. When NOOP ": "assez pour s'afficher, mais c'est mince. Fiable signifie que toutes les données d'entrée sont présentes. Quand NOOP ", +"entirely on your device. It is awareness only: not contraception, not a fertility ": "entièrement sur ton appareil. C'est purement informatif : pas une contraception, pas un ", +"estimate (weight × reps). It's a volume figure, not a measured strain, so it never ": "estimation (poids × répétitions). C'est un chiffre de volume, pas un effort mesuré, donc il ne ", +"estimate is.": "proche.", +"every device. They never make up data, and they'll tell you honestly when live isn't possible.": "tous les appareils. Ils n'inventent jamais de données, et te diront honnêtement quand le direct n'est pas possible.", +"every metric you can explore here in about a minute.": "toutes les métriques que tu peux explorer ici en une minute environ.", +"every night, your sleep stages and trends straight away.": "chaque nuit, tes phases de sommeil et tes tendances immédiatement.", "every reading you've entered": "chaque mesure que vous avez saisie", +"except through the file you choose.": "sauf via le fichier que tu choisis.", +"export in Data Sources and it backfills in about a minute.": "dans Sources de données et il se remplit en une minute environ.", +"extract keys for you and cannot help you find one. If you do not know what this means, go ": "extrait pas de clés pour toi et ne peut pas t'aider à en trouver une. Si tu ne sais pas ce que ça signifie, reviens ", +"factor), how efficiently (asleep versus in bed), how much was restorative ": "facteur), avec quelle efficacité (endormi versus au lit), quelle part était réparatrice ", +"failing, your ring may be a generation NOOP cannot adopt yet. You can still use file import.": "échoue, ta bague est peut-être d'une génération que NOOP ne peut pas encore adopter. Tu peux quand même utiliser l'import de fichier.", +"few days alongside the strap, they'll appear here so you can see how close the ": "quelques jours en parallèle du bracelet, ils apparaîtront ici pour que tu voies à quel point l'estimation est ", +"finish a workout. Everything is worked out on this phone.": "termines un entraînement. Tout est calculé sur ce téléphone.", +"first run). Once a day or two of motion lands, your step estimate and the calibration ": "premier lancement). Une fois qu'un jour ou deux de mouvement sont enregistrés, ton estimation de pas et la calibration ", "fit from your phone": "ajustée depuis votre téléphone", "flat": "stable", +"fold onto your existing markers; anything else comes in as a custom marker. ": "se rattachent à tes marqueurs existants ; tout le reste arrive comme marqueur personnalisé. ", +"folder a sync app (e.g. FolderSync / Autosync) keeps in your cloud.": "dossier qu'une appli de synchro (ex. FolderSync / Autosync) conserve dans ton cloud.", +"for the next automatic sync.": "pour la prochaine synchro automatique.", "from %@": "depuis %@", "from R-R": "à partir des R-R", +"from Whoop or Apple Health.": "depuis Whoop ou Apple Santé.", "from profile": "depuis le profil", "from your report": "depuis votre rapport", +"go back to Oura I would factory-reset the ring again and set it up in the Oura app.": "revenir à Oura, je réinitialiserais la bague aux réglages d'usine et je la reconfigurerais dans l'appli Oura.", +"harder zones more heavily (Edwards / Banister), and places it on a ": "zones plus intenses plus fortement (Edwards / Banister), et la place sur une ", +"has no step history to learn from, or the estimate runs consistently high or low. ": "n'a pas d'historique de pas pour apprendre, ou l'estimation est constamment trop haute ou trop basse. ", "hatch = typical": "hachures = typique", +"have it) in Data Sources to fill this in.": "l'as) dans Sources de données pour remplir ça.", +"heart rate from the standard profile. This isn't verified on 5/MG hardware yet, so ": "la fréquence cardiaque depuis le profil standard. Ce n'est pas encore vérifié sur le matériel 5/MG, donc ", +"heart rate, your scores, buzzing the strap), but a few firmware features differ. Here's ": "la fréquence cardiaque, tes scores, faire vibrer le bracelet), mais quelques fonctions du firmware diffèrent. Voici ", +"heart rate. Shows up after the session reaches NOOP on the next sync.": "la fréquence cardiaque. Apparaît une fois que la séance arrive dans NOOP à la synchro suivante.", +"heat-grid is part of the desktop app.": "la grille thermique fait partie de l'appli de bureau.", "high": "élevé", +"higher": "plus élevé", +"historical-data decode + offload format": "format de décodage + déchargement des données historiques", "hover for real values": "survolez pour les valeurs réelles", +"how much each extra unit tends to move your numbers. Until then it shows ": "de combien chaque unité supplémentaire tend à faire bouger tes chiffres. D'ici là, il affiche ", +"import the .zip here. Working now on Android.": "importe le .zip ici. Fonctionne désormais sur Android.", +"in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper ": "ajout en cours de développement : ils diffusent la fréquence cardiaque et la VFC en direct, mais pas les mesures plus poussées de WHOOP : ", +"includes its journal) backfills history instantly.": "inclut son journal) récupère l'historique instantanément.", +"isn't streaming (Bluetooth off, not worn, app killed), no early wake happens and the ": "ne diffuse pas (Bluetooth coupé, non porté, appli fermée), aucun réveil anticipé ne se produit et la ", +"it tells you why and what to do next. Everything here runs on your ": "il t'explique pourquoi et quoi faire ensuite. Tout ici fonctionne sur ton ", +"it. Works on any WHOOP (4.0 or 5.0/MG) because your phone does the broadcasting. ": "ça. Fonctionne avec n'importe quel WHOOP (4.0 ou 5.0/MG) parce que c'est ton téléphone qui émet. ", "just now": "à l'instant", +"keep everything on your device. Nothing is sent until you ask.": "garde tout sur ton appareil. Rien n'est envoyé tant que tu ne le demandes pas.", +"kept and you can re-add it any time.": "conservé et tu peux le rajouter à tout moment.", "large": "grand", "last 30 days": "30 derniers jours", "last 7 days": "7 derniers jours", @@ -2793,27 +3572,44 @@ "late-caffeine": "caféine tardive", "later step": "étape ultérieure", "latest": "dernier", +"learned your usual hours) the one nearest your normal sleep time. Everything ": "appris tes horaires habituels) celui le plus proche de ton heure de sommeil normale. Tout le ", +"learning your baseline, or doesn't have enough data yet. Building means there's ": "en train d'apprendre ta référence, ou n'a pas encore assez de données. En construction signifie qu'il y a ", +"learns your baseline. Want your full history instantly? Import your WHOOP ": "apprend ta référence. Tu veux tout ton historique tout de suite ? Importe ton export WHOOP ", +"leave none active and pair one later.": "n'en laisser aucun actif et en appairer un plus tard.", "load %@": "charge %@", "log": "enregistrer", "logged today": "enregistré aujourd'hui", +"longer control this ring. This is intended and it cannot be undone from NOOP.": "plus contrôler cette bague. C'est intentionnel et c'est irréversible depuis NOOP.", "low": "faible", "low-stress days · %lldd": "jours à faible stress · %lldj", +"lower": "plus bas", +"marker": "marqueur", +"marker = your mean": "marqueur = ta moyenne", +"markers": "marqueurs", +"medical advice, and not WHOOP's official scores.": "un avis médical, ni les scores officiels de WHOOP.", "mid": "moyen", "minus": "moins", "missing": "manquant", "moderate": "modéré", "month": "mois", +"more it trusts the estimate.": "plus il fait confiance à l'estimation.", "more overnight wear to unlock your %@ baseline": "davantage de nuits portées pour débloquer votre référence de %@", "most recent intake about %@ ago · %lld intakes still in the estimate. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %1$@ · %2$lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez enregistré.", "most recent intake about %@ ago. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %@. Simple indication, basée sur ce que vous avez enregistré.", +"motion maps to steps. Or set the coefficient manually below.": "le mouvement se traduit en pas. Ou règle le coefficient manuellement ci-dessous.", +"ms": "ms", +"n.s.": "n.s.", "near baseline": "proche de la référence", "needs a recent day": "nécessite un jour récent", "needs a tracked night": "nécessite une nuit suivie", "needs baseline + logged days": "nécessite une référence + des jours enregistrés", +"needs days both with and without it before NOOP can read its effect.": "a besoin de jours avec et sans pour que NOOP puisse en lire l'effet.", "needs history": "nécessite un historique", "negative": "négatif", "negligible": "négligeable", +"never a diagnosis. They never leave this phone.": "jamais un diagnostic. Elles ne quittent jamais ce téléphone.", "new": "nouveau", +"nights are split into stages (detection and scores are unchanged), and the default ": "les nuits sont découpées en phases (la détection et les scores sont inchangés), et le découpage ", "no": "non", "no calibration yet": "pas encore de calibration", "no change": "aucun changement", @@ -2831,9 +3627,14 @@ "no readings yet": "pas encore de mesures", "no session yet": "pas encore de séance", "no window yet": "pas encore de fenêtre", +"normal, high or low.": "normal, élevé ou bas.", "not connected": "non connecté", "not logged today": "non enregistré aujourd'hui", "not within 7d": "pas dans les 7 derniers j", +"not work, it will tell you plainly.": "fonctionne pas, il te le dira clairement.", +"not yet": "pas encore", +"now continues automatically across passes instead of waiting between syncs.": "continue maintenant automatiquement d'une passe à l'autre au lieu d'attendre entre les synchros.", +"nutrition CSV: a MyFitnessPal or Cronometer export, or any spreadsheet ": "CSV de nutrition : un export MyFitnessPal ou Cronometer, ou n'importe quel tableur ", "of %@": "sur %@", "of %@ L": "sur %@ L", "of 100": "sur 100", @@ -2842,69 +3643,150 @@ "of beats": "des battements", "old": "ancien", "on demand · today's R-R": "à la demande · R-R du jour", +"on the 0-3 autonomic-load scale": "sur l'échelle de charge autonome 0-3", +"on-device baseline. Approximate, not medical advice.": "référence sur l'appareil. Approximatif, pas un avis médical.", +"once a day, after your strap has synced the night.": "une fois par jour, après que ton bracelet a synchronisé la nuit.", +"one moves your recovery, HRV and sleep. Importing a WHOOP export (which ": "l'un modifie ta récupération, ta VFC et ton sommeil. Importer un export WHOOP (qui ", "open iPhone": "ouvrir iPhone", "optional": "facultatif", +"or a gym machine under Devices, or import from WHOOP, Apple Health, Oura, Fitbit, Garmin ": "ou une machine de salle dans Appareils, ou importe depuis WHOOP, Apple Santé, Oura, Fitbit, Garmin ", +"or move to a new phone, then import it there. Nothing leaves the device ": "ou passe à un nouveau téléphone, puis importe-le là-bas. Rien ne quitte l'appareil ", +"other app whenever you want it back.": "appli quand tu veux le récupérer.", "out of 100": "sur 100", +"overnight HRV is computed.": "VFC nocturne est calculée.", "p < 0.05": "p < 0,05", "partial": "partiel", "peak %@ · %@": "pic %1$@ · %2$@", +"peer-reviewed sport science, and computed entirely on your device. They are ": "évaluées par les pairs, et calculées entièrement sur ton appareil. Elles ne sont pas ", +"percentage comes off the ring (import an Oura file for those).": "pourcentage vient de la bague (importe un fichier Oura pour ceux-là).", +"personal alcohol/caffeine dose-response.": "réponse personnelle à la dose d'alcool/caféine.", +"personal baseline, plus resting heart rate, last night's Rest, breathing ": "référence personnelle, plus la FC au repos, le Repos de la nuit dernière, la fréquence ", "plus": "plus", "positive": "positif", +"predictor, not a medical service.": "prédicteur de fertilité, pas un service médical.", "projected · %@": "prévu · %@", "quarter": "trimestre", "range %@ to %@": "plage %@ à %@", +"rate, HRV, steps and more, where the export has them. Download it from the brand's app ": "cardiaque, la VFC, les pas et plus encore, quand l'export les contient. Télécharge-le depuis l'appli de la marque ", +"rate, and a skin-temperature signal (an early illness or overreach flag). ": "respiratoire, et un signal de température cutanée (indice précoce de maladie ou de surmenage). ", +"read, or judge them. Not medical advice.": "ni les lire, ni les juger. Pas un avis médical.", +"readiness or sleep score is kept for reference only. Your scores stay yours.": "score de préparation ou de sommeil est conservé à titre de référence uniquement. Tes scores restent les tiens.", +"reading": "relevé", +"readings": "relevés", "reading…": "lecture…", "ready": "prêt", +"reboots to apply, then reconnects with the new name.": "redémarre pour appliquer, puis se reconnecte avec le nouveau nom.", "recovery": "récupération", +"recovery baseline - not a measurement. Your real Charge is scored from ": "référence de récupération - pas une mesure. Ta vraie Charge est calculée à partir de ", +"recovery, skin temp, SpO₂ or steps.": "récupération, température cutanée, SpO₂ ou pas.", +"required too: an emulator has no Bluetooth.": "requis aussi : un émulateur n'a pas de Bluetooth.", +"reset\"). This wipes the ring's owner so NOOP can take it over.": "réinitialisation\"). Ça efface le propriétaire de la bague pour que NOOP puisse la reprendre.", +"respiratory rate, heart rate, steps, active energy and sleep) into ": "fréquence respiratoire, fréquence cardiaque, pas, énergie active et sommeil) dans ", +"resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness ": "FC au repos, phases de sommeil, SpO₂, stress et score de sommeil) directement depuis l'appli Mi Fitness ", +"ring with that key WITHOUT resetting it, so the Oura app keeps working too. NOOP does not ": "bague avec cette clé SANS la réinitialiser, donc l'appli Oura continue de fonctionner aussi. NOOP ne ", +"ring, and it can change between updates. NOOP never makes up a number. If something does ": "bague, et ça peut changer d'une mise à jour à l'autre. NOOP n'invente jamais de chiffre. Si quelque chose ne ", "running balance": "solde courant", +"scale. They're built from your strap's raw signals using published, ": "échelle. Elles sont construites à partir des signaux bruts de ton bracelet, avec des sciences du sport publiées, ", +"scored once a day's data is in. Charge needs about four nights of sleep to learn your ": "calculée une fois que les données d'une journée sont là. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre ta ", +"sec": "s", +"selected folder": "dossier sélectionné", +"send your question plus a short summary of your metrics to the provider you pick.": "envoie ta question ainsi qu'un bref résumé de tes métriques au fournisseur que tu choisis.", +"sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read ": "capteur, donc un tapis de course de salle, un vélo, Zwift, Peloton ou n'importe quelle appli de fitness à proximité peut lire ", +"shared. Imported data is never echoed back.": "partagé. Les données importées ne sont jamais renvoyées.", +"shows here.": "s'affiche ici.", "side by side · %@ before each reading": "côte à côte · %@ avant chaque mesure", +"signal against a typical adult range, not medical advice.": "signal par rapport à une plage adulte typique, pas un avis médical.", +"sitting side by side. It's not a medical finding, and it shows association, not cause.": "côte à côte. Ce n'est pas un constat médical, et ça montre une association, pas une cause.", +"sleep and recovery data.": "données de sommeil et de récupération.", "sleep debt": "dette de sommeil", "sleep duration": "durée du sommeil", +"sleep sits near your nightly low and stays steady; when your heart rate lifts above ": "le sommeil reste proche de ton minimum nocturne et stable ; quand ta fréquence cardiaque s'élève au-dessus de la ", +"sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "sommeil, pas, entraînements et plus encore. Tes données de bracelet en direct restent intactes. C'est irréversible.", "slight": "léger", "small": "petit", +"so back up first if unsure.": "donc fais d'abord une sauvegarde en cas de doute.", "solid": "solide", +"staging stays in place if you leave this off. Takes effect on the next nights staged.": "par défaut reste en place si tu laisses cette option désactivée. Prend effet dès les prochaines nuits découpées.", "stale": "obsolète", "stale · %@": "obsolète · %@", "steady": "stable", "steps / motion unit": "pas / unité de mouvement", "steps per motion unit": "pas par unité de mouvement", +"steps, then applies that to the strap-only days. The more matching days it has, the ": "pas, puis applique ça aux jours avec le bracelet seul. Plus il a de jours concordants, ", +"still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement.": "encore actif. C'est un repère basé sur une demi-vie typique de 5 à 6 heures, pas une mesure.", +"strain (0-21)": "effort (0-21)", "strain (0–21)": "effort (0–21)", "strong": "fort", +"surplus": "surplus", +"tap Rescan. A ring still owned by Oura will not list here.": "appuie sur Rescanner. Une bague encore possédée par Oura n'apparaîtra pas ici.", "tap or drag for real values": "appuyez ou glissez pour les valeurs réelles", "target": "objectif", +"temperature drifted from normal. Higher HRV and lower resting heart rate lift Charge; a big ": "cutanée par rapport à la normale. Une VFC plus élevée et une FC au repos plus basse font monter la Charge ; un gros ", +"tends to be higher": "a tendance à être plus élevé", +"tends to be lower": "a tendance à être plus bas", +"tends to fall": "a tendance à baisser", +"tends to rise": "a tendance à monter", "that signal": "ce signal", "the last %lld nights": "les %lld dernières nuits", "the last 7 days": "les 7 derniers jours", "the last night": "la dernière nuit", +"the official WHOOP app and fully close that app, then Connect again.": "l'appli officielle WHOOP et ferme complètement cette appli, puis appuie de nouveau sur Connecter.", +"then come back.": "puis reviens.", +"then your resting heart rate, how well you slept, your breathing rate, and how far your skin ": "puis ta FC au repos, la qualité de ton sommeil, ta fréquence respiratoire, et l'écart de ta température ", "this %@": "ce %@", "this Mac": "ce Mac", +"this any time.": "ça à tout moment.", "this device": "cet appareil", +"time you open NOOP, if it's been longer than the interval below. ": "fois que tu ouvres NOOP, si le délai ci-dessous est dépassé. ", "to": "à", "to baseline": "à la référence", +"to see weeks, months and years instantly.": "pour voir les semaines, mois et années instantanément.", +"to the official WHOOP app, and the other way round. It's reversible: pair it in the ": "vers l'appli officielle WHOOP, et inversement. C'est réversible : appaire-le dans l'autre ", "today": "aujourd'hui", +"training-impulse using heart-rate-reserve zones (Karvonen), weights time in ": "l'impulsion d'entraînement à partir des zones de réserve de fréquence cardiaque (Karvonen), pondère le temps passé dans les ", "typical": "typique", +"typical adult range (approximate, not medical advice).": "plage adulte typique (approximatif, pas un avis médical).", +"typical patterns, clearly labelled as not yet yours.": "des tendances types, clairement indiquées comme n'étant pas encore les tiennes.", +"typical patterns.": "des tendances types.", "under an hour": "moins d'une heure", "unscored": "non évalué", "untouched days": "jours intacts", "up %lld points": "en hausse de %lld points", "up 1 point": "en hausse de 1 point", +"up in the Oura app, NOOP no longer owns it and you would re-add it to take it over.": "dans l'appli Oura, NOOP ne la possède plus et tu devrais la rajouter pour la reprendre.", +"up in the Oura app.": "dans l'appli Oura.", "used": "utilisé", "variation index": "indice de variation", "very strong": "très fort", +"volume load shown per session": "charge de volume affichée par séance", "vs 30-day baseline": "vs référence sur 30 jours", "vs age %lld": "vs âge %lld", "vs behaviour-free baseline": "vs référence sans comportement", "vs need": "vs besoin", "vs prev %@": "vs %@ préc.", "vs typical": "vs typique", +"vs typical - ": "vs habituel - ", "vs typical —": "vs typique —", +"vs your baseline": "vs ta référence", "warmer than baseline, limiting recovery": "plus chaude que la référence, limite la récupération", +"we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, ": "qu'on ne peut pas encore faire. NOOP le dira honnêtement et n'affichera jamais un chiffre inventé. Pas de sommeil, ", "weak": "faible", "week": "semaine", "what are you logging?": "que voulez-vous enregistrer ?", +"what's missing and what to do, rather than showing a fake value.": "ce qui manque et quoi faire, plutôt que d'afficher une fausse valeur.", +"which keeps its radio hot and drains the battery faster. Turn it off when ": "ce qui garde sa radio active et vide la batterie plus vite. Désactive-le quand ", +"with a date column plus those values. Meal-level rows are summed per day.": "avec une colonne de date et ces valeurs. Les lignes par repas sont additionnées par jour.", "year": "année", +"you enter. It doesn't test, read, diagnose, or advise. Your records never leave this phone; ": "que tu saisis. Ça ne teste pas, ne lit pas, ne diagnostique pas et ne conseille pas. Tes enregistrements ne quittent jamais ce téléphone ; ", +"you how fresh it is. If it says \"Not recording\", reconnect.": "à quel point c'est récent. S'il indique \"Pas d'enregistrement\", reconnecte-toi.", +"you're not using it with another device.": "tu ne l'utilises pas avec un autre appareil.", +"you. Nothing leaves this device. Your choices are saved now and apply ": "toi. Rien ne quitte cet appareil. Tes choix sont enregistrés maintenant et s'appliquent à chaque ", +"your baseline, 3 is highly activated.": "ta référence, 3 correspond à une forte activation.", "your data": "vos données", "your number, date and any note": "votre chiffre, la date et une éventuelle note", +"your own data.": "tes propres données.", +"your own nightly temperature, heart rate and HRV: observations about your own numbers, ": "ta propre température nocturne, ta fréquence cardiaque et ta VFC : des observations sur tes propres chiffres, ", +"your phone and still feel what matters.": "ton téléphone et sentir quand même ce qui compte.", "your readings over time": "vos mesures dans le temps", "zone %lld %lld percent": "zone %1$lld %2$lld pour cent", "~%lld days left": "~%lld jours restants", @@ -2912,11 +3794,14 @@ "~1 day left": "~1 jour restant", "± %lld yr · a fitness comparison, not a biological age": "± %lld ans · une comparaison de forme physique, pas un âge biologique", "± %lld · %@": "± %1$lld · %2$@", +"± 5 yr": "± 5 ans", +"± 5 yr · a fitness comparison, not a biological age": "± 5 ans · une comparaison de forme physique, pas un âge biologique", "· ~day %lld": "· ~jour %lld", "· ~day %lld–%lld": "· ~jour %1$lld–%2$lld", "× %@ on save.": "× %@ à l'enregistrement.", "Δ vs prev": "Δ vs préc.", "“Dose” here is timing (later in the day = stronger), not milligrams.": "Ici, la « dose » est une question de moment (plus tard dans la journée = plus fort), pas de milligrammes.", "≈ %@ steps (auto)": "≈ %@ pas (auto)", -"≈ %@ steps at this setting": "≈ %@ pas avec ce réglage" +"≈ %@ steps at this setting": "≈ %@ pas avec ce réglage", +"✓ Strap accepted all 15 R22 flags": "✓ Le bracelet a accepté les 15 drapeaux R22" } \ No newline at end of file From 123ac341f939c8a3875251b97291efdf4202b66b Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:55:25 +0200 Subject: [PATCH 21/28] Bump to 8.2.4 French localisation: iOS/macOS catalog gap-filled, and the ~50 Android Compose screens now render in French on a French device. Plus the stable release-signing key wiring, in-app-update polish, auto-reload after a restore, and Support pointing at this fork. Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fa3f967b9..43c3c64d5 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 262 - versionName = "8.2.3" + versionCode = 263 + versionName = "8.2.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index fa243db87..f3a79abed 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.3" + MARKETING_VERSION: "8.2.4" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "173" + CURRENT_PROJECT_VERSION: "174" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From ed3177e0682249f134710f228e26464316967fe5 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:13:32 +0200 Subject: [PATCH 22/28] Publish 8.2.4 in the AltStore source Co-Authored-By: Claude Sonnet 5 --- altstore-source.json | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/altstore-source.json b/altstore-source.json index 02846b60a..3bdc1a2c4 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -17,6 +17,15 @@ "category": "healthcare-fitness", "screenshotURLs": [], "versions": [ + { + "version": "8.2.4", + "buildVersion": "174", + "date": "2026-09-11", + "localizedDescription": "NOOP 8.2.4. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.4/NOOP-v8.2.4-ios.ipa", + "size": 14166535, + "minOSVersion": "17.0" + }, { "version": "8.2.3", "buildVersion": "173", @@ -55,12 +64,12 @@ "NSHealthUpdateUsageDescription": "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." } }, - "version": "8.2.3", - "buildVersion": "173", - "versionDate": "2026-09-10", - "versionDescription": "NOOP 8.2.3. See the release notes for what changed.", - "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.3/NOOP-v8.2.3-ios.ipa", - "size": 14162400, + "version": "8.2.4", + "buildVersion": "174", + "versionDate": "2026-09-11", + "versionDescription": "NOOP 8.2.4. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.4/NOOP-v8.2.4-ios.ipa", + "size": 14166535, "minOSVersion": "17.0" } ], From 39e289ffb3c16f7a4b795983e6c09fbebba8c630 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:15:18 +0200 Subject: [PATCH 23/28] Android: auto-backup to Downloads right before an in-app update An update that turns into a forced uninstall (signature mismatch, or any other install hiccup) wipes the app-private database. AutoBackup.beforeUpdate now writes a fresh .noopbak to the public Downloads folder (survives an uninstall, unlike app-private storage) right before the APK download starts, from both the launch-time UpdateGate prompt and the Settings "Update" button. Best-effort and silent: any failure is swallowed and never blocks or delays the update. API 29+ only (no extra permission needed there); skipped on older devices rather than requesting one just for this. Co-Authored-By: Claude Sonnet 5 --- .../main/java/com/noop/ui/SettingsScreen.kt | 6 +++ .../src/main/java/com/noop/ui/UpdateGate.kt | 4 ++ .../main/java/com/noop/update/AutoBackup.kt | 48 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 android/app/src/main/java/com/noop/update/AutoBackup.kt diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt index 5fc58984a..314f241b3 100644 --- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt @@ -101,6 +101,7 @@ import com.noop.ble.WhoopModel import com.noop.data.DataBackup import com.noop.ingest.RawSensorExport import com.noop.ingest.WhoopCsvExporter +import com.noop.update.AutoBackup import com.noop.update.InAppUpdate import com.noop.update.UpdateCheck import kotlinx.coroutines.Dispatchers @@ -2162,6 +2163,11 @@ fun SettingsScreen(vm: AppViewModel, onOpenTestCentre: () -> Unit = {}) { updDownloadPct = 0f scope.launch { runCatching { + // Best-effort safety net (never blocks the + // update): a fresh backup to Downloads + // survives even if install turns into an + // uninstall. + AutoBackup.beforeUpdate(context, avail.version) InAppUpdate.download(context, avail.version, apk) { updDownloadPct = it } diff --git a/android/app/src/main/java/com/noop/ui/UpdateGate.kt b/android/app/src/main/java/com/noop/ui/UpdateGate.kt index 297c227b5..b6d48505d 100644 --- a/android/app/src/main/java/com/noop/ui/UpdateGate.kt +++ b/android/app/src/main/java/com/noop/ui/UpdateGate.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.noop.BuildConfig +import com.noop.update.AutoBackup import com.noop.update.InAppUpdate import com.noop.update.UpdateCheck import kotlinx.coroutines.launch @@ -93,6 +94,9 @@ fun UpdateGate() { state = UpdateGateState.Downloading(info, 0f) scope.launch { runCatching { + // Best-effort safety net (never blocks the update): a fresh backup to + // Downloads survives even if the install turns into an uninstall. + AutoBackup.beforeUpdate(context, info.version) InAppUpdate.download(context, info.version, apk) { p -> state = UpdateGateState.Downloading(info, p) } diff --git a/android/app/src/main/java/com/noop/update/AutoBackup.kt b/android/app/src/main/java/com/noop/update/AutoBackup.kt new file mode 100644 index 000000000..ec6c549b6 --- /dev/null +++ b/android/app/src/main/java/com/noop/update/AutoBackup.kt @@ -0,0 +1,48 @@ +package com.noop.update + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import com.noop.data.DataBackup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Best-effort safety-net backup fired right before an in-app update installs. + * + * A signature mismatch (or any other install hiccup) can force Android to uninstall-then-reinstall + * instead of updating in place, which wipes the app's private database. Writing one fresh snapshot + * to the public Downloads folder — which SURVIVES an uninstall, unlike app-private storage — right + * before the update starts means there's always something recent to restore from Backup & Sync + * afterwards, even for someone who never set up a backup folder. + * + * Never blocks or fails the update: every error is swallowed, and [InAppUpdate.download] runs + * regardless of the outcome here. Requires API 29+ (a `MediaStore.Downloads` insert needs no + * permission there); older devices are skipped rather than requesting a permission the app + * otherwise doesn't need. + */ +object AutoBackup { + + suspend fun beforeUpdate(context: Context, version: String) = withContext(Dispatchers.IO) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@withContext + runCatching { + val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date()) + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, "NOOP-preupdate-v$version-$stamp.noopbak") + put(MediaStore.MediaColumns.MIME_TYPE, "application/zip") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val resolver = context.applicationContext.contentResolver + val uri: Uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: return@runCatching + DataBackup.exportTo(context, uri) + } + Unit + } +} From 2e1ab085d950b817472716b369a8b8cdfe25cc9b Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:52:19 +0200 Subject: [PATCH 24/28] =?UTF-8?q?Bump=20to=208.2.5=20=E2=80=94=20first=20r?= =?UTF-8?q?elease=20signed=20with=20the=20stable=20Android=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the pre-update auto-backup to Downloads. This is the first release signed with the stable keystore (repo secrets), so a device on 8.2.4 or earlier needs one last manual uninstall/reinstall; every release after this one installs in place. Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 43c3c64d5..54fa13f3d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 263 - versionName = "8.2.4" + versionCode = 264 + versionName = "8.2.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index f3a79abed..8bd480b6a 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.4" + MARKETING_VERSION: "8.2.5" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "174" + CURRENT_PROJECT_VERSION: "175" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From 4e89d7bbf0145968a1115e439097580f9905feec Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:10:03 +0200 Subject: [PATCH 25/28] Publish 8.2.5 in the AltStore source Co-Authored-By: Claude Sonnet 5 --- altstore-source.json | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/altstore-source.json b/altstore-source.json index 3bdc1a2c4..cae269c03 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -17,6 +17,15 @@ "category": "healthcare-fitness", "screenshotURLs": [], "versions": [ + { + "version": "8.2.5", + "buildVersion": "175", + "date": "2026-09-11", + "localizedDescription": "NOOP 8.2.5. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.5/NOOP-v8.2.5-ios.ipa", + "size": 14166533, + "minOSVersion": "17.0" + }, { "version": "8.2.4", "buildVersion": "174", @@ -64,12 +73,12 @@ "NSHealthUpdateUsageDescription": "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." } }, - "version": "8.2.4", - "buildVersion": "174", + "version": "8.2.5", + "buildVersion": "175", "versionDate": "2026-09-11", - "versionDescription": "NOOP 8.2.4. See the release notes for what changed.", - "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.4/NOOP-v8.2.4-ios.ipa", - "size": 14166535, + "versionDescription": "NOOP 8.2.5. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.5/NOOP-v8.2.5-ios.ipa", + "size": 14166533, "minOSVersion": "17.0" } ], From 9f9c94762993ff11340a55fa081a678b07042c46 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:46:52 +0200 Subject: [PATCH 26/28] Android FR pass 2: enum/computed-property labels + analytics sentences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 1 wrapped literal Text("English") call-site arguments. It systematically missed English returned from a when/if-based computed property or function — DashboardCard.title/subtitle, ScoreState/RecordingState title+detail, sport display names, sync-state labels, and (a much bigger class) full sentences generated in com.noop.analytics (WeeklyDigest, ReadinessEngine, RecoveryDrivers "What shaped Charge", IllnessSignalEngine, FitnessAgeEngine, CyclePhaseEngine, VitalityEngine) that the UI only displays verbatim. - TodayScreen.kt: Charge/Effort/Rest dashboard-card labels, the Synthesis sheet (Steady/Depleted/Primed/Peak, "Charge is low/steady/strong"), calibrating notes, sync-state chip, "Start session", HR chart subtitle, "Synced from:" footer, Key Metrics editor. Onboarding gets a "Skip" button (top bar) so re-onboarding after a reinstall doesn't force the full flow. - WorkoutsScreen/WorkoutEditing/DevicesScreen/AddDeviceWizard/LiveWorkoutScreen: sport display names (fixes the "Walking, Walking, Walking" workout list), device pairing/sync labels. - SleepScreen: remaining labels (duration subtitle, ledger paragraph, time pickers) + a layout fix for French labels overflowing/wrapping onto their own line in fixed-width stat tiles (SparkTile, StageBreakdownRow, NapSummaryCell, ChartFooter). - TrendsScreen/TrendsExploreScreen/WeeklyDigestCard/HealthScreen/ SkinTempCardsScreen/StressScreen: chart-legend words (Avg/Peak/Low/Days), "Trailing N days", vitals detail stats, illness/cycle confidence lines. - TrendsReport.kt: the whole file was missed by pass 1 entirely (raw Canvas.drawText PDF renderer, not Compose Text()) — title, section headers, AVG/MIN/MAX/DAYS legend, footer legend, empty state, the export sheet, share-sheet strings. - analytics/*.kt (7 files): added `Fr.tr(...)` (the i18n bridge from a different package) around every static text fragment in the insight/ explanation sentence generators, preserving all interpolated numbers/ formatting exactly. Two `const val`s had to become `val` getters since Fr.tr() isn't a compile-time constant. fr_strings.json: 3805 -> 4034 (+229), 100% coverage of all tr()/Fr.tr() call sites app-wide (2233 distinct strings). Verified no double-wraps, every tr(/Fr.tr( call takes a string literal. Co-Authored-By: Claude Sonnet 5 --- .../com/noop/analytics/CyclePhaseEngine.kt | 33 ++- .../com/noop/analytics/FitnessAgeEngine.kt | 26 +- .../com/noop/analytics/IllnessSignalEngine.kt | 44 ++-- .../com/noop/analytics/ReadinessEngine.kt | 84 +++---- .../com/noop/analytics/RecoveryDrivers.kt | 49 ++-- .../java/com/noop/analytics/VitalityEngine.kt | 13 +- .../java/com/noop/analytics/WeeklyDigest.kt | 52 ++-- .../main/java/com/noop/ui/AddDeviceWizard.kt | 6 +- .../main/java/com/noop/ui/DevicesScreen.kt | 18 +- .../src/main/java/com/noop/ui/HealthScreen.kt | 24 +- .../java/com/noop/ui/LiveWorkoutScreen.kt | 4 +- .../main/java/com/noop/ui/OnboardingScreen.kt | 21 +- .../java/com/noop/ui/SkinTempCardsScreen.kt | 12 +- .../src/main/java/com/noop/ui/SleepScreen.kt | 109 ++++++--- .../src/main/java/com/noop/ui/StressScreen.kt | 14 +- .../src/main/java/com/noop/ui/TodayScreen.kt | 114 ++++----- .../java/com/noop/ui/TrendsExploreScreen.kt | 24 +- .../src/main/java/com/noop/ui/TrendsReport.kt | 82 ++++--- .../src/main/java/com/noop/ui/TrendsScreen.kt | 24 +- .../main/java/com/noop/ui/WeeklyDigestCard.kt | 18 +- .../main/java/com/noop/ui/WorkoutEditing.kt | 2 +- .../main/java/com/noop/ui/WorkoutsScreen.kt | 58 ++--- android/app/src/main/res/raw/fr_strings.json | 229 ++++++++++++++++++ 23 files changed, 686 insertions(+), 374 deletions(-) diff --git a/android/app/src/main/java/com/noop/analytics/CyclePhaseEngine.kt b/android/app/src/main/java/com/noop/analytics/CyclePhaseEngine.kt index 23a847e7e..689daee35 100644 --- a/android/app/src/main/java/com/noop/analytics/CyclePhaseEngine.kt +++ b/android/app/src/main/java/com/noop/analytics/CyclePhaseEngine.kt @@ -1,5 +1,6 @@ package com.noop.analytics +import com.noop.i18n.Fr import java.time.LocalDate import kotlin.math.abs import kotlin.math.roundToInt @@ -31,8 +32,10 @@ object CyclePhaseEngine { const val periOvulatoryHalfWidth: Int = 2 /** Standing awareness-only line shown on every cycle surface (legal/ethical framing). */ - const val awarenessLine = - "For awareness only. Not a medical device, not contraception, not a substitute for professional care." + val awarenessLine: String + get() = Fr.tr( + "For awareness only. Not a medical device, not contraception, not a substitute for professional care.", + ) // ── Inputs ── @@ -93,7 +96,7 @@ object CyclePhaseEngine { ): Result { if (!baselineUsable || nights.size < minNightsToClassify) { return Result(Phase.LEARNING, Confidence.LEARNING, null, null, null, null, emptyList(), - "Learning your pattern from your nightly temperature - keep wearing it overnight.") + Fr.tr("Learning your pattern from your nightly temperature - keep wearing it overnight.")) } val fused: List> = nights.map { n -> @@ -102,7 +105,7 @@ object CyclePhaseEngine { val values = fused.mapNotNull { it.second } if (values.size < minNightsToClassify) { return Result(Phase.LEARNING, Confidence.LEARNING, null, null, null, null, emptyList(), - "Learning your pattern from your nightly temperature - keep wearing it overnight.") + Fr.tr("Learning your pattern from your nightly temperature - keep wearing it overnight.")) } val center = median(values) @@ -121,8 +124,10 @@ object CyclePhaseEngine { val lastOnsetIdx = onsets.lastOrNull() ?: return Result(Phase.UNKNOWN, Confidence.BUILDING, null, null, null, null, shiftMarkers, - "No clear temperature pattern yet - this can happen with irregular cycles, " + - "hormonal birth control, or shift work.") + Fr.tr( + "No clear temperature pattern yet - this can happen with irregular cycles, " + + "hormonal birth control, or shift work.", + )) val onsetGaps = mutableListOf() if (onsets.size >= 2) { @@ -146,8 +151,10 @@ object CyclePhaseEngine { val delta = daysBetween(loggedStart, fused[lastOnsetIdx].first) val sinceLog = daysBetween(loggedStart, lastNightDay) ?: 0 if ((delta != null && (delta < 0 || delta > maxCycleDays)) || sinceLog > maxCycleDays) { - note = "Your temperature shift came at a different time than your logged date - " + - "the logged start may be off." + note = Fr.tr( + "Your temperature shift came at a different time than your logged date - " + + "the logged start may be off.", + ) } } @@ -200,11 +207,11 @@ object CyclePhaseEngine { // ── Copy ── internal fun phaseNote(phase: Phase): String = when (phase) { - Phase.FOLLICULAR -> "Follicular range - temperature sitting at your baseline." - Phase.PERI_OVULATORY -> "Around your mid-cycle shift - temperature is turning." - Phase.LUTEAL -> "Luteal range - temperature is running above your baseline." - Phase.UNKNOWN -> "No clear pattern yet." - Phase.LEARNING -> "Learning your pattern - keep wearing it overnight." + Phase.FOLLICULAR -> Fr.tr("Follicular range - temperature sitting at your baseline.") + Phase.PERI_OVULATORY -> Fr.tr("Around your mid-cycle shift - temperature is turning.") + Phase.LUTEAL -> Fr.tr("Luteal range - temperature is running above your baseline.") + Phase.UNKNOWN -> Fr.tr("No clear pattern yet.") + Phase.LEARNING -> Fr.tr("Learning your pattern - keep wearing it overnight.") } // ── Small stats / day helpers (self-contained, parity-clean) ── diff --git a/android/app/src/main/java/com/noop/analytics/FitnessAgeEngine.kt b/android/app/src/main/java/com/noop/analytics/FitnessAgeEngine.kt index 15d1fac0c..e15d3c8f3 100644 --- a/android/app/src/main/java/com/noop/analytics/FitnessAgeEngine.kt +++ b/android/app/src/main/java/com/noop/analytics/FitnessAgeEngine.kt @@ -1,5 +1,7 @@ package com.noop.analytics +import com.noop.i18n.Fr + // FitnessAgeEngine.kt — on-device "Fitness Age" from resting HR + activity + profile. // Byte-for-byte mirror of Strand/Packages/StrandAnalytics/Sources/StrandAnalytics/FitnessAgeEngine.swift. // @@ -133,28 +135,28 @@ object FitnessAgeEngine { fun assessReadiness(hasAge: Boolean, hasSex: Boolean, rhrDays: Int, activityDays: Int, hasHeightWeight: Boolean, hasWaist: Boolean): FitnessAgeReadiness { val items = listOf( - FitnessReadinessItem("age", "Your age", + FitnessReadinessItem("age", Fr.tr("Your age"), if (hasAge) FitnessReadinessStatus.SATISFIED else FitnessReadinessStatus.MISSING, required = true, role = FitnessReadinessRole.DRIVES_AGE, - detail = if (hasAge) "Set" else "Add it in Settings"), - FitnessReadinessItem("sex", "Biological sex", + detail = if (hasAge) Fr.tr("Set") else Fr.tr("Add it in Settings")), + FitnessReadinessItem("sex", Fr.tr("Biological sex"), if (hasSex) FitnessReadinessStatus.SATISFIED else FitnessReadinessStatus.MISSING, required = true, role = FitnessReadinessRole.DRIVES_AGE, - detail = if (hasSex) "Set" else "Add it in Settings"), - FitnessReadinessItem("rhr", "Resting heart rate", + detail = if (hasSex) Fr.tr("Set") else Fr.tr("Add it in Settings")), + FitnessReadinessItem("rhr", Fr.tr("Resting heart rate"), coverageStatus(rhrDays, minCoverageDays), required = true, - role = FitnessReadinessRole.DRIVES_AGE, detail = "$rhrDays of last 7 nights"), - FitnessReadinessItem("activity", "Recent activity", + role = FitnessReadinessRole.DRIVES_AGE, detail = "$rhrDays ${Fr.tr("of last 7 nights")}"), + FitnessReadinessItem("activity", Fr.tr("Recent activity"), coverageStatus(activityDays, minCoverageDays), required = false, - role = FitnessReadinessRole.DRIVES_AGE, detail = "$activityDays of last 7 days"), - FitnessReadinessItem("bodyMetrics", "Height & weight", + role = FitnessReadinessRole.DRIVES_AGE, detail = "$activityDays ${Fr.tr("of last 7 days")}"), + FitnessReadinessItem("bodyMetrics", Fr.tr("Height & weight"), if (hasHeightWeight) FitnessReadinessStatus.SATISFIED else FitnessReadinessStatus.MISSING, required = false, role = FitnessReadinessRole.UNLOCKS_VO2MAX, - detail = if (hasHeightWeight) "Unlocks your VO₂max" else "Add to also see VO₂max"), - FitnessReadinessItem("waist", "Waist (optional)", + detail = if (hasHeightWeight) Fr.tr("Unlocks your VO₂max") else Fr.tr("Add to also see VO₂max")), + FitnessReadinessItem("waist", Fr.tr("Waist (optional)"), if (hasWaist) FitnessReadinessStatus.SATISFIED else FitnessReadinessStatus.MISSING, required = false, role = FitnessReadinessRole.UNLOCKS_VO2MAX, - detail = if (hasWaist) "Sharpens VO₂max" else "Optional - sharpens VO₂max"), + detail = if (hasWaist) Fr.tr("Sharpens VO₂max") else Fr.tr("Optional - sharpens VO₂max")), ) val confidence = when { !hasAge || !hasSex || rhrDays < minCoverageDays -> FitnessAgeConfidence.NOT_READY diff --git a/android/app/src/main/java/com/noop/analytics/IllnessSignalEngine.kt b/android/app/src/main/java/com/noop/analytics/IllnessSignalEngine.kt index d0efd7234..8521b1540 100644 --- a/android/app/src/main/java/com/noop/analytics/IllnessSignalEngine.kt +++ b/android/app/src/main/java/com/noop/analytics/IllnessSignalEngine.kt @@ -1,5 +1,7 @@ package com.noop.analytics +import com.noop.i18n.Fr + // IllnessSignalEngine.kt — multi-signal "Heads-Up" early-warning with explicit false-positive suppression. // Byte-for-byte mirror of Strand/Packages/StrandAnalytics/Sources/StrandAnalytics/IllnessSignalEngine.swift. // @@ -24,7 +26,7 @@ object IllnessSignalEngine { const val confounderDampen: Double = 0.45 /** Standing not-a-diagnosis tail reused verbatim from the shipped IllnessNotifier copy. */ - const val disclaimerTail = "On-device estimate - not a diagnosis." + val disclaimerTail: String get() = Fr.tr("On-device estimate - not a diagnosis.") // ── Inputs ── @@ -110,53 +112,53 @@ object IllnessSignalEngine { // Gate 0: untrusted baseline → silent. if (!context.baselineTrusted) { return Result(score, Level.QUIET, firedSignals, emptyList(), signalCount, - "Still learning your baseline - keeping an eye out.") + Fr.tr("Still learning your baseline - keeping an eye out.")) } // Already-unwell path: switch from "early warning" to a gentle "rest up". if (context.alreadyUnwell) { val agreeing = score >= mildThreshold && signalCount >= 1 val copy = if (agreeing) - "Rest up - you logged feeling unwell, and your numbers agree. $disclaimerTail" + "${Fr.tr("Rest up - you logged feeling unwell, and your numbers agree.")} $disclaimerTail" else - "Rest up - you logged feeling unwell. Take it easy today. $disclaimerTail" + "${Fr.tr("Rest up - you logged feeling unwell. Take it easy today.")} $disclaimerTail" return Result(score, Level.ALREADY_UNWELL, firedSignals, emptyList(), signalCount, copy) } // Corroboration + magnitude gate. if (signalCount < minCorroboratingSignals || score < mildThreshold) { return Result(score, Level.QUIET, firedSignals, emptyList(), signalCount, - "Nothing notable - your signals look like your normal range.") + Fr.tr("Nothing notable - your signals look like your normal range.")) } // Confounder suppression — the differentiating part. val suppressedBy = mutableListOf() - if (context.alcohol) suppressedBy.add("alcohol") - if (context.stress) suppressedBy.add("stress") - if (context.sauna) suppressedBy.add("sauna") - if (context.hardOrLateWorkout) suppressedBy.add("a hard or late workout") - if (context.travelPhaseJump) suppressedBy.add("travel") + if (context.alcohol) suppressedBy.add(Fr.tr("alcohol")) + if (context.stress) suppressedBy.add(Fr.tr("stress")) + if (context.sauna) suppressedBy.add(Fr.tr("sauna")) + if (context.hardOrLateWorkout) suppressedBy.add(Fr.tr("a hard or late workout")) + if (context.travelPhaseJump) suppressedBy.add(Fr.tr("travel")) - val signalsPhrase = if (firedSignals.isEmpty()) "Some signals are up" else firedSignals.joinToString(", ") + val signalsPhrase = if (firedSignals.isEmpty()) Fr.tr("Some signals are up") else firedSignals.joinToString(", ") if (suppressedBy.isNotEmpty()) { val dampened = score * confounderDampen val reason = joinReasons(suppressedBy) - val copy = "Some signals are up ($signalsPhrase), but you logged $reason - likely that, " + - "not illness. $disclaimerTail" + val copy = "${Fr.tr("Some signals are up")} ($signalsPhrase), ${Fr.tr("but you logged")} $reason " + + "${Fr.tr("- likely that, not illness.")} $disclaimerTail" return Result(dampened, Level.SUPPRESSED, firedSignals, suppressedBy, signalCount, copy) } // No confounder. Mild stays in the detail view; a strong composite raises. if (score < raiseThreshold) { - val copy = "A few signals are mildly up ($signalsPhrase). Nothing alarming - worth a calmer " + - "day. $disclaimerTail" + val copy = "${Fr.tr("A few signals are mildly up")} ($signalsPhrase). " + + "${Fr.tr("Nothing alarming - worth a calmer day.")} $disclaimerTail" return Result(score, Level.MILD, firedSignals, emptyList(), signalCount, copy) } - val ruledOut = "no alcohol or travel logged" - val copy = "Heads-up - your body looks strained. $signalsPhrase. With $ruledOut, consider " + - "taking it easy. $disclaimerTail" + val ruledOut = Fr.tr("no alcohol or travel logged") + val copy = "${Fr.tr("Heads-up - your body looks strained.")} $signalsPhrase. ${Fr.tr("With")} $ruledOut, " + + "${Fr.tr("consider taking it easy.")} $disclaimerTail" return Result(score, Level.RAISED, firedSignals, emptyList(), signalCount, copy) } @@ -164,12 +166,12 @@ object IllnessSignalEngine { /** Join named confounders into a natural list ("alcohol", "alcohol and stress", "a, b and c"). */ internal fun joinReasons(reasons: List): String = when (reasons.size) { - 0 -> "something" + 0 -> Fr.tr("something") 1 -> reasons[0] - 2 -> "${reasons[0]} and ${reasons[1]}" + 2 -> "${reasons[0]} ${Fr.tr("and")} ${reasons[1]}" else -> { val head = reasons.dropLast(1).joinToString(", ") - "$head and ${reasons.last()}" + "$head ${Fr.tr("and")} ${reasons.last()}" } } } diff --git a/android/app/src/main/java/com/noop/analytics/ReadinessEngine.kt b/android/app/src/main/java/com/noop/analytics/ReadinessEngine.kt index 05e3d2f8f..10de6683b 100644 --- a/android/app/src/main/java/com/noop/analytics/ReadinessEngine.kt +++ b/android/app/src/main/java/com/noop/analytics/ReadinessEngine.kt @@ -1,6 +1,7 @@ package com.noop.analytics import com.noop.data.DailyMetric +import com.noop.i18n.Fr import java.util.Locale import kotlin.math.sqrt @@ -98,8 +99,8 @@ object ReadinessEngine { if (latest == null) { return Readiness( level = Level.INSUFFICIENT, - headline = "Readiness", - summary = "Wear the strap for a few nights and your readiness read will appear here.", + headline = Fr.tr("Readiness"), + summary = Fr.tr("Wear the strap for a few nights and your readiness read will appear here."), signals = emptyList(), acwr = null, monotony = null, ) } @@ -111,13 +112,13 @@ object ReadinessEngine { val hrvSignal = zSignal( value = latest.avgHrv, baseline = history.takeLast(baselineWindow).mapNotNull { it.avgHrv }, - key = "hrv", label = "HRV", - unit = "ms", decimals = 0, + key = "hrv", label = Fr.tr("HRV"), + unit = Fr.tr("ms"), decimals = 0, higherIsBetter = true, - goodText = "above your baseline - well recovered", - neutralText = "in your normal range", - watchText = "a touch below baseline", - badText = "suppressed - a sign of autonomic fatigue", + goodText = Fr.tr("above your baseline - well recovered"), + neutralText = Fr.tr("in your normal range"), + watchText = Fr.tr("a touch below baseline"), + badText = Fr.tr("suppressed - a sign of autonomic fatigue"), ) if (hrvSignal != null) signals.add(hrvSignal) @@ -125,13 +126,13 @@ object ReadinessEngine { val rhrSignal = zSignal( value = latest.restingHr?.toDouble(), baseline = history.takeLast(baselineWindow).mapNotNull { it.restingHr?.toDouble() }, - key = "rhr", label = "Resting HR", - unit = "bpm", decimals = 0, + key = "rhr", label = Fr.tr("Resting HR"), + unit = Fr.tr("bpm"), decimals = 0, higherIsBetter = false, - goodText = "at or below baseline", - neutralText = "in your normal range", - watchText = "running a little high", - badText = "elevated - overtraining or illness can do this", + goodText = Fr.tr("at or below baseline"), + neutralText = Fr.tr("in your normal range"), + watchText = Fr.tr("running a little high"), + badText = Fr.tr("elevated - overtraining or illness can do this"), ) if (rhrSignal != null) signals.add(rhrSignal) @@ -148,20 +149,20 @@ object ReadinessEngine { val sd = sampleSD(base) if (base.size >= minBaseline && m != null && m in respPlausibleRange && sd != null && sd > 0) { val z = (rr - m) / sd - val respEvidence = "${fmt(rr, 1)} vs ${fmt(m, 1)} rpm" + val respEvidence = "${fmt(rr, 1)} ${Fr.tr("vs")} ${fmt(m, 1)} ${Fr.tr("rpm")}" if (z >= respZBad) { signals.add( Signal( - key = "respRate", label = "Respiratory rate", - detail = "up vs baseline - sometimes an early sign of getting sick", flag = Flag.BAD, + key = "respRate", label = Fr.tr("Respiratory rate"), + detail = Fr.tr("up vs baseline - sometimes an early sign of getting sick"), flag = Flag.BAD, evidence = respEvidence, ) ) } else if (z >= respZWatch) { signals.add( Signal( - key = "respRate", label = "Respiratory rate", - detail = "slightly raised vs baseline", flag = Flag.WATCH, + key = "respRate", label = Fr.tr("Respiratory rate"), + detail = Fr.tr("slightly raised vs baseline"), flag = Flag.WATCH, evidence = respEvidence, ) ) @@ -191,9 +192,9 @@ object ReadinessEngine { if (mono >= 2.0) { signals.add( Signal( - key = "monotony", label = "Training variety", - detail = "low - similar strain every day raises strain/illness risk", flag = Flag.WATCH, - evidence = "monotony ${fmt(mono, 1)}", + key = "monotony", label = Fr.tr("Training variety"), + detail = Fr.tr("low - similar strain every day raises strain/illness risk"), flag = Flag.WATCH, + evidence = "${Fr.tr("monotony")} ${fmt(mono, 1)}", ) ) } @@ -234,7 +235,7 @@ object ReadinessEngine { else -> { flag = Flag.BAD; text = badText } } // The numbers behind the read: today's value vs the baseline mean, in the metric's units. - val evidence = "${fmt(value, decimals)} vs ${fmt(m, decimals)} $unit" + val evidence = "${fmt(value, decimals)} ${Fr.tr("vs")} ${fmt(m, decimals)} $unit" return Signal(key = key, label = label, detail = text, flag = flag, evidence = evidence) } @@ -253,25 +254,26 @@ object ReadinessEngine { val pct = String.format("%.2f", ratio) // Evidence: the two strain loads the ratio is built from, 1 dp each. val evidence = "7d ${fmt(acute, 1)} / 28d ${fmt(chronic, 1)}" + val label = Fr.tr("Training load") return when { ratio < 0.8 -> Signal( - key = "acwr", label = "Training load", - detail = "ramping down (acute:chronic $pct) - room to build", flag = Flag.WATCH, + key = "acwr", label = label, + detail = "${Fr.tr("ramping down (acute:chronic")} $pct) - ${Fr.tr("room to build")}", flag = Flag.WATCH, evidence = evidence, ) ratio < 1.3 -> Signal( - key = "acwr", label = "Training load", - detail = "in the sweet spot (acute:chronic $pct)", flag = Flag.GOOD, + key = "acwr", label = label, + detail = "${Fr.tr("in the sweet spot (acute:chronic")} $pct)", flag = Flag.GOOD, evidence = evidence, ) ratio < 1.5 -> Signal( - key = "acwr", label = "Training load", - detail = "building fast (acute:chronic $pct) - watch fatigue", flag = Flag.WATCH, + key = "acwr", label = label, + detail = "${Fr.tr("building fast (acute:chronic")} $pct) - ${Fr.tr("watch fatigue")}", flag = Flag.WATCH, evidence = evidence, ) else -> Signal( - key = "acwr", label = "Training load", - detail = "spiking (acute:chronic $pct) - higher injury risk", flag = Flag.BAD, + key = "acwr", label = label, + detail = "${Fr.tr("spiking (acute:chronic")} $pct) - ${Fr.tr("higher injury risk")}", flag = Flag.BAD, evidence = evidence, ) } @@ -282,8 +284,8 @@ object ReadinessEngine { private fun synthesize(signals: List, hasHistory: Boolean): Triple { if (!hasHistory || signals.isEmpty()) { return Triple( - Level.INSUFFICIENT, "Readiness", - "A few more nights of data and your readiness read will sharpen.", + Level.INSUFFICIENT, Fr.tr("Readiness"), + Fr.tr("A few more nights of data and your readiness read will sharpen."), ) } val bad = signals.filter { it.flag == Flag.BAD } @@ -294,25 +296,25 @@ object ReadinessEngine { if (bad.size >= 2 || (recoveryDown && loadHigh)) { return Triple( - Level.RUNDOWN, "Run down", - "Several signals are down at once. Treat today as recovery - easy movement, real sleep tonight.", + Level.RUNDOWN, Fr.tr("Run down"), + Fr.tr("Several signals are down at once. Treat today as recovery - easy movement, real sleep tonight."), ) } if (recoveryDown || loadHigh || bad.size >= 1) { return Triple( - Level.STRAINED, "Strained", - "One of your signals is flagging. You can train, but keep it controlled and bank the recovery.", + Level.STRAINED, Fr.tr("Strained"), + Fr.tr("One of your signals is flagging. You can train, but keep it controlled and bank the recovery."), ) } if (good.size >= 2 && watch.isEmpty()) { return Triple( - Level.PRIMED, "Primed", - "Your signals are aligned and your load is supported. A harder session is well backed today.", + Level.PRIMED, Fr.tr("Primed"), + Fr.tr("Your signals are aligned and your load is supported. A harder session is well backed today."), ) } return Triple( - Level.BALANCED, "Balanced", - "Nothing's flagging. Train to feel - your body's holding steady.", + Level.BALANCED, Fr.tr("Balanced"), + Fr.tr("Nothing's flagging. Train to feel - your body's holding steady."), ) } diff --git a/android/app/src/main/java/com/noop/analytics/RecoveryDrivers.kt b/android/app/src/main/java/com/noop/analytics/RecoveryDrivers.kt index a49c9fe8e..d8b88b384 100644 --- a/android/app/src/main/java/com/noop/analytics/RecoveryDrivers.kt +++ b/android/app/src/main/java/com/noop/analytics/RecoveryDrivers.kt @@ -1,5 +1,6 @@ package com.noop.analytics +import com.noop.i18n.Fr import kotlin.math.abs import kotlin.math.exp import kotlin.math.roundToInt @@ -143,48 +144,49 @@ object RecoveryDrivers { drivers.add( ChargeDriver( - label = "Heart rate variability", + label = Fr.tr("Heart rate variability"), deltaPoints = delta(hrvIdx), - valueText = "${hrv.roundToInt()} ms", - baselineText = "${hrvBaseline.baseline.roundToInt()} ms baseline", - verdict = directionVerdict(hrvZ, good = "above baseline, supporting recovery", - flat = "at baseline", bad = "below baseline, limiting recovery"), + valueText = "${hrv.roundToInt()} ${Fr.tr("ms")}", + baselineText = "${hrvBaseline.baseline.roundToInt()} ${Fr.tr("ms")} ${Fr.tr("baseline")}", + verdict = directionVerdict(hrvZ, good = Fr.tr("above baseline, supporting recovery"), + flat = Fr.tr("at baseline"), bad = Fr.tr("below baseline, limiting recovery")), ), ) if (rhrIdx >= 0 && rhrBaseline != null) { // RHR z is already oriented "higher z = better" (lower RHR), so a positive z is good. drivers.add( ChargeDriver( - label = "Resting heart rate", + label = Fr.tr("Resting heart rate"), deltaPoints = delta(rhrIdx), - valueText = "${rhr.roundToInt()} bpm", - baselineText = "${rhrBaseline.baseline.roundToInt()} bpm baseline", - verdict = directionVerdict(terms[rhrIdx].z, good = "below baseline, supporting recovery", - flat = "at baseline", bad = "above baseline, limiting recovery"), + valueText = "${rhr.roundToInt()} ${Fr.tr("bpm")}", + baselineText = "${rhrBaseline.baseline.roundToInt()} ${Fr.tr("bpm")} ${Fr.tr("baseline")}", + verdict = directionVerdict(terms[rhrIdx].z, good = Fr.tr("below baseline, supporting recovery"), + flat = Fr.tr("at baseline"), bad = Fr.tr("above baseline, limiting recovery")), ), ) } if (sleepIdx >= 0 && sleepPerf != null) { drivers.add( ChargeDriver( - label = "Sleep quality", + label = Fr.tr("Sleep quality"), deltaPoints = delta(sleepIdx), valueText = "${(sleepPerf * 100.0).roundToInt()}%", baselineText = "", // centred on a fixed "good night", not a learned baseline - verdict = directionVerdict(terms[sleepIdx].z, good = "a strong night, supporting recovery", - flat = "a typical night", bad = "below a good night, limiting recovery"), + verdict = directionVerdict(terms[sleepIdx].z, good = Fr.tr("a strong night, supporting recovery"), + flat = Fr.tr("a typical night"), bad = Fr.tr("below a good night, limiting recovery")), ), ) } if (respIdx >= 0 && resp != null && respBaseline != null) { drivers.add( ChargeDriver( - label = "Respiratory rate", + label = Fr.tr("Respiratory rate"), deltaPoints = delta(respIdx), - valueText = String.format(java.util.Locale.US, "%.1f br/min", resp), - baselineText = String.format(java.util.Locale.US, "%.1f br/min baseline", respBaseline.baseline), - verdict = directionVerdict(terms[respIdx].z, good = "below baseline, supporting recovery", - flat = "at baseline", bad = "above baseline, limiting recovery"), + valueText = "${String.format(java.util.Locale.US, "%.1f", resp)} ${Fr.tr("br/min")}", + baselineText = "${String.format(java.util.Locale.US, "%.1f", respBaseline.baseline)} " + + "${Fr.tr("br/min")} ${Fr.tr("baseline")}", + verdict = directionVerdict(terms[respIdx].z, good = Fr.tr("below baseline, supporting recovery"), + flat = Fr.tr("at baseline"), bad = Fr.tr("above baseline, limiting recovery")), ), ) } @@ -193,9 +195,10 @@ object RecoveryDrivers { // deviation (signed +/- C from baseline), never an absolute temperature. drivers.add( ChargeDriver( - label = "Skin temperature", + label = Fr.tr("Skin temperature"), deltaPoints = delta(skinIdx), - valueText = String.format(java.util.Locale.US, "%+.1f C vs baseline", skinTempDev), + valueText = "${String.format(java.util.Locale.US, "%+.1f", skinTempDev)} " + + "C ${Fr.tr("vs")} ${Fr.tr("baseline")}", baselineText = "", // a deviation already; the reference is the personal baseline (0) verdict = skinTempVerdict(skinTempDev), ), @@ -232,8 +235,8 @@ object RecoveryDrivers { * recovery, warmer or cooler. Mirrors the Swift skinTempVerdict exactly. */ private fun skinTempVerdict(dev: Double): String = when { - abs(dev) <= SKIN_TEMP_TYPICAL_BAND_C -> "near baseline" - dev > 0.0 -> "warmer than baseline, limiting recovery" - else -> "cooler than baseline, limiting recovery" + abs(dev) <= SKIN_TEMP_TYPICAL_BAND_C -> Fr.tr("near baseline") + dev > 0.0 -> Fr.tr("warmer than baseline, limiting recovery") + else -> Fr.tr("cooler than baseline, limiting recovery") } } diff --git a/android/app/src/main/java/com/noop/analytics/VitalityEngine.kt b/android/app/src/main/java/com/noop/analytics/VitalityEngine.kt index 97856b748..75656c033 100644 --- a/android/app/src/main/java/com/noop/analytics/VitalityEngine.kt +++ b/android/app/src/main/java/com/noop/analytics/VitalityEngine.kt @@ -1,5 +1,6 @@ package com.noop.analytics +import com.noop.i18n.Fr import kotlin.math.abs // VitalityEngine.kt — 0–100 "Vitality" wellness score + optional "Body Age in years". @@ -77,26 +78,26 @@ object VitalityEngine { fun contributions(inputs: Inputs): List { val out = ArrayList() inputs.restingHR?.let { - out.add(Contribution("rhr", "Resting heart rate", ((it - 65) / 10) * 0.100)) + out.add(Contribution("rhr", Fr.tr("Resting heart rate"), ((it - 65) / 10) * 0.100)) } val vo2 = inputs.vo2max; val exp = inputs.expectedVO2max if (vo2 != null && exp != null && exp > 0) { - out.add(Contribution("vo2max", "Cardio fitness", ((exp - vo2) / 3.5).coerceIn(-4.0, 4.0) * 0.130)) + out.add(Contribution("vo2max", Fr.tr("Cardio fitness"), ((exp - vo2) / 3.5).coerceIn(-4.0, 4.0) * 0.130)) } inputs.sleepHours?.let { val dev = maxOf(0.0, abs(it - 7.5) - 0.5) - out.add(Contribution("sleep", "Sleep duration", dev.coerceIn(0.0, 3.0) * 0.110)) + out.add(Contribution("sleep", Fr.tr("Sleep duration"), dev.coerceIn(0.0, 3.0) * 0.110)) } inputs.sleepConsistency?.let { - out.add(Contribution("consistency", "Sleep regularity", (0.75 - it.coerceIn(0.0, 1.0)) * 0.450)) + out.add(Contribution("consistency", Fr.tr("Sleep regularity"), (0.75 - it.coerceIn(0.0, 1.0)) * 0.450)) } val h = inputs.rmssd; val norm = inputs.rmssdNorm if (h != null && norm != null && norm > 0) { - out.add(Contribution("hrv", "Heart-rate variability", ((norm - h) / norm).coerceIn(-1.0, 1.0) * 0.160)) + out.add(Contribution("hrv", Fr.tr("Heart-rate variability"), ((norm - h) / norm).coerceIn(-1.0, 1.0) * 0.160)) } inputs.steps?.let { val deficit = (7000 - it.coerceIn(0.0, 11000.0)) / 1000 - out.add(Contribution("steps", "Daily steps", deficit.coerceIn(-4.0, 4.0) * 0.064)) + out.add(Contribution("steps", Fr.tr("Daily steps"), deficit.coerceIn(-4.0, 4.0) * 0.064)) } return out } diff --git a/android/app/src/main/java/com/noop/analytics/WeeklyDigest.kt b/android/app/src/main/java/com/noop/analytics/WeeklyDigest.kt index 6a71e8da3..fd9db8f1d 100644 --- a/android/app/src/main/java/com/noop/analytics/WeeklyDigest.kt +++ b/android/app/src/main/java/com/noop/analytics/WeeklyDigest.kt @@ -1,5 +1,6 @@ package com.noop.analytics +import com.noop.i18n.Fr import kotlin.math.abs import kotlin.math.roundToInt import kotlin.math.sqrt @@ -37,19 +38,19 @@ enum class WeeklyMetric(val key: String) { /** Human label (matches the rest of the app's naming). */ val label: String get() = when (this) { - CHARGE -> "Charge" - EFFORT -> "Effort" - REST -> "Rest" - RHR -> "Resting HR" - HRV -> "HRV" + CHARGE -> Fr.tr("Charge") + EFFORT -> Fr.tr("Effort") + REST -> Fr.tr("Rest") + RHR -> Fr.tr("Resting HR") + HRV -> Fr.tr("HRV") } /** Display unit suffix (empty for the unitless 0–100 scores). */ val unit: String get() = when (this) { CHARGE, EFFORT, REST -> "" - RHR -> "bpm" - HRV -> "ms" + RHR -> Fr.tr("bpm") + HRV -> Fr.tr("ms") } /** True when a HIGHER value is the better outcome. Resting HR is the lone exception. */ @@ -153,13 +154,13 @@ enum class BalanceRead { val sentence: String get() = when (this) { OVERREACHING -> - "Your Effort outpaced your Charge this week: you leaned into the red. Watch for a recovery dip." + Fr.tr("Your Effort outpaced your Charge this week: you leaned into the red. Watch for a recovery dip.") BALANCED -> - "Effort and Charge tracked together this week: a sustainable load." + Fr.tr("Effort and Charge tracked together this week: a sustainable load.") UNDERLOADED -> - "You carried more Charge than you spent this week: there's room to push if you want it." + Fr.tr("You carried more Charge than you spent this week: there's room to push if you want it.") INSUFFICIENT -> - "Not enough Effort and Charge days this week to read your balance." + Fr.tr("Not enough Effort and Charge days this week to read your balance.") } } @@ -370,21 +371,23 @@ object WeeklyDigestEngine { val currentDays = summaries.maxOfOrNull { it.weekOverWeek.current.n } ?: 0 val prevDays = summaries.maxOfOrNull { it.weekOverWeek.previous.n } ?: 0 if (currentDays in 1 until MIN_DAYS_FOR_FOCUS) { - val dayWord = if (currentDays == 1) "day" else "days" + val dayWord = if (currentDays == 1) Fr.tr("day") else Fr.tr("days") lines.add( - "Only $currentDays $dayWord into this week so far, too early to " + - "call a week-over-week trend yet.", + "${Fr.tr("Only")} $currentDays $dayWord " + + Fr.tr("into this week so far, too early to call a week-over-week trend yet."), ) } else if (currentDays >= MIN_DAYS_FOR_FOCUS && prevDays in 1 until MIN_DAYS_FOR_FOCUS) { - val dayWord = if (prevDays == 1) "day" else "days" + val dayWord = if (prevDays == 1) Fr.tr("day") else Fr.tr("days") lines.add( - "Last week only had $prevDays $dayWord of data, so week-over-week " + - "changes are rough, not a trend.", + "${Fr.tr("Last week only had")} $prevDays $dayWord " + + Fr.tr("of data, so week-over-week changes are rough, not a trend."), ) } else if (consistencySD != null && consistencySD <= 6.0) { - lines.add("A steady week: Rest held even (±${round1(consistencySD)} pts) and nothing moved much.") + lines.add( + "${Fr.tr("A steady week: Rest held even (±")}${round1(consistencySD)}${Fr.tr(" pts) and nothing moved much.")}", + ) } else { - lines.add("A steady week: no metric moved meaningfully from last week.") + lines.add(Fr.tr("A steady week: no metric moved meaningfully from last week.")) } } @@ -398,22 +401,23 @@ object WeeklyDigestEngine { */ private fun moverSentence(s: WeeklyMetricSummary, effortDisplayFactor: Double = 1.0): String { val f = if (s.metric == WeeklyMetric.EFFORT) effortDisplayFactor else 1.0 - val directionWord = if (s.wowDelta > 0) "up" else if (s.wowDelta < 0) "down" else "flat" + val directionWord = if (s.wowDelta > 0) Fr.tr("up") else if (s.wowDelta < 0) Fr.tr("down") else Fr.tr("flat") val pct = s.weekOverWeek.pctChange val magnitude = if (pct != null && abs(pct) >= 1) { "${abs(pct).roundToInt()}%" } else { - val suffix = if (s.metric.unit.isEmpty()) " pts" else " ${s.metric.unit}" + val suffix = if (s.metric.unit.isEmpty()) " ${Fr.tr("pts")}" else " ${s.metric.unit}" "${round1(abs(s.wowDelta) * f)}$suffix" } val frame = when (s.wowGoodness) { - 1 -> ", a good sign" - -1 -> ", worth a look" + 1 -> ", ${Fr.tr("a good sign")}" + -1 -> ", ${Fr.tr("worth a look")}" else -> "" } val thisAvg = (s.thisWeek.mean * f).roundToInt() val lastAvg = (s.weekOverWeek.previous.mean * f).roundToInt() - return "${s.metric.label} is $directionWord $magnitude week over week (avg $thisAvg vs $lastAvg)$frame." + return "${s.metric.label} ${Fr.tr("is")} $directionWord $magnitude " + + "${Fr.tr("week over week (avg")} $thisAvg ${Fr.tr("vs")} $lastAvg)$frame." } // MARK: - Range extraction diff --git a/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt b/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt index b3d622fda..1a4aea641 100644 --- a/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt +++ b/android/app/src/main/java/com/noop/ui/AddDeviceWizard.kt @@ -567,7 +567,7 @@ fun AddDeviceWizard( title = { Text(tr("Make this your active device?"), style = NoopType.title2, color = Palette.textPrimary) }, text = { Text( - "Make $confirmName your active device now? It will provide your live data. You can change " + + "${tr("Make")} $confirmName ${tr("your active device now? It will provide your live data. You can change")} " + tr("this any time."), style = NoopType.subhead, color = Palette.textSecondary, @@ -890,7 +890,7 @@ private fun PrepStep(type: DeviceType, onScan: () -> Unit) { .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Palette.accent) - .semantics { contentDescription = "Scan for ${type.title}" }, + .semantics { contentDescription = "${tr("Scan for")} ${type.title}" }, ) { Text(tr("Scan"), style = NoopType.headline, color = Palette.goldDeepText) } @@ -1615,7 +1615,7 @@ private fun DiscoveredRow(name: String, subtitle: String, rssi: Int, onTap: () - .clip(RoundedCornerShape(12.dp)) .frostedCardSurface(cornerRadius = 12.dp) .clickable(onClick = onTap) - .semantics { contentDescription = "$name, signal ${SignalBars.level(rssi)} of 4" } + .semantics { contentDescription = "$name, ${tr("signal")} ${SignalBars.level(rssi)} ${tr("of")} 4" } .padding(14.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/android/app/src/main/java/com/noop/ui/DevicesScreen.kt b/android/app/src/main/java/com/noop/ui/DevicesScreen.kt index 2484b0db1..592dc03cd 100644 --- a/android/app/src/main/java/com/noop/ui/DevicesScreen.kt +++ b/android/app/src/main/java/com/noop/ui/DevicesScreen.kt @@ -204,8 +204,8 @@ fun DevicesScreen( switchTarget?.let { device -> ConfirmDialog( title = tr("Make this your active strap?"), - message = "Make ${displayName(device)} your active strap? From now on it provides your live data. " + - "$currentActiveName's history stays exactly as it is. Only new days come from ${displayName(device)}.", + message = "${tr("Make")} ${displayName(device)} ${tr("your active strap? From now on it provides your live data.")} " + + "$currentActiveName${tr("'s history stays exactly as it is. Only new days come from")} ${displayName(device)}.", confirmLabel = tr("Make active"), onConfirm = { scope.launch { viewModel.setActiveDevice(device.id); reload() } @@ -231,7 +231,7 @@ fun DevicesScreen( removeTarget?.let { device -> ConfirmDialog( title = tr("Remove this device?"), - message = "Remove ${displayName(device)}? NOOP will stop connecting to it. Its recorded data is " + + message = "${tr("Remove")} ${displayName(device)}? ${tr("NOOP will stop connecting to it. Its recorded data is")} " + tr("kept and you can re-add it any time."), confirmLabel = tr("Remove"), destructive = true, @@ -256,7 +256,7 @@ fun DevicesScreen( deleteDataTarget?.let { device -> ConfirmDialog( title = tr("Delete all of this device's data?"), - message = "This permanently deletes all data recorded from ${displayName(device)}. This can't be undone.", + message = "${tr("This permanently deletes all data recorded from")} ${displayName(device)}. ${tr("This can't be undone.")}", confirmLabel = tr("Delete data"), destructive = true, onConfirm = { @@ -318,7 +318,7 @@ private fun DeviceCard( .clickable( interactionSource = interaction, indication = null, - onClickLabel = "Device actions for ${displayName(device)}", + onClickLabel = "${tr("Device actions for")} ${displayName(device)}", ) { menuOpen = true } // The ACTIVE device is the hero: the liquid translucent-black frosted card (rgba(13,14,20,.80), radius @@ -435,7 +435,7 @@ private fun BatteryTube(pct: Int) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.semantics { contentDescription = "Battery $clamped%" }, + modifier = Modifier.semantics { contentDescription = "${tr("Battery")} $clamped%" }, ) { Text(tr("Battery"), style = NoopType.footnote, color = Palette.textTertiary) LiquidTube( @@ -481,7 +481,7 @@ private fun DeviceActionsMenu( onClick = { onOpenChange(true) }, modifier = Modifier .size(32.dp) - .semantics { contentDescription = "Device actions for ${displayName(device)}" }, + .semantics { contentDescription = "${tr("Device actions for")} ${displayName(device)}" }, ) { Icon(Icons.Filled.MoreVert, contentDescription = null, tint = Palette.textSecondary, modifier = Modifier.size(20.dp)) } @@ -899,7 +899,7 @@ private fun OuraLocalStateNote() { private fun lastSeenLine(device: PairedDeviceRow, isLiveConnected: Boolean): String = when { device.status == DeviceStatus.archived.name -> tr("Removed · data kept") isLiveConnected -> tr("Connected now") - else -> "Last seen ${relativeAgo(device.lastSeenAt)}" + else -> "${tr("Last seen")} ${relativeAgo(device.lastSeenAt)}" } /** Best-effort brand from the advertised name. Falls back to a neutral label. Mirrors Swift brandGuess. */ @@ -913,6 +913,6 @@ internal fun brandGuess(name: String): String { lower.contains("scosche") || lower.contains("rhythm") -> "Scosche" lower.contains("magene") -> "Magene" lower.contains("amazfit") || lower.contains("helio") || lower.contains("zepp") -> "Amazfit" - else -> "Heart-rate strap" + else -> tr("Heart-rate strap") } } diff --git a/android/app/src/main/java/com/noop/ui/HealthScreen.kt b/android/app/src/main/java/com/noop/ui/HealthScreen.kt index f5b65a693..4dd52bebb 100644 --- a/android/app/src/main/java/com/noop/ui/HealthScreen.kt +++ b/android/app/src/main/java/com/noop/ui/HealthScreen.kt @@ -716,7 +716,7 @@ private fun VitalityHero( ) Text( if (delta == 0) tr("about your age") - else "${kotlin.math.abs(delta)} ${yearWord(delta)} ${if (younger) "younger" else "older"}", + else "${kotlin.math.abs(delta)} ${yearWord(delta)} ${if (younger) tr("younger") else tr("older")}", style = NoopType.footnote, color = if (delta == 0) Palette.textSecondary else if (younger) Palette.statusPositive else Palette.statusWarning, @@ -816,8 +816,8 @@ private fun FitnessAgeHero( val younger = fitnessAge < chronoAge val deltaWord = when { deltaYears == 0 -> tr("About your age") - younger -> "$deltaYears ${yearWord(deltaYears)} younger than your age" - else -> "${kotlin.math.abs(deltaYears)} ${yearWord(deltaYears)} older than your age" + younger -> "$deltaYears ${yearWord(deltaYears)} ${tr("younger than your age")}" + else -> "${kotlin.math.abs(deltaYears)} ${yearWord(deltaYears)} ${tr("older than your age")}" } // Vessel fill: a bounded, honest reading of the SAME younger/older signal the card already states, // mapped across the ±5 yr band the section advertises — "about your age" is half-full, younger fills @@ -997,7 +997,7 @@ private fun ReadinessRow(item: FitnessReadinessItem) { } } -private fun yearWord(years: Int): String = if (kotlin.math.abs(years) == 1) "year" else "years" +private fun yearWord(years: Int): String = if (kotlin.math.abs(years) == 1) tr("year") else tr("years") @Composable fun VitalSignsScreen(vm: AppViewModel, onVitalClick: (String) -> Unit = {}) { @@ -1276,14 +1276,14 @@ private fun HeartRateSection(vm: AppViewModel, hrMax: Int) { private fun zoneLabel(hasLiveHr: Boolean, zone: Int, fraction: Double): String { if (!hasLiveHr) return tr("Idle") - return "Zone $zone · ${(fraction * 100).roundToInt()}%" + return "${tr("Zone")} $zone · ${(fraction * 100).roundToInt()}%" } @Composable private fun HeartRateFooter(zone: String, percentMax: String, maxHr: String, state: String) { Row(modifier = Modifier.fillMaxWidth().padding(top = Metrics.space4)) { FooterStat(tr("Zone"), zone, Modifier.weight(1f)) - FooterStat("% Max", percentMax, Modifier.weight(1f)) + FooterStat(tr("% Max"), percentMax, Modifier.weight(1f)) FooterStat(tr("Max HR"), maxHr, Modifier.weight(1f)) FooterStat(tr("State"), state, Modifier.weight(1f)) } @@ -1569,7 +1569,7 @@ private fun vitalsFor( fun rangeCaption(allValues: List, unit: String, format: (Double) -> String): String? { val min = allValues.minOrNull() ?: return null val max = allValues.maxOrNull() ?: return null - return "within ${format(min)} -- ${format(max)} $unit" + return "${tr("within")} ${format(min)} -- ${format(max)} $unit" } // Trailing values (oldest → newest) feeding each tile's sparkline trail. Built from the same // history already gathered for banding, including the displayed day's value. Presentation-only. @@ -1836,7 +1836,7 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { var range by remember { mutableStateOf(VitalDetailRange.MONTH) } ScreenScaffold( - title = detail?.title ?: "Vital Signs", + title = detail?.title ?: tr("Vital Signs"), subtitle = tr("Historical trend from cached daily metrics."), ) { if (isSeriesBacked && !seriesLoaded) { @@ -1874,7 +1874,7 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { val max = values.maxOrNull() val avg = values.average() - SectionHeader(detail.title, overline = tr("Vital Signs"), trailing = "${filteredPoints.size} readings") + SectionHeader(detail.title, overline = tr("Vital Signs"), trailing = "${filteredPoints.size} ${tr("readings")}") NoopCard { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.Top) { @@ -1886,7 +1886,7 @@ fun VitalDetailScreen(vm: AppViewModel, key: String) { color = detail.color, ) Text( - text = "as of ${latest.first}", + text = "${tr("as of")} ${latest.first}", style = NoopType.footnote, color = Palette.textTertiary, ) @@ -1984,12 +1984,12 @@ private fun missingVitalsTitle(offset: Int): String = when (offset) { private fun asOfLabel(day: String?): String? { if (day.isNullOrBlank()) return null - val date = runCatching { LocalDate.parse(day) }.getOrNull() ?: return "as of $day" + val date = runCatching { LocalDate.parse(day) }.getOrNull() ?: return "${tr("as of")} $day" val today = LocalDate.now() return when (date) { today -> tr("as of today") today.minusDays(1) -> tr("as of yesterday") - else -> "as of ${date.format(DateTimeFormatter.ofPattern("d MMM", Locale.US))}" + else -> "${tr("as of")} ${date.format(DateTimeFormatter.ofPattern("d MMM", Locale.US))}" } } diff --git a/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt b/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt index 0375aee08..308e37d8e 100644 --- a/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt +++ b/android/app/src/main/java/com/noop/ui/LiveWorkoutScreen.kt @@ -247,7 +247,7 @@ private fun HeroHeartRate(bpm: Int?, zone: Int) { } Text(tr("bpm"), style = NoopType.subhead, color = Palette.textSecondary) Text( - if (zone >= 1) "Zone $zone · ${zoneName(zone)}" else tr("Below Zone 1"), + if (zone >= 1) "${tr("Zone")} $zone · ${zoneName(zone)}" else tr("Below Zone 1"), style = NoopType.captionNumber, color = tint, textAlign = TextAlign.Center, @@ -291,7 +291,7 @@ private fun ZoneRail(zone: Int, zoneSet: com.noop.analytics.HrZoneSet) { val band = zoneSet.zones.firstOrNull { it.number == zone } Text( if (band != null) - "Zone $zone: ${band.lower.toInt()} - ${band.upper.toInt()} bpm (${(band.lowerPct * 100).toInt()} - ${(band.upperPct * 100).toInt()}% max HR)" + "${tr("Zone")} $zone: ${band.lower.toInt()} - ${band.upper.toInt()} ${tr("bpm")} (${(band.lowerPct * 100).toInt()} - ${(band.upperPct * 100).toInt()}% ${tr("max HR")})" else tr("Warming up - keep moving to climb into Zone 1."), style = NoopType.footnote, color = Palette.textTertiary, diff --git a/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt b/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt index c8907cc7e..715c0b00e 100644 --- a/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt +++ b/android/app/src/main/java/com/noop/ui/OnboardingScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -168,6 +169,11 @@ fun OnboardingScreen(viewModel: AppViewModel, onFinished: () -> Unit) { page = pageIndex + 1, total = pages.size, progress = (pageIndex + 1).toFloat() / pages.size.toFloat(), + // Lets someone who's already set NOOP up before (e.g. after a reinstall) skip + // straight past the walkthrough instead of re-doing every step. Same completion + // path as finishing normally; nothing forces a re-pair, since Devices/Settings + // cover pairing and permissions any time afterward. + onSkip = if (page != OnboardingPage.Done) { { complete() } } else null, ) Column( @@ -234,7 +240,7 @@ private enum class OnboardingPage(val cta: String) { // MARK: - Shell @Composable -private fun OnboardingTopBar(page: Int, total: Int, progress: Float) { +private fun OnboardingTopBar(page: Int, total: Int, progress: Float, onSkip: (() -> Unit)? = null) { val animated by animateFloatAsState( targetValue = progress.coerceIn(0f, 1f), animationSpec = tween(Motion.durationStandard), @@ -249,6 +255,19 @@ private fun OnboardingTopBar(page: Int, total: Int, progress: Float) { Overline("NOOP", color = Palette.accent) Spacer(Modifier.weight(1f)) Text("$page / $total", style = NoopType.captionNumber, color = Palette.textTertiary) + if (onSkip != null) { + Spacer(Modifier.width(12.dp)) + Text( + tr("Skip"), + style = NoopType.captionNumber, + color = Palette.accent, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onSkip, + ), + ) + } } Box( modifier = Modifier diff --git a/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt b/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt index a21b5aee8..e98817011 100644 --- a/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SkinTempCardsScreen.kt @@ -345,7 +345,7 @@ private fun headsUpConfidenceLine( ): String? { if (level != IllnessSignalEngine.Level.RAISED) return null val d = distance ?: return null - return "Confidence: ${illnessConfidenceBand(d.distance)} (distance ${illnessConfidenceFormatted(d.distance)})" + return "${tr("Confidence:")} ${illnessConfidenceBand(d.distance)} ${tr("(distance")} ${illnessConfidenceFormatted(d.distance)})" } /** @@ -354,9 +354,9 @@ private fun headsUpConfidenceLine( * moderate, else slight. Identical to the Swift twin (IllnessConfidence.band). */ private fun illnessConfidenceBand(distance: Double): String = when { - distance >= 3.5 -> "strong" - distance >= 2.5 -> "moderate" - else -> "slight" + distance >= 3.5 -> tr("strong") + distance >= 2.5 -> tr("moderate") + else -> tr("slight") } /** One-decimal display value for the distance, locale-independent. Mirrors iOS String(format: "%.1f"). */ @@ -416,8 +416,8 @@ private fun bodyClockOffsetTitle(e: CircadianEngine.PhaseEstimate): String { if (e.confidence == CircadianEngine.PhaseConfidence.UNREADABLE) return tr("Hard to read right now") val mins = abs(e.offsetVsScheduleMinutes).roundToInt() if (mins <= 20) return tr("About in sync with your schedule") - val dir = if (e.offsetVsScheduleMinutes > 0) "later" else "earlier" - return "About $mins min $dir than your schedule" + val dir = if (e.offsetVsScheduleMinutes > 0) tr("later") else tr("earlier") + return "${tr("About")} $mins ${tr("min")} $dir ${tr("than your schedule")}" } private fun bodyClockConfidenceLabel(c: CircadianEngine.PhaseConfidence): String = when (c) { diff --git a/android/app/src/main/java/com/noop/ui/SleepScreen.kt b/android/app/src/main/java/com/noop/ui/SleepScreen.kt index 76d2f7d53..3a63bbb1b 100644 --- a/android/app/src/main/java/com/noop/ui/SleepScreen.kt +++ b/android/app/src/main/java/com/noop/ui/SleepScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -77,6 +78,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.noop.analytics.AnalyticsEngine import com.noop.analytics.SleepDebt @@ -618,7 +620,7 @@ private fun SleepUndoBanner(session: SleepSession, onUndo: () -> Unit) { val message = if (session.userEdited) { tr("Sleep deleted.") } else { - "Sleep deleted. NOOP won't detect sleep between $startText and $endText again." + "${tr("Sleep deleted. NOOP won't detect sleep between")} $startText ${tr("and")} $endText ${tr("again.")}" } NoopCard(tint = Palette.restColor) { Row( @@ -815,8 +817,8 @@ private fun Hero( val inBedMin = session?.let { (it.endTs - it.effectiveStartTs) / 60.0 } ?: s.total ChartCard( title = tr("Stage breakdown"), - subtitle = "${durationText(inBedMin)} in bed · ${display.efficiencyText} efficiency" + - (if (display.realSegments != null) " · approx. stages (on-device)" else ""), + subtitle = "${durationText(inBedMin)} ${tr("in bed")} · ${display.efficiencyText} ${tr("efficiency")}" + + (if (display.realSegments != null) " · ${tr("approx. stages (on-device)")}" else ""), trailing = durationText(s.asleep), tint = Palette.restColor, footer = { @@ -998,9 +1000,9 @@ internal fun mainSleepReasonText(blocks: List, habitualMidsleepSec SleepStageTotals.MainNightReason.onlyBlock -> tr("This is your only sleep block today.") SleepStageTotals.MainNightReason.longest -> - "Picked as your main sleep because it was your longest block ($dur)." + "${tr("Picked as your main sleep because it was your longest block")} ($dur)." SleepStageTotals.MainNightReason.longestNearUsual -> - "Picked as your main sleep because it was your longest block ($dur), near your usual bedtime." + "${tr("Picked as your main sleep because it was your longest block")} ($dur), ${tr("near your usual bedtime.")}" SleepStageTotals.MainNightReason.alignedToUsual -> tr("Picked as your main sleep because it started near your usual sleep time.") } @@ -1010,7 +1012,18 @@ internal fun mainSleepReasonText(blocks: List, habitualMidsleepSec @Composable private fun NapSummaryCell(label: String, value: String, modifier: Modifier = Modifier) { Column(modifier = modifier) { - Text(label, style = NoopType.overline, color = Palette.textTertiary) + // A touch smaller than the shared Overline(): these three cells share a plain (non-intrinsic) + // Row via equal weight(1f), so a longer French label (e.g. "Main sleep" -> "Sommeil principal") + // wrapping to 2 lines would push just that cell's value down and misalign it against its + // neighbours. maxLines=1 + ellipsis is a fallback for the rare case the smaller size still isn't + // enough. Mirrors the SourceBadge shrink pattern (Components.kt). + Text( + label, + style = NoopType.overline.copy(fontSize = 10.sp, letterSpacing = 0.8.sp), + color = Palette.textTertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Text(value, style = NoopType.captionNumber, color = Palette.textPrimary) } } @@ -1047,7 +1060,7 @@ private fun NapRow( modifier = Modifier .weight(1f) .semantics(mergeDescendants = true) { - contentDescription = "Nap $window, ${durationText(durMin)}" + contentDescription = "${tr("Nap")} $window, ${durationText(durMin)}" }, verticalAlignment = Alignment.CenterVertically, ) { @@ -1121,7 +1134,7 @@ private fun NapRow( editingEnd = true }, startCal.get(Calendar.HOUR_OF_DAY), startCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Nap started") } + ).apply { setTitle(tr("Nap started")) } dialog.setOnDismissListener { editingStart = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1147,7 +1160,7 @@ private fun NapRow( pendingStart = 0L }, endCal.get(Calendar.HOUR_OF_DAY), endCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Nap ended") } + ).apply { setTitle(tr("Nap ended")) } dialog.setOnDismissListener { editingEnd = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1187,7 +1200,7 @@ private fun StageBreakdownRow(stage: String, minutes: Double, total: Double, col .fillMaxWidth() .semantics { contentDescription = - "$stage: ${durationText(minutes)}, $percent percent of the night" + "$stage: ${durationText(minutes)}, $percent ${tr("percent of the night")}" }, ) { Box( @@ -1201,7 +1214,11 @@ private fun StageBreakdownRow(stage: String, minutes: Double, total: Double, col style = NoopType.overline, color = Palette.textPrimary, maxLines = 1, - modifier = Modifier.width(56.dp), + // widthIn(min=) instead of a hard width(): the French stage names ("Éveillé" for Awake, + // "Profond" for Deep) run longer than the English ones this row was sized for, so let the + // label grow past the old 56dp floor rather than clip — the LiquidTube next to it (weight(1f)) + // absorbs the difference. + modifier = Modifier.widthIn(min = 56.dp), ) Text( "$percent%", @@ -1423,7 +1440,7 @@ private fun SleepWindowRow(session: SleepSession) { // same colour world as the rest of the screen. Bevel treatment — content unchanged. NoopCard( modifier = Modifier.semantics(mergeDescendants = true) { - contentDescription = "Fell asleep at $asleep, woke at $woke" + contentDescription = "${tr("Fell asleep at")} $asleep, ${tr("woke at")} $woke" }, padding = Metrics.space14, tint = Palette.restColor, @@ -1596,7 +1613,7 @@ private fun NightNavHeader( startCal.get(Calendar.HOUR_OF_DAY), startCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Bedtime") } + ).apply { setTitle(tr("Bedtime")) } dialog.setOnDismissListener { editingBed = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1635,7 +1652,7 @@ private fun NightNavHeader( endCal.get(Calendar.HOUR_OF_DAY), endCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Wake-up time") } + ).apply { setTitle(tr("Wake-up time")) } dialog.setOnDismissListener { editingWake = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1695,7 +1712,7 @@ private fun NightNavHeader( startCal.get(Calendar.HOUR_OF_DAY), startCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Nap started") } + ).apply { setTitle(tr("Nap started")) } dialog.setOnDismissListener { addingNapStart = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1725,7 +1742,7 @@ private fun NightNavHeader( endCal.get(Calendar.HOUR_OF_DAY), endCal.get(Calendar.MINUTE), true, - ).apply { setTitle("Nap ended") } + ).apply { setTitle(tr("Nap ended")) } dialog.setOnDismissListener { addingNapEnd = false } dialog.show() onDispose { runCatching { dialog.dismiss() } } @@ -1765,7 +1782,7 @@ private fun NightNavHeader( val nightLabel = when (offset) { 0 -> tr("Last night") 1 -> tr("1 night ago") - else -> "$offset nights ago" + else -> "$offset ${tr("nights ago")}" } val blockShape = RoundedCornerShape(Metrics.cornerSm) val clockParts = clock?.split(" · ", limit = 2) @@ -2075,7 +2092,7 @@ private fun DebtDeltaBars(ledger: SleepDebtLedger) { .height(56.dp) .semantics { contentDescription = - "Per-night sleep balance: ${ledger.nightCount} nights, net ${debtSigned(ledger.balanceMin)}" + "${tr("Per-night sleep balance:")} ${ledger.nightCount} ${tr("nights, net")} ${debtSigned(ledger.balanceMin)}" } .drawBehind { val n = max(deltas.size, 1) @@ -2141,7 +2158,7 @@ private fun StageRow(label: String, last: Double, typical: Double?, color: Color } else { val diff = last - typical val sign = if (diff >= 0) "+" else "−" - "$sign${durationText(abs(diff))} vs typ" + "$sign${durationText(abs(diff))} ${tr("vs typ")}" } } Column(verticalArrangement = Arrangement.spacedBy(Metrics.space6)) { @@ -2166,7 +2183,7 @@ private fun StageRow(label: String, last: Double, typical: Double?, color: Color .height(Metrics.progressHeight) .clip(RoundedCornerShape(Metrics.cornerPill)) .background(Palette.surfaceInset) - .semantics { contentDescription = "$label minutes vs your typical bar" } + .semantics { contentDescription = "$label ${tr("minutes vs your typical bar")}" } .drawBehind { // last-night fill if (fillFrac > 0f) { @@ -2209,7 +2226,7 @@ private fun DurationTrend(m: SleepModel) { ChartCard( title = tr("Hours asleep"), subtitle = tr("Per night, trailing 14 days"), - trailing = avg?.let { String.format(Locale.US, "%.1f h avg", it) }, + trailing = avg?.let { "${String.format(Locale.US, "%.1f h", it)} ${tr("avg")}" }, tint = Palette.restColor, footer = { ChartFooter( @@ -2364,7 +2381,17 @@ private fun ChartFooter(items: List>) { Row(modifier = Modifier.fillMaxWidth()) { items.forEach { (label, value) -> Column(modifier = Modifier.weight(1f)) { - Overline(label, color = Palette.textTertiary) + // A touch smaller than the shared Overline(): these columns share a plain (non-intrinsic) + // Row via equal weight(1f), so a longer French label (e.g. "Per-night need") wrapping to + // a second line would push just that column's value down, misaligning it against its + // neighbours — the same #406 shape, now driven by translation length instead of digits. + Text( + label.uppercase(), + style = NoopType.overline.copy(fontSize = 10.sp, letterSpacing = 0.8.sp), + color = Palette.textTertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) // Stage-breakdown values like "1h 23m (24%)" wrapped to a second line in a narrow column, // pushing the row taller and clipping against the card edge (#406). Hold them to one line. Text( @@ -2406,7 +2433,19 @@ private fun SparkTile( } NoopCard(modifier = clickMod, padding = Metrics.space14) { Column(modifier = Modifier.fillMaxWidth()) { - Overline(label) + // A slightly smaller/tighter overline than the shared Overline() default: this tile's card + // height is FIXED (Metrics.tileHeight), and the longer French renderings of these labels + // (e.g. "Sleep Debt" -> "Dette de sommeil") could otherwise wrap to a second line and squeeze + // the value/caption below it. maxLines=1 + ellipsis is a belt-and-braces fallback so an + // extreme case (huge accessibility font scale) truncates cleanly instead of breaking the + // fixed-height grid. Mirrors the SourceBadge shrink pattern (Components.kt). + Text( + label.uppercase(), + style = NoopType.overline.copy(fontSize = 10.sp, letterSpacing = 0.8.sp), + color = Palette.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Spacer(Modifier.weight(1f)) Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) { Column(modifier = Modifier.weight(1f)) { @@ -3036,7 +3075,7 @@ private fun vsTypical(latest: Double?, typical: Double?, suffix: String, decimal val sign = if (diff >= 0) "+" else "−" val mag = abs(diff) val num = if (decimals == 0) "${mag.roundToInt()}" else String.format(Locale.US, "%.${decimals}f", mag) - return "$sign$num$suffix vs typical" + return "$sign$num$suffix ${tr("vs typical")}" } private fun debtCaption(debt: Double?): String { @@ -3071,15 +3110,15 @@ private fun debtTag(ledger: SleepDebtLedger): String = when { /** Plain-English read of the running balance over the window. */ private fun debtRead(ledger: SleepDebtLedger): String { val nights = ledger.nightCount - val span = "the last $nights night${if (nights == 1) "" else "s"}" + val span = "${tr("the last")} $nights ${tr("night")}${if (nights == 1) "" else "s"}" if (ledger.magnitudeMin < SleepDebt.ON_TARGET_BAND_MIN) { - return "You're roughly on top of your sleep across $span. Slept minutes balance out against your need." + return "${tr("You're roughly on top of your sleep across")} $span. ${tr("Slept minutes balance out against your need.")}" } val mag = durationText(ledger.magnitudeMin) return if (ledger.isDebt) { - "You've banked about $mag of sleep debt over $span. Surplus nights count back against it. An earlier night or two would clear it." + "${tr("You've banked about")} $mag ${tr("of sleep debt over")} $span. ${tr("Surplus nights count back against it. An earlier night or two would clear it.")}" } else { - "You're carrying about $mag of surplus over $span. You've slept past your need on balance. Nicely ahead." + "${tr("You're carrying about")} $mag ${tr("of surplus over")} $span. ${tr("You've slept past your need on balance. Nicely ahead.")}" } } @@ -3224,7 +3263,7 @@ internal fun HoursVsNeededCard(m: SleepModel) { .height(Metrics.progressHeight) .clip(RoundedCornerShape(Metrics.cornerPill)) .background(Palette.surfaceInset) - .semantics { contentDescription = "Hours vs Needed progress bar, ${score.roundToInt()} percent" }, + .semantics { contentDescription = "${tr("Hours vs Needed progress bar")}, ${score.roundToInt()} ${tr("percent")}" }, ) { Box( modifier = Modifier @@ -3426,15 +3465,15 @@ internal fun SleepConsistencyCard(sleeps: List) { } Row(horizontalArrangement = Arrangement.spacedBy(Metrics.space14)) { - LegendDot("Typical bedtime $typicalBedLabel", Palette.metricPurple) - LegendDot("Wake $typicalWakeLabel", Palette.restColor) + LegendDot("${tr("Typical bedtime")} $typicalBedLabel", Palette.metricPurple) + LegendDot("${tr("Wake")} $typicalWakeLabel", Palette.restColor) } Hairline() Row(modifier = Modifier.fillMaxWidth()) { listOf( tr("Score") to "${consistencyPct.roundToInt()}%", - tr("Typical") to "${((bedSdH + wakeSdH) / 2f * 60f).roundToInt()} min SD", + tr("Typical") to "${((bedSdH + wakeSdH) / 2f * 60f).roundToInt()} ${tr("min SD")}", tr("Nights") to "${recent.size}", ).forEach { (lbl, v) -> Column(modifier = Modifier.weight(1f)) { @@ -3565,9 +3604,9 @@ private fun SleepMetricDetailSheetContent(vm: AppViewModel, key: String) { Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Overline("Sleep · ${filteredPoints.size} nights") + Overline("${tr("Sleep")} · ${filteredPoints.size} ${tr("nights")}") Text(spec.title, style = NoopType.title2, color = Palette.textPrimary) - Text("as of ${latest.first}", style = NoopType.footnote, color = Palette.textTertiary) + Text("${tr("as of")} ${latest.first}", style = NoopType.footnote, color = Palette.textTertiary) } Text( "${spec.format(latest.second)} ${spec.unit}".trim(), @@ -3596,7 +3635,7 @@ private fun SleepMetricDetailSheetContent(vm: AppViewModel, key: String) { LineChart( values = values, modifier = Modifier.weight(1f).height(Metrics.chartHeight) - .semantics { contentDescription = "${spec.title} trend chart" }, + .semantics { contentDescription = "${spec.title} ${tr("trend chart")}" }, color = spec.color, fill = true, selectionEnabled = true, diff --git a/android/app/src/main/java/com/noop/ui/StressScreen.kt b/android/app/src/main/java/com/noop/ui/StressScreen.kt index 07458d3fc..70b66887a 100644 --- a/android/app/src/main/java/com/noop/ui/StressScreen.kt +++ b/android/app/src/main/java/com/noop/ui/StressScreen.kt @@ -806,7 +806,7 @@ private fun TimeInBandRow(band: StressTotalsBand, hours: Int, total: Double) { private fun timelineTrailing(day: DaytimeStress.Result): String { val n = day.scored.size val mean = day.dayMean ?: return "${n}h" - return "avg " + String.format(Locale.US, "%.1f", mean) + " · ${n}h" + return tr("avg ") + String.format(Locale.US, "%.1f", mean) + " · ${n}h" } /** @@ -826,8 +826,8 @@ private fun SustainedBreatheCard(day: DaytimeStress.Result, onBreathe: () -> Uni StatePill("${day.sustainedRun}h elevated", tone = StrandTone.Warning, showsDot = true) } Text( - "Your last ${day.sustainedRun} hours have stayed in the high band. A few minutes " + - "of paced breathing can help downshift your nervous system.", + "${tr("Your last")} ${day.sustainedRun} ${tr("hours have stayed in the high band. A few minutes ")}" + + tr("of paced breathing can help downshift your nervous system."), style = NoopType.subhead, color = Palette.textSecondary, ) @@ -961,7 +961,7 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { - Overline("Stress · ${range.label}") + Overline("${tr("Stress")} · ${range.label}") Text( tr("Daily 0-3 proxy"), style = NoopType.footnote, @@ -969,7 +969,7 @@ private fun StressTrendSection(model: StressModel, modifier: Modifier = Modifier ) } Text( - "avg " + String.format(Locale.US, "%.1f", avg), + tr("avg ") + String.format(Locale.US, "%.1f", avg), style = NoopType.captionNumber, color = Palette.textSecondary, ) @@ -1258,7 +1258,7 @@ internal class StressModel private constructor( val calm = recent.count { it.value < 1.0 } val pct = (calm.toDouble() / recent.size * 100).roundToInt() calmValue = "$pct%" - calmCaption = "low-stress days · ${recent.size}d" + calmCaption = "${tr("low-stress days")} · ${recent.size}d" } return StressModel( @@ -1320,7 +1320,7 @@ internal class StressModel private constructor( else -> tr("Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day.") } StressBand.Medium -> when { - rhrUp || hrvDn -> "Slightly off baseline (${if (rhrUp) "resting HR is a touch high" else "HRV is a little low"}), so you're moderately activated. Nothing alarming; just don't overreach." + rhrUp || hrvDn -> "${tr("Slightly off baseline (")}${if (rhrUp) tr("resting HR is a touch high") else tr("HRV is a little low")}${tr("), so you're moderately activated. Nothing alarming; just don't overreach.")}" else -> tr("You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day.") } StressBand.Low -> when { diff --git a/android/app/src/main/java/com/noop/ui/TodayScreen.kt b/android/app/src/main/java/com/noop/ui/TodayScreen.kt index cfbe90c9b..3862485bf 100644 --- a/android/app/src/main/java/com/noop/ui/TodayScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TodayScreen.kt @@ -1600,14 +1600,14 @@ private fun LiveSessionEntryCard(onOpen: () -> Unit) { } val teal = Palette.metricCyan val title = when { - running -> "Session running" - summaryWaiting -> "Session ended" - else -> "Start session" + running -> tr("Session running") + summaryWaiting -> tr("Session ended") + else -> tr("Start session") } val detail = when { - running -> "Guarding — silence means you're on track." - summaryWaiting -> "See the summary of your last session." - else -> "Strap-guided effort session. It only buzzes when you drift off today's band." + running -> tr("Guarding — silence means you're on track.") + summaryWaiting -> tr("See the summary of your last session.") + else -> tr("Strap-guided effort session. It only buzzes when you drift off today's band.") } // liquidPress on the whole tappable card (same interactionSource on clickable + press), matching the @@ -3132,7 +3132,7 @@ private fun DashboardCardRow( } // iOS row padding: 14h / 11v (tighter than the old 13/11 icon-box row). .padding(horizontal = 14.dp, vertical = 11.dp) - .semantics { contentDescription = "${card.title}: $value" }, + .semantics { contentDescription = "${tr(card.title)}: $value" }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -3151,14 +3151,14 @@ private fun DashboardCardRow( ) { // iOS: overline 11 / +1.0 tracking, textPrimary. Text( - card.title.uppercase(), + tr(card.title).uppercase(), style = NoopType.overline.copy(fontSize = 11.sp, letterSpacing = 1.0.sp), color = Palette.textPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Text( - card.subtitle, + tr(card.subtitle), style = NoopType.caption, color = Palette.textTertiary, maxLines = 1, @@ -3280,11 +3280,11 @@ private fun DashboardCardsEditorDialog( uncheckedTrackColor = Palette.surfaceInset, uncheckedBorderColor = Palette.hairline, ), - modifier = Modifier.semantics { contentDescription = "Show ${item.card.title}" }, + modifier = Modifier.semantics { contentDescription = "${tr("Show")} ${tr(item.card.title)}" }, ) Spacer(Modifier.width(12.dp)) Text( - item.card.title, + tr(item.card.title), style = NoopType.body, color = if (item.enabled) Palette.textPrimary else Palette.textTertiary, modifier = Modifier.weight(1f), @@ -3296,7 +3296,7 @@ private fun DashboardCardsEditorDialog( ) { Icon( Icons.Filled.KeyboardArrowUp, - contentDescription = "Move ${item.card.title} up", + contentDescription = "${tr("Move")} ${tr(item.card.title)} ${tr("up")}", tint = if (index > 0) Palette.textSecondary else Palette.textTertiary, modifier = Modifier.size(Metrics.iconSmall), ) @@ -3308,7 +3308,7 @@ private fun DashboardCardsEditorDialog( ) { Icon( Icons.Filled.KeyboardArrowDown, - contentDescription = "Move ${item.card.title} down", + contentDescription = "${tr("Move")} ${tr(item.card.title)} ${tr("down")}", tint = if (index < items.lastIndex) Palette.textSecondary else Palette.textTertiary, modifier = Modifier.size(Metrics.iconSmall), ) @@ -3804,7 +3804,7 @@ internal fun freshRestScore( * "Latest sleep · " so a weeks-old import is never surfaced as "Last night". Shared by every carried * recovery read-out so the prior-day provenance reads identically. Mirrors iOS carriedCaption. */ internal fun carriedCaption(priorDayKey: String, today: String = LocalDate.now().toString()): String { - val prefix = if (isCarryStale(priorDayKey, today)) "Latest sleep" else "Last night" + val prefix = if (isCarryStale(priorDayKey, today)) tr("Latest sleep") else tr("Last night") return "$prefix · ${lastChargeDateLabel(priorDayKey)}" } @@ -3848,9 +3848,9 @@ sealed class ScoreState { val title: String get() = when (this) { is Scored -> "" - is Calibrating -> "Calibrating" - is CarriedLastNight -> if (stale) "Latest sleep · $dateLabel" else "Last night · $dateLabel" - NeedsStrap -> "Needs the strap" + is Calibrating -> tr("Calibrating") + is CarriedLastNight -> if (stale) "${tr("Latest sleep")} · $dateLabel" else "${tr("Last night")} · $dateLabel" + NeedsStrap -> tr("Needs the strap") } /** The one-line plain-English what-to-do. VERBATIM, mirror Swift exactly. The night(s) plural in @@ -3859,15 +3859,15 @@ sealed class ScoreState { get() = when (this) { is Scored -> "" is Calibrating -> { - val nights = if (nightsRemaining == 1) "night" else "nights" - "Building your baseline. About $nightsRemaining more $nights until your scores are personal." + val nights = if (nightsRemaining == 1) tr("night") else tr("nights") + "${tr("Building your baseline. About")} $nightsRemaining ${tr("more")} $nights ${tr("until your scores are personal.")}" } is CarriedLastNight -> // A fresh post-rollover carry tells you tonight's score is on its way; a stale carry (an // older import, #779) instead explains the number is from that earlier session, not today. - if (stale) "This is your last scored session. Wear the strap overnight for a fresh score." - else "Tonight's lands after you sleep with the strap on." - NeedsStrap -> "No data for today. Was your strap worn and connected overnight?" + if (stale) tr("This is your last scored session. Wear the strap overnight for a fresh score.") + else tr("Tonight's lands after you sleep with the strap on.") + NeedsStrap -> tr("No data for today. Was your strap worn and connected overnight?") } } @@ -3964,19 +3964,19 @@ sealed class RecordingState { /** The chip's status word. VERBATIM, mirror Swift exactly. */ val title: String get() = when (this) { - Recording -> "Recording" - is LastSynced -> "Last synced ${minutesAgo}m ago" - NotRecording -> "Not recording" - HistoryExperimental -> "Connected" + Recording -> tr("Recording") + is LastSynced -> "${tr("Last synced")} ${minutesAgo}${tr("m ago")}" + NotRecording -> tr("Not recording") + HistoryExperimental -> tr("Connected") } /** The chip's one-line detail. VERBATIM, mirror Swift exactly. */ val detail: String get() = when (this) { - Recording -> "Your strap is connected and saving data." - is LastSynced -> "Reconnect to pull the latest." - NotRecording -> "Strap not connected. Tap to connect." - HistoryExperimental -> "History sync is experimental on 5.0." + Recording -> tr("Your strap is connected and saving data.") + is LastSynced -> tr("Reconnect to pull the latest.") + NotRecording -> tr("Strap not connected. Tap to connect.") + HistoryExperimental -> tr("History sync is experimental on 5.0.") } /** Chip hue: live recording reads positive (gold/green dot), a stale-but-recent sync reads neutral, @@ -4580,9 +4580,9 @@ private fun HeartRateTrendCard( // narrowing, no re-read), so the resolution half of the label never changes — only // the span half tells the truth about what's on screen. val subtitle = when { - selectedDay != today -> "5-minute average | selected day" - hrWindow == HrWindow.TODAY -> "5-minute average | since midnight" - else -> "5-minute average | last ${hrWindow.label}" + selectedDay != today -> "${tr("5-minute average")} | ${tr("selected day")}" + hrWindow == HrWindow.TODAY -> "${tr("5-minute average")} | ${tr("since midnight")}" + else -> "${tr("5-minute average")} | ${tr("last")} ${tr(hrWindow.label)}" } Text( subtitle, @@ -5413,10 +5413,10 @@ private fun ReadinessSection(days: List, carriedDay: DailyMetric? = * the Swift TodayView.readinessWord. */ internal fun readinessWord(level: ReadinessEngine.Level): String? = when (level) { - ReadinessEngine.Level.PRIMED -> "Push" - ReadinessEngine.Level.BALANCED -> "Maintain" - ReadinessEngine.Level.STRAINED -> "Rest" - ReadinessEngine.Level.RUNDOWN -> "Rest" + ReadinessEngine.Level.PRIMED -> tr("Push") + ReadinessEngine.Level.BALANCED -> tr("Maintain") + ReadinessEngine.Level.STRAINED -> tr("Rest") + ReadinessEngine.Level.RUNDOWN -> tr("Rest") ReadinessEngine.Level.INSUFFICIENT -> null } @@ -5431,7 +5431,7 @@ internal fun syncedFromSummary(hasWhoop: Boolean, hasApple: Boolean, hasXiaomi: if (hasApple) add("Apple Watch") if (hasXiaomi) add("Mi Band") } - return if (names.isEmpty()) "No sources yet" else "Synced from: " + names.joinToString(", ") + return if (names.isEmpty()) tr("No sources yet") else "${tr("Synced from:")} " + names.joinToString(", ") } /** S5: the Key-Metric overflow cap, mirroring TodayView.metricsCollapsedCap (two columns, three rows). */ @@ -5635,33 +5635,33 @@ private fun remember14(days: List, anchorDay: LocalDa private fun greetingWord(): String { val h = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) return when { - h < 12 -> "Good morning" - h < 17 -> "Good afternoon" - else -> "Good evening" + h < 12 -> tr("Good morning") + h < 17 -> tr("Good afternoon") + else -> tr("Good evening") } } private fun synthesisWord(score: Double?): String { - if (score == null) return "No Data" + if (score == null) return tr("No Data") return when { - score < 25 -> "Depleted" - score < 50 -> "Low" - score < 70 -> "Steady" - score < 88 -> "Primed" - else -> "Peak" + score < 25 -> tr("Depleted") + score < 50 -> tr("Low") + score < 70 -> tr("Steady") + score < 88 -> tr("Primed") + else -> tr("Peak") } } private fun synthesisDetail(d: DailyMetric?): String { val rec = d?.recovery - ?: return "No metrics yet. Import your WHOOP export or wear the strap to begin." + ?: return tr("No metrics yet. Import your WHOOP export or wear the strap to begin.") val recPart = when { - rec < 50 -> "Charge is low" - rec < 70 -> "Charge is steady" - else -> "Charge is strong" + rec < 50 -> tr("Charge is low") + rec < 70 -> tr("Charge is steady") + else -> tr("Charge is strong") } val sleepPart = d.totalSleepMin?.let { mins -> - if (mins / 60.0 >= 7) " and sleep was consistent" else " but sleep ran short" + if (mins / 60.0 >= 7) tr(" and sleep was consistent") else tr(" but sleep ran short") } ?: "" return "$recPart$sleepPart." } @@ -5679,7 +5679,7 @@ private fun sleepValue(d: DailyMetric?): String { */ private fun restCaption(d: DailyMetric?): String? = when { d?.totalSleepMin != null -> sleepValue(d) - d?.efficiency != null -> String.format(Locale.US, "%.0f%% eff", d.efficiency) + d?.efficiency != null -> String.format(Locale.US, "%.0f%% ", d.efficiency) + tr("eff") else -> null } @@ -5914,11 +5914,11 @@ private fun KeyMetricsEditorDialog( uncheckedTrackColor = Palette.surfaceInset, uncheckedBorderColor = Palette.hairline, ), - modifier = Modifier.semantics { contentDescription = "Show ${item.metric.title}" }, + modifier = Modifier.semantics { contentDescription = "${tr("Show")} ${tr(item.metric.title)}" }, ) Spacer(Modifier.width(12.dp)) Text( - item.metric.title, + tr(item.metric.title), style = NoopType.body, color = if (item.enabled) Palette.textPrimary else Palette.textTertiary, modifier = Modifier.weight(1f), @@ -5930,7 +5930,7 @@ private fun KeyMetricsEditorDialog( ) { Icon( Icons.Filled.KeyboardArrowUp, - contentDescription = "Move ${item.metric.title} up", + contentDescription = "${tr("Move")} ${tr(item.metric.title)} ${tr("up")}", tint = if (index > 0) Palette.textSecondary else Palette.textTertiary, modifier = Modifier.size(Metrics.iconSmall), ) @@ -5942,7 +5942,7 @@ private fun KeyMetricsEditorDialog( ) { Icon( Icons.Filled.KeyboardArrowDown, - contentDescription = "Move ${item.metric.title} down", + contentDescription = "${tr("Move")} ${tr(item.metric.title)} ${tr("down")}", tint = if (index < items.lastIndex) Palette.textSecondary else Palette.textTertiary, modifier = Modifier.size(Metrics.iconSmall), ) diff --git a/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt b/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt index 1c3048a95..f1064344a 100644 --- a/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TrendsExploreScreen.kt @@ -610,13 +610,13 @@ private fun HeroChartCard( fellBack: Boolean, ) { val heroValue = latest?.let { metric.format(it.value) } ?: "," - val asOf = latest?.let { "as of ${it.day}" } ?: "no readings yet" + val asOf = latest?.let { "${tr("as of")} ${it.day}" } ?: tr("no readings yet") // The range bar above already prints the authoritative reading-count caption; the hero only // names its window so the count isn't doubled in one card height. val subtitle = if (fellBack) { - "Trailing ${effectiveRange.windowName}" + "${tr("Trailing")} ${effectiveRange.windowName}" } else { - "Trailing ${range.windowName}" + "${tr("Trailing")} ${range.windowName}" } // Wash the hero card in the metric's domain world (Charge green / Effort amber / Rest indigo). NoopCard(tint = domainTint(metric.category)) { @@ -694,7 +694,7 @@ private fun HeroChartCard( ) { Text( if (windowed.isEmpty()) { - "No ${metric.title.lowercase()} recorded yet. Sync your strap to populate this trend." + "${tr("No")} ${metric.title.lowercase()} ${tr("recorded yet. Sync your strap to populate this trend.")}" } else { tr("Only one reading in range , widen the window to see a trend.") }, @@ -706,9 +706,9 @@ private fun HeroChartCard( // Footer chips, mirroring the macOS ChartFooter (Window / Points / Latest). Row(horizontalArrangement = Arrangement.spacedBy(Metrics.sectionGap)) { - ChartFootItem("Window", effectiveRange.label) - ChartFootItem("Points", "${windowed.size}") - ChartFootItem("Latest", heroValue) + ChartFootItem(tr("Window"), effectiveRange.label) + ChartFootItem(tr("Points"), "${windowed.size}") + ChartFootItem(tr("Latest"), heroValue) } } } @@ -783,9 +783,9 @@ private fun StatRow( else if ((delta > 0) == better) Palette.statusPositive else Palette.statusCritical } val deltaCaption = when { - hasDelta -> "vs prev ${effectiveRange.windowName}" - effectiveRange == ExploreRange.All -> "all history" - else -> "no prior ${effectiveRange.windowName}" + hasDelta -> "${tr("vs prev")} ${effectiveRange.windowName}" + effectiveRange == ExploreRange.All -> tr("all history") + else -> "${tr("no prior")} ${effectiveRange.windowName}" } Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { @@ -796,7 +796,7 @@ private fun StatRow( modifier = Modifier.weight(1f), label = tr("Average"), value = if (s.n > 0) metric.format(s.mean) else ",", - caption = "${s.n} days", + caption = "${s.n} ${tr("days")}", accent = metric.accent, ) StatTile( @@ -868,6 +868,6 @@ private fun rangeCaption( if (series.isEmpty()) return "," val n = windowed.size val unit = if (n == 1) tr("reading") else tr("readings") - return if (fellBack) "$n $unit · sparse , widened to ${effectiveRange.windowName}" + return if (fellBack) "$n $unit · ${tr("sparse , widened to")} ${effectiveRange.windowName}" else "$n $unit · ${range.windowName}" } diff --git a/android/app/src/main/java/com/noop/ui/TrendsReport.kt b/android/app/src/main/java/com/noop/ui/TrendsReport.kt index 56901bc0a..082a383c5 100644 --- a/android/app/src/main/java/com/noop/ui/TrendsReport.kt +++ b/android/app/src/main/java/com/noop/ui/TrendsReport.kt @@ -54,11 +54,11 @@ import kotlin.math.roundToInt /** The export window choices: trailing N days, or all history. */ enum class ReportRange(val days: Int?, val label: String, val longName: String) { - Days30(30, "30d", "Last 30 days"), - Days90(90, "90d", "Last 90 days"), - Days180(180, "6M", "Last 6 months"), - Days365(365, "1Y", "Last year"), - All(null, "All", "All history"), + Days30(30, "30d", tr("Last 30 days")), + Days90(90, "90d", tr("Last 90 days")), + Days180(180, "6M", tr("Last 6 months")), + Days365(365, "1Y", tr("Last year")), + All(null, tr("All"), tr("All history")), } // MARK: - Data builder (pure glue over the engine) @@ -242,10 +242,10 @@ object TrendsReportRenderer { text(canvas, "NOOP", left, ty, 11f, sansBold, ACCENT, letterSpacing = 0.12f) textRight(canvas, range.longName.uppercase(), PAGE_W - MARGIN - 16f, ty, 10f, sansBold, TEXT_TERTIARY) ty += 30f - text(canvas, "Trends report", left, ty, 26f, sansBold, TEXT_PRIMARY) + text(canvas, tr("Trends report"), left, ty, 26f, sansBold, TEXT_PRIMARY) ty += 22f val span = report.totalDays - val dayWord = if (span == 1) "day" else "days" + val dayWord = if (span == 1) tr("day") else tr("days") text( canvas, "${prettyDate(report.start)}-${prettyDate(report.end)} · $span $dayWord", @@ -265,9 +265,9 @@ object TrendsReportRenderer { val left = MARGIN + 16f var ty = top + 22f - text(canvas, "SUMMARY", left, ty, 10f, sansBold, ACCENT, letterSpacing = 0.1f) + text(canvas, tr("SUMMARY"), left, ty, 10f, sansBold, ACCENT, letterSpacing = 0.1f) ty += 8f - text(canvas, "What changed", left, ty + 10f, 16f, sansBold, TEXT_PRIMARY) + text(canvas, tr("What changed"), left, ty + 10f, 16f, sansBold, TEXT_PRIMARY) ty += 28f for (line in report.headlines) { text(canvas, "• $line", left, ty, 12f, sans, TEXT_PRIMARY, maxWidth = PAGE_W - MARGIN - left - 16f) @@ -285,9 +285,9 @@ object TrendsReportRenderer { top: Float, ): Float { var y = top - text(canvas, "BY THE NUMBERS", MARGIN, y + 4f, 10f, sansBold, TEXT_TERTIARY, letterSpacing = 0.1f) + text(canvas, tr("BY THE NUMBERS"), MARGIN, y + 4f, 10f, sansBold, TEXT_TERTIARY, letterSpacing = 0.1f) y += 10f - text(canvas, "Metrics", MARGIN, y + 14f, 16f, sansBold, TEXT_PRIMARY) + text(canvas, tr("Metrics"), MARGIN, y + 14f, 16f, sansBold, TEXT_PRIMARY) y += 26f for (stat in report.metrics) { @@ -307,7 +307,7 @@ object TrendsReportRenderer { var ty = top + 24f // Title + mean + trend chip. - text(canvas, stat.metric.label.uppercase(), left, ty, 11f, sansBold, accent, letterSpacing = 0.08f) + text(canvas, tr(stat.metric.label).uppercase(), left, ty, 11f, sansBold, accent, letterSpacing = 0.08f) val meanStr = meanText(stat) textRight(canvas, meanStr, right, ty, 14f, sansMedium, TEXT_PRIMARY) @@ -317,7 +317,7 @@ object TrendsReportRenderer { if (spark.size >= 2) { drawSparkline(canvas, spark, left, sparkTop, right - 8f, sparkBottom, accent) } else { - text(canvas, "Single reading in range", left, sparkTop + 16f, 11f, sans, TEXT_TERTIARY) + text(canvas, tr("Single reading in range"), left, sparkTop + 16f, 11f, sans, TEXT_TERTIARY) } // Divider. @@ -325,10 +325,10 @@ object TrendsReportRenderer { // Footer stats: Avg / Min(day) / Max(day) / Days, evenly spaced. val cols = listOf( - "AVG" to valueText(stat.mean, stat.metric), - "MIN" to "${valueText(stat.min.value, stat.metric)} · ${prettyDate(stat.min.day)}", - "MAX" to "${valueText(stat.max.value, stat.metric)} · ${prettyDate(stat.max.day)}", - "DAYS" to "${stat.n}", + tr("AVG") to valueText(stat.mean, stat.metric), + tr("MIN") to "${valueText(stat.min.value, stat.metric)} · ${prettyDate(stat.min.day)}", + tr("MAX") to "${valueText(stat.max.value, stat.metric)} · ${prettyDate(stat.max.day)}", + tr("DAYS") to "${stat.n}", ) val colW = (right - left) / cols.size cols.forEachIndexed { i, (label, value) -> @@ -381,10 +381,10 @@ object TrendsReportRenderer { val cardH = 110f drawCard(canvas, MARGIN, top, PAGE_W - MARGIN, top + cardH, null) val left = MARGIN + 16f - text(canvas, "Not enough data in this range yet", left, top + 30f, 16f, sansBold, TEXT_PRIMARY) - val body = "No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or " + - "skin-temp readings fell inside ${range.longName.lowercase()}. Wear your strap a few more days, " + - "or pick a wider range, then export again." + text(canvas, tr("Not enough data in this range yet"), left, top + 30f, 16f, sansBold, TEXT_PRIMARY) + val body = tr("No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or ") + + tr("skin-temp readings fell inside") + " ${range.longName.lowercase()}. " + + tr("Wear your strap a few more days, or pick a wider range, then export again.") drawWrapped(canvas, body, left, top + 52f, PAGE_W - MARGIN - left - 16f, 16f, 12f, sans, TEXT_SECONDARY) } @@ -395,20 +395,22 @@ object TrendsReportRenderer { // Provenance legend (#457): make clear which numbers are measured vs. NOOP's own derived scores, // so a clinician reading the PDF isn't misled into treating Recovery/Strain as clinical measures. // Sits above the hairline; wraps to the page width (~4 lines at this size). - val legend = "How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin " + - "temperature are measured from the strap (skin temp is shown as the deviation from your own " + - "baseline). Workouts is the count of activities you logged or that were detected. Recovery, " + - "Strain and Stress are NOOP's own on-device scores, not clinical measures - Recovery is a daily " + - "readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load " + - "derived from heart rate, and Stress is a 0-3 autonomic-load index from resting HR and HRV." + val legend = tr( + "How to read this: HRV, Resting HR, Sleep duration, Respiratory rate and Skin " + + "temperature are measured from the strap (skin temp is shown as the deviation from your own " + + "baseline). Workouts is the count of activities you logged or that were detected. Recovery, " + + "Strain and Stress are NOOP's own on-device scores, not clinical measures - Recovery is a daily " + + "readiness composite (HRV, resting HR, sleep and skin-temp trend), Strain is cardiovascular load " + + "derived from heart rate, and Stress is a 0-3 autonomic-load index from resting HR and HRV.", + ) drawWrapped(canvas, legend, MARGIN, y - 52f, PAGE_W - 2 * MARGIN, 11f, 9f, sans, TEXT_TERTIARY) line(canvas, MARGIN, y - 14f, PAGE_W - MARGIN, y - 14f, HAIRLINE) text( canvas, - "Generated by NOOP on $generatedOn · all on-device, no account, no cloud.", + "${tr("Generated by NOOP on")} $generatedOn · ${tr("all on-device, no account, no cloud.")}", MARGIN, y, 10f, sans, TEXT_TERTIARY, ) - text(canvas, "Informational only - not medical advice.", MARGIN, y + 12f, 10f, sans, TEXT_TERTIARY) + text(canvas, tr("Informational only - not medical advice."), MARGIN, y + 12f, 10f, sans, TEXT_TERTIARY) } // --- Primitives --- @@ -576,7 +578,7 @@ object TrendsReportShare { val generatedOn = LocalDate.now().format(DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.US)) val file = TrendsReportRenderer.renderPdf(context, report, range, series, generatedOn) ?: run { - Toast.makeText(context, "Couldn't build the report.", Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Couldn't build the report."), Toast.LENGTH_LONG).show() return } val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) @@ -586,9 +588,9 @@ object TrendsReportShare { putExtra(Intent.EXTRA_SUBJECT, "NOOP trends report") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - context.startActivity(Intent.createChooser(send, "Share trends report")) + context.startActivity(Intent.createChooser(send, tr("Share trends report"))) }.onFailure { - Toast.makeText(context, "Couldn't share the report: ${it.message}", Toast.LENGTH_LONG).show() + Toast.makeText(context, "${tr("Couldn't share the report:")} ${it.message}", Toast.LENGTH_LONG).show() } } } @@ -618,16 +620,18 @@ fun TrendsReportExportSection(vm: AppViewModel, modifier: Modifier = Modifier) { NoopCard(modifier = modifier, tint = Palette.accent) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Overline("Export") - Text("Trends report (PDF)", style = NoopType.title2, color = Palette.textPrimary) + Overline(tr("Export")) + Text(tr("Trends report (PDF)"), style = NoopType.title2, color = Palette.textPrimary) Text( - "A clean, shareable one-page PDF of your recovery, sleep, HRV, resting heart rate " + - "and strain over a date range. Built and saved on your phone - nothing leaves the device.", + tr( + "A clean, shareable one-page PDF of your recovery, sleep, HRV, resting heart rate " + + "and strain over a date range. Built and saved on your phone - nothing leaves the device.", + ), style = NoopType.subhead, color = Palette.textSecondary, ) - Overline("Range", color = Palette.textTertiary) + Overline(tr("Range"), color = Palette.textTertiary) SegmentedPillControl( items = ReportRange.entries.toList(), selection = range, @@ -639,7 +643,7 @@ fun TrendsReportExportSection(vm: AppViewModel, modifier: Modifier = Modifier) { // Routed through the unified NoopButton (crisp filled accent, no gold) — the same button // system every other CTA uses, mirroring the iOS exportReportRow. NoopButton( - text = "Export PDF", + text = tr("Export PDF"), leadingIcon = Icons.Filled.IosShare, kind = NoopButtonKind.Primary, fullWidth = true, @@ -647,7 +651,7 @@ fun TrendsReportExportSection(vm: AppViewModel, modifier: Modifier = Modifier) { ) Text( - "The share sheet can save the PDF to Files, or send it on.", + tr("The share sheet can save the PDF to Files, or send it on."), style = NoopType.footnote, color = Palette.textTertiary, ) diff --git a/android/app/src/main/java/com/noop/ui/TrendsScreen.kt b/android/app/src/main/java/com/noop/ui/TrendsScreen.kt index 8410d8bf2..fd98a3184 100644 --- a/android/app/src/main/java/com/noop/ui/TrendsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/TrendsScreen.kt @@ -240,10 +240,10 @@ fun TrendsScreen(vm: AppViewModel) { // mirrors the iOS hero's `valueRange: 0...106`. chartHeadroom = 0.06f, footer = listOf( - "Avg" to (recAvg?.let { "${it.roundToInt()}" } ?: EM_DASH), - "Peak" to (recovery.values.maxOrNull()?.let { "${it.roundToInt()}" } ?: EM_DASH), - "Low" to (recovery.values.minOrNull()?.let { "${it.roundToInt()}" } ?: EM_DASH), - "Days" to "${recovery.values.size}", + tr("Avg") to (recAvg?.let { "${it.roundToInt()}" } ?: EM_DASH), + tr("Peak") to (recovery.values.maxOrNull()?.let { "${it.roundToInt()}" } ?: EM_DASH), + tr("Low") to (recovery.values.minOrNull()?.let { "${it.roundToInt()}" } ?: EM_DASH), + tr("Days") to "${recovery.values.size}", ), ) } @@ -375,9 +375,9 @@ private fun WeekNavBar(weekOffset: Int, minWeekOffset: Int, onStep: (Int) -> Uni val atOldest = weekOffset <= minWeekOffset val atNewest = weekOffset >= 0 val label = when { - weekOffset == 0 -> "This week" - weekOffset == -1 -> "Last week" - else -> "${-weekOffset} weeks ago" + weekOffset == 0 -> tr("This week") + weekOffset == -1 -> tr("Last week") + else -> "${-weekOffset} ${tr("weeks ago")}" } // liquidPress on the two week-step chevrons (the screen's tappable controls): each settles inward on // press, wired to the SAME interactionSource the IconButton uses for its own ripple, matching the pilot. @@ -516,7 +516,7 @@ private enum class TrendsRange(val days: Int?, val label: String, val longName: All(null, "ALL", "all history"); /** "Trailing 90 days" / "All history" , the card/range subtitle. */ - val subtitle: String get() = days?.let { "Trailing $it days" } ?: "All history" + val subtitle: String get() = days?.let { "${tr("Trailing")} $it ${tr("days")}" } ?: tr("All history") /** This range plus every LARGER range, ascending , the auto-expand search order. */ val widening: List @@ -597,7 +597,7 @@ private fun windowPoints( private fun caption(count: Int, eff: TrendsRange, selected: TrendsRange): String { val unit = if (count == 1) tr("reading") else tr("readings") return if (eff != selected) { - "$count $unit · sparse , widened to ${eff.longName}" + "$count $unit · ${tr("sparse , widened to")} ${eff.longName}" } else { "$count $unit · ${selected.longName}" } @@ -859,9 +859,9 @@ private fun MetricTrendCard( footer = listOf( // Plain "Mean" to match the bare Min/Max columns; the unit moves into the value // (e.g. "58 ms") so uppercasing can't render a shouty "MEAN MS". - "Mean" to (avg?.let { "${fmt(it)} $unit" } ?: EM_DASH), - "Min" to (resolved.values.minOrNull()?.let { fmt(it) } ?: EM_DASH), - "Max" to (resolved.values.maxOrNull()?.let { fmt(it) } ?: EM_DASH), + tr("Mean") to (avg?.let { "${fmt(it)} $unit" } ?: EM_DASH), + tr("Min") to (resolved.values.minOrNull()?.let { fmt(it) } ?: EM_DASH), + tr("Max") to (resolved.values.maxOrNull()?.let { fmt(it) } ?: EM_DASH), ), ) } diff --git a/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt b/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt index 1adac129d..70dce68d3 100644 --- a/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt +++ b/android/app/src/main/java/com/noop/ui/WeeklyDigestCard.kt @@ -169,11 +169,11 @@ fun WeeklyDigestContent(digest: WeeklyDigest, compact: Boolean = false) { Text(weekRangeLabel(digest), style = NoopType.title2, color = Palette.textPrimary) } Text( - "${digest.daysWithData}/7 days", + "${digest.daysWithData}/7 ${tr("days")}", style = NoopType.footnote, color = Palette.textSecondary, modifier = Modifier.semantics { - contentDescription = "${digest.daysWithData} of 7 days had data this week" + contentDescription = "${digest.daysWithData} ${tr("of 7 days had data this week")}" }, ) } @@ -199,7 +199,7 @@ fun WeeklyDigestContent(digest: WeeklyDigest, compact: Boolean = false) { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { digest.sleepConsistencySD?.let { sd -> Text( - "Sleep steadiness: Rest varied ±${fmt1(sd)} pts night to night.", + "${tr("Sleep steadiness: Rest varied ±")}${fmt1(sd)}${tr(" pts night to night.")}", style = NoopType.footnote, color = Palette.textTertiary, ) @@ -305,7 +305,7 @@ internal fun meanText(s: WeeklyMetricSummary, effortScale: EffortScale): String } internal fun deltaText(s: WeeklyMetricSummary): String { - if (s.weekOverWeek.current.n == 0 || s.weekOverWeek.previous.n == 0) return "new" + if (s.weekOverWeek.current.n == 0 || s.weekOverWeek.previous.n == 0) return tr("new") val pct = s.weekOverWeek.pctChange // Sub-1% (or unpercentable) moves read "<1%", matching Swift. The old fallback printed the raw // points delta: a bare "0.1", and for Effort a stored 0-100 figure the scale toggle never saw. @@ -328,17 +328,17 @@ private fun chipTone(s: WeeklyMetricSummary): Color = when { private fun rowAccessibility(s: WeeklyMetricSummary, effortScale: EffortScale): String { val mean = meanText(s, effortScale) if (s.weekOverWeek.current.n == 0 || s.weekOverWeek.previous.n == 0) { - return "${s.metric.label}: $mean this week, no comparison." + return "${s.metric.label}: $mean ${tr("this week, no comparison.")}" } - val dir = if (s.wowDelta > 0) "up" else if (s.wowDelta < 0) "down" else "unchanged" + val dir = if (s.wowDelta > 0) tr("up") else if (s.wowDelta < 0) tr("down") else tr("unchanged") // A rough comparison drops the verdict framing too, so VoiceOver/TalkBack matches the neutral chip. val frame = when { s.isRoughComparison -> "" - s.wowGoodness == 1 -> ", a good sign" - s.wowGoodness == -1 -> ", worth a look" + s.wowGoodness == 1 -> tr(", a good sign") + s.wowGoodness == -1 -> tr(", worth a look") else -> "" } - return "${s.metric.label}: $mean this week, $dir ${deltaText(s)} week over week$frame." + return "${s.metric.label}: $mean ${tr("this week,")} $dir ${deltaText(s)} ${tr("week over week")}$frame." } private fun fmt1(x: Double): String = ((x * 10).roundToInt() / 10.0).toString() diff --git a/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt b/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt index 9cb9688d9..a67a6f290 100644 --- a/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt +++ b/android/app/src/main/java/com/noop/ui/WorkoutEditing.kt @@ -44,7 +44,7 @@ object WorkoutEditing { * renders "Traditional Strength Training". Already-spaced labels (manual/edited) pass through. (#175) */ fun displaySport(sport: String): String { - if (sport == "detected") return "Activity" + if (sport == "detected") return tr("Activity") if (sport.isEmpty() || sport.contains(" ")) return sport val out = StringBuilder() var prev: Char? = null diff --git a/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt b/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt index 01c9ca03c..ce4d5b73f 100644 --- a/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt +++ b/android/app/src/main/java/com/noop/ui/WorkoutsScreen.kt @@ -323,8 +323,8 @@ private fun EmptyWorkouts(loaded: Boolean, onAdd: () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { DataPendingNote( title = tr("No workouts yet"), - body = "No workouts yet. They come from your WHOOP and Apple Health history. " + - "Import in Data Sources to bring them in, or add one you tracked elsewhere.", + body = tr("No workouts yet. They come from your WHOOP and Apple Health history. ") + + tr("Import in Data Sources to bring them in, or add one you tracked elsewhere."), ) if (loaded) AddWorkoutButton(onAdd) } @@ -399,11 +399,11 @@ private fun RangeBar( label = { it.label }, onSelect = onSelect, ) - val unit = if (rowCount == 1) "session" else "sessions" + val unit = if (rowCount == 1) tr("session") else tr("sessions") // #64: append "· filtered" when a sport/source/search filter narrows the list. - val suffix = if (filterActive) " · filtered" else "" + val suffix = if (filterActive) " · ${tr("filtered")}" else "" val caption = if (fellBack) { - "$rowCount $unit · sparse, widened to ${effectiveRange.caption}$suffix" + "$rowCount $unit · ${tr("sparse, widened to")} ${effectiveRange.caption}$suffix" } else { "$rowCount $unit · ${effectiveRange.caption}$suffix" } @@ -428,10 +428,10 @@ private val SOURCE_FILTER_OPTIONS = listOf( private fun sourceFilterLabel(c: WorkoutSource): String = when (c) { WorkoutSource.WHOOP -> "Whoop" WorkoutSource.APPLE -> "Apple" - WorkoutSource.DETECTED -> "Detected" - WorkoutSource.MANUAL -> "Manual" - WorkoutSource.LIFTING -> "Lifting" - WorkoutSource.ACTIVITY_FILE -> "File" + WorkoutSource.DETECTED -> tr("Detected") + WorkoutSource.MANUAL -> tr("Manual") + WorkoutSource.LIFTING -> tr("Lifting") + WorkoutSource.ACTIVITY_FILE -> tr("File") } /** @@ -673,7 +673,7 @@ private fun EffortHero( verticalArrangement = Arrangement.spacedBy(10.dp), ) { Text( - "Effort this ${effectiveRange.heroWord}", + "${tr("Effort this")} ${effectiveRange.heroWord}", style = NoopType.headline, color = Palette.textPrimary, ) @@ -682,8 +682,8 @@ private fun EffortHero( HeroStat(tr("Active"), oneDecimal(totalTimeH) + "h", Palette.textPrimary, Modifier.weight(1f)) } Text( - if (modal != null) "Mostly ${WorkoutEditing.displaySport(modal.sport)} (${effectiveRange.caption})." - else "Logged sessions across ${effectiveRange.caption}.", + if (modal != null) "${tr("Mostly")} ${WorkoutEditing.displaySport(modal.sport)} (${effectiveRange.caption})." + else "${tr("Logged sessions across")} ${effectiveRange.caption}.", style = NoopType.footnote, color = Palette.textTertiary, ) @@ -732,7 +732,7 @@ private fun SummarySection( modifier = m, label = tr("Total Time"), value = oneDecimal(totalTimeH) + "h", - caption = "active", + caption = tr("active"), accent = Palette.textPrimary, ) }, @@ -750,7 +750,7 @@ private fun SummarySection( modifier = m, label = tr("Total Distance"), value = UnitFormatter.distanceFromKilometers(totalKm, unitSystem), - caption = "covered", + caption = tr("covered"), accent = Palette.metricCyan, ) }, @@ -759,7 +759,7 @@ private fun SummarySection( modifier = m, label = tr("Most Active"), value = modal?.sport ?: "–", - caption = modal?.let { "${it.count} session${if (it.count == 1) "" else "s"}" }, + caption = modal?.let { "${it.count} ${if (it.count == 1) tr("session") else tr("sessions")}" }, accent = Palette.textPrimary, ) }, @@ -784,7 +784,7 @@ private fun BreakdownSection(groups: List, rows: List) { SectionHeader( title = tr("Activity Breakdown"), overline = tr("By sport"), - trailing = "${groups.size} sport${if (groups.size == 1) "" else "s"}", + trailing = "${groups.size} ${if (groups.size == 1) tr("sport") else tr("sports")}", ) // This sport's own sessions, so each card can carry an HR-zone mini-bar. groups.forEach { g -> SportCard(g, zones = zoneSummary(rows.filter { it.sport == g.sport })) } @@ -860,7 +860,7 @@ private fun ZonesSection(rows: List) { SectionHeader( title = tr("HR Zones"), overline = tr("Whoop import"), - trailing = "${z.sessionsWithZones} of ${rows.size} session${if (rows.size == 1) "" else "s"}", + trailing = "${z.sessionsWithZones} ${tr("of")} ${rows.size} ${if (rows.size == 1) tr("session") else tr("sessions")}", ) NoopCard(tint = Palette.effortColor) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { @@ -948,7 +948,7 @@ private fun SessionsSection( Column(verticalArrangement = Arrangement.spacedBy(Metrics.gap)) { Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier.weight(1f)) { - SectionHeader(title = tr("All Sessions"), overline = tr("Log"), trailing = "${rows.size} total") + SectionHeader(title = tr("All Sessions"), overline = tr("Log"), trailing = "${rows.size} ${tr("total")}") } if (anySelectable) SelectPill(selectionMode, onToggleSelectMode) } @@ -980,12 +980,12 @@ private fun SessionsSection( modifier = Modifier .fillMaxWidth() .clickable { shownCount += SESSIONS_PAGE_SIZE } - .semantics { contentDescription = "Show $more more sessions" } + .semantics { contentDescription = "${tr("Show")} $more ${tr("more sessions")}" } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text( - "Show $more more ($remaining remaining)", + "${tr("Show")} $more ${tr("more")} ($remaining ${tr("remaining")})", style = NoopType.subhead, color = Palette.accent, ) @@ -1789,11 +1789,11 @@ private fun FullDivider(alpha: Float = 1f) { // MARK: - Range model private enum class WorkoutRange(val label: String, val caption: String, val days: Int?, val heroWord: String) { - Week("7D", "last 7 days", 7, "week"), - Month("30D", "last 30 days", 30, "month"), - Quarter("90D", "last 90 days", 90, "quarter"), - Year("1Y", "last year", 365, "year"), - All("All", "all time", null, "log"), + Week("7D", tr("last 7 days"), 7, tr("week")), + Month("30D", tr("last 30 days"), 30, tr("month")), + Quarter("90D", tr("last 90 days"), 90, tr("quarter")), + Year("1Y", tr("last year"), 365, tr("year")), + All(tr("All"), tr("all time"), null, tr("log")), } /** This range plus every larger range, ascending — the auto-expand search order. */ @@ -1933,10 +1933,10 @@ private val WorkoutRow.sourceBadge: Pair get() = when (WorkoutEditing.classify(source)) { // Detected (on-device auto-detector) is honestly labelled so a duplicate is recognisable + // removable (#107); manual = user-logged. Both classify on `source` BEFORE the import labels. - WorkoutSource.DETECTED -> "Detected" to Palette.metricPurple - WorkoutSource.MANUAL -> "Manual" to Palette.statusWarning - WorkoutSource.LIFTING -> "Lifting" to Palette.zone2 // imported Hevy / Liftosaur strength log - WorkoutSource.ACTIVITY_FILE -> "File" to Palette.metricAmber // imported GPX / TCX / FIT + WorkoutSource.DETECTED -> tr("Detected") to Palette.metricPurple + WorkoutSource.MANUAL -> tr("Manual") to Palette.statusWarning + WorkoutSource.LIFTING -> tr("Lifting") to Palette.zone2 // imported Hevy / Liftosaur strength log + WorkoutSource.ACTIVITY_FILE -> tr("File") to Palette.metricAmber // imported GPX / TCX / FIT else -> when (workoutSourceLabel(deviceId, source)) { "HC" -> "HC" to Palette.metricPurple "Whoop" -> "Whoop" to Palette.accent diff --git a/android/app/src/main/res/raw/fr_strings.json b/android/app/src/main/res/raw/fr_strings.json index 70558c631..28f6fa83f 100644 --- a/android/app/src/main/res/raw/fr_strings.json +++ b/android/app/src/main/res/raw/fr_strings.json @@ -1,6 +1,11 @@ { +" and sleep was consistent": " et le sommeil a été régulier", +" but sleep ran short": " mais le sommeil a manqué", " drinks": " boissons", +" pts night to night.": " pts d'une nuit à l'autre.", +" pts) and nothing moved much.": " pts) et rien n'a beaucoup bougé.", " · Charging": " · En charge", +"% Max": "% Max", "%@ %@ relationship (r = %@, n = %lld).": "Relation %1$@ %2$@ (r = %3$@, n = %4$lld).", "%@ (Beta)": "%@ (bêta)", "%@ (experimental)": "%@ (expérimental)", @@ -181,18 +186,24 @@ "%llds left · %lld beats": "%1$llds restantes · %2$lld battements", "%lld–%lld%@ · step %lld": "%lld–%lld%@ · étape %lld", "%lld′ %lld″": "%1$lld′ %2$lld″", +"'s history stays exactly as it is. Only new days come from": "son historique reste exactement tel quel. Seuls les nouveaux jours viennent de", "(Oura: Account → Export Data; Fitbit: Google Takeout; Garmin: Export Your Data), then ": "(Oura : Account → Export Data ; Fitbit : Google Takeout ; Garmin : Export Your Data), puis ", "(and keep wearing your strap).": "(et continue à porter ton bracelet).", "(deep + REM sleep), and how consistent your sleep and wake timing is.": "(sommeil profond + paradoxal), et à quel point tes heures de coucher et de lever sont régulières.", +"(distance": "(écart", "(nothing to share yet)": "(rien à partager pour l'instant)", "(range and ectopic-beat filtering) before computing RMSSD the same way your ": "(filtrage de la plage et des battements ectopiques) avant de calculer le RMSSD de la même façon que ton ", "(those are WHOOP-only).": "(réservés à WHOOP).", +"), so you're moderately activated. Nothing alarming; just don't overreach.": "), donc tu es modérément activé. Rien d'alarmant ; évite juste de trop en faire.", "* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); ": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (dépend du firmware) ; ", "* on-device estimate: skin temp is a nightly ±°C deviation (firmware-dependent); no steps over BLE on a 4.0. No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C (selon le firmware) ; pas de pas via BLE sur un 4.0. Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", "* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw ": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut ", "* on-device estimate: skin temp is a nightly ±°C deviation, steps are a raw motion count (#78). No SpO₂ % off the strap; import a WHOOP CSV for a real %.": "* estimation sur l'appareil : la température cutanée est un écart nocturne en ±°C, les pas sont un décompte brut de mouvement (#78). Pas de % de SpO₂ depuis le bracelet ; importez un CSV WHOOP pour un vrai %.", "+ drinks": "+ boissons", +", a good sign": ", bon signe", ", in use": ", utilisé", +", worth a look": ", à surveiller", +"- likely that, not illness.": "- probablement ça, pas une maladie.", "0 pts": "0 pt", "0–%@ scale": "Échelle 0–%@", "1 app on": "1 application active", @@ -242,6 +253,7 @@ "30d": "30j", "365 DAYS": "365 JOURS", "4.0 vs 5.0/MG": "4.0 vs 5.0/MG", +"5-minute average": "Moyenne sur 5 min", "5-minute average · selected day": "Moyenne sur 5 minutes · jour sélectionné", "5-minute average · since midnight": "Moyenne sur 5 minutes · depuis minuit", "6 months": "6 mois", @@ -284,6 +296,8 @@ "A few more days and we can show your Fitness Age.": "Encore quelques jours et nous pourrons afficher votre Âge de forme physique.", "A few more days and we can show your Vitality.": "Encore quelques jours et nous pourrons afficher votre Vitalité.", "A few more days of wear, plus the basics below, and we can show your Fitness Age.": "Encore quelques jours de port, plus les informations de base ci-dessous, et nous pourrons afficher votre Âge de forme physique.", +"A few more nights of data and your readiness read will sharpen.": "Encore quelques nuits de données et ta lecture de forme s'affinera.", +"A few signals are mildly up": "Quelques signaux sont légèrement en hausse", "A few signals are up": "Quelques signaux sont en hausse", "A flat or off-wrist strap won't advertise, so nothing shows up. A real phone is ": "Un bracelet déchargé ou retiré du poignet n'émet rien, donc rien n'apparaît. Un vrai téléphone est ", "A gentle buzz when your HRV drops while your heart rate is calm, a cue to take a paced breath. Rate-limited to once every 15 minutes; off by default.": "Une vibration douce quand votre VFC chute alors que votre fréquence cardiaque est calme, une invitation à respirer à un rythme régulier. Limité à une fois toutes les 15 minutes ; désactivé par défaut.", @@ -309,6 +323,8 @@ "A standalone companion for your WHOOP. Everything stays on this device, your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil, votre historique, votre flux en direct, vos chiffres. Rien n'est téléchargé. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", "A standalone companion for your WHOOP. Everything stays on this device: your history, your live stream, your numbers. Nothing is uploaded. NOOP is an independent, experimental project, not the WHOOP app.": "Un compagnon autonome pour votre WHOOP. Tout reste sur cet appareil : votre historique, votre flux en direct, vos chiffres. Rien n'est envoyé en ligne. NOOP est un projet indépendant et expérimental, pas l'application WHOOP.", "A standalone companion for your WHOOP. Everything stays on this phone: your history, your live stream, your numbers. Nothing is uploaded.": "Un compagnon autonome pour ton WHOOP. Tout reste sur ce téléphone : ton historique, ton flux en direct, tes chiffres. Rien n'est envoyé en ligne.", +"A steady week: Rest held even (±": "Une semaine stable : le Repos est resté régulier (±", +"A steady week: no metric moved meaningfully from last week.": "Une semaine stable : aucune métrique n'a vraiment bougé depuis la semaine dernière.", "A still, seated snapshot of your heart-rate variability": "Un instantané, assis et immobile, de votre variabilité de fréquence cardiaque", "A sync is already in progress.": "Une synchronisation est déjà en cours.", "A transparent cardiorespiratory recipe that recovers deep and REM better than the ": "Une recette cardiorespiratoire transparente qui récupère mieux le sommeil profond et paradoxal que ", @@ -370,8 +386,10 @@ "Add a nap": "Ajouter une sieste", "Add a reading": "Ajouter une mesure", "Add a workout": "Ajouter un entraînement", +"Add it in Settings": "Ajoute-le dans Réglages", "Add metric": "Ajouter une métrique", "Add nap": "Ajouter une sieste", +"Add to also see VO₂max": "Ajoute pour voir aussi le VO₂max", "Add workout": "Ajouter un entraînement", "Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On %@ only. Nothing is synced.": "Ajoute un journal d'hydratation simple avec un objectif quotidien qui s'ajuste à votre effort. Touchez pour ajouter une gorgée, une tasse ou une bouteille et regardez l'anneau de progression se remplir. Sur %@ uniquement. Rien n'est synchronisé.", "Adds a simple fluid log with a daily goal that adjusts to your effort. Tap to add a sip, cup or bottle and watch a progress ring fill. On this phone only. Nothing is synced.": "Ajoute un suivi d'hydratation simple avec un objectif quotidien qui s'adapte à ton effort. Appuie pour ajouter une gorgée, un verre ou une bouteille et regarde l'anneau de progression se remplir. Sur ce téléphone uniquement. Rien n'est synchronisé.", @@ -460,6 +478,7 @@ "Armed on the strap itself, so it can buzz at your wake time even if your phone is asleep or NOOP is closed. We send the same alarm command the official app sends, but a strap-driven wake-up hasn't been confirmed on our side yet, so please keep a backup alarm for now.": "Armée sur le bracelet lui-même, elle peut donc vibrer à votre heure de réveil même si votre téléphone est en veille ou NOOP fermé. Nous envoyons la même commande d'alarme que l'application officielle, mais un réveil piloté par le bracelet n'a pas encore été confirmé de notre côté, alors gardez pour l'instant une alarme de secours.", "Arms the strap to buzz at your wake time, even if NOOP is closed. Sends the exact alarm command the official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test, #535). Keep a backup alarm for anything you truly can't miss.": "Arme le bracelet pour vibrer à l'heure de réveil, même si NOOP est fermé. Envoie exactement la commande d'alarme de l'appli officielle, vibration confirmée sur une vraie WHOOP 4.0 (capture communautaire + test sur appareil, #535). Garde une alarme de secours pour ce que tu ne peux pas manquer.", "Arms the strap to buzz at your wake time, even if NOOP is closed. Still experimental on WHOOP 4.0, so keep a backup alarm until you've confirmed it wakes you.": "Arme le bracelet pour vibrer à votre heure de réveil, même si NOOP est fermé. Encore expérimental sur WHOOP 4.0, gardez donc une alarme de secours jusqu'à ce que vous ayez confirmé qu'il vous réveille.", +"Around your mid-cycle shift - temperature is turning.": "Autour de ton changement de mi-cycle - la température s'inverse.", "Ask Coach about your data…": "Demandez à Coach à propos de vos données…", "Ask about your charge, effort, rest and workouts, grounded in your own numbers.": "Posez des questions sur votre charge, votre effort, votre repos et vos entraînements, ancrées dans vos propres chiffres.", "Ask about your recovery, strain, sleep and HRV, grounded in your own numbers.": "Pose des questions sur ta récupération, ton effort, ton sommeil et ta VFC, à partir de tes propres chiffres.", @@ -510,6 +529,7 @@ "BEATS READ": "BATTEMENTS LUS", "BLE protocol reverse-engineering": "Rétro-ingénierie du protocole BLE", "BMI": "IMC", +"BY THE NUMBERS": "EN CHIFFRES", "Back": "Retour", "Back to standard setup": "Retour à la configuration standard", "Back to the marker list": "Retour à la liste des marqueurs", @@ -570,6 +590,7 @@ "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps ": "Bêta. * correspond à une estimation sur l'appareil. La température cutanée est une tendance par rapport à ta propre référence, les pas ", "Beta. * is an on-device estimate. Skin temp is a trend versus your own baseline, steps are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 percentage comes off the ring (import an Oura file for those).": "Bêta. * est une estimation calculée sur l'appareil. La température cutanée est une tendance par rapport à votre propre référence, les pas sont un simple comptage de mouvement, et la VFC nécessite que vous soyez immobile. Aucun pourcentage de préparation Oura ni de SpO2 ne provient de la bague (importez un fichier Oura pour cela).", "Beta. Read this first.": "Bêta. Lisez ceci d'abord.", +"Biological sex": "Sexe biologique", "Blood Oxygen": "Oxygène sanguin", "Blood O₂": "O₂ sanguin", "Blood oxygen": "Oxygène sanguin", @@ -631,6 +652,7 @@ "Building (1 window)": "Construction en cours, 1 fenêtre", "Building from your strap": "Construction depuis votre bracelet", "Building your baseline": "Construction de votre référence", +"Building your baseline. About": "Construction de ta référence. Environ", "Building your baseline. About %lld more %@ until your scores are personal.": "Construction de votre référence. Encore environ %1$lld %2$@ avant que vos scores ne soient personnalisés.", "Building your baseline. About %lld more nights until your scores are personal.": "Construction de votre référence. Encore environ %lld nuits avant que vos scores ne soient personnalisés.", "Building your baseline. About 1 more night until your scores are personal.": "Construction de votre référence. Encore environ 1 nuit avant que vos scores ne soient personnalisés.", @@ -708,6 +730,7 @@ "Capturing. %lld seconds remaining, %lld beats collected.": "Capture en cours. %1$lld secondes restantes, %2$lld battements collectés.", "Carbs": "Glucides", "Cardiac": "Cardiaque", +"Cardio fitness": "Forme cardio", "Cardiovascular load for the day, on a 0-100 scale (was 0-21).": "Charge cardiovasculaire de la journée, sur une échelle de 0-100 (auparavant 0-21).", "Cardiovascular load for the day, on a 0–100 scale (was 0–21).": "Charge cardiovasculaire de la journée, sur une échelle de 0 à 100 (auparavant 0 à 21).", "Cards with no value yet show a dash.": "Les cartes sans valeur pour l'instant affichent un tiret.", @@ -731,12 +754,15 @@ "Charge calibrating": "Charge en calibration", "Charge calibrating, needs more data": "Charge en calibration, besoin de plus de données", "Charge is NOOP's daily readiness score, learned from your own HRV, resting heart rate and more over time. Your history stays.": "La Charge est le score de préparation quotidien de NOOP, appris au fil du temps à partir de ta propre VFC, ta FC au repos et d'autres signaux. Ton historique est conservé.", +"Charge is low": "La Charge est basse", "Charge is low and sleep was consistent.": "La charge est basse et le sommeil a été régulier.", "Charge is low but sleep ran short.": "La charge est basse et le sommeil a été insuffisant.", "Charge is low.": "La charge est basse.", +"Charge is steady": "La Charge est stable", "Charge is steady and sleep was consistent.": "La charge est stable et le sommeil a été régulier.", "Charge is steady but sleep ran short.": "La charge est stable mais le sommeil a été insuffisant.", "Charge is steady.": "La charge est stable.", +"Charge is strong": "La Charge est forte", "Charge is strong and sleep was consistent.": "La charge est élevée et le sommeil a été régulier.", "Charge is strong but sleep ran short.": "La charge est élevée mais le sommeil a été insuffisant.", "Charge is strong.": "La charge est élevée.", @@ -839,6 +865,7 @@ "Compliance": "Conformité", "Computed live from your Apple Watch via Health. Recovery needs about a week of nights to calibrate, and every watch-derived score is labelled with its confidence. Only the metrics your watch actually records are listed above.": "Calculé en direct depuis votre Apple Watch via Santé. La récupération nécessite environ une semaine de nuits pour se calibrer, et chaque score dérivé de la montre est étiqueté avec son niveau de confiance. Seules les métriques réellement enregistrées par votre montre sont listées ci-dessus.", "Confidence": "Confiance", +"Confidence:": "Confiance :", "Confidence: %@ (distance %@)": "Confiance : %1$@ (distance %2$@)", "Confidence: calibrating": "Confiance : calibrage", "Confidence: estimate": "Confiance : estimation", @@ -897,6 +924,7 @@ "Copy the redacted report to the clipboard": "Copier le rapport caviardé dans le presse-papiers", "Correct when you went to bed and woke. Stages are re-derived from your data; the edit is kept through the next strap sync.": "Corrigez l'heure de coucher et de réveil. Les phases sont recalculées à partir de vos données ; la modification est conservée jusqu'à la prochaine synchronisation du bracelet.", "Could not take over": "Impossible de prendre le relais", +"Couldn't build the report.": "Impossible de générer le rapport.", "Couldn't change the app icon": "Impossible de changer l'icône de l'application", "Couldn't check. Try again.": "Vérification impossible. Réessayez.", "Couldn't locate the NOOP database. %@": "Base de données NOOP introuvable. %@", @@ -909,6 +937,7 @@ "Couldn't restore that backup.": "Impossible de restaurer cette sauvegarde.", "Couldn't safely back up right now. Recent changes are still in the write-ahead log.": "Sauvegarde impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées.", "Couldn't safely export right now. Recent changes are still in the database's write-ahead log. Close any in-flight sync, then try again.": "Export impossible en toute sécurité pour le moment. Les modifications récentes sont encore dans le journal des écritures anticipées de la base de données. Fermez toute synchronisation en cours, puis réessayez.", +"Couldn't share the report:": "Impossible de partager le rapport :", "Couldn't start. Needs a connected strap and a resting heart rate.": "Démarrage impossible. Un bracelet connecté et une fréquence cardiaque au repos sont nécessaires.", "Couldn't use that photo. Try another.": "Impossible d'utiliser cette photo. Essaie-en une autre.", "Couldn't write the debug export.": "Impossible d'écrire l'export de débogage.", @@ -943,6 +972,7 @@ "Cycle phase: %@. About day %lld.": "Phase du cycle : %1$@. Environ jour %2$lld.", "Cycling": "Vélo", "DAY %lld/%lld": "JOUR %1$lld/%2$lld", +"DAYS": "JOURS", "DAYTIME SLEEP": "SOMMEIL DE JOUR", "DEPLETED": "ÉPUISÉ", "DESCRIPTIVE STATS": "STATISTIQUES DESCRIPTIVES", @@ -958,6 +988,7 @@ "Daily export time": "Heure de l'export quotidien", "Daily reports": "Rapports quotidiens", "Daily signals": "Signaux quotidiens", +"Daily steps": "Pas quotidiens", "Dark": "Sombre", "Data & App": "Données et application", "Data Sources": "Sources de données", @@ -1007,6 +1038,7 @@ "Detected": "Détecté", "Detects call-style notifications from known calling apps.": "Détecte les notifications de type appel provenant d'applis d'appel connues.", "Device": "Appareil", +"Device actions for": "Actions pour l'appareil", "Device actions for %@": "Actions sur l'appareil pour %@", "Device metrics": "Métriques de l'appareil", "Device name": "Nom de l'appareil", @@ -1086,9 +1118,11 @@ "Efficiency": "Efficacité", "Effort": "Effort", "Effort (0-100)": "Effort (0-100)", +"Effort and Charge tracked together this week: a sustainable load.": "Effort et Charge ont évolué ensemble cette semaine : une charge soutenable.", "Effort building": "Effort en construction", "Effort needs the machine's heart rate; without it the session logs the machine metrics only.": "L'effort a besoin de la fréquence cardiaque de la machine ; sans elle, la séance n'enregistre que les données de la machine.", "Effort scale": "Échelle d'effort", +"Effort this": "Effort ce", "Effort this %@": "Effort sur ce %@", "Effort: how hard did your heart work?": "Effort : à quel point votre cœur a-t-il travaillé ?", "Eligible for live physiology": "Éligible à la physiologie en direct", @@ -1209,6 +1243,7 @@ "Features": "Fonctionnalités", "Feb": "Fév", "Feel the current time as a sequence of buzzes (#460). Does nothing unless your strap is connected.": "Ressens l'heure actuelle sous forme d'une série de vibrations (#460). Sans effet si ton bracelet n'est pas connecté.", +"Fell asleep at": "Endormi à", "Female": "Féminin", "Fetch models from provider": "Récupérer les modèles du fournisseur", "Fetch the available models from %@ using your saved key": "Récupérer les modèles disponibles depuis %@ avec votre clé enregistrée", @@ -1239,6 +1274,7 @@ "Flag drinks late enough to still be active at bedtime.": "Signale les boissons prises assez tard pour être encore actives au coucher.", "Flags accepted, but the enable sequence doesn't start a separate live stream. The deep records arrive as part of the normal history sync (#494).": "Indicateurs acceptés, mais la séquence d'activation ne démarre pas de flux en direct séparé. Les enregistrements détaillés arrivent dans le cadre de la synchronisation normale de l'historique (#494).", "Follicular": "Folliculaire", +"Follicular range - temperature sitting at your baseline.": "Phase folliculaire - température à ta référence.", "Following your phone's light/dark setting.": "Suit le réglage clair/sombre de ton téléphone.", "For a WHOOP 4.0, which sends no step count: NOOP estimates steps from motion, calibrated to your phone. Tap to see how close it is and adjust it.": "Pour un WHOOP 4.0, qui n'envoie aucun nombre de pas : NOOP estime les pas à partir du mouvement, calibré sur votre téléphone. Tapez pour voir sa précision et l'ajuster.", "For power users.": "Pour les utilisateurs avancés.", @@ -1264,6 +1300,7 @@ "GPX · TCX · FIT (one workout per file)": "GPX · TCX · FIT (un entraînement par fichier)", "Garmin": "Garmin", "Garmin watch": "Montre Garmin", +"Generated by NOOP on": "Généré par NOOP le", "Generated by NOOP on %@ · all on-device, no account, no cloud.": "Généré par NOOP sur %@ · tout sur l'appareil, aucun compte, aucun cloud.", "Get Started": "Commencer", "Get a notification when the strap battery runs low (15%) so you can top it up before tonight, and when it finishes charging.": "Recevez une notification quand la batterie du bracelet est faible (15 %) afin de la recharger avant ce soir, et quand elle a fini de charger.", @@ -1293,6 +1330,7 @@ "Group…": "Grouper…", "Guaranteed wake": "Réveil garanti", "Guarding your session. Silence means you're on track.": "Surveille ta séance. Le silence veut dire que tout va bien.", +"Guarding — silence means you're on track.": "En veille — le silence veut dire que tu es sur la bonne voie.", "Gym equipment": "Équipement de salle", "Gym equipment (FTMS)": "Équipement de salle (FTMS)", "HEART RATE": "FRÉQUENCE CARDIAQUE", @@ -1324,6 +1362,7 @@ "HRV (rMSSD)": "VFC (rMSSD)", "HRV Reading": "Mesure de la VFC", "HRV has dropped well below your baseline, pointing to elevated stress or fatigue. Ease off and give your body time to recover.": "La VFC est tombée bien en dessous de votre référence, signe de stress ou de fatigue élevés. Levez le pied et laissez à votre corps le temps de récupérer.", +"HRV is a little low": "la VFC est un peu basse", "HRV is above baseline, a sign of a relaxed, well-recovered nervous system. Stress is low.": "La VFC est au-dessus de la référence, signe d'un système nerveux détendu et bien récupéré. Le stress est faible.", "HRV is below your baseline, so ease into the day": "La VFC est en dessous de votre référence, alors abordez la journée en douceur", "HRV reading": "Mesure de la VFC", @@ -1337,6 +1376,7 @@ "Hard to read right now": "Difficile à lire pour le moment", "Heads up: this test mode is off, so the report has no capture for it. For a useful report, turn the mode on, reproduce the problem while wearing the strap, then report again.": "À noter : ce mode de test est désactivé, le rapport ne contient donc aucune capture pour lui. Pour un rapport utile, active le mode, reproduis le problème en portant le bracelet, puis renvoie le rapport.", "Heads-up": "Attention", +"Heads-up - your body looks strained.": "Attention - ton corps semble sous tension.", "Heads-up. Your WHOOP only talks to one phone at a time. Force-quit the official WHOOP app first, or pairing may fail.": "Attention. Votre WHOOP ne communique qu'avec un seul téléphone à la fois. Forcez d'abord la fermeture de l'application WHOOP officielle, sinon le jumelage risque d'échouer.", "Health": "Santé", "Health & wellness": "Santé et bien-être", @@ -1371,6 +1411,7 @@ "Heart-rate variability as the engine behind your charge score.": "La variabilité de la fréquence cardiaque comme moteur de votre score de charge.", "Heart-rate zone split: %@": "Répartition des zones de fréquence cardiaque : %@", "Height": "Taille", +"Height & weight": "Taille et poids", "Height in centimetres": "Taille en centimètres", "Height in inches": "Taille en pouces", "Height, %lld feet %lld inches": "Taille, %1$lld pieds %2$lld pouces", @@ -1401,6 +1442,7 @@ "Hours asleep trend": "Tendance des heures de sommeil", "Hours of sleep debt per day": "Heures de dette de sommeil par jour", "Hours vs Needed": "Heures vs nécessaires", +"Hours vs Needed progress bar": "Barre de progression heures vs besoin", "How %@ is calculated": "Comment %@ est calculé", "How Charge is calculated": "Comment la charge est calculée", "How Charge is calculated. The method behind the score.": "Comment la Charge est calculée. La méthode derrière le score.", @@ -1486,6 +1528,7 @@ "Import failed. Your existing data was kept. %@": "Échec de l'import. Vos données existantes ont été conservées. %@", "Import failed: %@": "Échec de l'import : %@", "Import from Health Connect": "Importer depuis Health Connect", +"Import in Data Sources to bring them in, or add one you tracked elsewhere.": "Importe-les depuis Sources de données, ou ajoute-en un que tu as suivi ailleurs.", "Import lifting log…": "Importer un journal de musculation…", "Import nutrition CSV…": "Importer un CSV de nutrition…", "Import readings": "Importer des mesures", @@ -1521,6 +1564,7 @@ "Incl. Helio. Live heart rate where the band exposes it. Help us test.": "Incl. Helio. Fréquence cardiaque en direct lorsque le bracelet la fournit. Aidez-nous à tester.", "Independent, and experimental": "Indépendant, et expérimental", "Independent: not affiliated with WHOOP": "Indépendant : non affilié à WHOOP", +"Informational only - not medical advice.": "Informatif uniquement - pas un avis médical.", "Informational only, not medical advice.": "À titre informatif uniquement, pas un avis médical.", "Insights": "Perspectives", "Insights read your journal and outcomes": "Les analyses lisent ton journal et tes résultats", @@ -1601,16 +1645,19 @@ "Last offload completed": "Dernier déchargement terminé", "Last offload result": "Résultat du dernier déchargement", "Last reported by strap": "Dernière donnée signalée par le bracelet", +"Last seen": "Vu pour la dernière fois", "Last seen %@": "Vu pour la dernière fois %@", "Last session": "Dernière séance", "Last session: %@": "Dernière séance : %@", "Last sleep": "Dernier sommeil", "Last sync": "Dernière synchronisation", "Last sync: ": "Dernière synchro : ", +"Last synced": "Dernière synchro", "Last synced %@.": "Dernière synchronisation %@.", "Last synced %lld minutes ago. Reconnect to pull the latest.": "Dernière synchronisation il y a %lld minutes. Reconnectez-vous pour récupérer les dernières données.", "Last synced %lldm ago": "Dernière synchronisation il y a %lldmin", "Last week": "La semaine dernière", +"Last week only had": "La semaine dernière n'avait que", "Last year": "L'année dernière", "Late-caffeine nudge": "Rappel caféine tardive", "Later": "Plus tard", @@ -1618,6 +1665,7 @@ "Latest entry: %@.": "Dernière entrée : %@.", "Latest reading": "Dernière lecture", "Latest readings": "Dernières mesures", +"Latest sleep": "Dernier sommeil", "Latest sleep · %@": "Dernier sommeil · %@", "Latest sleep, %@. This is your last scored session. Wear the strap overnight for a fresh score.": "Dernier sommeil, %@. C'est votre dernière séance notée. Portez le bracelet pendant la nuit pour obtenir un nouveau score.", "Lean Body Mass": "Masse maigre", @@ -1626,6 +1674,8 @@ "Learning": "Apprentissage", "Learning your baseline, %lld of %lld nights.": "Apprentissage de votre référence, %lld de %lld nuits.", "Learning your pattern": "Apprentissage de votre schéma", +"Learning your pattern - keep wearing it overnight.": "Apprentissage de ton profil - continue à le porter la nuit.", +"Learning your pattern from your nightly temperature - keep wearing it overnight.": "Apprentissage de ton profil à partir de ta température nocturne - continue à le porter la nuit.", "Leave none active": "Ne laisser aucune active", "Led by your heart-rate variability (HRV) measured against your own ": "Guidé par ta variabilité de la fréquence cardiaque (VFC) mesurée par rapport à ta propre ", "Led by your heart-rate variability (HRV) measured against your own personal baseline, plus resting heart rate, last night's Rest, breathing rate, and a skin-temperature signal (an early illness or overreach flag). Higher HRV versus your baseline means more Charge. NOOP needs a few nights to learn your baseline first. Until then you'll see “Calibrating”.": "Déterminée par votre variabilité de fréquence cardiaque (VFC) mesurée par rapport à votre référence personnelle, ainsi que par la fréquence cardiaque au repos, le Repos de la nuit dernière, la fréquence respiratoire et un signal de température cutanée (un indicateur précoce de maladie ou de surcharge). Une VFC plus élevée par rapport à votre référence signifie plus de Charge. NOOP a d'abord besoin de quelques nuits pour apprendre votre référence. Jusque-là, vous verrez « Calibrage ».", @@ -1705,6 +1755,7 @@ "Logged %lld millilitres at %@": "%1$lld millilitres enregistrés à %2$@", "Logged as a nap. Wrong? Tap Edit to adjust your sleep and wake times.": "Enregistré comme sieste. Une erreur ? Touchez Modifier pour ajuster vos heures de coucher et de réveil.", "Logged bedtime at %@.": "Heure de coucher enregistrée à %@.", +"Logged sessions across": "Séances enregistrées sur", "Logged sessions across %@.": "Séances enregistrées sur %@.", "Logged today": "Enregistré aujourd'hui", "Logged wake-up at %@.": "Réveil enregistré à %@.", @@ -1722,13 +1773,17 @@ "Low confidence": "Faible confiance", "Low confidence staging. This night scored high efficiency but very little deep or REM, more likely an estimate miss than a real restorative shortfall.": "Répartition à faible confiance. Cette nuit affiche une efficacité élevée mais très peu de sommeil profond ou paradoxal, plus probablement une erreur d'estimation qu'un véritable déficit réparateur.", "Luteal": "Lutéale", +"Luteal range - temperature is running above your baseline.": "Phase lutéale - température au-dessus de ta référence.", "M": "L", +"MAX": "MAX", "MEDIUM": "MOYEN", "METHOD": "MÉTHODE", +"MIN": "MIN", "MODERATE": "MODÉRÉ", "MS RMSSD": "MS RMSSD", "Main sleep": "Sommeil principal", "Maintain": "Maintenir", +"Make": "Faire de", "Make %@ your active device now? It will provide your live data. You can change this any time.": "Faire de %@ votre appareil actif maintenant ? Il fournira vos données en direct. Vous pouvez changer cela à tout moment.", "Make %@ your active strap? From now on it provides your live data. %@'s history stays exactly as it is. Only new days come from %@.": "Faire de %1$@ votre bracelet actif ? Dès maintenant, il fournira vos données en direct. L'historique de %2$@ reste exactement tel quel. Seuls les nouveaux jours proviendront de %3$@.", "Make active": "Rendre actif", @@ -1807,6 +1862,7 @@ "Morning": "Matin", "Morning recap": "Récap du matin", "Most Active": "Le plus actif", +"Mostly": "Surtout", "Mostly %@ (%@).": "Principalement %1$@ (%2$@).", "Mostly steady, with a few isolated extra or skipped beats. Very common and usually nothing.": "Globalement régulier, avec quelques battements supplémentaires ou manqués isolés. Très courant et généralement sans gravité.", "Motion": "Mouvement", @@ -1882,6 +1938,7 @@ "NOOP will install its own key on the ring and become its owner. The Oura app will no longer control this ring. This is intended and it cannot be undone from NOOP.": "NOOP installera sa propre clé sur la bague et en deviendra le propriétaire. L'application Oura ne contrôlera plus cette bague. C'est voulu et cela ne peut pas être annulé depuis NOOP.", "NOOP will look for it nearby.": "NOOP le recherchera à proximité.", "NOOP will re-learn your baseline from tonight's data onward. Your history is kept, and it takes a few nights to settle.": "NOOP réapprendra votre référence à partir des données de cette nuit. Votre historique est conservé, et il faut quelques nuits pour se stabiliser.", +"NOOP will stop connecting to it. Its recorded data is": "NOOP arrêtera de s'y connecter. Ses données enregistrées sont", "NOOP · open on iPhone": "NOOP · ouvrir sur iPhone", "NOOP's own Charge, Effort and Rest, computed on your device from published methods.": "La Charge, l'Effort et le Repos propres à NOOP, calculés sur votre appareil selon des méthodes publiées.", "NOOP's primary, fully-supported band": "Le bracelet principal de NOOP, entièrement pris en charge", @@ -1893,6 +1950,7 @@ "Name": "Nom", "Name & confirm": "Nommer et confirmer", "Name the merged session": "Nomme la session fusionnée", +"Nap": "Sieste", "Nap detection": "Détection des siestes", "Nap ended": "Sieste terminée", "Nap started": "Sieste commencée", @@ -1945,6 +2003,7 @@ "No cardio load yet. Effort builds once your heart rate climbs into your effort zone (around 50% of your heart-rate reserve). A calm day honestly reads near zero.": "Aucune charge cardio pour l'instant. L'Effort augmente quand votre fréquence cardiaque atteint votre zone d'effort (environ 50 % de votre réserve de fréquence cardiaque). Une journée calme affiche honnêtement une valeur proche de zéro.", "No chest strap? No problem. NOOP can run off only your Apple Watch. It reads your watch's data through Apple Health and works out your Charge, Rest, Effort and Fitness Age right here on your phone. Everything stays on the device.": "Pas de ceinture pectorale ? Aucun problème. NOOP peut fonctionner avec votre seule Apple Watch. Il lit les données de votre montre via Apple Santé et calcule votre Charge, Repos, Effort et âge de forme physique directement sur votre téléphone. Tout reste sur l'appareil.", "No clear pattern": "Aucun schéma clair", +"No clear pattern yet.": "Pas encore de profil clair.", "No clear reading yet": "Aucune mesure claire pour l'instant", "No completed offload yet": "Aucun déchargement terminé pour l'instant", "No data": "Pas de données", @@ -1968,6 +2027,7 @@ "No live heart rate yet. Open Live to pair your strap.": "Aucune fréquence cardiaque en direct pour l'instant. Ouvrez En direct pour coupler votre bracelet.", "No live stream.": "Aucun flux en direct.", "No match. Add it as a custom marker below.": "Aucune correspondance. Ajoutez-le comme marqueur personnalisé ci-dessous.", +"No metrics yet. Import your WHOOP export or wear the strap to begin.": "Pas encore de métriques. Importe ton export WHOOP ou porte le bracelet pour commencer.", "No metrics yet. Import your Whoop export or wear the strap to begin.": "Aucune métrique pour l'instant. Importez votre export Whoop ou portez le bracelet pour commencer.", "No motion synced yet": "Aucun mouvement synchronisé pour l'instant", "No movement detail for this night": "Aucun détail de mouvement pour cette nuit", @@ -2005,9 +2065,11 @@ "No usable activity found. Point at a .gpx, .tcx or .fit workout file.": "Aucune activité utilisable trouvée. Indiquez un fichier d'entraînement .gpx, .tcx ou .fit.", "No usable rows found. Check the file has a date column (yyyy-MM-dd) and daily totals.": "Aucune ligne utilisable trouvée. Vérifiez que le fichier a une colonne de date (yyyy-MM-dd) et des totaux journaliers.", "No warranty; liability limited": "Aucune garantie ; responsabilité limitée", +"No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or ": "Aucun entraînement, stress, récupération, sommeil, VFC, FC au repos, effort, fréquence respiratoire ou ", "No workout, stress, recovery, sleep, HRV, resting-HR, strain, respiratory-rate or skin-temp readings fell inside %@. Wear your strap a few more days, or pick a wider range, then export again.": "Aucune mesure d'entraînement, de stress, de récupération, de sommeil, de VFC, de fréquence cardiaque au repos, d'effort, de fréquence respiratoire ou de température cutanée ne se situait dans %@. Portez votre bracelet quelques jours de plus, ou choisissez une plage plus large, puis exportez à nouveau.", "No workouts found. Point at a Hevy CSV export or a Liftosaur JSON export.": "Aucun entraînement trouvé. Indiquez un export CSV Hevy ou un export JSON Liftosaur.", "No workouts yet": "Pas encore d'entraînements", +"No workouts yet. They come from your WHOOP and Apple Health history. ": "Pas encore d'entraînements. Ils viennent de ton historique WHOOP et Apple Santé. ", "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in, or add one you tracked elsewhere.": "Aucun entraînement pour l'instant. Ils proviennent de l'historique de votre WHOOP et d'Apple Santé. Importez dans Sources de données pour les récupérer, ou ajoutez-en un que vous avez suivi ailleurs.", "No workouts yet. They come from your WHOOP and Apple Health history. Import in Data Sources to bring them in.": "Pas encore d'entraînements. Ils proviennent de votre historique WHOOP et Apple Santé. Importez dans Sources de données pour les intégrer.", "Non-binary": "Non-binaire", @@ -2024,6 +2086,7 @@ "Not calibrated yet": "Pas encore calibré", "Not connected": "Non connecté", "Not connected. Open Live to pair.": "Non connecté. Ouvrez En direct pour coupler.", +"Not enough Effort and Charge days this week to read your balance.": "Pas assez de jours d'Effort et de Charge cette semaine pour lire ton équilibre.", "Not enough clean beat data to lock a pace today. Try again rested, sitting still with the strap snug. For now we'll pace you at 5.5 br/min (coherence).": "Pas assez de données de battements propres pour fixer un rythme aujourd'hui. Réessayez reposé, assis immobile avec le bracelet bien ajusté. Pour l'instant, nous vous guiderons à 5,5 resp/min (cohérence).", "Not enough clean beats - sit still and try again.": "Pas assez de battements exploitables - reste immobile et réessaie.", "Not enough clean beats. Sit still and try again.": "Pas assez de battements propres. Restez immobile et réessayez.", @@ -2055,6 +2118,7 @@ "Note (optional)": "Note (facultatif)", "Notes": "Notes", "Nothing": "Rien", +"Nothing alarming - worth a calmer day.": "Rien d'alarmant - une journée plus calme ne ferait pas de mal.", "Nothing here yet. Tap Enable Apple Health above to read your data live, or import a Health export .zip in Data Sources.": "Rien pour l'instant. Appuyez sur Activer Apple Santé ci-dessus pour lire vos données en direct, ou importez un fichier .zip d'export Santé dans Sources de données.", "Nothing here yet. This sideloaded install can't read Apple Health directly. Import a Health export .zip in Data Sources, or turn on Shortcuts Export to bring your strap data into Health.": "Rien pour l'instant. Cette installation sideloadée ne peut pas lire Apple Santé directement. Importez un fichier .zip d'export Santé dans Sources de données, ou activez l'export via Raccourcis pour faire entrer les données de votre bracelet dans Santé.", "Nothing imported": "Rien d'importé", @@ -2066,11 +2130,13 @@ "Nothing leaves your %@": "Rien ne quitte votre %@", "Nothing leaves your phone": "Rien ne quitte ton téléphone", "Nothing notable": "Rien de notable", +"Nothing notable - your signals look like your normal range.": "Rien de notable - tes signaux sont dans ta plage normale.", "Nothing offloaded for this window yet.": "Rien de déchargé pour cette période pour l'instant.", "Nothing selected yet.": "Rien de sélectionné encore.", "Nothing to export": "Rien à exporter", "Nothing to fuse yet": "Rien à fusionner pour l'instant", "Nothing to reclaim right now. NOOP already cleans up import scratch space automatically.": "Rien à récupérer pour l'instant. NOOP nettoie déjà automatiquement l'espace temporaire d'import.", +"Nothing's flagging. Train to feel - your body's holding steady.": "Rien ne ressort. Entraîne-toi au feeling - ton corps tient bon.", "Notifications": "Notifications", "Notifications are off, so this can't buzz yet. Turn on the master switch in ": "Les notifications sont désactivées, donc ça ne peut pas encore vibrer. Active l'interrupteur principal dans ", "Notifications are off, so this can't buzz yet. Turn on the master switch in Notifications to let it through.": "Les notifications sont désactivées, donc cela ne peut pas encore vibrer. Activez l'interrupteur principal dans Notifications pour le laisser passer.", @@ -2106,6 +2172,7 @@ "On, your recovery, sleep, HRV and workouts are shared with the provider for tailored coaching.": "Activé, votre récupération, sommeil, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", "On-device": "Sur l'appareil", "On-device estimate (approximate), not a diagnosis.": "Estimation sur l'appareil (approximative), pas un diagnostic.", +"On-device estimate - not a diagnosis.": "Estimation sur l'appareil - pas un diagnostic.", "On-device footprint": "Empreinte sur l'appareil", "On: a short summary of your strongest patterns and logged health numbers is added. Summaries only, never raw readings.": "Activé : un court résumé de vos tendances les plus marquées et de vos données de santé enregistrées est ajouté. Uniquement des résumés, jamais de mesures brutes.", "On: your charge, rest, HRV and workouts are shared with the provider for tailored coaching.": "Activé : votre charge, repos, VFC et entraînements sont partagés avec le fournisseur pour un coaching personnalisé.", @@ -2115,8 +2182,10 @@ "Once a day at a time you choose, NOOP writes a timestamped strap log (plus the raw 5/MG capture, if you have one) to its export folder. No sharing, nothing leaves the phone. Useful for chasing an intermittent overnight fault. Off by default.": "Une fois par jour à l'heure de ton choix, NOOP écrit un journal du bracelet horodaté (plus la capture brute 5/MG, si tu en as une) dans son dossier d'export. Aucun partage, rien ne quitte le téléphone. Utile pour traquer un défaut nocturne intermittent. Désactivé par défaut.", "Once this week has a day or two of data, your week-in-review appears here.": "Une fois que cette semaine a un ou deux jours de données, votre bilan de la semaine apparaît ici.", "One check-in per day; picking another face overwrites today's.": "Un check-in par jour ; choisir un autre visage remplace celui d'aujourd'hui.", +"One of your signals is flagging. You can train, but keep it controlled and bank the recovery.": "Un de tes signaux ressort. Tu peux t'entraîner, mais reste contrôlé et engrange la récupération.", "One phone at a time": "Un seul téléphone à la fois", "One reading so far: %@ %@. Log a few more to see a trend.": "Une seule mesure pour l'instant : %@ %@. Enregistrez-en quelques-unes de plus pour voir une tendance.", +"Only": "Seulement", "Only buzz when worn": "Vibrer uniquement quand porté", "Only during active hours": "Uniquement pendant les heures actives", "Only if your server requires one": "Uniquement si votre serveur en exige une", @@ -2147,6 +2216,7 @@ "Opens workout detail": "Ouvre le détail de l'entraînement", "Optimal": "Optimal", "Optional": "Facultatif", +"Optional - sharpens VO₂max": "Optionnel - affine le VO₂max", "Optional note": "Note facultative", "Optional phone notifications, off by default. These arrive after your strap syncs ": "Notifications téléphone facultatives, désactivées par défaut. Elles arrivent après la synchro de ton bracelet ", "Optional trackers, off by default. Turn them on to add their cards. Everything stays on %@.": "Trackers facultatifs, désactivés par défaut. Activez-les pour ajouter leurs cartes. Tout reste sur %@.", @@ -2218,6 +2288,7 @@ "Per night, trailing 30 days": "Par nuit, sur les 30 derniers jours", "Per-day wake time": "Heure de réveil par jour", "Per-night need": "Besoin par nuit", +"Per-night sleep balance:": "Équilibre du sommeil par nuit :", "Per-night sleep balance: %lld nights, net %@": "Bilan de sommeil par nuit : %1$lld nuits, net %2$@", "Permission granted.": "Autorisation accordée.", "Personal Experiment": "Expérience personnelle", @@ -2242,6 +2313,7 @@ "Pick your device": "Choisissez votre appareil", "Pick your strap below, then tap Scan. NOOP will find it.": "Choisissez votre bracelet ci-dessous, puis appuyez sur Scanner. NOOP le trouvera.", "Picked as your main sleep because it started near your usual sleep time.": "Choisi comme sommeil principal car il a commencé près de votre heure de sommeil habituelle.", +"Picked as your main sleep because it was your longest block": "Choisi comme ton sommeil principal car c'était ton plus long bloc", "Picked as your main sleep because it was your longest block (%@), near your usual bedtime.": "Choisi comme sommeil principal car c'était votre plus long bloc (%@), près de votre heure de coucher habituelle.", "Picked as your main sleep because it was your longest block (%@).": "Choisi comme sommeil principal car c'était votre plus long bloc (%@).", "Pinch to zoom · drag to pan": "Pincer pour zoomer · glisser pour déplacer", @@ -2289,6 +2361,7 @@ "Pulling your strap's stored history. This drains oldest-first; a deep backlog ": "Récupération de l'historique stocké sur ton bracelet. Ça se vide du plus ancien au plus récent ; un gros retard ", "Pulling your strap's stored history. This drains oldest-first; a deep backlog now continues automatically across passes instead of waiting between syncs.": "Récupération de l'historique stocké sur votre bracelet. Il se vide en commençant par le plus ancien ; un arriéré important se poursuit désormais automatiquement sur plusieurs passages au lieu d'attendre entre les synchronisations.", "Pulls your strap's stored history immediately, without waiting for the next automatic sync.": "Récupère immédiatement l'historique stocké sur votre bracelet, sans attendre la prochaine synchronisation automatique.", +"Push": "Pousse", "Put the band into pairing mode, on your wrist and awake.": "Mettez le bracelet en mode couplage, au poignet et actif.", "Put the strap on first. The deep stream is on-wrist only.": "Mettez d'abord le bracelet. Le flux approfondi fonctionne uniquement au poignet.", "Put your WHOOP 4.0 on your wrist and make sure it's awake.": "Mettez votre WHOOP 4.0 au poignet et assurez-vous qu'il est actif.", @@ -2369,6 +2442,7 @@ "Recalibrate Charge baseline": "Recalibrer la référence de Charge", "Recalibrate your Charge baseline?": "Recalibrer votre référence de Charge ?", "Recent": "Récent", +"Recent activity": "Activité récente", "Recent changes and what to expect": "Changements récents et ce à quoi t'attendre", "Recent intervals: ": "Intervalles récents : ", "Recent intervals: %@ ms": "Intervalles récents : %@ ms", @@ -2473,6 +2547,8 @@ "Rest baseline": "Référence de repos", "Rest quality": "Qualité du repos", "Rest up": "Reposez-vous", +"Rest up - you logged feeling unwell, and your numbers agree.": "Repose-toi - tu as signalé ne pas te sentir bien, et tes chiffres le confirment.", +"Rest up - you logged feeling unwell. Take it easy today.": "Repose-toi - tu as signalé ne pas te sentir bien. Vas-y doucement aujourd'hui.", "Rest ↔ Charge": "Repos ↔ Charge", "Rest: how restorative was your sleep?": "Repos : à quel point votre sommeil a-t-il été réparateur ?", "Restart": "Redémarrer", @@ -2546,6 +2622,7 @@ "STREAMING": "DIFFUSION", "STRENUOUS": "INTENSE", "STRONGER SIGNAL": "SIGNAL PLUS FORT", +"SUMMARY": "RÉSUMÉ", "SYNCING %lld": "SYNCHRONISATION %lld", "SYNTHESIS": "SYNTHÈSE", "Same cardiovascular-load idea as WHOOP's Day Strain (0–21). We rescaled the top of the ladder from 21 to 100 so all three scores share one scale. The rungs didn't move, so a 100 is as rare as a 21.0 was.": "Même principe de charge cardiovasculaire que l'Effort du jour de WHOOP (0-21). Nous avons redimensionné le haut de l'échelle de 21 à 100 pour que les trois scores partagent une seule échelle. Les échelons n'ont pas bougé, un 100 est donc aussi rare que l'était un 21,0.", @@ -2580,6 +2657,7 @@ "Scan again": "Scanner à nouveau", "Scan and connect to start a live stream.": "Scanne et connecte-toi pour démarrer un flux en direct.", "Scan connects to %@. To pair or switch bands, open Devices.": "« Scanner » se connecte à %@. Pour appairer ou changer de bracelet, ouvrez Appareils.", +"Scan for": "Rechercher", "Scan for %@": "Rechercher %@", "Scan for your Oura ring": "Rechercher votre bague Oura", "Scan for your ring": "Rechercher votre bague", @@ -2601,6 +2679,7 @@ "See how it works": "Voir comment ça marche", "See recovery, beautifully": "Visualisez la récupération, avec élégance", "See recovery, clearly": "Vois ta récupération, clairement", +"See the summary of your last session.": "Voir le résumé de ta dernière séance.", "See what shaped your Charge": "Découvrez ce qui a façonné votre Charge", "See your full readiness": "Voir votre préparation complète", "Select": "Sélectionner", @@ -2616,9 +2695,11 @@ "Server URL": "URL du serveur", "Session": "Séance", "Session done": "Séance terminée", +"Session ended": "Séance terminée", "Session ended. Try a paced breath instead.": "Séance terminée. Essayez plutôt une respiration cadencée.", "Session live": "Séance en direct", "Session progress": "Progression de la séance", +"Session running": "Séance en cours", "Session summary": "Résumé de la séance", "Sessions": "Séances", "Set": "Définir", @@ -2631,6 +2712,7 @@ "Settings → Notifications to let it through.": "Réglages → Notifications pour l'autoriser.", "Settling": "Stabilisation", "Setup": "Configuration", +"Several signals are down at once. Treat today as recovery - easy movement, real sleep tonight.": "Plusieurs signaux sont en baisse en même temps. Traite aujourd'hui comme un jour de récupération - mouvement léger, vrai sommeil ce soir.", "Sex": "Sexe", "Share": "Partager", "Share 5/MG capture (for the decode effort)": "Partager la capture 5/MG (pour l'effort de décodage)", @@ -2640,13 +2722,16 @@ "Share of imported zone time, duration-weighted across sessions, approximate.": "Part du temps de zone importé, pondéré par la durée entre les séances, approximatif.", "Share on-device signals with the Coach": "Partager les signaux sur l'appareil avec le Coach", "Share strap log (for bug reports)": "Partager le journal du bracelet (pour les rapports de bug)", +"Share trends report": "Partager le rapport de tendances", "Sharing %lld bpm. Waiting for a device to pair.": "Partage de %lld bpm. En attente d'un appareil à appairer.", +"Sharpens VO₂max": "Affine le VO₂max", "Short axis": "Petit axe", "Shortcut name": "Nom du raccourci", "Shortcuts Export": "Export Raccourcis", "Shortcuts file export": "Export de fichier Raccourcis", "Shorten window": "Raccourcir la fenêtre", "Should you push today?": "Devriez-vous forcer aujourd'hui ?", +"Show": "Afficher", "Show %@": "Afficher %@", "Show & reorder": "Afficher et réorganiser", "Show all metrics": "Afficher toutes les métriques", @@ -2698,11 +2783,14 @@ "Sleep consistency nightly bed and wake chart": "Graphique de régularité du sommeil : heures de coucher et de réveil par nuit", "Sleep debt trend chart": "Graphique de tendance de la dette de sommeil", "Sleep deleted.": "Sommeil supprimé.", +"Sleep deleted. NOOP won't detect sleep between": "Sommeil supprimé. NOOP ne détectera plus de sommeil entre", +"Sleep duration": "Durée de sommeil", "Sleep hours trend chart": "Graphique de tendance des heures de sommeil", "Sleep marks": "Marqueurs de sommeil", "Sleep performance": "Performance de sommeil", "Sleep performance %lld of 100": "Performance de sommeil %lld sur 100", "Sleep quality": "Qualité du sommeil", +"Sleep regularity": "Régularité du sommeil", "Sleep score": "Score de sommeil", "Sleep scored from how long you slept versus how much you needed, how efficient the night was, and the restorative (deep and REM) share of it.": "Sommeil évalué à partir de la durée dormie par rapport à votre besoin, de l'efficacité de la nuit et de la part réparatrice (sommeil profond et paradoxal).", "Sleep sorting, scores, recording, and where your numbers come from.": "Tri du sommeil, scores, enregistrement et provenance de vos chiffres.", @@ -2710,11 +2798,14 @@ "Sleep stages, %@": "Stades de sommeil, %@", "Sleep stages, no data": "Stades de sommeil, aucune donnée", "Sleep staging": "Répartition des phases de sommeil", +"Sleep steadiness: Rest varied ±": "Stabilité du sommeil : le Repos a varié de ±", "Sleep steadiness: Rest varied ±%@ pts night to night.": "Stabilité du sommeil : le Repos a varié de ±%@ pts d'une nuit à l'autre.", "Sleep window over recent nights": "Fenêtre de sommeil sur les nuits récentes", "Sleep · scores · recording · where your numbers come from": "Sommeil · scores · enregistrement · provenance de vos chiffres", "Sleep-debt ledger": "Registre de dette de sommeil", "Slept": "Dormi", +"Slept minutes balance out against your need.": "Les minutes dormies s'équilibrent avec ton besoin.", +"Slightly off baseline (": "Légèrement écarté de ta référence (", "Slightly off baseline (HRV is a little low), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la VFC est un peu basse), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", "Slightly off baseline (resting HR is a touch high), so you're moderately activated. Nothing alarming; just don't overreach.": "Légèrement écarté de la référence (la FC au repos est un peu élevée), vous êtes donc modérément activé. Rien d'alarmant, évitez juste de trop en faire.", "Slope source": "Source de la pente", @@ -2727,6 +2818,7 @@ "Solid": "Solide", "Solid signal.": "Signal solide.", "Some occasional extra or skipped beats": "Quelques battements supplémentaires ou manqués occasionnels", +"Some signals are up": "Certains signaux sont en hausse", "Source": "Source", "Source Apple Health": "Source Apple Santé", "Source imported activity file": "Source : fichier d'activité importé", @@ -2787,6 +2879,7 @@ "Steps, heart, sleep, body composition and VO₂ max - synced from the desktop app.": "Pas, cœur, sommeil, composition corporelle et VO₂ max - synchronisés depuis l'app de bureau.", "Steps, heart, sleep, body composition and VO₂ max, read locally on %@.": "Pas, cœur, sommeil, composition corporelle et VO₂ max, lus localement sur %@.", "Still": "Immobile", +"Still learning your baseline - keeping an eye out.": "Encore en train d'apprendre ta référence - on garde un œil dessus.", "Stop": "Arrêter", "Stop caffeine after about %@ to keep most of it cleared by %@.": "Arrêtez la caféine vers %1$@ pour qu'elle soit en grande partie éliminée d'ici %2$@.", "Stop session": "Arrêter la séance", @@ -2820,6 +2913,7 @@ "Strap not worn": "Bracelet non porté", "Strap sync": "Sync du bracelet", "Strap wake-alarm": "Réveil du bracelet", +"Strap-guided effort session. It only buzzes when you drift off today's band.": "Séance d'effort guidée par le bracelet. Il ne vibre que si tu sors de la zone du jour.", "Streaming live": "Diffusion en direct", "Streaming locally": "Diffusion en local", "Streaming now": "En cours de diffusion", @@ -2848,6 +2942,7 @@ "Support NOOP: help and contact.": "Soutenir NOOP : aide et contact.", "Support the build": "Soutenir le développement", "Suppress auto-nudges overnight (10pm–7am).": "Désactiver les alertes automatiques la nuit (22h–7h).", +"Surplus nights count back against it. An earlier night or two would clear it.": "Les nuits en surplus le compensent. Une ou deux nuits plus tôt suffiraient à le résorber.", "Sustained high stress": "Stress élevé prolongé", "Sweep progress": "Progression du balayage", "Sweeping…": "Balayage en cours…", @@ -2859,6 +2954,7 @@ "Sync now. Connect your strap first.": "Synchroniser maintenant. Connecte d'abord ton bracelet.", "Sync now. Pulls your strap's stored history immediately, without waiting ": "Synchroniser maintenant. Récupère aussitôt l'historique stocké sur ton bracelet, sans attendre ", "Synced from": "Synchronisé depuis", +"Synced from:": "Synchronisé depuis :", "Synced from: %@": "Synchronisé depuis : %@", "Syncing": "Synchronisation", "Syncing strap history…": "Synchronisation de l'historique du bracelet…", @@ -2936,6 +3032,7 @@ "The ring is not bricked. To go back to where you started, factory-reset it again and set it ": "La bague n'est pas HS. Pour revenir au point de départ, refais une réinitialisation d'usine et configure-la ", "The scatter looked rounder and more spread out than a tight, steady beat. This has many ordinary causes and is not a diagnosis.": "La dispersion semblait plus ronde et plus étalée qu'un battement compact et régulier. Cela a de nombreuses causes ordinaires et n'est pas un diagnostic.", "The sensor needs skin contact before data starts to mean anything.": "Le capteur a besoin d'un contact avec la peau pour que les données commencent à avoir un sens.", +"The share sheet can save the PDF to Files, or send it on.": "Le menu de partage permet d'enregistrer le PDF dans Fichiers, ou de l'envoyer.", "The strap alarm is a silent buzz, not a sound": "L'alarme du bracelet est une vibration silencieuse, pas un son", "The strap buzzes a gentle rhythm just below your current heart rate, a felt metronome to relax toward. It trails your heart down rather than yanking it, and stops on its own.": "Le bracelet vibre selon un rythme doux juste en dessous de votre fréquence cardiaque actuelle, un métronome ressenti vers lequel vous détendre. Il accompagne votre cœur vers le bas plutôt que de le tirer brusquement, et s'arrête de lui-même.", "The strap signal was gone for 10 minutes, so the session ended itself.": "Le signal du bracelet a disparu pendant 10 minutes, donc la séance s'est arrêtée d'elle-même.", @@ -2970,6 +3067,7 @@ "This builds from the strap as it syncs. Effort and rest appear after you have worn it and slept a night. Charge needs about four nights of sleep to learn your baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first couple of weeks. On a WHOOP 5 or MG the strap banks little history, so the night count can climb slowly or sit at 0 of 4 until you have worn it across a few nights. That's its sync limit, not a fault. Import your WHOOP export to skip the wait.": "Cela se construit à partir du bracelet au fil de sa synchronisation. L'Effort et le Repos apparaissent après l'avoir porté et dormi une nuit. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre votre référence (vous verrez \"Étalonnage\" jusque-là), et continue de s'affiner pendant vos deux premières semaines. Sur un WHOOP 5 ou MG, le bracelet conserve peu d'historique, donc le compteur de nuits peut progresser lentement ou rester à 0 sur 4 tant que vous ne l'avez pas porté quelques nuits. C'est sa limite de synchronisation, pas un défaut. Importez votre export WHOOP pour éviter l'attente.", "This builds from the strap as it syncs. Strain and sleep appear after you have worn it and slept a night. Recovery needs about a week of nights to learn your baseline, or import your WHOOP export to skip the wait.": "Ceci se construit depuis le bracelet au fil de la synchronisation. L'effort et le sommeil apparaissent après que vous l'ayez porté et dormi une nuit. La récupération nécessite environ une semaine de nuits pour apprendre votre référence, ou importez votre export WHOOP pour éviter l'attente.", "This can run while you finish setup": "Ça peut tourner pendant que tu termines la configuration", +"This can't be undone.": "C'est irréversible.", "This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", "This install can't connect to Apple Health directly. It was sideloaded with a free signing profile, which doesn't include Apple's Health permission, so there's nothing to grant here.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été installée en sideload avec un profil de signature gratuit, qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à autoriser ici.", "This install can't connect to Apple Health directly. It was signed with a profile that doesn't include Apple's Health permission, so there's nothing to enable, and NOOP won't appear under Settings › Health.": "Cette installation ne peut pas se connecter directement à Apple Santé. Elle a été signée avec un profil qui n'inclut pas l'autorisation Santé d'Apple : il n'y a donc rien à activer, et NOOP n'apparaîtra pas dans Réglages › Santé.", @@ -2987,6 +3085,7 @@ "This metric needs at least two nights of data.": "Cette métrique a besoin d'au moins deux nuits de données.", "This moves the night to a time with no recorded data. Stages can't be derived there, so it may show as empty until data covers it.": "Cela déplace la nuit vers une période sans données enregistrées. Les stades ne peuvent pas y être calculés, la nuit peut donc apparaître vide tant que des données ne la couvrent pas.", "This night scored high efficiency but very little deep or REM, more likely a staging estimate miss than a real restorative shortfall. The totals are kept as-is; read the split with care.": "Cette nuit a obtenu une efficacité élevée mais très peu de sommeil profond ou paradoxal, ce qui ressemble plus à une erreur d'estimation des phases qu'à un réel déficit de récupération. Les totaux sont conservés tels quels ; interprétez la répartition avec prudence.", +"This permanently deletes all data recorded from": "Ceci supprime définitivement toutes les données enregistrées depuis", "This permanently deletes all data recorded from %@. This can't be undone.": "Cela supprime définitivement toutes les données enregistrées depuis %@. Cette action est irréversible.", "This permanently deletes everything imported from Apple Health: heart rate, HRV, ": "Ceci supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, ", "This permanently deletes everything imported from Apple Health: heart rate, HRV, sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "Cela supprime définitivement tout ce qui a été importé depuis Apple Santé : fréquence cardiaque, VFC, sommeil, pas, entraînements et plus encore. Les données en direct de votre bracelet ne sont pas concernées. Cette action est irréversible.", @@ -3047,9 +3146,12 @@ "Total Workouts": "Total des entraînements", "Track GPS route": "Suivre le tracé GPS", "Track a workout manually. Records heart rate and effort until you end it.": "Suivez un entraînement manuellement. Enregistre la fréquence cardiaque et l'effort jusqu'à ce que vous le terminiez.", +"Trailing": "Sur les derniers", "Trailing %lld days": "%lld derniers jours", "Trailing window": "Fenêtre glissante", "Train by feel. The strap buzzes so you don't have to watch a screen.": "Entraînez-vous au ressenti. Le bracelet vibre pour que vous n'ayez pas à regarder un écran.", +"Training load": "Charge d'entraînement", +"Training variety": "Variété d'entraînement", "Treadmill, indoor bike, rower or cross-trainer (Bluetooth FTMS)": "Tapis de course, vélo d'intérieur, rameur ou vélo elliptique (Bluetooth FTMS)", "Treat this as permanent.": "Considère ça comme définitif.", "Trend": "Tendance", @@ -3060,6 +3162,7 @@ "Trends need history to draw. Import your WHOOP export in Data Sources ": "Les tendances ont besoin d'historique pour s'afficher. Importe ton export WHOOP dans Sources de données ", "Trends need history to draw. Import your WHOOP export in Data Sources to see weeks, months and years instantly.": "Les tendances ont besoin d'historique pour s'afficher. Importez votre export WHOOP dans Sources de données pour voir semaines, mois et années instantanément.", "Trends report": "Rapport de tendances", +"Trends report (PDF)": "Rapport de tendances (PDF)", "Try WHOOP 5/MG protocol probes": "Essayer les sondes de protocole WHOOP 5/MG", "Try Yesterday or 2 days ago from the bar above if the strap or import did not produce a daily vitals snapshot yet.": "Essaie Hier ou Il y a 2 jours dans la barre ci-dessus si le bracelet ou l'import n'a pas encore produit d'instantané quotidien des constantes.", "Try a longer interval like 3M, 6M, 1Y, or ALL to see this vital’s trend.": "Essaie un intervalle plus long comme 3M, 6M, 1Y ou ALL pour voir la tendance de cette constante.", @@ -3083,6 +3186,7 @@ "Type in a number from your own report. It stays on %@.": "Saisissez un chiffre issu de votre propre rapport. Il reste sur %@.", "Type in a number from your own report. It stays on this phone.": "Saisis un chiffre issu de ton propre compte rendu. Il reste sur ce téléphone.", "Typical": "Habituel", +"Typical bedtime": "Heure de coucher habituelle", "Typical effort": "Effort habituel", "Typical for you": "Typique pour vous", "Typical range": "Plage typique", @@ -3194,6 +3298,7 @@ "Wake your Mi Band and make sure it isn't connected to the Mi Fitness / Zepp Life app right now.": "Réveillez votre Mi Band et assurez-vous qu'elle n'est pas connectée à l'application Mi Fitness / Zepp Life en ce moment.", "Wake your strap. Put it on, or dampen the contacts.": "Réveillez votre bracelet. Portez-le, ou humidifiez les contacts.", "Wake-up": "Réveil", +"Wake-up time": "Heure de réveil", "Walking": "Marche", "Want the full breakdown of every metric and how sure NOOP is about each one? The “About Apple Watch data” page in Settings has the honest table.": "Vous voulez le détail complet de chaque métrique et le degré de certitude de NOOP pour chacune ? La page « À propos des données Apple Watch » dans Paramètres contient le tableau honnête.", "Warmer than your baseline": "Plus chaude que votre référence", @@ -3217,10 +3322,12 @@ "Wear & presence": "Port et présence", "Wear it snug on your wrist or bicep, sensor against skin.": "Portez-le ajusté au poignet ou au biceps, capteur contre la peau.", "Wear state": "État de port", +"Wear the strap for a few nights and your readiness read will appear here.": "Porte le bracelet quelques nuits et ta lecture de forme apparaîtra ici.", "Wear the strap for scoring": "Portez le bracelet pour obtenir un score", "Wear the strap overnight and these read from your nightly skin temperature.": "Portez le bracelet pendant la nuit et ces valeurs seront lues à partir de votre température cutanée nocturne.", "Wear the strap overnight to score a night first.": "Porte le bracelet la nuit pour qu'une nuit soit d'abord évaluée.", "Wear the strap, tap once, then let it sync and share your strap log.": "Portez le bracelet, touchez une fois, laissez-le se synchroniser, puis partagez le journal du bracelet.", +"Wear your strap a few more days, or pick a wider range, then export again.": "Porte ton bracelet encore quelques jours, ou choisis une plage plus large, puis exporte à nouveau.", "Wear your strap for a full week and it appears here.": "Porte ton bracelet pendant une semaine complète et ça apparaît ici.", "Wear your strap or import your WHOOP export in Data Sources. Once this week has a ": "Porte ton bracelet ou importe ton export WHOOP dans Sources de données. Une fois que cette semaine aura ", "Wed": "Mer", @@ -3335,26 +3442,32 @@ "You are replacing Oura.": "Tu remplaces Oura.", "You can change what you share any time in Settings › Health › Data Access & Devices.": "Vous pouvez modifier ce que vous partagez à tout moment dans Paramètres › Santé › Accès aux données et appareils.", "You can still bring your data in by importing a Health export from Data Sources. A build from the App Store, or one signed with a paid Apple Developer account, connects directly.": "Vous pouvez tout de même importer vos données via un export Santé depuis les Sources de données. Une version issue de l'App Store, ou signée avec un compte Apple Developer payant, se connecte directement.", +"You carried more Charge than you spent this week: there's room to push if you want it.": "Tu as porté plus de Charge que tu n'en as dépensé cette semaine : il y a de la marge si tu veux pousser.", "You removed your active strap. Choose which paired band provides your live data, or ": "Tu as retiré ton bracelet actif. Choisis quel bracelet appairé fournit tes données en direct, ou ", "You removed your active strap. Choose which paired band provides your live data, or leave none active and pair one later.": "Vous avez retiré votre bracelet actif. Choisissez quel bracelet associé fournira vos données en direct, ou n'en laissez aucun actif et associez-en un plus tard.", "You said: %@": "Vous avez dit : %@", "You'll be reminded around %@.": "Vous recevrez un rappel vers %@.", "You'll likely wake around %lld ± %lld Charge if you sleep about %@ tonight.": "Vous vous réveillerez probablement avec une Charge d'environ %1$lld ± %2$lld si vous dormez environ %3$@ cette nuit.", "You're all caught up.": "Vous êtes à jour.", +"You're carrying about": "Tu portes environ", "You're carrying about %@ of surplus over %@. You've slept past your need on balance. Nicely ahead.": "Vous portez un surplus d'environ %@ sur %@. Vous avez dormi au-delà de vos besoins, en solde. Bien en avance.", "You're connected.": "Tu es connecté.", "You're connected. NOOP is reading your Apple Watch data now. Your Charge score will spend its first week or so calibrating, then settle in.": "Vous êtes connecté. NOOP lit désormais les données de votre Apple Watch. Votre score de Charge passera sa première semaine environ à se calibrer, puis se stabilisera.", "You're in balance with your baseline, so moderate strain is well-judged": "Vous êtes en équilibre avec votre référence, un effort modéré est donc bien choisi", "You're not missing the broadcast feature. To share your heart rate with a gym machine, ": "Tu ne rates rien avec la fonction de diffusion. Pour partager ta fréquence cardiaque avec une machine de salle, ", "You're on the latest (%@).": "Vous avez la dernière version (%@).", +"You're roughly on top of your sleep across": "Tu es à peu près à jour sur ton sommeil sur", "You're roughly on top of your sleep across %@. Slept minutes balance out against your need.": "Vous êtes globalement à jour sur votre sommeil sur %@. Les minutes dormies compensent vos besoins.", "You're sitting around your typical autonomic baseline: moderate stress, a normal, balanced day.": "Vous êtes proche de votre référence autonome habituelle : stress modéré, une journée normale et équilibrée.", +"You've banked about": "Tu as accumulé environ", "You've banked about %@ of sleep debt over %@. Surplus nights count back against it. An earlier night or two would clear it.": "Vous avez accumulé environ %@ de dette de sommeil sur %@. Les nuits en surplus viennent la réduire. Une ou deux nuits plus tôt suffiraient à l'effacer.", "You've been seated for about %lld min. Time to move.": "Vous êtes assis depuis environ %lld min. Il est temps de bouger.", +"You've slept past your need on balance. Nicely ahead.": "Tu as dormi au-delà de ton besoin en moyenne. Bien joué.", "Your Apple Watch as a device": "Votre Apple Watch en tant qu'appareil", "Your Charge (recovery) on the watch face, with Effort and Rest in the rectangular card.": "Votre Charge (récupération) sur le cadran, avec Effort et Repos dans la carte rectangulaire.", "Your Charge score learns a personal baseline from your heart-rate variability, resting heart rate and more over time. If a bad first week set it off, you can re-learn it from tonight. Your history stays.": "Votre score de Charge apprend une référence personnelle à partir de votre variabilité de fréquence cardiaque, de votre fréquence cardiaque au repos et plus encore, au fil du temps. Si une mauvaise première semaine l'a faussée, vous pouvez la faire réapprendre dès ce soir. Votre historique reste.", "Your Data, Fused": "Vos données, fusionnées", +"Your Effort outpaced your Charge this week: you leaned into the red. Watch for a recovery dip.": "Ton Effort a dépassé ta Charge cette semaine : tu as tiré dans le rouge. Surveille une baisse de récupération.", "Your GPS route for this session, recorded and stored on your device. Nothing leaves your phone.": "Votre trajet GPS pour cette séance, enregistré et stocké sur votre appareil. Rien ne quitte votre téléphone.", "Your HRV dipped while you were still. Want a minute to breathe?": "Votre VFC a chuté pendant que vous étiez immobile. Envie d'une minute pour respirer ?", "Your Monday-to-Sunday, read in one glance.": "Votre semaine du lundi au dimanche, lue en un coup d'œil.", @@ -3362,6 +3475,7 @@ "Your WHOOP 5/MG won't arm this until Experimental mode is on (Settings, Experimental). Right now your wake time is saved but the strap is NOT armed. Even with Experimental on, a 5/MG strap-driven wake is still unconfirmed on our side, so keep a backup alarm.": "Votre WHOOP 5/MG n'activera pas cette fonction tant que le mode Expérimental n'est pas activé (Paramètres, Expérimental). Pour l'instant, votre heure de réveil est enregistrée mais le bracelet n'est PAS armé. Même avec le mode Expérimental activé, un réveil piloté par le bracelet 5/MG reste non confirmé de notre côté, gardez donc un réveil de secours.", "Your WHOOP is at 100%.": "Votre WHOOP est à 100 %.", "Your WHOOP only talks to one phone at a time.": "Votre WHOOP ne communique qu'avec un seul téléphone à la fois.", +"Your age": "Ton âge", "Your autonomic markers are skewed toward stress today. Treat it as a recovery-focused day.": "Vos marqueurs autonomes penchent vers le stress aujourd'hui. Considérez-la comme une journée axée sur la récupération.", "Your bands report different numbers. Here's every source, and the one NOOP is using.": "Vos bracelets rapportent des chiffres différents. Voici chaque source, et celle que NOOP utilise.", "Your bloods, BP and body numbers. Kept private here.": "Tes analyses sanguines, ta tension et tes mensurations. Gardées privées ici.", @@ -3376,6 +3490,7 @@ "Your data has been restored. Quit and reopen NOOP for it to take effect.": "Vos données ont été restaurées. Quittez et rouvrez NOOP pour que cela prenne effet.", "Your fluid intake today, on %@ only.": "Votre apport en liquides aujourd'hui, sur %@ uniquement.", "Your fluid intake today, on this phone only.": "Ton apport en liquides aujourd'hui, sur ce téléphone uniquement.", +"Your last": "Tes", "Your last %lld hours have stayed in the high band. A few minutes of paced breathing can help downshift your nervous system.": "Vos dernières %lld heures sont restées dans la bande haute. Quelques minutes de respiration rythmée peuvent aider à apaiser votre système nerveux.", "Your last %lld readings: %@ %@, holding steady.": "Vos %1$lld dernières mesures : %2$@ %3$@, stables.", "Your last %lld readings: %@ %@, trending down.": "Vos %1$lld dernières mesures : %2$@ %3$@, tendance à la baisse.", @@ -3399,6 +3514,7 @@ "Your ring": "Votre anneau", "Your ring talks to NOOP only, fully offline, no Oura account.": "Votre anneau ne communique qu'avec NOOP, entièrement hors ligne, sans compte Oura.", "Your scores build over a few nights": "Vos scores se construisent en quelques nuits", +"Your signals are aligned and your load is supported. A harder session is well backed today.": "Tes signaux sont alignés et ta charge est soutenue. Une séance plus dure est bien soutenue aujourd'hui.", "Your strap": "Ton bracelet", "Your strap in real time, heart rate and frames as they arrive.": "Votre bracelet en temps réel, fréquence cardiaque et trames à mesure qu'ils arrivent.", "Your strap is bonded and ready to stream.": "Votre bracelet est appairé et prêt à transmettre.", @@ -3420,9 +3536,12 @@ "Zoomed in · drag to pan": "Zoom activé · glissez pour déplacer", "Zoomed in. Drag to pan": "Zoom activé. Glissez pour déplacer", "Zwift, Peloton or a Garmin, open Data Sources and turn on \"Broadcast heart rate\": ": "Zwift, Peloton ou un Garmin, ouvre Sources de données et active \"Diffuser la fréquence cardiaque\" : ", +"a good sign": "bon signe", +"a hard or late workout": "un entraînement dur ou tardif", "a moderate": "un modéré", "a strong": "un fort", "a strong night, supporting recovery": "une nuit forte, favorise la récupération", +"a touch below baseline": "un peu en dessous de ta référence", "a typical night": "une nuit typique", "a very strong": "un très fort", "a weak": "un faible", @@ -3430,15 +3549,20 @@ "about your age": "environ votre âge", "above baseline, limiting recovery": "au-dessus de la référence, limite la récupération", "above baseline, supporting recovery": "au-dessus de la référence, favorise la récupération", +"above your baseline - well recovered": "au-dessus de ta référence - bien récupéré", "above, read hour by hour). Hours without enough data are skipped.": "ci-dessus, lu heure par heure). Les heures sans assez de données sont ignorées.", "active": "actif", "advice. Everything stays on this phone.": "conseil. Tout reste sur ce téléphone.", +"again.": "à nouveau.", "against your own calm hours today (the same 0-3 proxy as the score ": "par rapport à tes propres heures calmes d'aujourd'hui (le même indicateur 0-3 que le score ", "aggregates from your most recent imported day; resting HR and HRV update daily. ": "s'agrègent à partir de ton jour importé le plus récent ; la FC au repos et la VFC se mettent à jour chaque jour. ", +"alcohol": "l'alcool", "all history": "tout l'historique", +"all on-device, no account, no cloud.": "tout sur l'appareil, pas de compte, pas de cloud.", "all time": "depuis toujours", "all your data, none of the cloud": "toutes vos données, sans le cloud", "an iPhone: Health app → tap your photo → Export All Health Data, then ": "un iPhone : app Santé → appuie sur ta photo → Exporter toutes les données de santé, puis ", +"and": "et", "and HRV needs you to be still. No Oura Readiness or SpO₂ ": "et la VFC exige que tu restes immobile. Pas de Oura Readiness ni de SpO₂ ", "and HRV, but not WHOOP's deeper sleep and recovery data.": "et la VFC, mais pas les données de sommeil et de récupération plus poussées de WHOOP.", "and NOOP scores the data, so they land soon after, not the exact second you wake or ": "et NOOP note les données, donc elles arrivent peu après, pas à la seconde exacte où tu te réveilles ou ", @@ -3449,17 +3573,22 @@ "anything to count.": "quoi que ce soit à comptabiliser.", "app is still running. Reset the ring again, force-quit Oura, then try once more. If it keeps ": "l'app tourne encore. Réinitialise à nouveau la bague, force la fermeture d'Oura, puis réessaie. Si ça persiste ", "app's on-device database. Fully offline; no Xiaomi account or Bluetooth. Export the Mi ": "base de données de l'app sur l'appareil. Entièrement hors ligne ; pas de compte Xiaomi ni de Bluetooth. Exporte le Mi ", +"approx. stages (on-device)": "stades approx. (sur l'appareil)", "are a raw motion count, and HRV needs you to be still. No Oura Readiness or SpO2 ": "sont un décompte de mouvement brut, et la VFC exige que tu restes immobile. Pas de Oura Readiness ni de SpO2 ", "are informational and do not change the stress score above.": "sont informatifs et ne modifient pas le score de stress ci-dessus.", "are shown as “typical” and are always overridden by your own data once ": "sont affichées comme “typiques” et sont toujours remplacées par tes propres données une fois ", +"as of": "au", "as of %@": "au %@", "as of today": "aujourd'hui", "as of yesterday": "hier", "asleep %@": "endormi %@", "asleep last night": "endormi la nuit dernière", "at baseline": "au niveau de référence", +"at or below baseline": "à ou sous ta référence", "automatically once access is granted.": "automatiquement une fois l'accès accordé.", "average %@ bpm": "moyenne %@ bpm", +"avg": "moy.", +"avg ": "moy. ", "avg %@": "moy. %@", "avg %@ · %lldh": "moy. %1$@ · %2$lldh", "avg %lld": "moy. %lld", @@ -3469,6 +3598,7 @@ "balanced": "équilibré", "banked motion history, so your strap needs to sync that history before NOOP has ": "l'historique de mouvement accumulé, donc ton bracelet doit synchroniser cet historique avant que NOOP ait ", "based on typical patterns": "d'après des tendances types", +"baseline": "référence", "baseline (approximate, not medical advice); until then typical adult ranges apply.": "référence (approximatif, pas un avis médical) ; d'ici là, les plages adultes types s'appliquent.", "baseline (you'll see \"Calibrating\" until then), and keeps sharpening over your first ": "référence (tu verras \"Calibrage\" jusque-là), et continue de s'affiner pendant tes premiers ", "baseline. A higher-than-usual resting HR and a lower-than-usual HRV ": "référence. Une FC au repos plus élevée que d'habitude et une VFC plus basse que d'habitude ", @@ -3483,23 +3613,29 @@ "bpm": "bpm", "br/min": "resp/min", "building": "en construction", +"building fast (acute:chronic": "montée rapide (aigu:chronique", +"but you logged": "mais tu as noté", "calls": "appels", "changes your Effort.": "change jamais ton Effort.", "choose the file here. Fully offline; nothing leaves your phone. Each brand's own ": "choisis le fichier ici. Entièrement hors ligne ; rien ne quitte ton téléphone. Le format propre à chaque marque ", "clean intervals": "intervalles propres", "combined shift is mapped onto a 0-3 scale: 0 is calm, 1.5 sits at ": "le décalage combiné est reporté sur une échelle 0-3 : 0 correspond au calme, 1,5 se situe à ", "compares the days you log the behaviour against your behaviour-free ": "compare les jours où tu notes le comportement à tes jours sans ce comportement ", +"consider taking it easy.": "envisage de lever le pied.", "cooler than baseline, limiting recovery": "plus fraîche que la référence, limite la récupération", "count. It's an estimate, not a step counter. A WHOOP 4.0 doesn't transmit steps.": "compte. C'est une estimation, pas un podomètre. Un WHOOP 4.0 ne transmet pas les pas.", "counted as nights of 4 on the ring), and keeps sharpening over your first couple of weeks. ": "comptées comme des nuits de 4 sur la bague), et continue de s'affiner pendant tes deux premières semaines. ", "covered": "couvert", +"day": "jour", "day or two of data, your week-in-review appears here.": "jour ou deux de données, ton bilan de la semaine apparaît ici.", +"days": "jours", "days before the start.": "jours avant le début.", "decoded for 5/MG yet. Nothing's wrong with your strap - WHOOP 4.0 is fully supported.": "décodé pour le 5/MG pour l'instant. Rien ne cloche avec ton bracelet - le WHOOP 4.0 est entièrement pris en charge.", "default staging. Opt-in and experimental: it only changes how already-detected ": "le découpage par défaut. Optionnel et expérimental : ça ne change que la façon dont les nuits déjà détectées ", "device, from your strap.": "appareil, depuis ton bracelet.", "do not open the Oura app.": "n'ouvre pas l'appli Oura.", "doesn't let NOOP see every installed app, so this is how you cover the rest. ": "ne laisse pas NOOP voir toutes les applis installées, c'est donc comme ça que tu couvres le reste. ", +"down": "en baisse", "down %lld points": "en baisse de %lld points", "down 1 point": "en baisse de 1 point", "drink": "boisson", @@ -3512,7 +3648,11 @@ "e.g. fasting, morning draw": "ex. à jeun, prélèvement matinal", "e.g. gpt-4o": "ex. gpt-4o", "e.g. mmol/L": "ex. mmol/L", +"earlier": "plus tôt", +"eff": "eff.", "effect size and confidence, never a cause or a diagnosis. Population patterns ": "la taille d'effet et la confiance, jamais une cause ni un diagnostic. Les tendances de population ", +"efficiency": "efficacité", +"elevated - overtraining or illness can do this": "élevée - le surentraînement ou la maladie peuvent causer ça", "else that day is a nap. You can always edit bed and wake times.": "reste ce jour-là est une sieste. Tu peux toujours modifier les heures de coucher et de lever.", "enough to show, but it's thin. Solid means full inputs are present. When NOOP ": "assez pour s'afficher, mais c'est mince. Fiable signifie que toutes les données d'entrée sont présentes. Quand NOOP ", "entirely on your device. It is awareness only: not contraception, not a fertility ": "entièrement sur ton appareil. C'est purement informatif : pas une contraception, pas un ", @@ -3528,6 +3668,7 @@ "factor), how efficiently (asleep versus in bed), how much was restorative ": "facteur), avec quelle efficacité (endormi versus au lit), quelle part était réparatrice ", "failing, your ring may be a generation NOOP cannot adopt yet. You can still use file import.": "échoue, ta bague est peut-être d'une génération que NOOP ne peut pas encore adopter. Tu peux quand même utiliser l'import de fichier.", "few days alongside the strap, they'll appear here so you can see how close the ": "quelques jours en parallèle du bracelet, ils apparaîtront ici pour que tu voies à quel point l'estimation est ", +"filtered": "filtré(s)", "finish a workout. Everything is worked out on this phone.": "termines un entraînement. Tout est calculé sur ce téléphone.", "first run). Once a day or two of motion lands, your step estimate and the calibration ": "premier lancement). Une fois qu'un jour ou deux de mouvement sont enregistrés, ton estimation de pas et la calibration ", "fit from your phone": "ajustée depuis votre téléphone", @@ -3551,12 +3692,19 @@ "heat-grid is part of the desktop app.": "la grille thermique fait partie de l'appli de bureau.", "high": "élevé", "higher": "plus élevé", +"higher injury risk": "risque de blessure plus élevé", "historical-data decode + offload format": "format de décodage + déchargement des données historiques", +"hours have stayed in the high band. A few minutes ": "heures sont restées dans la zone haute. Quelques minutes ", "hover for real values": "survolez pour les valeurs réelles", "how much each extra unit tends to move your numbers. Until then it shows ": "de combien chaque unité supplémentaire tend à faire bouger tes chiffres. D'ici là, il affiche ", "import the .zip here. Working now on Android.": "importe le .zip ici. Fonctionne désormais sur Android.", +"in bed": "au lit", +"in the sweet spot (acute:chronic": "dans la zone idéale (aigu:chronique", +"in your normal range": "dans ta plage normale", "in-development addition: they stream live heart rate and HRV, but not WHOOP's deeper ": "ajout en cours de développement : ils diffusent la fréquence cardiaque et la VFC en direct, mais pas les mesures plus poussées de WHOOP : ", "includes its journal) backfills history instantly.": "inclut son journal) récupère l'historique instantanément.", +"into this week so far, too early to call a week-over-week trend yet.": "dans cette semaine pour l'instant, trop tôt pour parler d'une tendance semaine sur semaine.", +"is": "est", "isn't streaming (Bluetooth off, not worn, app killed), no early wake happens and the ": "ne diffuse pas (Bluetooth coupé, non porté, appli fermée), aucun réveil anticipé ne se produit et la ", "it tells you why and what to do next. Everything here runs on your ": "il t'explique pourquoi et quoi faire ensuite. Tout ici fonctionne sur ton ", "it. Works on any WHOOP (4.0 or 5.0/MG) because your phone does the broadcasting. ": "ça. Fonctionne avec n'importe quel WHOOP (4.0 ou 5.0/MG) parce que c'est ton téléphone qui émet. ", @@ -3564,12 +3712,14 @@ "keep everything on your device. Nothing is sent until you ask.": "garde tout sur ton appareil. Rien n'est envoyé tant que tu ne le demandes pas.", "kept and you can re-add it any time.": "conservé et tu peux le rajouter à tout moment.", "large": "grand", +"last": "dernier(s)", "last 30 days": "30 derniers jours", "last 7 days": "7 derniers jours", "last 90 days": "90 derniers jours", "last taken %@": "dernière prise %@", "last year": "l'année dernière", "late-caffeine": "caféine tardive", +"later": "plus tard", "later step": "étape ultérieure", "latest": "dernier", "learned your usual hours) the one nearest your normal sleep time. Everything ": "appris tes horaires habituels) celui le plus proche de ton heure de sommeil normale. Tout le ", @@ -3581,25 +3731,36 @@ "logged today": "enregistré aujourd'hui", "longer control this ring. This is intended and it cannot be undone from NOOP.": "plus contrôler cette bague. C'est intentionnel et c'est irréversible depuis NOOP.", "low": "faible", +"low - similar strain every day raises strain/illness risk": "bas - un effort similaire chaque jour augmente le risque d'effort excessif/maladie", +"low-stress days": "jours de stress bas", "low-stress days · %lldd": "jours à faible stress · %lldj", "lower": "plus bas", +"m ago": "min", "marker": "marqueur", "marker = your mean": "marqueur = ta moyenne", "markers": "marqueurs", +"max HR": "FC max", "medical advice, and not WHOOP's official scores.": "un avis médical, ni les scores officiels de WHOOP.", "mid": "moyen", +"min": "min", +"min SD": "min ET", "minus": "moins", +"minutes vs your typical bar": "minutes vs ta barre habituelle", "missing": "manquant", "moderate": "modéré", +"monotony": "monotonie", "month": "mois", +"more": "de plus", "more it trusts the estimate.": "plus il fait confiance à l'estimation.", "more overnight wear to unlock your %@ baseline": "davantage de nuits portées pour débloquer votre référence de %@", +"more sessions": "séances de plus", "most recent intake about %@ ago · %lld intakes still in the estimate. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %1$@ · %2$lld prises encore dans l'estimation. Simple indication, basée sur ce que vous avez enregistré.", "most recent intake about %@ ago. Rough guide only, based on what you logged.": "prise la plus récente il y a environ %@. Simple indication, basée sur ce que vous avez enregistré.", "motion maps to steps. Or set the coefficient manually below.": "le mouvement se traduit en pas. Ou règle le coefficient manuellement ci-dessous.", "ms": "ms", "n.s.": "n.s.", "near baseline": "proche de la référence", +"near your usual bedtime.": "proche de ton heure de coucher habituelle.", "needs a recent day": "nécessite un jour récent", "needs a tracked night": "nécessite une nuit suivie", "needs baseline + logged days": "nécessite une référence + des jours enregistrés", @@ -3609,8 +3770,13 @@ "negligible": "négligeable", "never a diagnosis. They never leave this phone.": "jamais un diagnostic. Elles ne quittent jamais ce téléphone.", "new": "nouveau", +"night": "nuit", +"nights": "nuits", +"nights ago": "nuits", "nights are split into stages (detection and scores are unchanged), and the default ": "les nuits sont découpées en phases (la détection et les scores sont inchangés), et le découpage ", +"nights, net": "nuits, solde net", "no": "non", +"no alcohol or travel logged": "aucun alcool ni voyage noté", "no calibration yet": "pas encore de calibration", "no change": "aucun changement", "no data": "aucune donnée", @@ -3622,6 +3788,7 @@ "no night scored yet": "pas encore de nuit évaluée", "no night yet": "pas encore de nuit", "no offload yet": "pas encore de transfert", +"no prior": "pas de référence antérieure", "no prior %@": "aucun %@ précédent", "no reading yet": "pas encore de mesure", "no readings yet": "pas encore de mesures", @@ -3635,13 +3802,23 @@ "not yet": "pas encore", "now continues automatically across passes instead of waiting between syncs.": "continue maintenant automatiquement d'une passe à l'autre au lieu d'attendre entre les synchros.", "nutrition CSV: a MyFitnessPal or Cronometer export, or any spreadsheet ": "CSV de nutrition : un export MyFitnessPal ou Cronometer, ou n'importe quel tableur ", +"of": "sur", "of %@": "sur %@", "of %@ L": "sur %@ L", "of 100": "sur 100", "of 3": "sur 3", "of 3 · %@": "de 3 · %@", +"of 7 days had data this week": "des 7 jours avaient des données cette semaine", "of beats": "des battements", +"of data, so week-over-week changes are rough, not a trend.": "de données, donc les changements semaine sur semaine sont approximatifs, pas une tendance.", +"of last 7 days": "des 7 derniers jours", +"of last 7 nights": "des 7 dernières nuits", +"of paced breathing can help downshift your nervous system.": "de respiration guidée peuvent aider à apaiser ton système nerveux.", +"of sleep debt over": "de dette de sommeil sur", +"of surplus over": "de surplus sur", "old": "ancien", +"older": "plus âgé", +"older than your age": "plus âgé que ton âge", "on demand · today's R-R": "à la demande · R-R du jour", "on the 0-3 autonomic-load scale": "sur l'échelle de charge autonome 0-3", "on-device baseline. Approximate, not medical advice.": "référence sur l'appareil. Approximatif, pas un avis médical.", @@ -3658,6 +3835,8 @@ "partial": "partiel", "peak %@ · %@": "pic %1$@ · %2$@", "peer-reviewed sport science, and computed entirely on your device. They are ": "évaluées par les pairs, et calculées entièrement sur ton appareil. Elles ne sont pas ", +"percent": "pour cent", +"percent of the night": "pour cent de la nuit", "percentage comes off the ring (import an Oura file for those).": "pourcentage vient de la bague (importe un fichier Oura pour ceux-là).", "personal alcohol/caffeine dose-response.": "réponse personnelle à la dose d'alcool/caféine.", "personal baseline, plus resting heart rate, last night's Rest, breathing ": "référence personnelle, plus la FC au repos, le Repos de la nuit dernière, la fréquence ", @@ -3665,7 +3844,9 @@ "positive": "positif", "predictor, not a medical service.": "prédicteur de fertilité, pas un service médical.", "projected · %@": "prévu · %@", +"pts": "pts", "quarter": "trimestre", +"ramping down (acute:chronic": "en baisse (aigu:chronique", "range %@ to %@": "plage %@ à %@", "rate, HRV, steps and more, where the export has them. Download it from the brand's app ": "cardiaque, la VFC, les pas et plus encore, quand l'export les contient. Télécharge-le depuis l'appli de la marque ", "rate, and a skin-temperature signal (an early illness or overreach flag). ": "respiratoire, et un signal de température cutanée (indice précoce de maladie ou de surmenage). ", @@ -3676,36 +3857,56 @@ "reading…": "lecture…", "ready": "prêt", "reboots to apply, then reconnects with the new name.": "redémarre pour appliquer, puis se reconnecte avec le nouveau nom.", +"recorded yet. Sync your strap to populate this trend.": "enregistré pour l'instant. Synchronise ton bracelet pour remplir cette tendance.", "recovery": "récupération", "recovery baseline - not a measurement. Your real Charge is scored from ": "référence de récupération - pas une mesure. Ta vraie Charge est calculée à partir de ", "recovery, skin temp, SpO₂ or steps.": "récupération, température cutanée, SpO₂ ou pas.", +"remaining": "restant(es)", "required too: an emulator has no Bluetooth.": "requis aussi : un émulateur n'a pas de Bluetooth.", "reset\"). This wipes the ring's owner so NOOP can take it over.": "réinitialisation\"). Ça efface le propriétaire de la bague pour que NOOP puisse la reprendre.", "respiratory rate, heart rate, steps, active energy and sleep) into ": "fréquence respiratoire, fréquence cardiaque, pas, énergie active et sommeil) dans ", +"resting HR is a touch high": "la FC au repos est un peu haute", "resting HR, sleep stages, SpO₂, stress and sleep score) straight from the Mi Fitness ": "FC au repos, phases de sommeil, SpO₂, stress et score de sommeil) directement depuis l'appli Mi Fitness ", "ring with that key WITHOUT resetting it, so the Oura app keeps working too. NOOP does not ": "bague avec cette clé SANS la réinitialiser, donc l'appli Oura continue de fonctionner aussi. NOOP ne ", "ring, and it can change between updates. NOOP never makes up a number. If something does ": "bague, et ça peut changer d'une mise à jour à l'autre. NOOP n'invente jamais de chiffre. Si quelque chose ne ", +"room to build": "de la marge pour progresser", +"rpm": "rpm", +"running a little high": "un peu élevée", "running balance": "solde courant", +"sauna": "le sauna", "scale. They're built from your strap's raw signals using published, ": "échelle. Elles sont construites à partir des signaux bruts de ton bracelet, avec des sciences du sport publiées, ", "scored once a day's data is in. Charge needs about four nights of sleep to learn your ": "calculée une fois que les données d'une journée sont là. La Charge a besoin d'environ quatre nuits de sommeil pour apprendre ta ", "sec": "s", +"selected day": "jour sélectionné", "selected folder": "dossier sélectionné", "send your question plus a short summary of your metrics to the provider you pick.": "envoie ta question ainsi qu'un bref résumé de tes métriques au fournisseur que tu choisis.", "sensor, so a gym treadmill, bike, Zwift, Peloton or any fitness app nearby can read ": "capteur, donc un tapis de course de salle, un vélo, Zwift, Peloton ou n'importe quelle appli de fitness à proximité peut lire ", +"session": "séance", +"sessions": "séances", "shared. Imported data is never echoed back.": "partagé. Les données importées ne sont jamais renvoyées.", "shows here.": "s'affiche ici.", "side by side · %@ before each reading": "côte à côte · %@ avant chaque mesure", +"signal": "signal", "signal against a typical adult range, not medical advice.": "signal par rapport à une plage adulte typique, pas un avis médical.", +"since midnight": "depuis minuit", "sitting side by side. It's not a medical finding, and it shows association, not cause.": "côte à côte. Ce n'est pas un constat médical, et ça montre une association, pas une cause.", +"skin-temp readings fell inside": "de relevés de température cutanée ne sont tombés dans", "sleep and recovery data.": "données de sommeil et de récupération.", "sleep debt": "dette de sommeil", "sleep duration": "durée du sommeil", "sleep sits near your nightly low and stays steady; when your heart rate lifts above ": "le sommeil reste proche de ton minimum nocturne et stable ; quand ta fréquence cardiaque s'élève au-dessus de la ", "sleep, steps, workouts and more. Your live strap data is untouched. This can't be undone.": "sommeil, pas, entraînements et plus encore. Tes données de bracelet en direct restent intactes. C'est irréversible.", "slight": "léger", +"slightly raised vs baseline": "légèrement élevée vs référence", "small": "petit", "so back up first if unsure.": "donc fais d'abord une sauvegarde en cas de doute.", "solid": "solide", +"something": "quelque chose", +"sparse , widened to": "clairsemé, élargi à", +"sparse, widened to": "clairsemé, élargi à", +"spiking (acute:chronic": "en pic (aigu:chronique", +"sport": "sport", +"sports": "sports", "staging stays in place if you leave this off. Takes effect on the next nights staged.": "par défaut reste en place si tu laisses cette option désactivée. Prend effet dès les prochaines nuits découpées.", "stale": "obsolète", "stale · %@": "obsolète · %@", @@ -3716,7 +3917,9 @@ "still be active. It's a guide based on a typical 5 to 6 hour half-life, not a measurement.": "encore actif. C'est un repère basé sur une demi-vie typique de 5 à 6 heures, pas une mesure.", "strain (0-21)": "effort (0-21)", "strain (0–21)": "effort (0–21)", +"stress": "le stress", "strong": "fort", +"suppressed - a sign of autonomic fatigue": "supprimée - signe de fatigue autonome", "surplus": "surplus", "tap Rescan. A ring still owned by Oura will not list here.": "appuie sur Rescanner. Une bague encore possédée par Oura n'apparaîtra pas ici.", "tap or drag for real values": "appuyez ou glissez pour les valeurs réelles", @@ -3726,7 +3929,9 @@ "tends to be lower": "a tendance à être plus bas", "tends to fall": "a tendance à baisser", "tends to rise": "a tendance à monter", +"than your schedule": "que ton horaire", "that signal": "ce signal", +"the last": "les derniers", "the last %lld nights": "les %lld dernières nuits", "the last 7 days": "les 7 derniers jours", "the last night": "la dernière nuit", @@ -3737,50 +3942,74 @@ "this Mac": "ce Mac", "this any time.": "ça à tout moment.", "this device": "cet appareil", +"this week,": "cette semaine,", +"this week, no comparison.": "cette semaine, pas de comparaison.", "time you open NOOP, if it's been longer than the interval below. ": "fois que tu ouvres NOOP, si le délai ci-dessous est dépassé. ", "to": "à", "to baseline": "à la référence", "to see weeks, months and years instantly.": "pour voir les semaines, mois et années instantanément.", "to the official WHOOP app, and the other way round. It's reversible: pair it in the ": "vers l'appli officielle WHOOP, et inversement. C'est réversible : appaire-le dans l'autre ", "today": "aujourd'hui", +"total": "total", "training-impulse using heart-rate-reserve zones (Karvonen), weights time in ": "l'impulsion d'entraînement à partir des zones de réserve de fréquence cardiaque (Karvonen), pondère le temps passé dans les ", +"travel": "le voyage", +"trend chart": "graphique de tendance", "typical": "typique", "typical adult range (approximate, not medical advice).": "plage adulte typique (approximatif, pas un avis médical).", "typical patterns, clearly labelled as not yet yours.": "des tendances types, clairement indiquées comme n'étant pas encore les tiennes.", "typical patterns.": "des tendances types.", +"unchanged": "inchangé", "under an hour": "moins d'une heure", "unscored": "non évalué", +"until your scores are personal.": "jusqu'à ce que tes scores soient personnels.", "untouched days": "jours intacts", +"up": "en hausse", "up %lld points": "en hausse de %lld points", "up 1 point": "en hausse de 1 point", "up in the Oura app, NOOP no longer owns it and you would re-add it to take it over.": "dans l'appli Oura, NOOP ne la possède plus et tu devrais la rajouter pour la reprendre.", "up in the Oura app.": "dans l'appli Oura.", +"up vs baseline - sometimes an early sign of getting sick": "en hausse vs référence - parfois un signe précoce de maladie", "used": "utilisé", "variation index": "indice de variation", "very strong": "très fort", "volume load shown per session": "charge de volume affichée par séance", +"vs": "vs", "vs 30-day baseline": "vs référence sur 30 jours", "vs age %lld": "vs âge %lld", "vs behaviour-free baseline": "vs référence sans comportement", "vs need": "vs besoin", +"vs prev": "vs préc.", "vs prev %@": "vs %@ préc.", +"vs typ": "vs typ.", "vs typical": "vs typique", "vs typical - ": "vs habituel - ", "vs typical —": "vs typique —", "vs your baseline": "vs ta référence", "warmer than baseline, limiting recovery": "plus chaude que la référence, limite la récupération", +"watch fatigue": "surveille la fatigue", "we can't do yet. NOOP will say so honestly and never show a made-up number. No sleep, ": "qu'on ne peut pas encore faire. NOOP le dira honnêtement et n'affichera jamais un chiffre inventé. Pas de sommeil, ", "weak": "faible", "week": "semaine", +"week over week": "semaine sur semaine", +"week over week (avg": "semaine sur semaine (moy.", +"weeks ago": "semaines", "what are you logging?": "que voulez-vous enregistrer ?", "what's missing and what to do, rather than showing a fake value.": "ce qui manque et quoi faire, plutôt que d'afficher une fausse valeur.", "which keeps its radio hot and drains the battery faster. Turn it off when ": "ce qui garde sa radio active et vide la batterie plus vite. Désactive-le quand ", "with a date column plus those values. Meal-level rows are summed per day.": "avec une colonne de date et ces valeurs. Les lignes par repas sont additionnées par jour.", +"within": "dans", +"woke at": "réveillé à", +"worth a look": "à surveiller", "year": "année", +"years": "ans", "you enter. It doesn't test, read, diagnose, or advise. Your records never leave this phone; ": "que tu saisis. Ça ne teste pas, ne lit pas, ne diagnostique pas et ne conseille pas. Tes enregistrements ne quittent jamais ce téléphone ; ", "you how fresh it is. If it says \"Not recording\", reconnect.": "à quel point c'est récent. S'il indique \"Pas d'enregistrement\", reconnecte-toi.", "you're not using it with another device.": "tu ne l'utilises pas avec un autre appareil.", "you. Nothing leaves this device. Your choices are saved now and apply ": "toi. Rien ne quitte cet appareil. Tes choix sont enregistrés maintenant et s'appliquent à chaque ", +"younger": "plus jeune", +"younger than your age": "plus jeune que ton âge", +"your active device now? It will provide your live data. You can change": "ton appareil actif maintenant ? Il fournira tes données en direct. Tu peux changer", +"your active strap? From now on it provides your live data.": "ton bracelet actif ? Il fournira désormais tes données en direct.", "your baseline, 3 is highly activated.": "ta référence, 3 correspond à une forte activation.", "your data": "vos données", "your number, date and any note": "votre chiffre, la date et une éventuelle note", From fc3cbc3cd74094628318b064234644b06cd9cf11 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:53:52 +0200 Subject: [PATCH 27/28] =?UTF-8?q?Bump=20to=208.2.6=20=E2=80=94=20French=20?= =?UTF-8?q?localization=20pass=202=20+=20onboarding=20skip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the remaining English left after the first FR pass: dashboard-card labels, the Synthesis sheet, sync/calibration states, sport display names, device labels, sleep-screen layout overflow, chart legends, the Trends Report PDF (entirely missed by pass 1), and full sentences generated in the analytics layer (weekly digest, readiness, recovery drivers, illness, fitness age, cycle phase, vitality). Onboarding also gets a "Skip" button. This installs in place over 8.2.5 (stable signing key already active). Co-Authored-By: Claude Sonnet 5 --- android/app/build.gradle.kts | 4 ++-- project.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 54fa13f3d..df51fce4c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.noop.whoop" minSdk = 26 targetSdk = 34 - versionCode = 264 - versionName = "8.2.5" + versionCode = 265 + versionName = "8.2.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/project.yml b/project.yml index 8bd480b6a..51a11205f 100644 --- a/project.yml +++ b/project.yml @@ -11,11 +11,11 @@ options: developmentLanguage: en settings: base: - MARKETING_VERSION: "8.2.5" + MARKETING_VERSION: "8.2.6" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. - CURRENT_PROJECT_VERSION: "175" + CURRENT_PROJECT_VERSION: "176" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). From 92fa3340d24ba035634293b9bb745b9b3727fc91 Mon Sep 17 00:00:00 2001 From: Matthias <72629134+MatB57@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:16:20 +0200 Subject: [PATCH 28/28] Publish 8.2.6 in the AltStore source Co-Authored-By: Claude Sonnet 5 --- altstore-source.json | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/altstore-source.json b/altstore-source.json index cae269c03..b08b2b18b 100644 --- a/altstore-source.json +++ b/altstore-source.json @@ -17,6 +17,15 @@ "category": "healthcare-fitness", "screenshotURLs": [], "versions": [ + { + "version": "8.2.6", + "buildVersion": "176", + "date": "2026-09-11", + "localizedDescription": "NOOP 8.2.6. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.6/NOOP-v8.2.6-ios.ipa", + "size": 14166545, + "minOSVersion": "17.0" + }, { "version": "8.2.5", "buildVersion": "175", @@ -73,12 +82,12 @@ "NSHealthUpdateUsageDescription": "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." } }, - "version": "8.2.5", - "buildVersion": "175", + "version": "8.2.6", + "buildVersion": "176", "versionDate": "2026-09-11", - "versionDescription": "NOOP 8.2.5. See the release notes for what changed.", - "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.5/NOOP-v8.2.5-ios.ipa", - "size": 14166533, + "versionDescription": "NOOP 8.2.6. See the release notes for what changed.", + "downloadURL": "https://github.com/MatB57/noop/releases/download/v8.2.6/NOOP-v8.2.6-ios.ipa", + "size": 14166545, "minOSVersion": "17.0" } ],