@indaco/garmin-personal-mcp 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -95
- package/dist/dashboard.html +7 -7
- package/dist/index.js +3163 -546
- package/package.json +1 -1
- package/prompts/dashboard-language.md +3 -0
- package/prompts/dashboard-setup.md +21 -3
- package/prompts/dashboard-widget.md +6 -1
- package/prompts/training-dashboard-freeform.md +15 -0
- package/prompts/training-dashboard-update.md +12 -0
- package/prompts/training-dashboard.md +39 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process$1 from "node:process";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync } from "node:fs";
|
|
5
5
|
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
8
|
import { randomBytes } from "node:crypto";
|
|
8
9
|
import { createRequire } from "module";
|
|
9
10
|
// node_modules/.pnpm/@modelcontextprotocol+server@2.4.0/node_modules/@modelcontextprotocol/server/dist/chunk-vYduTEsO.mjs
|
|
@@ -3297,7 +3298,7 @@ const error = () => {
|
|
|
3297
3298
|
}
|
|
3298
3299
|
};
|
|
3299
3300
|
};
|
|
3300
|
-
function en_default() {
|
|
3301
|
+
function en_default$1() {
|
|
3301
3302
|
return { localeError: error() };
|
|
3302
3303
|
}
|
|
3303
3304
|
// node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/registries.js
|
|
@@ -5026,7 +5027,7 @@ const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
|
|
|
5026
5027
|
const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
|
|
5027
5028
|
// node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/schemas.js
|
|
5028
5029
|
function _ensureDefaultLocale() {
|
|
5029
|
-
if (!globalConfig.localeError) config(en_default());
|
|
5030
|
+
if (!globalConfig.localeError) config(en_default$1());
|
|
5030
5031
|
}
|
|
5031
5032
|
function _ensureDefaultMemoizer() {
|
|
5032
5033
|
if (!globalConfig.memoizer) config({ memoizer: memoizer() });
|
|
@@ -24093,17 +24094,9 @@ function serveStdio(factory, options = {}) {
|
|
|
24093
24094
|
function toError(value) {
|
|
24094
24095
|
return value instanceof Error ? value : new Error(String(value));
|
|
24095
24096
|
}
|
|
24096
|
-
// src/config.ts
|
|
24097
|
-
/** Defaults reuse an existing ~/.config/garmin-mcp/api-key and ~/workspace/garmin-data. */
|
|
24098
|
-
function loadConfig(env = process.env) {
|
|
24099
|
-
return {
|
|
24100
|
-
baseUrl: env.GARMIN_BASE_URL || "https://personal.connectapi.garmin.com",
|
|
24101
|
-
apiKeyFile: env.GARMIN_API_KEY_FILE || join(homedir(), ".config/garmin-mcp/api-key"),
|
|
24102
|
-
dataDir: env.GARMIN_DATA_DIR || join(homedir(), "workspace/garmin-data")
|
|
24103
|
-
};
|
|
24104
|
-
}
|
|
24105
24097
|
// package.json
|
|
24106
|
-
var
|
|
24098
|
+
var name = "@indaco/garmin-personal-mcp";
|
|
24099
|
+
var version = "0.3.0";
|
|
24107
24100
|
// src/log.ts
|
|
24108
24101
|
/** Past this size the log moves to `<file>.1`, replacing the one before, and a new log starts. */
|
|
24109
24102
|
const MAX_BYTES = 1e6;
|
|
@@ -59005,13 +58998,13 @@ function round(v, decimals) {
|
|
|
59005
58998
|
const f = 10 ** decimals;
|
|
59006
58999
|
return Math.round(v * f) / f;
|
|
59007
59000
|
}
|
|
59008
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
59001
|
+
const pad$1 = (n) => String(n).padStart(2, "0");
|
|
59009
59002
|
function hms(seconds) {
|
|
59010
59003
|
const s = Math.round(seconds);
|
|
59011
59004
|
const h = Math.floor(s / 3600);
|
|
59012
59005
|
const m = Math.floor(s % 3600 / 60);
|
|
59013
59006
|
const sec = s % 60;
|
|
59014
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
|
|
59007
|
+
return h > 0 ? `${h}:${pad$1(m)}:${pad$1(sec)}` : `${m}:${pad$1(sec)}`;
|
|
59015
59008
|
}
|
|
59016
59009
|
// src/tools.ts
|
|
59017
59010
|
const DAY_MS = 864e5;
|
|
@@ -59029,7 +59022,8 @@ var GarminTools = class {
|
|
|
59029
59022
|
this.api = api;
|
|
59030
59023
|
this.dataDir = dataDir;
|
|
59031
59024
|
}
|
|
59032
|
-
async listActivities(startDate, endDate, activityType, limit, raw) {
|
|
59025
|
+
async listActivities(startDate, endDate, activityType, limit, raw, names) {
|
|
59026
|
+
const wanted = names?.map((n) => n.toLowerCase()).filter(Boolean) ?? [];
|
|
59033
59027
|
const asked = limit == null || limit <= 0 ? 20 : Math.min(limit, 200);
|
|
59034
59028
|
const search = (max, type, subType) => this.api.getJson("/activitylist-service/activities/search/activities", {
|
|
59035
59029
|
start: "0",
|
|
@@ -59040,14 +59034,15 @@ var GarminTools = class {
|
|
|
59040
59034
|
activitySubType: subType
|
|
59041
59035
|
});
|
|
59042
59036
|
const refused = (e) => activityType !== void 0 && e instanceof GarminApiError && /-> HTTP 400\b/.test(e.message);
|
|
59037
|
+
const max = wanted.length > 0 ? 200 : asked;
|
|
59043
59038
|
let json;
|
|
59044
59039
|
let local = false;
|
|
59045
59040
|
try {
|
|
59046
|
-
json = await search(
|
|
59041
|
+
json = await search(max, activityType);
|
|
59047
59042
|
} catch (e) {
|
|
59048
59043
|
if (!refused(e)) throw e;
|
|
59049
59044
|
try {
|
|
59050
|
-
json = await search(
|
|
59045
|
+
json = await search(max, void 0, activityType);
|
|
59051
59046
|
log("INFO", `activityType ${activityType} refused, asked as activitySubType`);
|
|
59052
59047
|
} catch (e2) {
|
|
59053
59048
|
if (!refused(e2)) throw e2;
|
|
@@ -59058,12 +59053,20 @@ var GarminTools = class {
|
|
|
59058
59053
|
}
|
|
59059
59054
|
const list = Array.isArray(json) ? json : firstArray(json);
|
|
59060
59055
|
if (!list) return JSON.stringify(json);
|
|
59061
|
-
const
|
|
59062
|
-
|
|
59056
|
+
const named = (a) => {
|
|
59057
|
+
const name = isObject$3(a) ? a.activityName : void 0;
|
|
59058
|
+
return typeof name === "string" && wanted.some((n) => name.toLowerCase().includes(n));
|
|
59059
|
+
};
|
|
59060
|
+
const filtered = local || wanted.length > 0;
|
|
59061
|
+
const rows = filtered ? list.filter((a) => (!local || field$2(isObject$3(a) ? a.activityType : void 0, "typeKey") === activityType) && (wanted.length === 0 || named(a))).slice(0, asked) : list;
|
|
59062
|
+
return JSON.stringify(raw === true ? filtered ? rows : json : rows.map((a) => summarize(isObject$3(a) ? a : {})));
|
|
59063
59063
|
}
|
|
59064
59064
|
async getActivity(activityId) {
|
|
59065
59065
|
return JSON.stringify(await this.api.getJson(`/activity-service/activity/${activityId}`, {}));
|
|
59066
59066
|
}
|
|
59067
|
+
async getActivityWorkouts(activityId) {
|
|
59068
|
+
return JSON.stringify(await this.api.getJson(`/activity-service/activity/${activityId}/workouts`, {}), (_, v) => v === null ? void 0 : v);
|
|
59069
|
+
}
|
|
59067
59070
|
async downloadActivityFit(activityId) {
|
|
59068
59071
|
const fit = await this.fetchFit(activityId);
|
|
59069
59072
|
return JSON.stringify({
|
|
@@ -59094,6 +59097,17 @@ var GarminTools = class {
|
|
|
59094
59097
|
endDate: toDate
|
|
59095
59098
|
}));
|
|
59096
59099
|
}
|
|
59100
|
+
async getWeight(fromDate, toDate, includeAll) {
|
|
59101
|
+
checkRange(fromDate, toDate, Infinity);
|
|
59102
|
+
return JSON.stringify(await this.api.getJson(`/weight-service/weight/range/${fromDate}/${toDate}`, { includeAll: includeAll === true ? "true" : void 0 }));
|
|
59103
|
+
}
|
|
59104
|
+
async getBodyBattery(fromDate, toDate) {
|
|
59105
|
+
checkRange(fromDate, toDate);
|
|
59106
|
+
return JSON.stringify(await this.api.getJson("/wellness-service/wellness/bodyBattery/reports/daily", {
|
|
59107
|
+
startDate: fromDate,
|
|
59108
|
+
endDate: toDate
|
|
59109
|
+
}));
|
|
59110
|
+
}
|
|
59097
59111
|
async getHeartRateZones() {
|
|
59098
59112
|
return JSON.stringify(await this.api.getJson("/biometric-service/heartRateZones/v2", {}));
|
|
59099
59113
|
}
|
|
@@ -59168,7 +59182,7 @@ function summarize(a) {
|
|
|
59168
59182
|
const o = {};
|
|
59169
59183
|
copy$1(a, o, "activityId", "id");
|
|
59170
59184
|
copy$1(a, o, "activityName", "name");
|
|
59171
|
-
const typeKey = asString$1(field$
|
|
59185
|
+
const typeKey = asString$1(field$2(a.activityType, "typeKey"));
|
|
59172
59186
|
if (typeKey !== null) o.type = typeKey;
|
|
59173
59187
|
const sport = Number.isInteger(a.sportTypeId) ? SPORTS[a.sportTypeId] : void 0;
|
|
59174
59188
|
if (sport !== void 0) o.sport = sport;
|
|
@@ -59184,7 +59198,7 @@ function summarize(a) {
|
|
|
59184
59198
|
copy$1(a, o, "elevationGain", "ascentM");
|
|
59185
59199
|
const elapsedS = asDouble(a.duration, 0);
|
|
59186
59200
|
if (elapsedS - durS >= 60) o.elapsed = hms(elapsedS);
|
|
59187
|
-
if (asString$1(field$
|
|
59201
|
+
if (asString$1(field$2(a.eventType, "typeKey")) === "race") o.race = true;
|
|
59188
59202
|
putRounded(a, o, "activityTrainingLoad", "trainingLoad", 1);
|
|
59189
59203
|
putRounded(a, o, "aerobicTrainingEffect", "aerobicTE", 1);
|
|
59190
59204
|
putRounded(a, o, "anaerobicTrainingEffect", "anaerobicTE", 1);
|
|
@@ -59228,13 +59242,13 @@ function summarize(a) {
|
|
|
59228
59242
|
}
|
|
59229
59243
|
if (Object.keys(best).length > 0) o.best = best;
|
|
59230
59244
|
for (const split of Array.isArray(a.splitSummaries) ? a.splitSummaries : []) {
|
|
59231
|
-
const type = asString$1(field$
|
|
59232
|
-
const speed = asDouble(field$
|
|
59245
|
+
const type = asString$1(field$2(split, "splitType")) ?? "";
|
|
59246
|
+
const speed = asDouble(field$2(split, "averageSpeed"), 0);
|
|
59233
59247
|
if (type === "RWD_RUN" && speed > 0) {
|
|
59234
59248
|
const runWalk = o.runWalk ??= {};
|
|
59235
59249
|
runWalk.runPace = `${hms(1e3 / speed)}/km`;
|
|
59236
|
-
runWalk.runKm = round(asDouble(field$
|
|
59237
|
-
} else if (type === "RWD_WALK") (o.runWalk ??= {}).walkTime = hms(asDouble(field$
|
|
59250
|
+
runWalk.runKm = round(asDouble(field$2(split, "distance"), 0) / 1e3, 2);
|
|
59251
|
+
} else if (type === "RWD_WALK") (o.runWalk ??= {}).walkTime = hms(asDouble(field$2(split, "duration"), 0));
|
|
59238
59252
|
}
|
|
59239
59253
|
return o;
|
|
59240
59254
|
}
|
|
@@ -59274,7 +59288,7 @@ function copy$1(from, to, name, as) {
|
|
|
59274
59288
|
function isObject$3(v) {
|
|
59275
59289
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
59276
59290
|
}
|
|
59277
|
-
function field$
|
|
59291
|
+
function field$2(v, name) {
|
|
59278
59292
|
return isObject$3(v) ? v[name] : void 0;
|
|
59279
59293
|
}
|
|
59280
59294
|
/** A number, or text holding one (spaces around it allowed); the default otherwise (null too). */
|
|
@@ -59294,11 +59308,11 @@ function isDate(d) {
|
|
|
59294
59308
|
const t = Date.parse(`${d}T00:00:00Z`);
|
|
59295
59309
|
return /^\d{4}-\d{2}-\d{2}$/.test(d) && !Number.isNaN(t) && new Date(t).toISOString().startsWith(d);
|
|
59296
59310
|
}
|
|
59297
|
-
/** Throws unless from and to are real yyyy-MM-dd dates, from <= to, and the range has at most
|
|
59298
|
-
function checkRange(from, to) {
|
|
59311
|
+
/** Throws unless from and to are real yyyy-MM-dd dates, from <= to, and the range has at most maxDays days. */
|
|
59312
|
+
function checkRange(from, to, maxDays = RANGE_DAYS) {
|
|
59299
59313
|
if (!isDate(from) || !isDate(to) || from > to) throw new Error("fromDate and toDate must be real dates yyyy-MM-dd with fromDate <= toDate");
|
|
59300
59314
|
const days = (Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 864e5 + 1;
|
|
59301
|
-
if (days >
|
|
59315
|
+
if (days > maxDays) throw new Error(`A range has at most ${maxDays} days; this one has ${days}. Ask for it in parts.`);
|
|
59302
59316
|
}
|
|
59303
59317
|
/** A tool call for one date or for a range: `date` alone, or `fromDate` and `toDate` without it. */
|
|
59304
59318
|
function dayOrRange(a, day, range) {
|
|
@@ -59317,7 +59331,7 @@ const text = (s) => ({ content: [{
|
|
|
59317
59331
|
type: "text",
|
|
59318
59332
|
text: s
|
|
59319
59333
|
}] });
|
|
59320
|
-
/** Registers the
|
|
59334
|
+
/** Registers the 15 tools. */
|
|
59321
59335
|
function registerTools(server, tools) {
|
|
59322
59336
|
server.registerTool("listActivities", {
|
|
59323
59337
|
description: [
|
|
@@ -59335,13 +59349,26 @@ function registerTools(server, tools) {
|
|
|
59335
59349
|
endDate: string().describe("End date yyyy-MM-dd (optional)").optional(),
|
|
59336
59350
|
activityType: string().describe("Activity type key (optional): a parent type such as running or cycling, or a subtype such as trail_running").optional(),
|
|
59337
59351
|
limit: number$1().int().describe("Max number of activities, default 20").optional(),
|
|
59338
|
-
raw: boolean().describe("Return Garmin's raw JSON instead of the summary").optional()
|
|
59352
|
+
raw: boolean().describe("Return Garmin's raw JSON instead of the summary").optional(),
|
|
59353
|
+
names: array(string()).describe("Activity names (optional): keep only the activities whose name contains one of them, ignoring case").optional()
|
|
59339
59354
|
})
|
|
59340
|
-
}, async (a) => text(await tools.listActivities(a.startDate, a.endDate, a.activityType, a.limit, a.raw)));
|
|
59355
|
+
}, async (a) => text(await tools.listActivities(a.startDate, a.endDate, a.activityType, a.limit, a.raw, a.names)));
|
|
59341
59356
|
server.registerTool("getActivity", {
|
|
59342
59357
|
description: "Get the full detail of one activity (summary, metadata, splits/laps) as JSON.",
|
|
59343
59358
|
inputSchema: object({ activityId })
|
|
59344
59359
|
}, async (a) => text(await tools.getActivity(a.activityId)));
|
|
59360
|
+
server.registerTool("getActivityWorkouts", {
|
|
59361
|
+
description: [
|
|
59362
|
+
"The planned workout that an activity followed, as created in Garmin Connect: its steps in",
|
|
59363
|
+
"order, each with its intensity (warmup/active/rest/recovery/cooldown), its duration (time in",
|
|
59364
|
+
"seconds, reps, or open), its target (cadence, heart rate, pace, or open, with low and high",
|
|
59365
|
+
"values), the exercise for strength steps, and the coach's notes. A step with durationType",
|
|
59366
|
+
"REPEAT_UNTIL_STEPS_CMPLT repeats the steps from stepIndex durationValue up to it, targetValue",
|
|
59367
|
+
"times in all. Compare it with getActivitySets or getActivityLaps for plan against done.",
|
|
59368
|
+
"An empty list means the activity followed no planned workout. Null fields are left out."
|
|
59369
|
+
].join("\n"),
|
|
59370
|
+
inputSchema: object({ activityId })
|
|
59371
|
+
}, async (a) => text(await tools.getActivityWorkouts(a.activityId)));
|
|
59345
59372
|
server.registerTool("downloadActivityFit", {
|
|
59346
59373
|
description: ["Download the ORIGINAL .FIT file of an activity and save it on the user's computer.", "Returns the absolute path of the saved .fit file (second-by-second HR, cadence, speed, laps)."].join("\n"),
|
|
59347
59374
|
inputSchema: object({ activityId })
|
|
@@ -59383,6 +59410,30 @@ function registerTools(server, tools) {
|
|
|
59383
59410
|
description: "Sleep data for one night (date: the morning you woke up), or for up to 14 nights (fromDate and toDate), yyyy-MM-dd.",
|
|
59384
59411
|
inputSchema: dayOrRangeInput
|
|
59385
59412
|
}, async (a) => text(await dayOrRange(a, (d) => tools.getSleep(d), (f, t) => tools.getSleepRange(f, t))));
|
|
59413
|
+
server.registerTool("getWeight", {
|
|
59414
|
+
description: [
|
|
59415
|
+
"Weight for a date range, yyyy-MM-dd: the latest weight of each day (with body fat, muscle",
|
|
59416
|
+
"and bone mass, BMI and body water when a scale records them), the average of the range, and",
|
|
59417
|
+
"the last weight before and the first after the range. Set includeAll=true for every entry",
|
|
59418
|
+
"of each day. Weight, muscle mass and bone mass are in grams."
|
|
59419
|
+
].join("\n"),
|
|
59420
|
+
inputSchema: object({
|
|
59421
|
+
fromDate: string().describe("From date yyyy-MM-dd"),
|
|
59422
|
+
toDate: string().describe("To date yyyy-MM-dd"),
|
|
59423
|
+
includeAll: boolean().describe("Every weight entry of each day, not only the latest").optional()
|
|
59424
|
+
})
|
|
59425
|
+
}, async (a) => text(await tools.getWeight(a.fromDate, a.toDate, a.includeAll)));
|
|
59426
|
+
server.registerTool("getBodyBattery", {
|
|
59427
|
+
description: [
|
|
59428
|
+
"Body Battery for up to 14 days, yyyy-MM-dd: for each day how much it charged and drained,",
|
|
59429
|
+
"its readings through the day, and the events that changed it (sleep, activities, stress,",
|
|
59430
|
+
"naps) with their impact."
|
|
59431
|
+
].join("\n"),
|
|
59432
|
+
inputSchema: object({
|
|
59433
|
+
fromDate: string().describe("From date yyyy-MM-dd"),
|
|
59434
|
+
toDate: string().describe("To date yyyy-MM-dd")
|
|
59435
|
+
})
|
|
59436
|
+
}, async (a) => text(await tools.getBodyBattery(a.fromDate, a.toDate)));
|
|
59386
59437
|
server.registerTool("getHeartRateZones", {
|
|
59387
59438
|
description: "Heart rate zones currently configured in Garmin Connect.",
|
|
59388
59439
|
inputSchema: object({})
|
|
@@ -59456,7 +59507,13 @@ var GarminDashboard = class {
|
|
|
59456
59507
|
await this.#syncRecovery(cache, addDays(today, -13), today, sync);
|
|
59457
59508
|
cache.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
59458
59509
|
await writeAtomically(this.#file(), JSON.stringify(cache));
|
|
59459
|
-
const [zones, fitnessAge] = await Promise.all([
|
|
59510
|
+
const [zones, fitnessAge, settings] = await Promise.all([
|
|
59511
|
+
optional(sync, "zones", () => this.tools.getHeartRateZones()),
|
|
59512
|
+
optional(sync, "fitnessAge", () => this.tools.getFitnessAge(today)),
|
|
59513
|
+
optional(sync, "units", () => this.api.getJson("/userprofile-service/userprofile/settings"))
|
|
59514
|
+
]);
|
|
59515
|
+
const units = isObject$2(settings) ? settings.measurementSystem : void 0;
|
|
59516
|
+
const locale = isObject$2(settings) ? String(settings.preferredLocale).replace("_", "-") : "";
|
|
59460
59517
|
const out = {
|
|
59461
59518
|
generated: today,
|
|
59462
59519
|
weeks: w,
|
|
@@ -59465,7 +59522,9 @@ var GarminDashboard = class {
|
|
|
59465
59522
|
prevStart,
|
|
59466
59523
|
prevEnd: addDays(start, -1),
|
|
59467
59524
|
zones,
|
|
59468
|
-
fitnessAge
|
|
59525
|
+
fitnessAge,
|
|
59526
|
+
...typeof units === "string" && UNITS.includes(units) ? { units } : {},
|
|
59527
|
+
.../^[a-z]{2,3}(-[A-Z]{2})?$/.test(locale) ? { locale } : {}
|
|
59469
59528
|
};
|
|
59470
59529
|
const activities = activitiesBetween(cache, start, today).map((a) => structuredClone(a));
|
|
59471
59530
|
await this.#attachDetails(activities);
|
|
@@ -59530,16 +59589,16 @@ var GarminDashboard = class {
|
|
|
59530
59589
|
failed = true;
|
|
59531
59590
|
continue;
|
|
59532
59591
|
}
|
|
59533
|
-
const summaries = field(json, "hrvSummaries");
|
|
59592
|
+
const summaries = field$1(json, "hrvSummaries");
|
|
59534
59593
|
for (const h of Array.isArray(summaries) ? summaries : []) {
|
|
59535
|
-
const day = date(field(h, "calendarDate"));
|
|
59594
|
+
const day = date(field$1(h, "calendarDate"));
|
|
59536
59595
|
if (day === void 0) continue;
|
|
59537
59596
|
const o = { date: day };
|
|
59538
59597
|
copy(h, o, "lastNightAvg", "hrv");
|
|
59539
59598
|
copy(h, o, "weeklyAvg", "weeklyAvg");
|
|
59540
59599
|
copy(h, o, "status", "status");
|
|
59541
|
-
copy(field(h, "baseline"), o, "balancedLow", "low");
|
|
59542
|
-
copy(field(h, "baseline"), o, "balancedUpper", "high");
|
|
59600
|
+
copy(field$1(h, "baseline"), o, "balancedLow", "low");
|
|
59601
|
+
copy(field$1(h, "baseline"), o, "balancedUpper", "high");
|
|
59543
59602
|
byDate[day] = o;
|
|
59544
59603
|
}
|
|
59545
59604
|
}
|
|
@@ -59633,10 +59692,10 @@ function recoveryRow(day, sleep, summary) {
|
|
|
59633
59692
|
hours(sleep, r, "lightSleepSeconds", "light");
|
|
59634
59693
|
hours(sleep, r, "remSleepSeconds", "rem");
|
|
59635
59694
|
hours(sleep, r, "awakeSleepSeconds", "awake");
|
|
59636
|
-
copy(field(field(sleep, "sleepScores"), "overall"), r, "value", "sleepScore");
|
|
59637
|
-
const need = field(field(sleep, "sleepNeed"), "actual");
|
|
59695
|
+
copy(field$1(field$1(sleep, "sleepScores"), "overall"), r, "value", "sleepScore");
|
|
59696
|
+
const need = field$1(field$1(sleep, "sleepNeed"), "actual");
|
|
59638
59697
|
if (typeof need === "number") r.needH = round2(need / 60);
|
|
59639
|
-
const bed = field(sleep, "sleepStartTimestampLocal");
|
|
59698
|
+
const bed = field$1(sleep, "sleepStartTimestampLocal");
|
|
59640
59699
|
if (typeof bed === "number") {
|
|
59641
59700
|
const t = new Date(Math.trunc(bed));
|
|
59642
59701
|
r.bedMin = t.getUTCHours() * 60 + t.getUTCMinutes();
|
|
@@ -59647,7 +59706,7 @@ function recoveryRow(day, sleep, summary) {
|
|
|
59647
59706
|
copy(summary, r, "averageStressLevel", "stress");
|
|
59648
59707
|
copy(summary, r, "totalSteps", "steps");
|
|
59649
59708
|
copy(summary, r, "dailyStepGoal", "stepGoal");
|
|
59650
|
-
const floors = field(summary, "floorsAscended");
|
|
59709
|
+
const floors = field$1(summary, "floorsAscended");
|
|
59651
59710
|
if (typeof floors === "number") r.floors = Math.round(floors);
|
|
59652
59711
|
return r;
|
|
59653
59712
|
}
|
|
@@ -59708,7 +59767,7 @@ function setCovered(cache, fromField, toField, [from, to]) {
|
|
|
59708
59767
|
cache[toField] = to;
|
|
59709
59768
|
}
|
|
59710
59769
|
function dayOf$1(a) {
|
|
59711
|
-
const s = field(a, "start");
|
|
59770
|
+
const s = field$1(a, "start");
|
|
59712
59771
|
return typeof s === "string" && s.length >= 10 ? date(s.slice(0, 10)) : void 0;
|
|
59713
59772
|
}
|
|
59714
59773
|
/** A yyyy-MM-dd date; undefined when missing or not a real date, so the range is fetched again. */
|
|
@@ -59718,11 +59777,11 @@ function date(v) {
|
|
|
59718
59777
|
return !Number.isNaN(t) && new Date(t).toISOString().startsWith(v) ? v : void 0;
|
|
59719
59778
|
}
|
|
59720
59779
|
function copy(from, to, name, as) {
|
|
59721
|
-
const v = field(from, name);
|
|
59780
|
+
const v = field$1(from, name);
|
|
59722
59781
|
if (v !== void 0 && v !== null) to[as] = v;
|
|
59723
59782
|
}
|
|
59724
59783
|
function hours(from, to, name, as) {
|
|
59725
|
-
const v = field(from, name);
|
|
59784
|
+
const v = field$1(from, name);
|
|
59726
59785
|
if (typeof v === "number") to[as] = round2(v / 3600);
|
|
59727
59786
|
}
|
|
59728
59787
|
function round2(v) {
|
|
@@ -59731,7 +59790,7 @@ function round2(v) {
|
|
|
59731
59790
|
function isObject$2(v) {
|
|
59732
59791
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
59733
59792
|
}
|
|
59734
|
-
function field(v, name) {
|
|
59793
|
+
function field$1(v, name) {
|
|
59735
59794
|
return isObject$2(v) ? v[name] : void 0;
|
|
59736
59795
|
}
|
|
59737
59796
|
/** Text, or a number as text; "" for anything else. */
|
|
@@ -59743,6 +59802,12 @@ function asLong(v) {
|
|
|
59743
59802
|
if (typeof v === "number") return Math.trunc(v);
|
|
59744
59803
|
return typeof v === "string" && /^[+-]?\d+$/.test(v) ? Number(v) : 0;
|
|
59745
59804
|
}
|
|
59805
|
+
/** The measurement systems of a Garmin profile. */
|
|
59806
|
+
const UNITS = [
|
|
59807
|
+
"metric",
|
|
59808
|
+
"statute_us",
|
|
59809
|
+
"statute_uk"
|
|
59810
|
+
];
|
|
59746
59811
|
/** Registers getDashboardData. */
|
|
59747
59812
|
function registerDashboard(server, dashboard) {
|
|
59748
59813
|
server.registerTool("getDashboardData", {
|
|
@@ -59750,8 +59815,11 @@ function registerDashboard(server, dashboard) {
|
|
|
59750
59815
|
"All the data of the training dashboard in one call: HR zones, fitness age, the activities of the",
|
|
59751
59816
|
"last `weeks` weeks (compact summaries, with strength sets and laps where they apply), the activities",
|
|
59752
59817
|
"of the weeks before for the period comparison, daily HRV, and the last 14 days of sleep and daily",
|
|
59753
|
-
"summaries
|
|
59754
|
-
"
|
|
59818
|
+
"summaries, and `units`: the measurement system of the Garmin profile (metric, statute_us or",
|
|
59819
|
+
"statute_uk) that the page shows; the data itself is always metric; `locale`: the language of",
|
|
59820
|
+
"the Garmin profile, e.g. it or pt-BR. Keeps a local cache and",
|
|
59821
|
+
"fetches from Garmin only what is new since the last call, so a rebuild or an update needs few",
|
|
59822
|
+
"calls. `sync` in the result says what was fetched."
|
|
59755
59823
|
].join("\n"),
|
|
59756
59824
|
inputSchema: object({ weeks: number$1().int().describe("Weeks to cover, default 12, max 52").optional() })
|
|
59757
59825
|
}, async (a) => ({ content: [{
|
|
@@ -60266,452 +60334,2275 @@ const ABOUT = {
|
|
|
60266
60334
|
* The built-in languages besides English. The page script does not hold them: the server writes the
|
|
60267
60335
|
* languages that the page's settings choose into its languages block, so the file has no other texts.
|
|
60268
60336
|
*/
|
|
60269
|
-
const BUILT_IN = {
|
|
60270
|
-
|
|
60271
|
-
|
|
60272
|
-
|
|
60273
|
-
|
|
60274
|
-
|
|
60275
|
-
|
|
60276
|
-
|
|
60277
|
-
|
|
60278
|
-
|
|
60279
|
-
|
|
60280
|
-
|
|
60281
|
-
|
|
60282
|
-
|
|
60283
|
-
|
|
60284
|
-
|
|
60285
|
-
|
|
60286
|
-
|
|
60287
|
-
|
|
60288
|
-
|
|
60289
|
-
|
|
60290
|
-
|
|
60291
|
-
|
|
60292
|
-
|
|
60293
|
-
|
|
60294
|
-
|
|
60295
|
-
|
|
60296
|
-
|
|
60297
|
-
|
|
60298
|
-
|
|
60299
|
-
|
|
60300
|
-
|
|
60301
|
-
|
|
60302
|
-
|
|
60303
|
-
|
|
60304
|
-
|
|
60305
|
-
|
|
60306
|
-
|
|
60307
|
-
|
|
60308
|
-
|
|
60309
|
-
|
|
60310
|
-
|
|
60311
|
-
|
|
60312
|
-
|
|
60313
|
-
|
|
60314
|
-
|
|
60315
|
-
|
|
60316
|
-
|
|
60317
|
-
|
|
60318
|
-
|
|
60319
|
-
|
|
60320
|
-
|
|
60321
|
-
|
|
60322
|
-
|
|
60323
|
-
|
|
60324
|
-
|
|
60325
|
-
|
|
60326
|
-
|
|
60327
|
-
|
|
60328
|
-
|
|
60329
|
-
|
|
60330
|
-
|
|
60331
|
-
|
|
60332
|
-
|
|
60333
|
-
|
|
60334
|
-
|
|
60335
|
-
|
|
60336
|
-
|
|
60337
|
-
|
|
60338
|
-
|
|
60339
|
-
|
|
60340
|
-
|
|
60341
|
-
|
|
60342
|
-
|
|
60343
|
-
|
|
60344
|
-
|
|
60345
|
-
|
|
60346
|
-
|
|
60347
|
-
|
|
60348
|
-
|
|
60349
|
-
|
|
60350
|
-
|
|
60351
|
-
|
|
60352
|
-
|
|
60353
|
-
|
|
60354
|
-
|
|
60355
|
-
|
|
60356
|
-
|
|
60357
|
-
|
|
60358
|
-
|
|
60359
|
-
|
|
60360
|
-
|
|
60361
|
-
|
|
60362
|
-
|
|
60363
|
-
|
|
60364
|
-
|
|
60365
|
-
|
|
60366
|
-
|
|
60367
|
-
|
|
60368
|
-
|
|
60369
|
-
|
|
60370
|
-
|
|
60371
|
-
|
|
60372
|
-
|
|
60373
|
-
|
|
60374
|
-
|
|
60375
|
-
|
|
60376
|
-
|
|
60377
|
-
|
|
60378
|
-
|
|
60379
|
-
|
|
60380
|
-
|
|
60381
|
-
|
|
60382
|
-
|
|
60383
|
-
|
|
60384
|
-
|
|
60385
|
-
|
|
60386
|
-
|
|
60387
|
-
|
|
60388
|
-
|
|
60389
|
-
|
|
60390
|
-
|
|
60391
|
-
|
|
60392
|
-
|
|
60393
|
-
|
|
60394
|
-
|
|
60395
|
-
|
|
60396
|
-
|
|
60397
|
-
|
|
60398
|
-
|
|
60399
|
-
|
|
60400
|
-
|
|
60401
|
-
|
|
60402
|
-
|
|
60403
|
-
|
|
60404
|
-
|
|
60405
|
-
|
|
60406
|
-
|
|
60407
|
-
|
|
60408
|
-
|
|
60409
|
-
|
|
60410
|
-
|
|
60411
|
-
|
|
60412
|
-
|
|
60413
|
-
|
|
60414
|
-
|
|
60415
|
-
|
|
60416
|
-
|
|
60417
|
-
|
|
60418
|
-
|
|
60419
|
-
|
|
60420
|
-
|
|
60421
|
-
|
|
60422
|
-
|
|
60423
|
-
|
|
60424
|
-
|
|
60425
|
-
|
|
60426
|
-
|
|
60427
|
-
|
|
60428
|
-
|
|
60429
|
-
|
|
60430
|
-
|
|
60431
|
-
|
|
60432
|
-
|
|
60433
|
-
|
|
60434
|
-
|
|
60435
|
-
|
|
60436
|
-
|
|
60437
|
-
|
|
60438
|
-
|
|
60439
|
-
|
|
60440
|
-
|
|
60441
|
-
|
|
60442
|
-
|
|
60443
|
-
|
|
60444
|
-
|
|
60445
|
-
|
|
60446
|
-
|
|
60447
|
-
|
|
60448
|
-
|
|
60449
|
-
|
|
60450
|
-
|
|
60451
|
-
|
|
60452
|
-
|
|
60453
|
-
|
|
60454
|
-
|
|
60455
|
-
|
|
60456
|
-
|
|
60457
|
-
|
|
60458
|
-
|
|
60459
|
-
|
|
60460
|
-
|
|
60461
|
-
|
|
60462
|
-
|
|
60463
|
-
|
|
60464
|
-
|
|
60465
|
-
|
|
60466
|
-
|
|
60467
|
-
|
|
60468
|
-
|
|
60469
|
-
|
|
60470
|
-
|
|
60471
|
-
|
|
60472
|
-
|
|
60473
|
-
|
|
60474
|
-
|
|
60475
|
-
|
|
60476
|
-
|
|
60477
|
-
|
|
60478
|
-
|
|
60479
|
-
|
|
60480
|
-
|
|
60481
|
-
|
|
60482
|
-
|
|
60483
|
-
|
|
60484
|
-
|
|
60485
|
-
|
|
60486
|
-
|
|
60487
|
-
|
|
60488
|
-
|
|
60489
|
-
|
|
60490
|
-
|
|
60491
|
-
|
|
60492
|
-
|
|
60493
|
-
|
|
60494
|
-
|
|
60495
|
-
|
|
60496
|
-
|
|
60497
|
-
|
|
60498
|
-
|
|
60499
|
-
|
|
60500
|
-
|
|
60501
|
-
|
|
60502
|
-
|
|
60503
|
-
|
|
60504
|
-
|
|
60505
|
-
|
|
60506
|
-
|
|
60507
|
-
|
|
60508
|
-
|
|
60509
|
-
|
|
60510
|
-
|
|
60511
|
-
|
|
60512
|
-
|
|
60513
|
-
|
|
60514
|
-
|
|
60515
|
-
|
|
60516
|
-
|
|
60517
|
-
|
|
60518
|
-
|
|
60519
|
-
|
|
60520
|
-
|
|
60521
|
-
|
|
60522
|
-
|
|
60523
|
-
|
|
60524
|
-
|
|
60525
|
-
|
|
60526
|
-
|
|
60527
|
-
|
|
60528
|
-
|
|
60529
|
-
|
|
60530
|
-
|
|
60531
|
-
|
|
60532
|
-
|
|
60533
|
-
|
|
60534
|
-
|
|
60535
|
-
|
|
60536
|
-
|
|
60537
|
-
|
|
60538
|
-
|
|
60539
|
-
|
|
60540
|
-
|
|
60541
|
-
|
|
60542
|
-
|
|
60543
|
-
|
|
60544
|
-
|
|
60545
|
-
|
|
60546
|
-
|
|
60547
|
-
|
|
60548
|
-
|
|
60549
|
-
|
|
60550
|
-
|
|
60551
|
-
|
|
60552
|
-
|
|
60553
|
-
|
|
60554
|
-
|
|
60555
|
-
|
|
60556
|
-
|
|
60557
|
-
|
|
60558
|
-
|
|
60559
|
-
|
|
60560
|
-
|
|
60561
|
-
|
|
60562
|
-
|
|
60563
|
-
|
|
60564
|
-
|
|
60565
|
-
|
|
60566
|
-
|
|
60567
|
-
|
|
60568
|
-
|
|
60569
|
-
|
|
60570
|
-
|
|
60571
|
-
|
|
60572
|
-
|
|
60573
|
-
|
|
60574
|
-
|
|
60575
|
-
|
|
60576
|
-
|
|
60577
|
-
|
|
60578
|
-
|
|
60579
|
-
|
|
60580
|
-
|
|
60581
|
-
|
|
60582
|
-
|
|
60583
|
-
|
|
60584
|
-
|
|
60585
|
-
|
|
60586
|
-
|
|
60587
|
-
|
|
60588
|
-
|
|
60589
|
-
|
|
60590
|
-
|
|
60591
|
-
|
|
60592
|
-
|
|
60593
|
-
|
|
60594
|
-
|
|
60595
|
-
|
|
60596
|
-
|
|
60597
|
-
|
|
60598
|
-
|
|
60599
|
-
|
|
60600
|
-
|
|
60601
|
-
|
|
60602
|
-
|
|
60603
|
-
|
|
60604
|
-
|
|
60605
|
-
|
|
60606
|
-
|
|
60607
|
-
|
|
60608
|
-
|
|
60609
|
-
|
|
60610
|
-
|
|
60611
|
-
|
|
60612
|
-
|
|
60613
|
-
|
|
60614
|
-
|
|
60615
|
-
|
|
60616
|
-
|
|
60617
|
-
|
|
60618
|
-
|
|
60619
|
-
|
|
60620
|
-
|
|
60621
|
-
|
|
60622
|
-
|
|
60623
|
-
|
|
60624
|
-
|
|
60625
|
-
|
|
60626
|
-
|
|
60627
|
-
|
|
60628
|
-
|
|
60629
|
-
|
|
60630
|
-
|
|
60631
|
-
|
|
60632
|
-
|
|
60633
|
-
|
|
60634
|
-
|
|
60635
|
-
|
|
60636
|
-
|
|
60637
|
-
|
|
60638
|
-
|
|
60639
|
-
|
|
60640
|
-
|
|
60641
|
-
|
|
60642
|
-
|
|
60643
|
-
|
|
60644
|
-
|
|
60645
|
-
|
|
60646
|
-
|
|
60647
|
-
|
|
60648
|
-
|
|
60649
|
-
|
|
60650
|
-
|
|
60651
|
-
|
|
60652
|
-
|
|
60653
|
-
|
|
60654
|
-
|
|
60655
|
-
|
|
60656
|
-
|
|
60657
|
-
|
|
60658
|
-
|
|
60659
|
-
|
|
60660
|
-
|
|
60661
|
-
|
|
60662
|
-
|
|
60663
|
-
|
|
60664
|
-
|
|
60665
|
-
|
|
60666
|
-
|
|
60667
|
-
|
|
60668
|
-
|
|
60669
|
-
|
|
60670
|
-
|
|
60671
|
-
|
|
60672
|
-
|
|
60673
|
-
|
|
60674
|
-
|
|
60675
|
-
|
|
60676
|
-
|
|
60677
|
-
|
|
60678
|
-
|
|
60679
|
-
|
|
60680
|
-
|
|
60681
|
-
|
|
60682
|
-
|
|
60683
|
-
|
|
60684
|
-
|
|
60685
|
-
|
|
60686
|
-
|
|
60687
|
-
|
|
60688
|
-
|
|
60689
|
-
|
|
60690
|
-
|
|
60691
|
-
|
|
60692
|
-
|
|
60693
|
-
|
|
60694
|
-
|
|
60695
|
-
|
|
60696
|
-
|
|
60697
|
-
|
|
60698
|
-
|
|
60699
|
-
|
|
60700
|
-
|
|
60701
|
-
|
|
60702
|
-
|
|
60703
|
-
|
|
60704
|
-
|
|
60705
|
-
|
|
60706
|
-
|
|
60707
|
-
|
|
60708
|
-
|
|
60709
|
-
|
|
60710
|
-
|
|
60711
|
-
|
|
60712
|
-
|
|
60713
|
-
|
|
60714
|
-
|
|
60337
|
+
const BUILT_IN = {
|
|
60338
|
+
it: {
|
|
60339
|
+
title: "Allenamento + Recupero",
|
|
60340
|
+
headline: "Carico di allenamento, recupero e progressi da Garmin Connect",
|
|
60341
|
+
na: "n/d",
|
|
60342
|
+
noData: "Questa pagina non ha ancora dati. Chiedi al tuo assistente di scrivere la dashboard.",
|
|
60343
|
+
period: "dal {from} al {to}",
|
|
60344
|
+
inProgress: "* settimana in corso",
|
|
60345
|
+
"menu.settings": "Impostazioni",
|
|
60346
|
+
"menu.theme": "Tema",
|
|
60347
|
+
"footer.updated": "Dati da Garmin Connect, aggiornati il {date}",
|
|
60348
|
+
"footer.failed": "Giorni non letti da Garmin: {n} (i loro valori mostrano n/d)",
|
|
60349
|
+
"footer.license": "Licenza MIT",
|
|
60350
|
+
"menu.language": "Lingua",
|
|
60351
|
+
"lang.auto": "Automatica",
|
|
60352
|
+
"lang.missing": "{language} non è ancora in questa pagina. Chiedi al tuo assistente di aggiungere la lingua.",
|
|
60353
|
+
"themeGroup.auto": "Automatico",
|
|
60354
|
+
"themeGroup.dark": "Scuri",
|
|
60355
|
+
"themeGroup.light": "Chiari",
|
|
60356
|
+
"themeGroup.accessible": "Accessibili",
|
|
60357
|
+
"theme.system": "Sistema",
|
|
60358
|
+
"theme.midnight": "Mezzanotte",
|
|
60359
|
+
"theme.graphite": "Grafite",
|
|
60360
|
+
"theme.ocean": "Oceano",
|
|
60361
|
+
"theme.forest": "Foresta",
|
|
60362
|
+
"theme.plum": "Prugna",
|
|
60363
|
+
"theme.light": "Chiaro",
|
|
60364
|
+
"theme.paper": "Carta",
|
|
60365
|
+
"theme.mist": "Nebbia",
|
|
60366
|
+
"theme.hcDark": "Alto contrasto scuro",
|
|
60367
|
+
"theme.hcLight": "Alto contrasto chiaro",
|
|
60368
|
+
"window.label": "Settimane",
|
|
60369
|
+
"window.last": "Ultime {n}",
|
|
60370
|
+
"window.all": "Tutte",
|
|
60371
|
+
"window.from": "Dalla settimana",
|
|
60372
|
+
"window.to": "Alla settimana",
|
|
60373
|
+
"names.label": "Nomi delle attività",
|
|
60374
|
+
"names.all": "Tutti",
|
|
60375
|
+
"names.count": "{n} scelto|{n} scelti",
|
|
60376
|
+
"names.search": "Cerca nomi",
|
|
60377
|
+
"names.noMatch": "Nessun nome corrisponde.",
|
|
60378
|
+
"names.remove": "Togli {name}",
|
|
60379
|
+
"names.clear": "Mostra tutte",
|
|
60380
|
+
"type.all": "Tutte",
|
|
60381
|
+
"type.recovery": "Recupero",
|
|
60382
|
+
"type.running": "Corsa",
|
|
60383
|
+
"type.strengthTraining": "Forza",
|
|
60384
|
+
"type.walking": "Camminata",
|
|
60385
|
+
"type.hiking": "Escursionismo",
|
|
60386
|
+
"type.cycling": "Bici",
|
|
60387
|
+
"type.swimming": "Nuoto",
|
|
60388
|
+
"type.racket": "Sport di racchetta",
|
|
60389
|
+
"type.generic": "Altro",
|
|
60390
|
+
"type.transition": "Transizione",
|
|
60391
|
+
"type.fitnessEquipment": "Attrezzi fitness",
|
|
60392
|
+
"type.basketball": "Pallacanestro",
|
|
60393
|
+
"type.soccer": "Calcio",
|
|
60394
|
+
"type.tennis": "Tennis",
|
|
60395
|
+
"type.americanFootball": "Football americano",
|
|
60396
|
+
"type.training": "Allenamento",
|
|
60397
|
+
"type.crossCountrySkiing": "Sci di fondo",
|
|
60398
|
+
"type.alpineSkiing": "Sci alpino",
|
|
60399
|
+
"type.snowboarding": "Snowboard",
|
|
60400
|
+
"type.rowing": "Canottaggio",
|
|
60401
|
+
"type.mountaineering": "Alpinismo",
|
|
60402
|
+
"type.multisport": "Multisport",
|
|
60403
|
+
"type.paddling": "Pagaia",
|
|
60404
|
+
"type.flying": "Volo",
|
|
60405
|
+
"type.eBiking": "E-bike",
|
|
60406
|
+
"type.motorcycling": "Motociclismo",
|
|
60407
|
+
"type.boating": "Nautica",
|
|
60408
|
+
"type.driving": "Guida",
|
|
60409
|
+
"type.golf": "Golf",
|
|
60410
|
+
"type.hangGliding": "Deltaplano",
|
|
60411
|
+
"type.horsebackRiding": "Equitazione",
|
|
60412
|
+
"type.hunting": "Caccia",
|
|
60413
|
+
"type.fishing": "Pesca",
|
|
60414
|
+
"type.inlineSkating": "Pattinaggio in linea",
|
|
60415
|
+
"type.rockClimbing": "Arrampicata",
|
|
60416
|
+
"type.sailing": "Vela",
|
|
60417
|
+
"type.iceSkating": "Pattinaggio su ghiaccio",
|
|
60418
|
+
"type.skyDiving": "Paracadutismo",
|
|
60419
|
+
"type.snowshoeing": "Ciaspolata",
|
|
60420
|
+
"type.snowmobiling": "Motoslitta",
|
|
60421
|
+
"type.standUpPaddleboarding": "Stand up paddle",
|
|
60422
|
+
"type.surfing": "Surf",
|
|
60423
|
+
"type.wakeboarding": "Wakeboard",
|
|
60424
|
+
"type.waterSkiing": "Sci nautico",
|
|
60425
|
+
"type.kayaking": "Kayak",
|
|
60426
|
+
"type.rafting": "Rafting",
|
|
60427
|
+
"type.windsurfing": "Windsurf",
|
|
60428
|
+
"type.kitesurfing": "Kitesurf",
|
|
60429
|
+
"type.tactical": "Tattico",
|
|
60430
|
+
"type.jumpmaster": "Jumpmaster",
|
|
60431
|
+
"type.boxing": "Pugilato",
|
|
60432
|
+
"type.floorClimbing": "Salita di piani",
|
|
60433
|
+
"type.baseball": "Baseball",
|
|
60434
|
+
"type.diving": "Immersione",
|
|
60435
|
+
"type.shooting": "Tiro",
|
|
60436
|
+
"type.winterSport": "Sport invernali",
|
|
60437
|
+
"type.grinding": "Grinding",
|
|
60438
|
+
"type.hiit": "HIIT",
|
|
60439
|
+
"type.videoGaming": "Videogiochi",
|
|
60440
|
+
"type.wheelchairPushWalk": "Passeggiata in carrozzina",
|
|
60441
|
+
"type.wheelchairPushRun": "Corsa in carrozzina",
|
|
60442
|
+
"type.meditation": "Meditazione",
|
|
60443
|
+
"type.paraSport": "Sport paralimpici",
|
|
60444
|
+
"type.discGolf": "Disc golf",
|
|
60445
|
+
"type.teamSport": "Sport di squadra",
|
|
60446
|
+
"type.cricket": "Cricket",
|
|
60447
|
+
"type.rugby": "Rugby",
|
|
60448
|
+
"type.hockey": "Hockey",
|
|
60449
|
+
"type.lacrosse": "Lacrosse",
|
|
60450
|
+
"type.volleyball": "Pallavolo",
|
|
60451
|
+
"type.waterTubing": "Tubing",
|
|
60452
|
+
"type.wakesurfing": "Wakesurf",
|
|
60453
|
+
"type.waterSport": "Sport acquatici",
|
|
60454
|
+
"type.archery": "Tiro con l'arco",
|
|
60455
|
+
"type.mixedMartialArts": "Arti marziali miste",
|
|
60456
|
+
"type.motorSports": "Motorsport",
|
|
60457
|
+
"type.snorkeling": "Snorkeling",
|
|
60458
|
+
"type.dance": "Danza",
|
|
60459
|
+
"type.jumpRope": "Salto della corda",
|
|
60460
|
+
"type.poolApnea": "Apnea in piscina",
|
|
60461
|
+
"type.mobility": "Mobilità",
|
|
60462
|
+
"type.geocaching": "Geocaching",
|
|
60463
|
+
"type.canoeing": "Canoa",
|
|
60464
|
+
"tabs.label": "Tipi di attività",
|
|
60465
|
+
"edit.start": "Modifica layout",
|
|
60466
|
+
"edit.done": "Fine",
|
|
60467
|
+
"edit.add": "Aggiungi widget",
|
|
60468
|
+
"edit.reset": "Ripristina scheda",
|
|
60469
|
+
"edit.discard": "Annulla",
|
|
60470
|
+
"edit.bar": "Modifica layout",
|
|
60471
|
+
"edit.editing": "Modifica · {board}",
|
|
60472
|
+
"edit.unsaved": "Modifiche non salvate",
|
|
60473
|
+
"discard.title": "Annullare le modifiche?",
|
|
60474
|
+
"discard.text": "Il layout torna com'era prima di selezionare Modifica layout, in ogni scheda.",
|
|
60475
|
+
"discard.keep": "Continua a modificare",
|
|
60476
|
+
"discard.confirm": "Annulla le modifiche",
|
|
60477
|
+
"edit.copy": "Copia layout",
|
|
60478
|
+
"edit.hide": "Nascondi {title}",
|
|
60479
|
+
"edit.show": "Mostra {title}",
|
|
60480
|
+
"edit.remove": "Rimuovi {title}",
|
|
60481
|
+
"edit.move": "Sposta {title}",
|
|
60482
|
+
"edit.moveHint": "Trascina per spostare, oppure premi Spazio e poi le frecce",
|
|
60483
|
+
"edit.width": "Larghezza di {title}",
|
|
60484
|
+
"edit.widthValue": "{n} colonne su {cols}",
|
|
60485
|
+
"edit.addHidden": "Aggiungi widget · {n} nascosti",
|
|
60486
|
+
"add.addOne": "Aggiungi {title}",
|
|
60487
|
+
"said.picked": "{title} preso, posizione {n} di {count}. Le frecce lo spostano, Spazio lo rilascia, Esc lo rimette al suo posto.",
|
|
60488
|
+
"said.position": "Posizione {n} di {count}",
|
|
60489
|
+
"said.cancelled": "{title} è di nuovo al suo posto",
|
|
60490
|
+
"said.moved": "{title} spostato in posizione {n} di {count}",
|
|
60491
|
+
"said.width": "{title}: {n} colonne su {cols}",
|
|
60492
|
+
"said.hidden": "{title} nascosto",
|
|
60493
|
+
"said.shown": "{title} visibile",
|
|
60494
|
+
"said.removed": "{title} rimosso",
|
|
60495
|
+
"said.added": "{title} aggiunto",
|
|
60496
|
+
"said.saved": "Layout salvato",
|
|
60497
|
+
"said.unchanged": "Nessuna modifica da salvare",
|
|
60498
|
+
"said.discarded": "Modifiche annullate",
|
|
60499
|
+
"stale.label": "Nuovo layout",
|
|
60500
|
+
"stale.text": "Il tuo assistente ha scritto un nuovo layout per questa dashboard. Il layout che hai fatto in questo browser resta finché non scegli.",
|
|
60501
|
+
"stale.keep": "Tieni il mio",
|
|
60502
|
+
"stale.new": "Usa il nuovo",
|
|
60503
|
+
"stale.used": "Ora usi il nuovo layout",
|
|
60504
|
+
"stale.kept": "Ora usi il tuo layout",
|
|
60505
|
+
"add.title": "Aggiungi un widget",
|
|
60506
|
+
"add.type": "Tipo",
|
|
60507
|
+
"add.add": "Aggiungi",
|
|
60508
|
+
"add.close": "Chiudi",
|
|
60509
|
+
"add.hiddenGroup": "Nascosti in questa scheda",
|
|
60510
|
+
"add.none": "Tutti i widget sono già in questa scheda.",
|
|
60511
|
+
"add.search": "Cerca widget",
|
|
60512
|
+
"add.noMatch": "Nessun widget corrisponde.",
|
|
60513
|
+
"copy.title": "Copia layout",
|
|
60514
|
+
"copy.hint": "Questo layout è salvato solo in questo browser. Per tenerlo nella pagina, incolla questo testo al tuo assistente e chiedigli di salvare il layout della dashboard.",
|
|
60515
|
+
"copy.copy": "Copia",
|
|
60516
|
+
"copy.copied": "Copiato",
|
|
60517
|
+
"board.empty": "Questa scheda non ha widget.",
|
|
60518
|
+
"board.emptyEdit": "Questa scheda non ha widget: usa “Aggiungi widget” per metterne.",
|
|
60519
|
+
"widget.failed": "Questo widget non funziona: {message}",
|
|
60520
|
+
"widget.unknown": "Widget sconosciuto: {id}",
|
|
60521
|
+
"empty.window": "Nessuna sessione nelle settimane scelte.",
|
|
60522
|
+
"w.coachNotes": "Note del coach",
|
|
60523
|
+
"notes.none": "Nessuna nota.",
|
|
60524
|
+
"notes.disclaimer": "Suggerimenti di allenamento sui dati fino al {date}. Non sono consigli medici.",
|
|
60525
|
+
"w.kpis": "Panoramica",
|
|
60526
|
+
"kpi.totals": "{distance} · {time} · {n} sessione|{distance} · {time} · {n} sessioni",
|
|
60527
|
+
"kpi.distance": "Distanza · ultimi 7 giorni",
|
|
60528
|
+
"kpi.time": "Tempo · ultimi 7 giorni",
|
|
60529
|
+
"kpi.vs4w": "rispetto alla media di 4 settimane",
|
|
60530
|
+
"delta.good": "meglio",
|
|
60531
|
+
"delta.bad": "peggio",
|
|
60532
|
+
"kpi.ratio": "Rapporto di carico 7:28",
|
|
60533
|
+
"kpi.band": "Fascia 0,8-1,3",
|
|
60534
|
+
"kpi.ratioAll": "Usa tutte le attività, anche con un filtro per tipo",
|
|
60535
|
+
"kpi.hrv": "HRV della notte",
|
|
60536
|
+
"kpi.vsBaseline": "rispetto alla media di 7 giorni {v} ms",
|
|
60537
|
+
"w.readiness": "Prontezza di oggi",
|
|
60538
|
+
"ready.easy": "Vai piano",
|
|
60539
|
+
"ready.push": "Pronto a spingere",
|
|
60540
|
+
"ready.normal": "Normale",
|
|
60541
|
+
"ready.hrv": "Stato HRV",
|
|
60542
|
+
"ready.sleep": "Punteggio sonno",
|
|
60543
|
+
"ready.rhr": "FC a riposo",
|
|
60544
|
+
"ready.rhrVs": "7 giorni prima: {avg} bpm",
|
|
60545
|
+
"ready.ratio": "Rapporto di carico 7:28",
|
|
60546
|
+
"ready.note": "Una regola della dashboard, non la prontezza di Garmin. Mostra oggi: la finestra di settimane non la cambia.",
|
|
60547
|
+
"w.fitnessAge": "Età fitness",
|
|
60548
|
+
"fa.age": "Età fitness",
|
|
60549
|
+
"fa.actual": "Età reale",
|
|
60550
|
+
"fa.achievable": "Raggiungibile",
|
|
60551
|
+
"fa.details": "Dettagli",
|
|
60552
|
+
"fa.component": "Componente",
|
|
60553
|
+
"fa.value": "Ora",
|
|
60554
|
+
"fa.target": "Obiettivo Garmin",
|
|
60555
|
+
"fa.potential": "Età possibile",
|
|
60556
|
+
"fa.bodyFat": "Grasso corporeo %",
|
|
60557
|
+
"fa.bmi": "IMC",
|
|
60558
|
+
"fa.rhr": "FC a riposo",
|
|
60559
|
+
"fa.vigorousDaysAvg": "Giorni intensi / settimana",
|
|
60560
|
+
"fa.vigorousMinutesAvg": "Minuti intensi / settimana",
|
|
60561
|
+
"today.note": "Mostra oggi: la finestra di settimane non la cambia.",
|
|
60562
|
+
"w.periodComparison": "Confronto tra periodi",
|
|
60563
|
+
"pc.vs": "dal {from} al {to} rispetto a dal {pfrom} al {pto}",
|
|
60564
|
+
"pc.partial": "parziale: queste settimane iniziano prima dei dati",
|
|
60565
|
+
"pc.type": "Tipo",
|
|
60566
|
+
"pc.sessions": "Sessioni",
|
|
60567
|
+
"pc.distance": "Distanza",
|
|
60568
|
+
"pc.time": "Tempo",
|
|
60569
|
+
"pc.load": "Carico",
|
|
60570
|
+
"pc.total": "Totale",
|
|
60571
|
+
"w.records": "Record",
|
|
60572
|
+
"rec.best": "{d} più veloce",
|
|
60573
|
+
"d.1k": "1 km",
|
|
60574
|
+
"d.mile": "miglio",
|
|
60575
|
+
"d.5k": "5 km",
|
|
60576
|
+
"d.10k": "10 km",
|
|
60577
|
+
"d.half": "mezza maratona",
|
|
60578
|
+
"d.marathon": "maratona",
|
|
60579
|
+
"rec.longestRun": "Corsa più lunga",
|
|
60580
|
+
"rec.longestRide": "Uscita in bici più lunga",
|
|
60581
|
+
"rec.topSet": "Serie più pesante",
|
|
60582
|
+
"rec.e1rm": "Miglior 1RM stimato · {name}",
|
|
60583
|
+
"rec.none": "Nessun record nelle settimane scelte.",
|
|
60584
|
+
"rec.estimate": "I valori 1RM sono stime (Epley, serie da 1-12 ripetizioni).",
|
|
60585
|
+
"w.weeklyVolume": "Volume settimanale",
|
|
60586
|
+
"m.time": "Tempo",
|
|
60587
|
+
"m.distance": "Distanza",
|
|
60588
|
+
"m.sessions": "Sessioni",
|
|
60589
|
+
"m.load": "Carico",
|
|
60590
|
+
"vol.avg": "Media di 4 settimane",
|
|
60591
|
+
"w.intensityMinutes": "Minuti di intensità",
|
|
60592
|
+
"im.guide": "Linea guida 150 min",
|
|
60593
|
+
"im.note": "Minuti moderati + 2 x intensi delle sole attività, non di tutta la giornata.",
|
|
60594
|
+
"w.loadSafety": "Sicurezza del carico",
|
|
60595
|
+
"ls.weekLoad": "Carico · ultimi 7 giorni",
|
|
60596
|
+
"ls.monotony": "Monotonia · 7 giorni",
|
|
60597
|
+
"ls.monotonyFlag": "Sopra 2,0",
|
|
60598
|
+
"ls.strain": "Strain · 7 giorni",
|
|
60599
|
+
"ls.flagged": "Settimane sopra +10%",
|
|
60600
|
+
"ls.load": "Carico settimanale",
|
|
60601
|
+
"ls.ramp": "Variazione sulla settimana prima (%)",
|
|
60602
|
+
"ls.note": "Tutte le attività. Le settimane rosse sono salite più del 10%; una settimana in corso non viene segnalata. Monotonia e strain (Foster) sono stime.",
|
|
60603
|
+
"ls.estimated": "Alcuni carichi sono stime: l'attività non ha un carico Garmin.",
|
|
60604
|
+
"w.loadVsRecovery": "Carico e recupero",
|
|
60605
|
+
"lvr.hrv": "HRV (ms)",
|
|
60606
|
+
"lvr.note": "Carico giornaliero per tipo (barre) e HRV notturna (linea).",
|
|
60607
|
+
"w.calendar": "Costanza",
|
|
60608
|
+
"cal.minutes": "{n} min",
|
|
60609
|
+
"cal.rest": "Giorno di riposo",
|
|
60610
|
+
"cal.label": "Giorni di allenamento, per settimana; le frecce si spostano",
|
|
60611
|
+
"cal.less": "Meno",
|
|
60612
|
+
"cal.more": "Più",
|
|
60613
|
+
"m.label": "Misura",
|
|
60614
|
+
skip: "Vai alla scheda",
|
|
60615
|
+
"w.hrv": "HRV",
|
|
60616
|
+
"hrv.low": "Minimo dell'intervallo",
|
|
60617
|
+
"hrv.band": "Intervallo bilanciato",
|
|
60618
|
+
"hrv.nightly": "HRV notturna",
|
|
60619
|
+
"hrv.avg": "Media di 7 giorni",
|
|
60620
|
+
rec14: "Ultimi 14 giorni: la finestra di settimane non li cambia.",
|
|
60621
|
+
"rec.board": "I widget di recupero mostrano gli ultimi 14 giorni: la finestra di settimane non li cambia.",
|
|
60622
|
+
"w.restingHr": "FC a riposo",
|
|
60623
|
+
"w.sleep": "Sonno",
|
|
60624
|
+
"sl.deep": "Profondo",
|
|
60625
|
+
"sl.light": "Leggero",
|
|
60626
|
+
"sl.rem": "REM",
|
|
60627
|
+
"sl.awake": "Sveglio",
|
|
60628
|
+
"sl.score": "Punteggio",
|
|
60629
|
+
"w.sleepNeed": "Fabbisogno di sonno",
|
|
60630
|
+
"sn.gap": "Sotto il fabbisogno · media",
|
|
60631
|
+
"sn.actual": "Dormito",
|
|
60632
|
+
"sn.need": "Fabbisogno",
|
|
60633
|
+
"w.bedtime": "Ora di andare a letto",
|
|
60634
|
+
"bt.spread": "Variabilità dell'orario",
|
|
60635
|
+
"w.bodyBattery": "Body Battery",
|
|
60636
|
+
"bb.high": "Massimo",
|
|
60637
|
+
"bb.low": "Minimo",
|
|
60638
|
+
"w.activities": "Attività",
|
|
60639
|
+
"col.date": "Data",
|
|
60640
|
+
"col.type": "Tipo",
|
|
60641
|
+
"col.name": "Nome",
|
|
60642
|
+
"col.distance": "Distanza",
|
|
60643
|
+
"col.time": "Tempo",
|
|
60644
|
+
"col.pace": "Passo / velocità",
|
|
60645
|
+
"col.hr": "FC media",
|
|
60646
|
+
"col.ascent": "Dislivello",
|
|
60647
|
+
"col.race": "Gara",
|
|
60648
|
+
"laps.show": "giri",
|
|
60649
|
+
"lap.n": "Giro",
|
|
60650
|
+
"lap.intensity": "Intensità",
|
|
60651
|
+
"act.more": "Mostra tutte ({n})",
|
|
60652
|
+
"act.less": "Mostra meno",
|
|
60653
|
+
"one.session": "Una sola sessione finora: i grafici di tendenza iniziano da 2 sessioni.",
|
|
60654
|
+
"w.typeKpis": "Riepilogo",
|
|
60655
|
+
"tk.sessions": "Sessioni",
|
|
60656
|
+
"tk.time": "Tempo",
|
|
60657
|
+
"tk.avg": "{v} a sessione",
|
|
60658
|
+
"tk.distance": "Distanza",
|
|
60659
|
+
"tk.speed": "Velocità media",
|
|
60660
|
+
"tk.pace": "Passo medio",
|
|
60661
|
+
"tk.ascent": "Dislivello",
|
|
60662
|
+
"tk.hr": "FC media",
|
|
60663
|
+
"tk.peak": "Picco {v} bpm",
|
|
60664
|
+
"tk.load": "Carico di allenamento Garmin",
|
|
60665
|
+
"tk.share": "{v}% di tutto il carico",
|
|
60666
|
+
"w.typeWeekly": "Ogni settimana",
|
|
60667
|
+
"w.hrZones": "Tempo nelle zone FC",
|
|
60668
|
+
"zones.approximate": "Approssimato: alcune attività non hanno il tempo per zona, quindi tutti i loro minuti contano nella zona della FC media.",
|
|
60669
|
+
"form.cadence": "Cadenza",
|
|
60670
|
+
"form.stride": "Falcata",
|
|
60671
|
+
"form.gct": "Contatto al suolo",
|
|
60672
|
+
"form.vertOsc": "Oscillazione verticale",
|
|
60673
|
+
"form.vertRatio": "Rapporto verticale",
|
|
60674
|
+
"form.power": "Potenza",
|
|
60675
|
+
"form.normPower": "Potenza normalizzata",
|
|
60676
|
+
"w.bestEfforts": "Migliori prestazioni",
|
|
60677
|
+
"be.distance": "Distanza",
|
|
60678
|
+
"be.activity": "Attività",
|
|
60679
|
+
"w.races": "Gare",
|
|
60680
|
+
"races.none": "Nessuna gara nelle settimane scelte.",
|
|
60681
|
+
"te.aerobic": "TE aerobico",
|
|
60682
|
+
"te.anaerobic": "TE anaerobico",
|
|
60683
|
+
"w.aerobicFitness": "Forma aerobica",
|
|
60684
|
+
"af.runs": "Corse (le più vecchie più chiare)",
|
|
60685
|
+
"af.pace": "Passo (min{unit})",
|
|
60686
|
+
"af.weekPace": "Passo settimanale",
|
|
60687
|
+
"af.efficiency": "Efficienza",
|
|
60688
|
+
"af.effUnit": "m per battito",
|
|
60689
|
+
"w.intensityShare": "Corsa facile e intensa",
|
|
60690
|
+
"is.easy": "Z1-2 (facile)",
|
|
60691
|
+
"is.hard": "Z4-5 (intensa)",
|
|
60692
|
+
"w.runningForm": "Tecnica di corsa",
|
|
60693
|
+
"w.runWalk": "Corsa e camminata",
|
|
60694
|
+
"rw.none": "Nessuna sessione di corsa e camminata nelle settimane scelte.",
|
|
60695
|
+
"rw.runPace": "Passo di corsa",
|
|
60696
|
+
"rw.pace": "Passo totale",
|
|
60697
|
+
"rw.walk": "Tempo di camminata",
|
|
60698
|
+
"w.trainingEffect": "Effetto allenante",
|
|
60699
|
+
"w.racePredictor": "Previsione gare",
|
|
60700
|
+
"rp.none": "Nessuna prestazione di 5 km o più nelle settimane scelte.",
|
|
60701
|
+
"rp.note": "Stima (Riegel) dai {d} in {time} del {date}.",
|
|
60702
|
+
"w.longRuns": "Lunghi e ripartizione del passo",
|
|
60703
|
+
"lr.share": "Quota del lungo sulla distanza settimanale",
|
|
60704
|
+
"lr.ascent": "Dislivello settimanale",
|
|
60705
|
+
"lr.note": "Settimane rosse: il lungo supera il 35% della distanza della settimana. La tabella mostra le gare e le corse oltre 15 km con i giri.",
|
|
60706
|
+
"lr.first": "Prima metà",
|
|
60707
|
+
"lr.second": "Seconda metà",
|
|
60708
|
+
"lr.split": "Ripartizione",
|
|
60709
|
+
"lr.gap": "Passo corretto per pendenza",
|
|
60710
|
+
"lr.positive": "positiva",
|
|
60711
|
+
"lr.negative": "negativa",
|
|
60712
|
+
"lr.even": "regolare",
|
|
60713
|
+
"w.setsPerCategory": "Serie per movimento",
|
|
60714
|
+
"ex.sets": "Serie completate",
|
|
60715
|
+
"w.muscleBalance": "Equilibrio tra gruppi muscolari",
|
|
60716
|
+
"mb.pushPull": "Serie di spinta : tirata",
|
|
60717
|
+
"mb.avg": "Media di serie a settimana",
|
|
60718
|
+
"mb.perWeek": "serie a settimana",
|
|
60719
|
+
"mb.note": "Una serie conta 1 per ogni gruppo muscolare primario e 0,5 per ogni gruppo secondario. Intervallo usuale: 10-20 serie a settimana (giallo sotto, rosso sopra).",
|
|
60720
|
+
"w.exercises": "Esercizi",
|
|
60721
|
+
"ex.groups": "Gruppi muscolari",
|
|
60722
|
+
"ex.search": "Cerca esercizi",
|
|
60723
|
+
"ex.name": "Esercizio",
|
|
60724
|
+
"ex.sessions": "Sessioni",
|
|
60725
|
+
"ex.last": "Ultima",
|
|
60726
|
+
"ex.top": "Ultima serie migliore",
|
|
60727
|
+
"ex.best": "Miglior 1RM stimato",
|
|
60728
|
+
"ex.change": "Variazione",
|
|
60729
|
+
"ex.trend": "Tendenza",
|
|
60730
|
+
"ex.groupRow": "{n} esercizio · {sets} serie completate|{n} esercizi · {sets} serie completate",
|
|
60731
|
+
"ex.reps": "{n} ripetizione|{n} ripetizioni",
|
|
60732
|
+
"ex.rest": "recupero mediano {v}",
|
|
60733
|
+
"ex.weight": "Serie migliore ({unit})",
|
|
60734
|
+
"ex.e1rm": "1RM stimato ({unit})",
|
|
60735
|
+
"ex.volume": "Volume ({unit})",
|
|
60736
|
+
"ex.maxReps": "Ripetizioni massime",
|
|
60737
|
+
"ex.totalReps": "Ripetizioni totali",
|
|
60738
|
+
"ex.longest": "Serie più lunga (s)",
|
|
60739
|
+
"ex.restAfter": "recupero {v}",
|
|
60740
|
+
"ex.notDone": "non completata",
|
|
60741
|
+
"w.strengthSessions": "Sessioni",
|
|
60742
|
+
"ss.count": "{n} esercizio|{n} esercizi",
|
|
60743
|
+
"mg.chest": "Petto",
|
|
60744
|
+
"mg.back": "Schiena",
|
|
60745
|
+
"mg.shoulders": "Spalle",
|
|
60746
|
+
"mg.biceps": "Bicipiti",
|
|
60747
|
+
"mg.triceps": "Tricipiti",
|
|
60748
|
+
"mg.forearms": "Avambracci",
|
|
60749
|
+
"mg.quads": "Quadricipiti",
|
|
60750
|
+
"mg.hamstrings": "Femorali",
|
|
60751
|
+
"mg.glutes": "Glutei",
|
|
60752
|
+
"mg.calves": "Polpacci",
|
|
60753
|
+
"mg.abs": "Addominali",
|
|
60754
|
+
"mg.hips": "Anche",
|
|
60755
|
+
"mg.other": "Altro",
|
|
60756
|
+
"w.walkDetails": "Passo e camminate",
|
|
60757
|
+
"wd.steps": "Passi",
|
|
60758
|
+
"wd.battery": "Variazione Body Battery",
|
|
60759
|
+
"wd.goal": "Obiettivo passi",
|
|
60760
|
+
"wd.floors": "Piani",
|
|
60761
|
+
"w.steps": "Passi giornalieri",
|
|
60762
|
+
"w.hikeVertical": "Velocità verticale",
|
|
60763
|
+
"hv.vam": "Dislivello ({unit}) all'ora",
|
|
60764
|
+
"w.bikeDetails": "Uscite in bici",
|
|
60765
|
+
"bd.outdoor": "All'aperto",
|
|
60766
|
+
"bd.indoor": "Al chiuso",
|
|
60767
|
+
"bd.rides": "{n} uscita|{n} uscite",
|
|
60768
|
+
"bd.where": "Dove",
|
|
60769
|
+
"w.swimDetails": "Nuotate",
|
|
60770
|
+
"sw.pace": "Passo",
|
|
60771
|
+
"sw.lengths": "Vasche",
|
|
60772
|
+
"sw.strokes": "Bracciate",
|
|
60773
|
+
"sw.pool": "Piscina",
|
|
60774
|
+
"w.racketDetails": "Sessioni e intensità",
|
|
60775
|
+
"pd.peak": "FC di picco",
|
|
60776
|
+
"pd.z3": "Tempo in Z3+",
|
|
60777
|
+
"pd.z45": "Quota Z4-5",
|
|
60778
|
+
"pd.kcal": "kcal all'ora",
|
|
60779
|
+
"pd.load": "Carico / corsa della stessa durata",
|
|
60780
|
+
"pd.note": "Il secondo carico è una stima: il carico Garmin medio al minuto delle tue corse, per la durata della sessione.",
|
|
60781
|
+
"col.sport": "Sport",
|
|
60782
|
+
"sport.tennis": "Tennis",
|
|
60783
|
+
"sport.padel": "Padel",
|
|
60784
|
+
"sport.pickleball": "Pickleball",
|
|
60785
|
+
"sport.racquetball": "Racquetball",
|
|
60786
|
+
"sport.squash": "Squash",
|
|
60787
|
+
"sport.badminton": "Badminton",
|
|
60788
|
+
"sport.tableTennis": "Tennis tavolo",
|
|
60789
|
+
"sport.platformTennis": "Platform tennis"
|
|
60790
|
+
},
|
|
60791
|
+
de: {
|
|
60792
|
+
title: "Training + Erholung",
|
|
60793
|
+
headline: "Trainingsbelastung, Erholung und Fortschritt aus Garmin Connect",
|
|
60794
|
+
na: "k. A.",
|
|
60795
|
+
noData: "Diese Seite hat noch keine Daten. Bitte deinen Assistenten, das Dashboard zu schreiben.",
|
|
60796
|
+
period: "{from} bis {to}",
|
|
60797
|
+
inProgress: "* laufende Woche",
|
|
60798
|
+
"menu.settings": "Einstellungen",
|
|
60799
|
+
"menu.theme": "Design",
|
|
60800
|
+
"footer.updated": "Daten aus Garmin Connect, aktualisiert am {date}",
|
|
60801
|
+
"footer.failed": "Nicht von Garmin gelesene Tage: {n} (ihre Werte zeigen k. A.)",
|
|
60802
|
+
"footer.license": "MIT-Lizenz",
|
|
60803
|
+
"menu.language": "Sprache",
|
|
60804
|
+
"lang.auto": "Automatisch",
|
|
60805
|
+
"lang.missing": "{language} ist noch nicht auf dieser Seite. Bitte deinen Assistenten, die Sprache hinzuzufügen.",
|
|
60806
|
+
"themeGroup.auto": "Automatisch",
|
|
60807
|
+
"themeGroup.dark": "Dunkel",
|
|
60808
|
+
"themeGroup.light": "Hell",
|
|
60809
|
+
"themeGroup.accessible": "Barrierefrei",
|
|
60810
|
+
"theme.system": "System",
|
|
60811
|
+
"theme.midnight": "Mitternacht",
|
|
60812
|
+
"theme.graphite": "Graphit",
|
|
60813
|
+
"theme.ocean": "Ozean",
|
|
60814
|
+
"theme.forest": "Wald",
|
|
60815
|
+
"theme.plum": "Pflaume",
|
|
60816
|
+
"theme.light": "Hell",
|
|
60817
|
+
"theme.paper": "Papier",
|
|
60818
|
+
"theme.mist": "Nebel",
|
|
60819
|
+
"theme.hcDark": "Hoher Kontrast dunkel",
|
|
60820
|
+
"theme.hcLight": "Hoher Kontrast hell",
|
|
60821
|
+
"window.label": "Wochen",
|
|
60822
|
+
"window.last": "Letzte {n}",
|
|
60823
|
+
"window.all": "Alle",
|
|
60824
|
+
"window.from": "Von Woche",
|
|
60825
|
+
"window.to": "Bis Woche",
|
|
60826
|
+
"names.label": "Aktivitätsnamen",
|
|
60827
|
+
"names.all": "Alle",
|
|
60828
|
+
"names.count": "{n} gewählt|{n} gewählt",
|
|
60829
|
+
"names.search": "Namen suchen",
|
|
60830
|
+
"names.noMatch": "Kein Name passt.",
|
|
60831
|
+
"names.remove": "{name} entfernen",
|
|
60832
|
+
"names.clear": "Alle zeigen",
|
|
60833
|
+
"type.all": "Alle",
|
|
60834
|
+
"type.recovery": "Erholung",
|
|
60835
|
+
"type.running": "Laufen",
|
|
60836
|
+
"type.strengthTraining": "Kraft",
|
|
60837
|
+
"type.walking": "Gehen",
|
|
60838
|
+
"type.hiking": "Wandern",
|
|
60839
|
+
"type.cycling": "Rad",
|
|
60840
|
+
"type.swimming": "Schwimmen",
|
|
60841
|
+
"type.racket": "Rückschlagsport",
|
|
60842
|
+
"type.generic": "Sonstiges",
|
|
60843
|
+
"type.transition": "Wechsel",
|
|
60844
|
+
"type.fitnessEquipment": "Fitnessgeräte",
|
|
60845
|
+
"type.basketball": "Basketball",
|
|
60846
|
+
"type.soccer": "Fußball",
|
|
60847
|
+
"type.tennis": "Tennis",
|
|
60848
|
+
"type.americanFootball": "American Football",
|
|
60849
|
+
"type.training": "Training",
|
|
60850
|
+
"type.crossCountrySkiing": "Skilanglauf",
|
|
60851
|
+
"type.alpineSkiing": "Ski alpin",
|
|
60852
|
+
"type.snowboarding": "Snowboarden",
|
|
60853
|
+
"type.rowing": "Rudern",
|
|
60854
|
+
"type.mountaineering": "Bergsteigen",
|
|
60855
|
+
"type.multisport": "Multisport",
|
|
60856
|
+
"type.paddling": "Paddeln",
|
|
60857
|
+
"type.flying": "Fliegen",
|
|
60858
|
+
"type.eBiking": "E-Bike",
|
|
60859
|
+
"type.motorcycling": "Motorradfahren",
|
|
60860
|
+
"type.boating": "Bootfahren",
|
|
60861
|
+
"type.driving": "Autofahren",
|
|
60862
|
+
"type.golf": "Golf",
|
|
60863
|
+
"type.hangGliding": "Drachenfliegen",
|
|
60864
|
+
"type.horsebackRiding": "Reiten",
|
|
60865
|
+
"type.hunting": "Jagen",
|
|
60866
|
+
"type.fishing": "Angeln",
|
|
60867
|
+
"type.inlineSkating": "Inlineskaten",
|
|
60868
|
+
"type.rockClimbing": "Klettern",
|
|
60869
|
+
"type.sailing": "Segeln",
|
|
60870
|
+
"type.iceSkating": "Eislaufen",
|
|
60871
|
+
"type.skyDiving": "Fallschirmspringen",
|
|
60872
|
+
"type.snowshoeing": "Schneeschuhwandern",
|
|
60873
|
+
"type.snowmobiling": "Schneemobil",
|
|
60874
|
+
"type.standUpPaddleboarding": "Stand-up-Paddling",
|
|
60875
|
+
"type.surfing": "Surfen",
|
|
60876
|
+
"type.wakeboarding": "Wakeboarden",
|
|
60877
|
+
"type.waterSkiing": "Wasserski",
|
|
60878
|
+
"type.kayaking": "Kajak",
|
|
60879
|
+
"type.rafting": "Rafting",
|
|
60880
|
+
"type.windsurfing": "Windsurfen",
|
|
60881
|
+
"type.kitesurfing": "Kitesurfen",
|
|
60882
|
+
"type.tactical": "Taktisch",
|
|
60883
|
+
"type.jumpmaster": "Jumpmaster",
|
|
60884
|
+
"type.boxing": "Boxen",
|
|
60885
|
+
"type.floorClimbing": "Etagensteigen",
|
|
60886
|
+
"type.baseball": "Baseball",
|
|
60887
|
+
"type.diving": "Tauchen",
|
|
60888
|
+
"type.shooting": "Schießen",
|
|
60889
|
+
"type.winterSport": "Wintersport",
|
|
60890
|
+
"type.grinding": "Grinding",
|
|
60891
|
+
"type.hiit": "HIIT",
|
|
60892
|
+
"type.videoGaming": "Videospiele",
|
|
60893
|
+
"type.wheelchairPushWalk": "Rollstuhl Gehen",
|
|
60894
|
+
"type.wheelchairPushRun": "Rollstuhl Laufen",
|
|
60895
|
+
"type.meditation": "Meditation",
|
|
60896
|
+
"type.paraSport": "Parasport",
|
|
60897
|
+
"type.discGolf": "Discgolf",
|
|
60898
|
+
"type.teamSport": "Mannschaftssport",
|
|
60899
|
+
"type.cricket": "Cricket",
|
|
60900
|
+
"type.rugby": "Rugby",
|
|
60901
|
+
"type.hockey": "Hockey",
|
|
60902
|
+
"type.lacrosse": "Lacrosse",
|
|
60903
|
+
"type.volleyball": "Volleyball",
|
|
60904
|
+
"type.waterTubing": "Tubing",
|
|
60905
|
+
"type.wakesurfing": "Wakesurfen",
|
|
60906
|
+
"type.waterSport": "Wassersport",
|
|
60907
|
+
"type.archery": "Bogenschießen",
|
|
60908
|
+
"type.mixedMartialArts": "Mixed Martial Arts",
|
|
60909
|
+
"type.motorSports": "Motorsport",
|
|
60910
|
+
"type.snorkeling": "Schnorcheln",
|
|
60911
|
+
"type.dance": "Tanzen",
|
|
60912
|
+
"type.jumpRope": "Seilspringen",
|
|
60913
|
+
"type.poolApnea": "Apnoetauchen im Becken",
|
|
60914
|
+
"type.mobility": "Mobilität",
|
|
60915
|
+
"type.geocaching": "Geocaching",
|
|
60916
|
+
"type.canoeing": "Kanufahren",
|
|
60917
|
+
"tabs.label": "Aktivitätstypen",
|
|
60918
|
+
"edit.start": "Layout bearbeiten",
|
|
60919
|
+
"edit.done": "Fertig",
|
|
60920
|
+
"edit.add": "Widget hinzufügen",
|
|
60921
|
+
"edit.reset": "Board zurücksetzen",
|
|
60922
|
+
"edit.discard": "Verwerfen",
|
|
60923
|
+
"edit.bar": "Layout bearbeiten",
|
|
60924
|
+
"edit.editing": "Bearbeiten · {board}",
|
|
60925
|
+
"edit.unsaved": "Ungespeicherte Änderungen",
|
|
60926
|
+
"discard.title": "Änderungen verwerfen?",
|
|
60927
|
+
"discard.text": "Das Layout kehrt auf jedem Board zum Stand vor „Layout bearbeiten“ zurück.",
|
|
60928
|
+
"discard.keep": "Weiter bearbeiten",
|
|
60929
|
+
"discard.confirm": "Änderungen verwerfen",
|
|
60930
|
+
"edit.copy": "Layout kopieren",
|
|
60931
|
+
"edit.hide": "{title} ausblenden",
|
|
60932
|
+
"edit.show": "{title} einblenden",
|
|
60933
|
+
"edit.remove": "{title} entfernen",
|
|
60934
|
+
"edit.move": "{title} verschieben",
|
|
60935
|
+
"edit.moveHint": "Zum Verschieben ziehen, oder Leertaste und dann die Pfeiltasten drücken",
|
|
60936
|
+
"edit.width": "Breite von {title}",
|
|
60937
|
+
"edit.widthValue": "{n} von {cols} Spalten",
|
|
60938
|
+
"edit.addHidden": "Widget hinzufügen · {n} ausgeblendet",
|
|
60939
|
+
"add.addOne": "{title} hinzufügen",
|
|
60940
|
+
"said.picked": "{title} aufgenommen, Position {n} von {count}. Die Pfeiltasten verschieben es, die Leertaste legt es ab, Escape setzt es zurück.",
|
|
60941
|
+
"said.position": "Position {n} von {count}",
|
|
60942
|
+
"said.cancelled": "{title} ist wieder an seinem Platz",
|
|
60943
|
+
"said.moved": "{title} auf Position {n} von {count} verschoben",
|
|
60944
|
+
"said.width": "{title}: {n} von {cols} Spalten",
|
|
60945
|
+
"said.hidden": "{title} ausgeblendet",
|
|
60946
|
+
"said.shown": "{title} eingeblendet",
|
|
60947
|
+
"said.removed": "{title} entfernt",
|
|
60948
|
+
"said.added": "{title} hinzugefügt",
|
|
60949
|
+
"said.saved": "Layout gespeichert",
|
|
60950
|
+
"said.unchanged": "Keine Änderungen zu speichern",
|
|
60951
|
+
"said.discarded": "Änderungen verworfen",
|
|
60952
|
+
"stale.label": "Neues Layout",
|
|
60953
|
+
"stale.text": "Dein Assistent hat ein neues Layout für dieses Dashboard geschrieben. Das Layout, das du in diesem Browser erstellt hast, bleibt, bis du wählst.",
|
|
60954
|
+
"stale.keep": "Meins behalten",
|
|
60955
|
+
"stale.new": "Das neue verwenden",
|
|
60956
|
+
"stale.used": "Das neue Layout ist aktiv",
|
|
60957
|
+
"stale.kept": "Dein Layout ist aktiv",
|
|
60958
|
+
"add.title": "Widget hinzufügen",
|
|
60959
|
+
"add.type": "Typ",
|
|
60960
|
+
"add.add": "Hinzufügen",
|
|
60961
|
+
"add.close": "Schließen",
|
|
60962
|
+
"add.hiddenGroup": "Auf diesem Board ausgeblendet",
|
|
60963
|
+
"add.none": "Alle Widgets sind auf diesem Board.",
|
|
60964
|
+
"add.search": "Widgets suchen",
|
|
60965
|
+
"add.noMatch": "Kein Widget passt.",
|
|
60966
|
+
"copy.title": "Layout kopieren",
|
|
60967
|
+
"copy.hint": "Dieses Layout ist nur in diesem Browser gespeichert. Um es in der Seite zu behalten, füge diesen Text bei deinem Assistenten ein und bitte ihn, das Dashboard-Layout zu speichern.",
|
|
60968
|
+
"copy.copy": "Kopieren",
|
|
60969
|
+
"copy.copied": "Kopiert",
|
|
60970
|
+
"board.empty": "Dieses Board hat keine Widgets.",
|
|
60971
|
+
"board.emptyEdit": "Dieses Board hat keine Widgets: Füge mit „Widget hinzufügen“ welche hinzu.",
|
|
60972
|
+
"widget.failed": "Dieses Widget ist fehlgeschlagen: {message}",
|
|
60973
|
+
"widget.unknown": "Unbekanntes Widget: {id}",
|
|
60974
|
+
"empty.window": "Keine Einheiten in den gewählten Wochen.",
|
|
60975
|
+
"w.coachNotes": "Coach-Notizen",
|
|
60976
|
+
"notes.none": "Keine Notizen.",
|
|
60977
|
+
"notes.disclaimer": "Trainingsvorschläge für die Daten bis {date}. Kein medizinischer Rat.",
|
|
60978
|
+
"w.kpis": "Überblick",
|
|
60979
|
+
"kpi.totals": "{distance} · {time} · {n} Einheit|{distance} · {time} · {n} Einheiten",
|
|
60980
|
+
"kpi.distance": "Distanz · letzte 7 Tage",
|
|
60981
|
+
"kpi.time": "Zeit · letzte 7 Tage",
|
|
60982
|
+
"kpi.vs4w": "ggü. 4-Wochen-Schnitt",
|
|
60983
|
+
"delta.good": "besser",
|
|
60984
|
+
"delta.bad": "schlechter",
|
|
60985
|
+
"kpi.ratio": "Belastungsverhältnis 7:28",
|
|
60986
|
+
"kpi.band": "Bereich 0,8-1,3",
|
|
60987
|
+
"kpi.ratioAll": "Nutzt alle Aktivitäten, auch mit einem Typfilter",
|
|
60988
|
+
"kpi.hrv": "HRV letzte Nacht",
|
|
60989
|
+
"kpi.vsBaseline": "ggü. 7-Tage-Schnitt {v} ms",
|
|
60990
|
+
"w.readiness": "Bereitschaft heute",
|
|
60991
|
+
"ready.easy": "Locker bleiben",
|
|
60992
|
+
"ready.push": "Bereit für mehr",
|
|
60993
|
+
"ready.normal": "Normal",
|
|
60994
|
+
"ready.hrv": "HRV-Status",
|
|
60995
|
+
"ready.sleep": "Schlafwert",
|
|
60996
|
+
"ready.rhr": "Ruhe-HF",
|
|
60997
|
+
"ready.rhrVs": "7 Tage davor: {avg} bpm",
|
|
60998
|
+
"ready.ratio": "Belastungsverhältnis 7:28",
|
|
60999
|
+
"ready.note": "Eine eigene Regel des Dashboards, nicht die Trainingsbereitschaft von Garmin. Sie zeigt heute: Das Wochenfenster ändert sie nicht.",
|
|
61000
|
+
"w.fitnessAge": "Fitnessalter",
|
|
61001
|
+
"fa.age": "Fitnessalter",
|
|
61002
|
+
"fa.actual": "Tatsächliches Alter",
|
|
61003
|
+
"fa.achievable": "Erreichbar",
|
|
61004
|
+
"fa.details": "Details",
|
|
61005
|
+
"fa.component": "Faktor",
|
|
61006
|
+
"fa.value": "Jetzt",
|
|
61007
|
+
"fa.target": "Garmin-Ziel",
|
|
61008
|
+
"fa.potential": "Mögliches Alter",
|
|
61009
|
+
"fa.bodyFat": "Körperfett %",
|
|
61010
|
+
"fa.bmi": "BMI",
|
|
61011
|
+
"fa.rhr": "Ruhe-HF",
|
|
61012
|
+
"fa.vigorousDaysAvg": "Intensive Tage / Woche",
|
|
61013
|
+
"fa.vigorousMinutesAvg": "Intensive Minuten / Woche",
|
|
61014
|
+
"today.note": "Zeigt heute: Das Wochenfenster ändert es nicht.",
|
|
61015
|
+
"w.periodComparison": "Zeitraumvergleich",
|
|
61016
|
+
"pc.vs": "{from} bis {to} gegenüber {pfrom} bis {pto}",
|
|
61017
|
+
"pc.partial": "unvollständig: diese Wochen beginnen vor den Daten",
|
|
61018
|
+
"pc.type": "Typ",
|
|
61019
|
+
"pc.sessions": "Einheiten",
|
|
61020
|
+
"pc.distance": "Distanz",
|
|
61021
|
+
"pc.time": "Zeit",
|
|
61022
|
+
"pc.load": "Belastung",
|
|
61023
|
+
"pc.total": "Gesamt",
|
|
61024
|
+
"w.records": "Rekorde",
|
|
61025
|
+
"rec.best": "Bestzeit {d}",
|
|
61026
|
+
"d.1k": "1 km",
|
|
61027
|
+
"d.mile": "Meile",
|
|
61028
|
+
"d.5k": "5 km",
|
|
61029
|
+
"d.10k": "10 km",
|
|
61030
|
+
"d.half": "Halbmarathon",
|
|
61031
|
+
"d.marathon": "Marathon",
|
|
61032
|
+
"rec.longestRun": "Längster Lauf",
|
|
61033
|
+
"rec.longestRide": "Längste Radfahrt",
|
|
61034
|
+
"rec.topSet": "Schwerster Satz",
|
|
61035
|
+
"rec.e1rm": "Bestes geschätztes 1RM · {name}",
|
|
61036
|
+
"rec.none": "Keine Rekorde in den gewählten Wochen.",
|
|
61037
|
+
"rec.estimate": "1RM-Werte sind Schätzungen (Epley, Sätze mit 1-12 Wiederholungen).",
|
|
61038
|
+
"w.weeklyVolume": "Wochenumfang",
|
|
61039
|
+
"m.time": "Zeit",
|
|
61040
|
+
"m.distance": "Distanz",
|
|
61041
|
+
"m.sessions": "Einheiten",
|
|
61042
|
+
"m.load": "Belastung",
|
|
61043
|
+
"vol.avg": "4-Wochen-Schnitt",
|
|
61044
|
+
"w.intensityMinutes": "Intensitätsminuten",
|
|
61045
|
+
"im.guide": "Richtwert 150 min",
|
|
61046
|
+
"im.note": "Moderate + 2 x intensive Minuten nur der Aktivitäten, nicht des ganzen Tages.",
|
|
61047
|
+
"w.loadSafety": "Belastungssicherheit",
|
|
61048
|
+
"ls.weekLoad": "Belastung · letzte 7 Tage",
|
|
61049
|
+
"ls.monotony": "Monotonie · 7 Tage",
|
|
61050
|
+
"ls.monotonyFlag": "Über 2,0",
|
|
61051
|
+
"ls.strain": "Strain · 7 Tage",
|
|
61052
|
+
"ls.flagged": "Wochen über +10%",
|
|
61053
|
+
"ls.load": "Wochenbelastung",
|
|
61054
|
+
"ls.ramp": "Änderung ggü. Vorwoche (%)",
|
|
61055
|
+
"ls.note": "Alle Aktivitäten. Rote Wochen sind um mehr als 10% gestiegen; eine laufende Woche wird nicht markiert. Monotonie und Strain (Foster) sind Schätzungen.",
|
|
61056
|
+
"ls.estimated": "Einige Belastungen sind Schätzungen: Die Aktivität hat keine Garmin-Belastung.",
|
|
61057
|
+
"w.loadVsRecovery": "Belastung und Erholung",
|
|
61058
|
+
"lvr.hrv": "HRV (ms)",
|
|
61059
|
+
"lvr.note": "Tägliche Belastung nach Typ (Balken) und nächtliche HRV (Linie).",
|
|
61060
|
+
"w.calendar": "Regelmäßigkeit",
|
|
61061
|
+
"cal.minutes": "{n} min",
|
|
61062
|
+
"cal.rest": "Ruhetag",
|
|
61063
|
+
"cal.label": "Trainingstage, nach Woche; die Pfeiltasten bewegen",
|
|
61064
|
+
"cal.less": "Weniger",
|
|
61065
|
+
"cal.more": "Mehr",
|
|
61066
|
+
"m.label": "Messwert",
|
|
61067
|
+
skip: "Zum Board springen",
|
|
61068
|
+
"w.hrv": "HRV",
|
|
61069
|
+
"hrv.low": "Untergrenze",
|
|
61070
|
+
"hrv.band": "Ausgeglichener Bereich",
|
|
61071
|
+
"hrv.nightly": "Nächtliche HRV",
|
|
61072
|
+
"hrv.avg": "7-Tage-Schnitt",
|
|
61073
|
+
rec14: "Letzte 14 Tage: Das Wochenfenster ändert sie nicht.",
|
|
61074
|
+
"rec.board": "Die Erholungs-Widgets zeigen die letzten 14 Tage: Das Wochenfenster ändert sie nicht.",
|
|
61075
|
+
"w.restingHr": "Ruhe-HF",
|
|
61076
|
+
"w.sleep": "Schlaf",
|
|
61077
|
+
"sl.deep": "Tief",
|
|
61078
|
+
"sl.light": "Leicht",
|
|
61079
|
+
"sl.rem": "REM",
|
|
61080
|
+
"sl.awake": "Wach",
|
|
61081
|
+
"sl.score": "Wert",
|
|
61082
|
+
"w.sleepNeed": "Schlafbedarf",
|
|
61083
|
+
"sn.gap": "Unter Bedarf · Schnitt",
|
|
61084
|
+
"sn.actual": "Geschlafen",
|
|
61085
|
+
"sn.need": "Bedarf",
|
|
61086
|
+
"w.bedtime": "Schlafenszeit",
|
|
61087
|
+
"bt.spread": "Streuung der Schlafenszeit",
|
|
61088
|
+
"w.bodyBattery": "Body Battery",
|
|
61089
|
+
"bb.high": "Hoch",
|
|
61090
|
+
"bb.low": "Tief",
|
|
61091
|
+
"w.activities": "Aktivitäten",
|
|
61092
|
+
"col.date": "Datum",
|
|
61093
|
+
"col.type": "Typ",
|
|
61094
|
+
"col.name": "Name",
|
|
61095
|
+
"col.distance": "Distanz",
|
|
61096
|
+
"col.time": "Zeit",
|
|
61097
|
+
"col.pace": "Pace / Tempo",
|
|
61098
|
+
"col.hr": "Ø HF",
|
|
61099
|
+
"col.ascent": "Anstieg",
|
|
61100
|
+
"col.race": "Wettkampf",
|
|
61101
|
+
"laps.show": "Runden",
|
|
61102
|
+
"lap.n": "Runde",
|
|
61103
|
+
"lap.intensity": "Intensität",
|
|
61104
|
+
"act.more": "Alle {n} zeigen",
|
|
61105
|
+
"act.less": "Weniger zeigen",
|
|
61106
|
+
"one.session": "Bisher eine Einheit: Trends beginnen ab 2 Einheiten.",
|
|
61107
|
+
"w.typeKpis": "Zusammenfassung",
|
|
61108
|
+
"tk.sessions": "Einheiten",
|
|
61109
|
+
"tk.time": "Zeit",
|
|
61110
|
+
"tk.avg": "{v} pro Einheit",
|
|
61111
|
+
"tk.distance": "Distanz",
|
|
61112
|
+
"tk.speed": "Ø Geschwindigkeit",
|
|
61113
|
+
"tk.pace": "Ø Pace",
|
|
61114
|
+
"tk.ascent": "Anstieg",
|
|
61115
|
+
"tk.hr": "Ø HF",
|
|
61116
|
+
"tk.peak": "Spitze {v} bpm",
|
|
61117
|
+
"tk.load": "Garmin-Trainingsbelastung",
|
|
61118
|
+
"tk.share": "{v}% der gesamten Belastung",
|
|
61119
|
+
"w.typeWeekly": "Jede Woche",
|
|
61120
|
+
"w.hrZones": "Zeit in HF-Zonen",
|
|
61121
|
+
"zones.approximate": "Näherungswert: Einige Aktivitäten haben keine Zonenzeit, daher zählen alle ihre Minuten in der Zone ihrer Ø HF.",
|
|
61122
|
+
"form.cadence": "Schrittfrequenz",
|
|
61123
|
+
"form.stride": "Schrittlänge",
|
|
61124
|
+
"form.gct": "Bodenkontaktzeit",
|
|
61125
|
+
"form.vertOsc": "Vertikale Bewegung",
|
|
61126
|
+
"form.vertRatio": "Vertikales Verhältnis",
|
|
61127
|
+
"form.power": "Leistung",
|
|
61128
|
+
"form.normPower": "Normalisierte Leistung",
|
|
61129
|
+
"w.bestEfforts": "Bestleistungen",
|
|
61130
|
+
"be.distance": "Distanz",
|
|
61131
|
+
"be.activity": "Aktivität",
|
|
61132
|
+
"w.races": "Wettkämpfe",
|
|
61133
|
+
"races.none": "Keine Wettkämpfe in den gewählten Wochen.",
|
|
61134
|
+
"te.aerobic": "Aerober TE",
|
|
61135
|
+
"te.anaerobic": "Anaerober TE",
|
|
61136
|
+
"w.aerobicFitness": "Aerobe Fitness",
|
|
61137
|
+
"af.runs": "Läufe (ältere heller)",
|
|
61138
|
+
"af.pace": "Pace (min{unit})",
|
|
61139
|
+
"af.weekPace": "Wochen-Pace",
|
|
61140
|
+
"af.efficiency": "Effizienz",
|
|
61141
|
+
"af.effUnit": "m pro Herzschlag",
|
|
61142
|
+
"w.intensityShare": "Lockeres und hartes Laufen",
|
|
61143
|
+
"is.easy": "Z1-2 (locker)",
|
|
61144
|
+
"is.hard": "Z4-5 (hart)",
|
|
61145
|
+
"w.runningForm": "Laufeffizienz",
|
|
61146
|
+
"w.runWalk": "Laufen und Gehen",
|
|
61147
|
+
"rw.none": "Keine Lauf-Geh-Einheiten in den gewählten Wochen.",
|
|
61148
|
+
"rw.runPace": "Lauf-Pace",
|
|
61149
|
+
"rw.pace": "Gesamt-Pace",
|
|
61150
|
+
"rw.walk": "Gehzeit",
|
|
61151
|
+
"w.trainingEffect": "Trainingseffekt",
|
|
61152
|
+
"w.racePredictor": "Wettkampfprognose",
|
|
61153
|
+
"rp.none": "Keine Bestleistung über 5 km oder mehr in den gewählten Wochen.",
|
|
61154
|
+
"rp.note": "Schätzung (Riegel) aus {d} in {time} am {date}.",
|
|
61155
|
+
"w.longRuns": "Lange Läufe und Pace-Aufteilung",
|
|
61156
|
+
"lr.share": "Anteil des längsten Laufs an der Wochendistanz",
|
|
61157
|
+
"lr.ascent": "Wochenanstieg",
|
|
61158
|
+
"lr.note": "Rote Wochen: Der längste Lauf ist mehr als 35% der Wochendistanz. Die Tabelle zeigt Wettkämpfe und Läufe über 15 km mit Runden.",
|
|
61159
|
+
"lr.first": "Erste Hälfte",
|
|
61160
|
+
"lr.second": "Zweite Hälfte",
|
|
61161
|
+
"lr.split": "Split",
|
|
61162
|
+
"lr.gap": "Steigungsbereinigte Pace",
|
|
61163
|
+
"lr.positive": "positiv",
|
|
61164
|
+
"lr.negative": "negativ",
|
|
61165
|
+
"lr.even": "gleichmäßig",
|
|
61166
|
+
"w.setsPerCategory": "Sätze pro Bewegung",
|
|
61167
|
+
"ex.sets": "Absolvierte Sätze",
|
|
61168
|
+
"w.muscleBalance": "Muskelgruppen-Balance",
|
|
61169
|
+
"mb.pushPull": "Druck- : Zugsätze",
|
|
61170
|
+
"mb.avg": "Ø Sätze pro Woche",
|
|
61171
|
+
"mb.perWeek": "Sätze pro Woche",
|
|
61172
|
+
"mb.note": "Ein Satz zählt 1 für jede primäre Muskelgruppe und 0,5 für jede sekundäre. Üblicher Bereich: 10-20 Sätze pro Woche (gelb darunter, rot darüber).",
|
|
61173
|
+
"w.exercises": "Übungen",
|
|
61174
|
+
"ex.groups": "Muskelgruppen",
|
|
61175
|
+
"ex.search": "Übungen suchen",
|
|
61176
|
+
"ex.name": "Übung",
|
|
61177
|
+
"ex.sessions": "Einheiten",
|
|
61178
|
+
"ex.last": "Zuletzt",
|
|
61179
|
+
"ex.top": "Letzter Top-Satz",
|
|
61180
|
+
"ex.best": "Bestes geschätztes 1RM",
|
|
61181
|
+
"ex.change": "Änderung",
|
|
61182
|
+
"ex.trend": "Trend",
|
|
61183
|
+
"ex.groupRow": "{n} Übung · {sets} absolvierte Sätze|{n} Übungen · {sets} absolvierte Sätze",
|
|
61184
|
+
"ex.reps": "{n} Wiederholung|{n} Wiederholungen",
|
|
61185
|
+
"ex.rest": "Median-Pause {v}",
|
|
61186
|
+
"ex.weight": "Top-Satz ({unit})",
|
|
61187
|
+
"ex.e1rm": "Geschätztes 1RM ({unit})",
|
|
61188
|
+
"ex.volume": "Volumen ({unit})",
|
|
61189
|
+
"ex.maxReps": "Max. Wiederholungen",
|
|
61190
|
+
"ex.totalReps": "Wiederholungen gesamt",
|
|
61191
|
+
"ex.longest": "Längster Satz (s)",
|
|
61192
|
+
"ex.restAfter": "Pause {v}",
|
|
61193
|
+
"ex.notDone": "nicht absolviert",
|
|
61194
|
+
"w.strengthSessions": "Einheiten",
|
|
61195
|
+
"ss.count": "{n} Übung|{n} Übungen",
|
|
61196
|
+
"mg.chest": "Brust",
|
|
61197
|
+
"mg.back": "Rücken",
|
|
61198
|
+
"mg.shoulders": "Schultern",
|
|
61199
|
+
"mg.biceps": "Bizeps",
|
|
61200
|
+
"mg.triceps": "Trizeps",
|
|
61201
|
+
"mg.forearms": "Unterarme",
|
|
61202
|
+
"mg.quads": "Quadrizeps",
|
|
61203
|
+
"mg.hamstrings": "Beinbeuger",
|
|
61204
|
+
"mg.glutes": "Gesäß",
|
|
61205
|
+
"mg.calves": "Waden",
|
|
61206
|
+
"mg.abs": "Bauch",
|
|
61207
|
+
"mg.hips": "Hüfte",
|
|
61208
|
+
"mg.other": "Sonstiges",
|
|
61209
|
+
"w.walkDetails": "Tempo und Spaziergänge",
|
|
61210
|
+
"wd.steps": "Schritte",
|
|
61211
|
+
"wd.battery": "Body-Battery-Änderung",
|
|
61212
|
+
"wd.goal": "Schrittziel",
|
|
61213
|
+
"wd.floors": "Etagen",
|
|
61214
|
+
"w.steps": "Tägliche Schritte",
|
|
61215
|
+
"w.hikeVertical": "Vertikalgeschwindigkeit",
|
|
61216
|
+
"hv.vam": "Höhe ({unit}) pro Stunde",
|
|
61217
|
+
"w.bikeDetails": "Radfahrten",
|
|
61218
|
+
"bd.outdoor": "Draußen",
|
|
61219
|
+
"bd.indoor": "Drinnen",
|
|
61220
|
+
"bd.rides": "{n} Fahrt|{n} Fahrten",
|
|
61221
|
+
"bd.where": "Wo",
|
|
61222
|
+
"w.swimDetails": "Schwimmeinheiten",
|
|
61223
|
+
"sw.pace": "Pace",
|
|
61224
|
+
"sw.lengths": "Bahnen",
|
|
61225
|
+
"sw.strokes": "Züge",
|
|
61226
|
+
"sw.pool": "Becken",
|
|
61227
|
+
"w.racketDetails": "Einheiten und Intensität",
|
|
61228
|
+
"pd.peak": "Spitzen-HF",
|
|
61229
|
+
"pd.z3": "Zeit in Z3+",
|
|
61230
|
+
"pd.z45": "Anteil Z4-5",
|
|
61231
|
+
"pd.kcal": "kcal pro Stunde",
|
|
61232
|
+
"pd.load": "Belastung / Lauf gleicher Dauer",
|
|
61233
|
+
"pd.note": "Die zweite Belastung ist eine Schätzung: die durchschnittliche Garmin-Belastung pro Minute deiner Läufe, mal die Dauer der Einheit.",
|
|
61234
|
+
"col.sport": "Sportart",
|
|
61235
|
+
"sport.tennis": "Tennis",
|
|
61236
|
+
"sport.padel": "Padel",
|
|
61237
|
+
"sport.pickleball": "Pickleball",
|
|
61238
|
+
"sport.racquetball": "Racquetball",
|
|
61239
|
+
"sport.squash": "Squash",
|
|
61240
|
+
"sport.badminton": "Badminton",
|
|
61241
|
+
"sport.tableTennis": "Tischtennis",
|
|
61242
|
+
"sport.platformTennis": "Platform-Tennis"
|
|
61243
|
+
},
|
|
61244
|
+
fr: {
|
|
61245
|
+
title: "Entraînement + Récupération",
|
|
61246
|
+
headline: "Charge d'entraînement, récupération et progrès depuis Garmin Connect",
|
|
61247
|
+
na: "n.d.",
|
|
61248
|
+
noData: "Cette page n'a pas encore de données. Demande à ton assistant d'écrire le tableau de bord.",
|
|
61249
|
+
period: "du {from} au {to}",
|
|
61250
|
+
inProgress: "* semaine en cours",
|
|
61251
|
+
"menu.settings": "Réglages",
|
|
61252
|
+
"menu.theme": "Thème",
|
|
61253
|
+
"footer.updated": "Données de Garmin Connect, mises à jour le {date}",
|
|
61254
|
+
"footer.failed": "Jours non lus depuis Garmin : {n} (leurs valeurs affichent n.d.)",
|
|
61255
|
+
"footer.license": "Licence MIT",
|
|
61256
|
+
"menu.language": "Langue",
|
|
61257
|
+
"lang.auto": "Automatique",
|
|
61258
|
+
"lang.missing": "{language} n'est pas encore dans cette page. Demande à ton assistant d'ajouter la langue.",
|
|
61259
|
+
"themeGroup.auto": "Automatique",
|
|
61260
|
+
"themeGroup.dark": "Sombres",
|
|
61261
|
+
"themeGroup.light": "Clairs",
|
|
61262
|
+
"themeGroup.accessible": "Accessibles",
|
|
61263
|
+
"theme.system": "Système",
|
|
61264
|
+
"theme.midnight": "Minuit",
|
|
61265
|
+
"theme.graphite": "Graphite",
|
|
61266
|
+
"theme.ocean": "Océan",
|
|
61267
|
+
"theme.forest": "Forêt",
|
|
61268
|
+
"theme.plum": "Prune",
|
|
61269
|
+
"theme.light": "Clair",
|
|
61270
|
+
"theme.paper": "Papier",
|
|
61271
|
+
"theme.mist": "Brume",
|
|
61272
|
+
"theme.hcDark": "Contraste élevé sombre",
|
|
61273
|
+
"theme.hcLight": "Contraste élevé clair",
|
|
61274
|
+
"window.label": "Semaines",
|
|
61275
|
+
"window.last": "{n} dernières",
|
|
61276
|
+
"window.all": "Toutes",
|
|
61277
|
+
"window.from": "De la semaine",
|
|
61278
|
+
"window.to": "À la semaine",
|
|
61279
|
+
"names.label": "Noms des activités",
|
|
61280
|
+
"names.all": "Tous",
|
|
61281
|
+
"names.count": "{n} choisi|{n} choisis",
|
|
61282
|
+
"names.search": "Rechercher un nom",
|
|
61283
|
+
"names.noMatch": "Aucun nom ne correspond.",
|
|
61284
|
+
"names.remove": "Retirer {name}",
|
|
61285
|
+
"names.clear": "Tout afficher",
|
|
61286
|
+
"type.all": "Toutes",
|
|
61287
|
+
"type.recovery": "Récupération",
|
|
61288
|
+
"type.running": "Course",
|
|
61289
|
+
"type.strengthTraining": "Musculation",
|
|
61290
|
+
"type.walking": "Marche",
|
|
61291
|
+
"type.hiking": "Randonnée",
|
|
61292
|
+
"type.cycling": "Vélo",
|
|
61293
|
+
"type.swimming": "Natation",
|
|
61294
|
+
"type.racket": "Sports de raquette",
|
|
61295
|
+
"type.generic": "Autre",
|
|
61296
|
+
"type.transition": "Transition",
|
|
61297
|
+
"type.fitnessEquipment": "Appareils de fitness",
|
|
61298
|
+
"type.basketball": "Basket-ball",
|
|
61299
|
+
"type.soccer": "Football",
|
|
61300
|
+
"type.tennis": "Tennis",
|
|
61301
|
+
"type.americanFootball": "Football américain",
|
|
61302
|
+
"type.training": "Entraînement",
|
|
61303
|
+
"type.crossCountrySkiing": "Ski de fond",
|
|
61304
|
+
"type.alpineSkiing": "Ski alpin",
|
|
61305
|
+
"type.snowboarding": "Snowboard",
|
|
61306
|
+
"type.rowing": "Aviron",
|
|
61307
|
+
"type.mountaineering": "Alpinisme",
|
|
61308
|
+
"type.multisport": "Multisport",
|
|
61309
|
+
"type.paddling": "Pagaie",
|
|
61310
|
+
"type.flying": "Vol",
|
|
61311
|
+
"type.eBiking": "Vélo électrique",
|
|
61312
|
+
"type.motorcycling": "Moto",
|
|
61313
|
+
"type.boating": "Navigation",
|
|
61314
|
+
"type.driving": "Conduite",
|
|
61315
|
+
"type.golf": "Golf",
|
|
61316
|
+
"type.hangGliding": "Deltaplane",
|
|
61317
|
+
"type.horsebackRiding": "Équitation",
|
|
61318
|
+
"type.hunting": "Chasse",
|
|
61319
|
+
"type.fishing": "Pêche",
|
|
61320
|
+
"type.inlineSkating": "Roller",
|
|
61321
|
+
"type.rockClimbing": "Escalade",
|
|
61322
|
+
"type.sailing": "Voile",
|
|
61323
|
+
"type.iceSkating": "Patinage sur glace",
|
|
61324
|
+
"type.skyDiving": "Parachutisme",
|
|
61325
|
+
"type.snowshoeing": "Raquettes à neige",
|
|
61326
|
+
"type.snowmobiling": "Motoneige",
|
|
61327
|
+
"type.standUpPaddleboarding": "Stand up paddle",
|
|
61328
|
+
"type.surfing": "Surf",
|
|
61329
|
+
"type.wakeboarding": "Wakeboard",
|
|
61330
|
+
"type.waterSkiing": "Ski nautique",
|
|
61331
|
+
"type.kayaking": "Kayak",
|
|
61332
|
+
"type.rafting": "Rafting",
|
|
61333
|
+
"type.windsurfing": "Planche à voile",
|
|
61334
|
+
"type.kitesurfing": "Kitesurf",
|
|
61335
|
+
"type.tactical": "Tactique",
|
|
61336
|
+
"type.jumpmaster": "Jumpmaster",
|
|
61337
|
+
"type.boxing": "Boxe",
|
|
61338
|
+
"type.floorClimbing": "Montée d'étages",
|
|
61339
|
+
"type.baseball": "Baseball",
|
|
61340
|
+
"type.diving": "Plongée",
|
|
61341
|
+
"type.shooting": "Tir",
|
|
61342
|
+
"type.winterSport": "Sports d'hiver",
|
|
61343
|
+
"type.grinding": "Grinding",
|
|
61344
|
+
"type.hiit": "HIIT",
|
|
61345
|
+
"type.videoGaming": "Jeux vidéo",
|
|
61346
|
+
"type.wheelchairPushWalk": "Marche en fauteuil roulant",
|
|
61347
|
+
"type.wheelchairPushRun": "Course en fauteuil roulant",
|
|
61348
|
+
"type.meditation": "Méditation",
|
|
61349
|
+
"type.paraSport": "Parasport",
|
|
61350
|
+
"type.discGolf": "Disc golf",
|
|
61351
|
+
"type.teamSport": "Sports d'équipe",
|
|
61352
|
+
"type.cricket": "Cricket",
|
|
61353
|
+
"type.rugby": "Rugby",
|
|
61354
|
+
"type.hockey": "Hockey",
|
|
61355
|
+
"type.lacrosse": "Lacrosse",
|
|
61356
|
+
"type.volleyball": "Volley-ball",
|
|
61357
|
+
"type.waterTubing": "Bouée tractée",
|
|
61358
|
+
"type.wakesurfing": "Wakesurf",
|
|
61359
|
+
"type.waterSport": "Sports nautiques",
|
|
61360
|
+
"type.archery": "Tir à l'arc",
|
|
61361
|
+
"type.mixedMartialArts": "Arts martiaux mixtes",
|
|
61362
|
+
"type.motorSports": "Sports mécaniques",
|
|
61363
|
+
"type.snorkeling": "Snorkeling",
|
|
61364
|
+
"type.dance": "Danse",
|
|
61365
|
+
"type.jumpRope": "Corde à sauter",
|
|
61366
|
+
"type.poolApnea": "Apnée en piscine",
|
|
61367
|
+
"type.mobility": "Mobilité",
|
|
61368
|
+
"type.geocaching": "Géocaching",
|
|
61369
|
+
"type.canoeing": "Canoë",
|
|
61370
|
+
"tabs.label": "Types d'activité",
|
|
61371
|
+
"edit.start": "Modifier la mise en page",
|
|
61372
|
+
"edit.done": "Terminé",
|
|
61373
|
+
"edit.add": "Ajouter un widget",
|
|
61374
|
+
"edit.reset": "Réinitialiser le tableau",
|
|
61375
|
+
"edit.discard": "Annuler",
|
|
61376
|
+
"edit.bar": "Modifier la mise en page",
|
|
61377
|
+
"edit.editing": "Modification · {board}",
|
|
61378
|
+
"edit.unsaved": "Modifications non enregistrées",
|
|
61379
|
+
"discard.title": "Annuler tes modifications ?",
|
|
61380
|
+
"discard.text": "La mise en page revient à celle d'avant ton choix de Modifier la mise en page, sur chaque tableau.",
|
|
61381
|
+
"discard.keep": "Continuer à modifier",
|
|
61382
|
+
"discard.confirm": "Annuler les modifications",
|
|
61383
|
+
"edit.copy": "Copier la mise en page",
|
|
61384
|
+
"edit.hide": "Masquer {title}",
|
|
61385
|
+
"edit.show": "Afficher {title}",
|
|
61386
|
+
"edit.remove": "Retirer {title}",
|
|
61387
|
+
"edit.move": "Déplacer {title}",
|
|
61388
|
+
"edit.moveHint": "Fais glisser pour déplacer, ou appuie sur Espace puis sur les flèches",
|
|
61389
|
+
"edit.width": "Largeur de {title}",
|
|
61390
|
+
"edit.widthValue": "{n} colonnes sur {cols}",
|
|
61391
|
+
"edit.addHidden": "Ajouter un widget · {n} masqués",
|
|
61392
|
+
"add.addOne": "Ajouter {title}",
|
|
61393
|
+
"said.picked": "{title} saisi, position {n} sur {count}. Les flèches le déplacent, Espace le dépose, Échap le remet à sa place.",
|
|
61394
|
+
"said.position": "Position {n} sur {count}",
|
|
61395
|
+
"said.cancelled": "{title} est revenu à sa place",
|
|
61396
|
+
"said.moved": "{title} déplacé en position {n} sur {count}",
|
|
61397
|
+
"said.width": "{title} : {n} colonnes sur {cols}",
|
|
61398
|
+
"said.hidden": "{title} masqué",
|
|
61399
|
+
"said.shown": "{title} affiché",
|
|
61400
|
+
"said.removed": "{title} retiré",
|
|
61401
|
+
"said.added": "{title} ajouté",
|
|
61402
|
+
"said.saved": "Mise en page enregistrée",
|
|
61403
|
+
"said.unchanged": "Aucune modification à enregistrer",
|
|
61404
|
+
"said.discarded": "Modifications annulées",
|
|
61405
|
+
"stale.label": "Nouvelle mise en page",
|
|
61406
|
+
"stale.text": "Ton assistant a écrit une nouvelle mise en page pour ce tableau de bord. Celle que tu as faite dans ce navigateur reste jusqu'à ce que tu choisisses.",
|
|
61407
|
+
"stale.keep": "Garder la mienne",
|
|
61408
|
+
"stale.new": "Utiliser la nouvelle",
|
|
61409
|
+
"stale.used": "La nouvelle mise en page est utilisée",
|
|
61410
|
+
"stale.kept": "Ta mise en page est utilisée",
|
|
61411
|
+
"add.title": "Ajouter un widget",
|
|
61412
|
+
"add.type": "Type",
|
|
61413
|
+
"add.add": "Ajouter",
|
|
61414
|
+
"add.close": "Fermer",
|
|
61415
|
+
"add.hiddenGroup": "Masqués sur ce tableau",
|
|
61416
|
+
"add.none": "Tous les widgets sont sur ce tableau.",
|
|
61417
|
+
"add.search": "Rechercher un widget",
|
|
61418
|
+
"add.noMatch": "Aucun widget ne correspond.",
|
|
61419
|
+
"copy.title": "Copier la mise en page",
|
|
61420
|
+
"copy.hint": "Cette mise en page est enregistrée uniquement dans ce navigateur. Pour la garder dans la page, colle ce texte à ton assistant et demande-lui d'enregistrer la mise en page du tableau de bord.",
|
|
61421
|
+
"copy.copy": "Copier",
|
|
61422
|
+
"copy.copied": "Copié",
|
|
61423
|
+
"board.empty": "Ce tableau n'a aucun widget.",
|
|
61424
|
+
"board.emptyEdit": "Ce tableau n'a aucun widget : utilise « Ajouter un widget » pour en mettre.",
|
|
61425
|
+
"widget.failed": "Ce widget a échoué : {message}",
|
|
61426
|
+
"widget.unknown": "Widget inconnu : {id}",
|
|
61427
|
+
"empty.window": "Aucune séance dans les semaines choisies.",
|
|
61428
|
+
"w.coachNotes": "Notes du coach",
|
|
61429
|
+
"notes.none": "Aucune note.",
|
|
61430
|
+
"notes.disclaimer": "Conseils d'entraînement sur les données jusqu'au {date}. Ce ne sont pas des conseils médicaux.",
|
|
61431
|
+
"w.kpis": "Vue d'ensemble",
|
|
61432
|
+
"kpi.totals": "{distance} · {time} · {n} séance|{distance} · {time} · {n} séances",
|
|
61433
|
+
"kpi.distance": "Distance · 7 derniers jours",
|
|
61434
|
+
"kpi.time": "Temps · 7 derniers jours",
|
|
61435
|
+
"kpi.vs4w": "par rapport à la moyenne sur 4 semaines",
|
|
61436
|
+
"delta.good": "mieux",
|
|
61437
|
+
"delta.bad": "moins bien",
|
|
61438
|
+
"kpi.ratio": "Ratio de charge 7:28",
|
|
61439
|
+
"kpi.band": "Plage 0,8-1,3",
|
|
61440
|
+
"kpi.ratioAll": "Utilise toutes les activités, même avec un filtre par type",
|
|
61441
|
+
"kpi.hrv": "HRV de la nuit",
|
|
61442
|
+
"kpi.vsBaseline": "par rapport à la moyenne sur 7 jours {v} ms",
|
|
61443
|
+
"w.readiness": "Forme du jour",
|
|
61444
|
+
"ready.easy": "Vas-y doucement",
|
|
61445
|
+
"ready.push": "Prêt pour l'intensité",
|
|
61446
|
+
"ready.normal": "Normal",
|
|
61447
|
+
"ready.hrv": "Statut HRV",
|
|
61448
|
+
"ready.sleep": "Score de sommeil",
|
|
61449
|
+
"ready.rhr": "FC au repos",
|
|
61450
|
+
"ready.rhrVs": "7 jours avant : {avg} bpm",
|
|
61451
|
+
"ready.ratio": "Ratio de charge 7:28",
|
|
61452
|
+
"ready.note": "Une règle propre au tableau de bord, pas la disposition à l'entraînement de Garmin. Affiche aujourd'hui : la fenêtre de semaines ne la change pas.",
|
|
61453
|
+
"w.fitnessAge": "Âge physique",
|
|
61454
|
+
"fa.age": "Âge physique",
|
|
61455
|
+
"fa.actual": "Âge réel",
|
|
61456
|
+
"fa.achievable": "Atteignable",
|
|
61457
|
+
"fa.details": "Détails",
|
|
61458
|
+
"fa.component": "Composante",
|
|
61459
|
+
"fa.value": "Actuel",
|
|
61460
|
+
"fa.target": "Objectif Garmin",
|
|
61461
|
+
"fa.potential": "Âge potentiel",
|
|
61462
|
+
"fa.bodyFat": "Masse grasse %",
|
|
61463
|
+
"fa.bmi": "IMC",
|
|
61464
|
+
"fa.rhr": "FC au repos",
|
|
61465
|
+
"fa.vigorousDaysAvg": "Jours intenses / semaine",
|
|
61466
|
+
"fa.vigorousMinutesAvg": "Minutes intenses / semaine",
|
|
61467
|
+
"today.note": "Affiche aujourd'hui : la fenêtre de semaines ne le change pas.",
|
|
61468
|
+
"w.periodComparison": "Comparaison de périodes",
|
|
61469
|
+
"pc.vs": "du {from} au {to} par rapport à la période du {pfrom} au {pto}",
|
|
61470
|
+
"pc.partial": "partiel : ces semaines commencent avant les données",
|
|
61471
|
+
"pc.type": "Type",
|
|
61472
|
+
"pc.sessions": "Séances",
|
|
61473
|
+
"pc.distance": "Distance",
|
|
61474
|
+
"pc.time": "Temps",
|
|
61475
|
+
"pc.load": "Charge",
|
|
61476
|
+
"pc.total": "Total",
|
|
61477
|
+
"w.records": "Records",
|
|
61478
|
+
"rec.best": "Meilleur {d}",
|
|
61479
|
+
"d.1k": "1 km",
|
|
61480
|
+
"d.mile": "mile",
|
|
61481
|
+
"d.5k": "5 km",
|
|
61482
|
+
"d.10k": "10 km",
|
|
61483
|
+
"d.half": "semi-marathon",
|
|
61484
|
+
"d.marathon": "marathon",
|
|
61485
|
+
"rec.longestRun": "Plus longue course",
|
|
61486
|
+
"rec.longestRide": "Plus longue sortie à vélo",
|
|
61487
|
+
"rec.topSet": "Série la plus lourde",
|
|
61488
|
+
"rec.e1rm": "Meilleur 1RM estimé · {name}",
|
|
61489
|
+
"rec.none": "Aucun record dans les semaines choisies.",
|
|
61490
|
+
"rec.estimate": "Les valeurs 1RM sont des estimations (Epley, séries de 1-12 répétitions).",
|
|
61491
|
+
"w.weeklyVolume": "Volume hebdomadaire",
|
|
61492
|
+
"m.time": "Temps",
|
|
61493
|
+
"m.distance": "Distance",
|
|
61494
|
+
"m.sessions": "Séances",
|
|
61495
|
+
"m.load": "Charge",
|
|
61496
|
+
"vol.avg": "Moyenne sur 4 semaines",
|
|
61497
|
+
"w.intensityMinutes": "Minutes d'intensité",
|
|
61498
|
+
"im.guide": "Recommandation 150 min",
|
|
61499
|
+
"im.note": "Minutes modérées + 2 x intenses des activités seulement, pas de toute la journée.",
|
|
61500
|
+
"w.loadSafety": "Sécurité de la charge",
|
|
61501
|
+
"ls.weekLoad": "Charge · 7 derniers jours",
|
|
61502
|
+
"ls.monotony": "Monotonie · 7 jours",
|
|
61503
|
+
"ls.monotonyFlag": "Au-dessus de 2,0",
|
|
61504
|
+
"ls.strain": "Contrainte · 7 jours",
|
|
61505
|
+
"ls.flagged": "Semaines au-dessus de +10 %",
|
|
61506
|
+
"ls.load": "Charge hebdomadaire",
|
|
61507
|
+
"ls.ramp": "Variation par rapport à la semaine précédente (%)",
|
|
61508
|
+
"ls.note": "Toutes les activités. Les semaines en rouge ont augmenté de plus de 10 % ; une semaine en cours n'est pas signalée. La monotonie et la contrainte (Foster) sont des estimations.",
|
|
61509
|
+
"ls.estimated": "Certaines charges sont des estimations : l'activité n'a pas de charge Garmin.",
|
|
61510
|
+
"w.loadVsRecovery": "Charge et récupération",
|
|
61511
|
+
"lvr.hrv": "HRV (ms)",
|
|
61512
|
+
"lvr.note": "Charge quotidienne par type (barres) et HRV nocturne (ligne).",
|
|
61513
|
+
"w.calendar": "Régularité",
|
|
61514
|
+
"cal.minutes": "{n} min",
|
|
61515
|
+
"cal.rest": "Jour de repos",
|
|
61516
|
+
"cal.label": "Jours d'entraînement, par semaine ; les flèches permettent de se déplacer",
|
|
61517
|
+
"cal.less": "Moins",
|
|
61518
|
+
"cal.more": "Plus",
|
|
61519
|
+
"m.label": "Mesure",
|
|
61520
|
+
skip: "Aller au tableau",
|
|
61521
|
+
"w.hrv": "HRV",
|
|
61522
|
+
"hrv.low": "Bas de la plage",
|
|
61523
|
+
"hrv.band": "Plage équilibrée",
|
|
61524
|
+
"hrv.nightly": "HRV nocturne",
|
|
61525
|
+
"hrv.avg": "Moyenne sur 7 jours",
|
|
61526
|
+
rec14: "14 derniers jours : la fenêtre de semaines ne les change pas.",
|
|
61527
|
+
"rec.board": "Les widgets de récupération affichent les 14 derniers jours : la fenêtre de semaines ne les change pas.",
|
|
61528
|
+
"w.restingHr": "FC au repos",
|
|
61529
|
+
"w.sleep": "Sommeil",
|
|
61530
|
+
"sl.deep": "Profond",
|
|
61531
|
+
"sl.light": "Léger",
|
|
61532
|
+
"sl.rem": "Paradoxal",
|
|
61533
|
+
"sl.awake": "Éveillé",
|
|
61534
|
+
"sl.score": "Score",
|
|
61535
|
+
"w.sleepNeed": "Besoin de sommeil",
|
|
61536
|
+
"sn.gap": "En dessous du besoin · moyenne",
|
|
61537
|
+
"sn.actual": "Dormi",
|
|
61538
|
+
"sn.need": "Besoin",
|
|
61539
|
+
"w.bedtime": "Heure du coucher",
|
|
61540
|
+
"bt.spread": "Écart des heures de coucher",
|
|
61541
|
+
"w.bodyBattery": "Body Battery",
|
|
61542
|
+
"bb.high": "Maximum",
|
|
61543
|
+
"bb.low": "Minimum",
|
|
61544
|
+
"w.activities": "Activités",
|
|
61545
|
+
"col.date": "Date",
|
|
61546
|
+
"col.type": "Type",
|
|
61547
|
+
"col.name": "Nom",
|
|
61548
|
+
"col.distance": "Distance",
|
|
61549
|
+
"col.time": "Temps",
|
|
61550
|
+
"col.pace": "Allure / vitesse",
|
|
61551
|
+
"col.hr": "FC moy.",
|
|
61552
|
+
"col.ascent": "Dénivelé",
|
|
61553
|
+
"col.race": "Compétition",
|
|
61554
|
+
"laps.show": "tours",
|
|
61555
|
+
"lap.n": "Tour",
|
|
61556
|
+
"lap.intensity": "Intensité",
|
|
61557
|
+
"act.more": "Tout afficher ({n})",
|
|
61558
|
+
"act.less": "Afficher moins",
|
|
61559
|
+
"one.session": "Une seule séance pour l'instant : les tendances commencent à 2 séances.",
|
|
61560
|
+
"w.typeKpis": "Résumé",
|
|
61561
|
+
"tk.sessions": "Séances",
|
|
61562
|
+
"tk.time": "Temps",
|
|
61563
|
+
"tk.avg": "{v} par séance",
|
|
61564
|
+
"tk.distance": "Distance",
|
|
61565
|
+
"tk.speed": "Vitesse moyenne",
|
|
61566
|
+
"tk.pace": "Allure moyenne",
|
|
61567
|
+
"tk.ascent": "Dénivelé",
|
|
61568
|
+
"tk.hr": "FC moyenne",
|
|
61569
|
+
"tk.peak": "Pic {v} bpm",
|
|
61570
|
+
"tk.load": "Charge d'entraînement Garmin",
|
|
61571
|
+
"tk.share": "{v} % de toute la charge",
|
|
61572
|
+
"w.typeWeekly": "Chaque semaine",
|
|
61573
|
+
"w.hrZones": "Temps dans les zones FC",
|
|
61574
|
+
"zones.approximate": "Approximatif : certaines activités n'ont pas de temps par zone, donc toutes leurs minutes comptent dans la zone de leur FC moyenne.",
|
|
61575
|
+
"form.cadence": "Cadence",
|
|
61576
|
+
"form.stride": "Longueur de foulée",
|
|
61577
|
+
"form.gct": "Temps de contact au sol",
|
|
61578
|
+
"form.vertOsc": "Oscillation verticale",
|
|
61579
|
+
"form.vertRatio": "Rapport vertical",
|
|
61580
|
+
"form.power": "Puissance",
|
|
61581
|
+
"form.normPower": "Puissance normalisée",
|
|
61582
|
+
"w.bestEfforts": "Meilleurs efforts",
|
|
61583
|
+
"be.distance": "Distance",
|
|
61584
|
+
"be.activity": "Activité",
|
|
61585
|
+
"w.races": "Compétitions",
|
|
61586
|
+
"races.none": "Aucune compétition dans les semaines choisies.",
|
|
61587
|
+
"te.aerobic": "TE aérobie",
|
|
61588
|
+
"te.anaerobic": "TE anaérobie",
|
|
61589
|
+
"w.aerobicFitness": "Forme aérobie",
|
|
61590
|
+
"af.runs": "Courses (les plus anciennes plus pâles)",
|
|
61591
|
+
"af.pace": "Allure (min{unit})",
|
|
61592
|
+
"af.weekPace": "Allure hebdomadaire",
|
|
61593
|
+
"af.efficiency": "Efficacité",
|
|
61594
|
+
"af.effUnit": "m par battement",
|
|
61595
|
+
"w.intensityShare": "Course facile et intense",
|
|
61596
|
+
"is.easy": "Z1-2 (facile)",
|
|
61597
|
+
"is.hard": "Z4-5 (intense)",
|
|
61598
|
+
"w.runningForm": "Technique de course",
|
|
61599
|
+
"w.runWalk": "Course-marche",
|
|
61600
|
+
"rw.none": "Aucune séance de course-marche dans les semaines choisies.",
|
|
61601
|
+
"rw.runPace": "Allure de course",
|
|
61602
|
+
"rw.pace": "Allure globale",
|
|
61603
|
+
"rw.walk": "Temps de marche",
|
|
61604
|
+
"w.trainingEffect": "Effet d'entraînement",
|
|
61605
|
+
"w.racePredictor": "Prédicteur de course",
|
|
61606
|
+
"rp.none": "Aucun meilleur effort de 5 km ou plus dans les semaines choisies.",
|
|
61607
|
+
"rp.note": "Estimation (Riegel) à partir du {d} en {time} le {date}.",
|
|
61608
|
+
"w.longRuns": "Sorties longues et répartition de l'allure",
|
|
61609
|
+
"lr.share": "Part de la sortie la plus longue dans la distance hebdomadaire",
|
|
61610
|
+
"lr.ascent": "Dénivelé hebdomadaire",
|
|
61611
|
+
"lr.note": "Semaines en rouge : la sortie la plus longue dépasse 35 % de la distance de la semaine. Le tableau affiche les compétitions et les courses de plus de 15 km avec leurs tours.",
|
|
61612
|
+
"lr.first": "Première moitié",
|
|
61613
|
+
"lr.second": "Seconde moitié",
|
|
61614
|
+
"lr.split": "Répartition",
|
|
61615
|
+
"lr.gap": "Allure ajustée à la pente",
|
|
61616
|
+
"lr.positive": "positive",
|
|
61617
|
+
"lr.negative": "négative",
|
|
61618
|
+
"lr.even": "régulière",
|
|
61619
|
+
"w.setsPerCategory": "Séries par mouvement",
|
|
61620
|
+
"ex.sets": "Séries terminées",
|
|
61621
|
+
"w.muscleBalance": "Équilibre des groupes musculaires",
|
|
61622
|
+
"mb.pushPull": "Séries poussée : tirage",
|
|
61623
|
+
"mb.avg": "Moyenne de séries par semaine",
|
|
61624
|
+
"mb.perWeek": "séries par semaine",
|
|
61625
|
+
"mb.note": "Une série compte 1 pour chaque groupe musculaire principal et 0,5 pour chaque groupe secondaire. Plage habituelle : 10-20 séries par semaine (jaune en dessous, rouge au-dessus).",
|
|
61626
|
+
"w.exercises": "Exercices",
|
|
61627
|
+
"ex.groups": "Groupes musculaires",
|
|
61628
|
+
"ex.search": "Rechercher des exercices",
|
|
61629
|
+
"ex.name": "Exercice",
|
|
61630
|
+
"ex.sessions": "Séances",
|
|
61631
|
+
"ex.last": "Dernière",
|
|
61632
|
+
"ex.top": "Dernière meilleure série",
|
|
61633
|
+
"ex.best": "Meilleur 1RM estimé",
|
|
61634
|
+
"ex.change": "Variation",
|
|
61635
|
+
"ex.trend": "Tendance",
|
|
61636
|
+
"ex.groupRow": "{n} exercice · {sets} séries terminées|{n} exercices · {sets} séries terminées",
|
|
61637
|
+
"ex.reps": "{n} répétition|{n} répétitions",
|
|
61638
|
+
"ex.rest": "repos médian {v}",
|
|
61639
|
+
"ex.weight": "Meilleure série ({unit})",
|
|
61640
|
+
"ex.e1rm": "1RM estimé ({unit})",
|
|
61641
|
+
"ex.volume": "Volume ({unit})",
|
|
61642
|
+
"ex.maxReps": "Répétitions max",
|
|
61643
|
+
"ex.totalReps": "Répétitions totales",
|
|
61644
|
+
"ex.longest": "Série la plus longue (s)",
|
|
61645
|
+
"ex.restAfter": "repos {v}",
|
|
61646
|
+
"ex.notDone": "non faite",
|
|
61647
|
+
"w.strengthSessions": "Séances",
|
|
61648
|
+
"ss.count": "{n} exercice|{n} exercices",
|
|
61649
|
+
"mg.chest": "Pectoraux",
|
|
61650
|
+
"mg.back": "Dos",
|
|
61651
|
+
"mg.shoulders": "Épaules",
|
|
61652
|
+
"mg.biceps": "Biceps",
|
|
61653
|
+
"mg.triceps": "Triceps",
|
|
61654
|
+
"mg.forearms": "Avant-bras",
|
|
61655
|
+
"mg.quads": "Quadriceps",
|
|
61656
|
+
"mg.hamstrings": "Ischio-jambiers",
|
|
61657
|
+
"mg.glutes": "Fessiers",
|
|
61658
|
+
"mg.calves": "Mollets",
|
|
61659
|
+
"mg.abs": "Abdominaux",
|
|
61660
|
+
"mg.hips": "Hanches",
|
|
61661
|
+
"mg.other": "Autre",
|
|
61662
|
+
"w.walkDetails": "Allure et marches",
|
|
61663
|
+
"wd.steps": "Pas",
|
|
61664
|
+
"wd.battery": "Variation de Body Battery",
|
|
61665
|
+
"wd.goal": "Objectif de pas",
|
|
61666
|
+
"wd.floors": "Étages",
|
|
61667
|
+
"w.steps": "Pas quotidiens",
|
|
61668
|
+
"w.hikeVertical": "Vitesse verticale",
|
|
61669
|
+
"hv.vam": "Dénivelé ({unit}) par heure",
|
|
61670
|
+
"w.bikeDetails": "Sorties à vélo",
|
|
61671
|
+
"bd.outdoor": "Extérieur",
|
|
61672
|
+
"bd.indoor": "Intérieur",
|
|
61673
|
+
"bd.rides": "{n} sortie|{n} sorties",
|
|
61674
|
+
"bd.where": "Où",
|
|
61675
|
+
"w.swimDetails": "Séances de natation",
|
|
61676
|
+
"sw.pace": "Allure",
|
|
61677
|
+
"sw.lengths": "Longueurs",
|
|
61678
|
+
"sw.strokes": "Mouvements",
|
|
61679
|
+
"sw.pool": "Piscine",
|
|
61680
|
+
"w.racketDetails": "Séances et intensité",
|
|
61681
|
+
"pd.peak": "FC maximale",
|
|
61682
|
+
"pd.z3": "Temps en Z3+",
|
|
61683
|
+
"pd.z45": "Part Z4-5",
|
|
61684
|
+
"pd.kcal": "kcal par heure",
|
|
61685
|
+
"pd.load": "Charge / course de même durée",
|
|
61686
|
+
"pd.note": "La seconde charge est une estimation : la charge Garmin moyenne par minute de tes courses, multipliée par la durée de la séance.",
|
|
61687
|
+
"col.sport": "Sport",
|
|
61688
|
+
"sport.tennis": "Tennis",
|
|
61689
|
+
"sport.padel": "Padel",
|
|
61690
|
+
"sport.pickleball": "Pickleball",
|
|
61691
|
+
"sport.racquetball": "Racquetball",
|
|
61692
|
+
"sport.squash": "Squash",
|
|
61693
|
+
"sport.badminton": "Badminton",
|
|
61694
|
+
"sport.tableTennis": "Tennis de table",
|
|
61695
|
+
"sport.platformTennis": "Platform tennis"
|
|
61696
|
+
},
|
|
61697
|
+
es: {
|
|
61698
|
+
title: "Entrenamiento + Recuperación",
|
|
61699
|
+
headline: "Carga de entrenamiento, recuperación y progreso desde Garmin Connect",
|
|
61700
|
+
na: "n/d",
|
|
61701
|
+
noData: "Esta página aún no tiene datos. Pide a tu asistente que escriba el panel.",
|
|
61702
|
+
period: "del {from} al {to}",
|
|
61703
|
+
inProgress: "* semana en curso",
|
|
61704
|
+
"menu.settings": "Ajustes",
|
|
61705
|
+
"menu.theme": "Tema",
|
|
61706
|
+
"footer.updated": "Datos de Garmin Connect, actualizados el {date}",
|
|
61707
|
+
"footer.failed": "Días no leídos de Garmin: {n} (sus valores muestran n/d)",
|
|
61708
|
+
"footer.license": "Licencia MIT",
|
|
61709
|
+
"menu.language": "Idioma",
|
|
61710
|
+
"lang.auto": "Automático",
|
|
61711
|
+
"lang.missing": "{language} aún no está en esta página. Pide a tu asistente que añada el idioma.",
|
|
61712
|
+
"themeGroup.auto": "Automático",
|
|
61713
|
+
"themeGroup.dark": "Oscuros",
|
|
61714
|
+
"themeGroup.light": "Claros",
|
|
61715
|
+
"themeGroup.accessible": "Accesibles",
|
|
61716
|
+
"theme.system": "Sistema",
|
|
61717
|
+
"theme.midnight": "Medianoche",
|
|
61718
|
+
"theme.graphite": "Grafito",
|
|
61719
|
+
"theme.ocean": "Océano",
|
|
61720
|
+
"theme.forest": "Bosque",
|
|
61721
|
+
"theme.plum": "Ciruela",
|
|
61722
|
+
"theme.light": "Claro",
|
|
61723
|
+
"theme.paper": "Papel",
|
|
61724
|
+
"theme.mist": "Niebla",
|
|
61725
|
+
"theme.hcDark": "Alto contraste oscuro",
|
|
61726
|
+
"theme.hcLight": "Alto contraste claro",
|
|
61727
|
+
"window.label": "Semanas",
|
|
61728
|
+
"window.last": "Últimas {n}",
|
|
61729
|
+
"window.all": "Todas",
|
|
61730
|
+
"window.from": "Desde la semana",
|
|
61731
|
+
"window.to": "Hasta la semana",
|
|
61732
|
+
"names.label": "Nombres de actividad",
|
|
61733
|
+
"names.all": "Todos",
|
|
61734
|
+
"names.count": "{n} elegido|{n} elegidos",
|
|
61735
|
+
"names.search": "Buscar nombres",
|
|
61736
|
+
"names.noMatch": "Ningún nombre coincide.",
|
|
61737
|
+
"names.remove": "Quitar {name}",
|
|
61738
|
+
"names.clear": "Mostrar todas",
|
|
61739
|
+
"type.all": "Todas",
|
|
61740
|
+
"type.recovery": "Recuperación",
|
|
61741
|
+
"type.running": "Carrera",
|
|
61742
|
+
"type.strengthTraining": "Fuerza",
|
|
61743
|
+
"type.walking": "Caminata",
|
|
61744
|
+
"type.hiking": "Senderismo",
|
|
61745
|
+
"type.cycling": "Bici",
|
|
61746
|
+
"type.swimming": "Natación",
|
|
61747
|
+
"type.racket": "Deportes de raqueta",
|
|
61748
|
+
"type.generic": "Otro",
|
|
61749
|
+
"type.transition": "Transición",
|
|
61750
|
+
"type.fitnessEquipment": "Máquinas de fitness",
|
|
61751
|
+
"type.basketball": "Baloncesto",
|
|
61752
|
+
"type.soccer": "Fútbol",
|
|
61753
|
+
"type.tennis": "Tenis",
|
|
61754
|
+
"type.americanFootball": "Fútbol americano",
|
|
61755
|
+
"type.training": "Entrenamiento",
|
|
61756
|
+
"type.crossCountrySkiing": "Esquí de fondo",
|
|
61757
|
+
"type.alpineSkiing": "Esquí alpino",
|
|
61758
|
+
"type.snowboarding": "Snowboard",
|
|
61759
|
+
"type.rowing": "Remo",
|
|
61760
|
+
"type.mountaineering": "Montañismo",
|
|
61761
|
+
"type.multisport": "Multideporte",
|
|
61762
|
+
"type.paddling": "Remo con pala",
|
|
61763
|
+
"type.flying": "Vuelo",
|
|
61764
|
+
"type.eBiking": "Bici eléctrica",
|
|
61765
|
+
"type.motorcycling": "Motociclismo",
|
|
61766
|
+
"type.boating": "Navegación",
|
|
61767
|
+
"type.driving": "Conducción",
|
|
61768
|
+
"type.golf": "Golf",
|
|
61769
|
+
"type.hangGliding": "Ala delta",
|
|
61770
|
+
"type.horsebackRiding": "Equitación",
|
|
61771
|
+
"type.hunting": "Caza",
|
|
61772
|
+
"type.fishing": "Pesca",
|
|
61773
|
+
"type.inlineSkating": "Patinaje en línea",
|
|
61774
|
+
"type.rockClimbing": "Escalada",
|
|
61775
|
+
"type.sailing": "Vela",
|
|
61776
|
+
"type.iceSkating": "Patinaje sobre hielo",
|
|
61777
|
+
"type.skyDiving": "Paracaidismo",
|
|
61778
|
+
"type.snowshoeing": "Raquetas de nieve",
|
|
61779
|
+
"type.snowmobiling": "Moto de nieve",
|
|
61780
|
+
"type.standUpPaddleboarding": "Stand up paddle",
|
|
61781
|
+
"type.surfing": "Surf",
|
|
61782
|
+
"type.wakeboarding": "Wakeboard",
|
|
61783
|
+
"type.waterSkiing": "Esquí acuático",
|
|
61784
|
+
"type.kayaking": "Kayak",
|
|
61785
|
+
"type.rafting": "Rafting",
|
|
61786
|
+
"type.windsurfing": "Windsurf",
|
|
61787
|
+
"type.kitesurfing": "Kitesurf",
|
|
61788
|
+
"type.tactical": "Táctico",
|
|
61789
|
+
"type.jumpmaster": "Jumpmaster",
|
|
61790
|
+
"type.boxing": "Boxeo",
|
|
61791
|
+
"type.floorClimbing": "Subida de pisos",
|
|
61792
|
+
"type.baseball": "Béisbol",
|
|
61793
|
+
"type.diving": "Buceo",
|
|
61794
|
+
"type.shooting": "Tiro",
|
|
61795
|
+
"type.winterSport": "Deportes de invierno",
|
|
61796
|
+
"type.grinding": "Grinding",
|
|
61797
|
+
"type.hiit": "HIIT",
|
|
61798
|
+
"type.videoGaming": "Videojuegos",
|
|
61799
|
+
"type.wheelchairPushWalk": "Paseo en silla de ruedas",
|
|
61800
|
+
"type.wheelchairPushRun": "Carrera en silla de ruedas",
|
|
61801
|
+
"type.meditation": "Meditación",
|
|
61802
|
+
"type.paraSport": "Deporte adaptado",
|
|
61803
|
+
"type.discGolf": "Disc golf",
|
|
61804
|
+
"type.teamSport": "Deportes de equipo",
|
|
61805
|
+
"type.cricket": "Críquet",
|
|
61806
|
+
"type.rugby": "Rugby",
|
|
61807
|
+
"type.hockey": "Hockey",
|
|
61808
|
+
"type.lacrosse": "Lacrosse",
|
|
61809
|
+
"type.volleyball": "Voleibol",
|
|
61810
|
+
"type.waterTubing": "Tubing",
|
|
61811
|
+
"type.wakesurfing": "Wakesurf",
|
|
61812
|
+
"type.waterSport": "Deportes acuáticos",
|
|
61813
|
+
"type.archery": "Tiro con arco",
|
|
61814
|
+
"type.mixedMartialArts": "Artes marciales mixtas",
|
|
61815
|
+
"type.motorSports": "Deportes de motor",
|
|
61816
|
+
"type.snorkeling": "Snorkel",
|
|
61817
|
+
"type.dance": "Baile",
|
|
61818
|
+
"type.jumpRope": "Salto de cuerda",
|
|
61819
|
+
"type.poolApnea": "Apnea en piscina",
|
|
61820
|
+
"type.mobility": "Movilidad",
|
|
61821
|
+
"type.geocaching": "Geocaching",
|
|
61822
|
+
"type.canoeing": "Canoa",
|
|
61823
|
+
"tabs.label": "Tipos de actividad",
|
|
61824
|
+
"edit.start": "Editar diseño",
|
|
61825
|
+
"edit.done": "Listo",
|
|
61826
|
+
"edit.add": "Añadir widget",
|
|
61827
|
+
"edit.reset": "Restablecer pestaña",
|
|
61828
|
+
"edit.discard": "Descartar",
|
|
61829
|
+
"edit.bar": "Editar diseño",
|
|
61830
|
+
"edit.editing": "Editando · {board}",
|
|
61831
|
+
"edit.unsaved": "Cambios sin guardar",
|
|
61832
|
+
"discard.title": "¿Descartar los cambios?",
|
|
61833
|
+
"discard.text": "El diseño vuelve a como estaba antes de seleccionar Editar diseño, en todas las pestañas.",
|
|
61834
|
+
"discard.keep": "Seguir editando",
|
|
61835
|
+
"discard.confirm": "Descartar cambios",
|
|
61836
|
+
"edit.copy": "Copiar diseño",
|
|
61837
|
+
"edit.hide": "Ocultar {title}",
|
|
61838
|
+
"edit.show": "Mostrar {title}",
|
|
61839
|
+
"edit.remove": "Quitar {title}",
|
|
61840
|
+
"edit.move": "Mover {title}",
|
|
61841
|
+
"edit.moveHint": "Arrastra para mover, o pulsa Espacio y luego las flechas",
|
|
61842
|
+
"edit.width": "Ancho de {title}",
|
|
61843
|
+
"edit.widthValue": "{n} de {cols} columnas",
|
|
61844
|
+
"edit.addHidden": "Añadir widget · {n} ocultos",
|
|
61845
|
+
"add.addOne": "Añadir {title}",
|
|
61846
|
+
"said.picked": "{title} seleccionado, posición {n} de {count}. Las flechas lo mueven, Espacio lo suelta, Escape lo devuelve a su sitio.",
|
|
61847
|
+
"said.position": "Posición {n} de {count}",
|
|
61848
|
+
"said.cancelled": "{title} vuelve a estar en su sitio",
|
|
61849
|
+
"said.moved": "{title} movido a la posición {n} de {count}",
|
|
61850
|
+
"said.width": "{title}: {n} de {cols} columnas",
|
|
61851
|
+
"said.hidden": "{title} oculto",
|
|
61852
|
+
"said.shown": "{title} visible",
|
|
61853
|
+
"said.removed": "{title} quitado",
|
|
61854
|
+
"said.added": "{title} añadido",
|
|
61855
|
+
"said.saved": "Diseño guardado",
|
|
61856
|
+
"said.unchanged": "No hay cambios que guardar",
|
|
61857
|
+
"said.discarded": "Cambios descartados",
|
|
61858
|
+
"stale.label": "Nuevo diseño",
|
|
61859
|
+
"stale.text": "Tu asistente escribió un nuevo diseño para este panel. El diseño que hiciste en este navegador se mantiene hasta que elijas.",
|
|
61860
|
+
"stale.keep": "Mantener el mío",
|
|
61861
|
+
"stale.new": "Usar el nuevo",
|
|
61862
|
+
"stale.used": "El nuevo diseño está en uso",
|
|
61863
|
+
"stale.kept": "Tu diseño está en uso",
|
|
61864
|
+
"add.title": "Añadir un widget",
|
|
61865
|
+
"add.type": "Tipo",
|
|
61866
|
+
"add.add": "Añadir",
|
|
61867
|
+
"add.close": "Cerrar",
|
|
61868
|
+
"add.hiddenGroup": "Ocultos en esta pestaña",
|
|
61869
|
+
"add.none": "Todos los widgets ya están en esta pestaña.",
|
|
61870
|
+
"add.search": "Buscar widgets",
|
|
61871
|
+
"add.noMatch": "Ningún widget coincide.",
|
|
61872
|
+
"copy.title": "Copiar diseño",
|
|
61873
|
+
"copy.hint": "Este diseño solo está guardado en este navegador. Para conservarlo en la página, pega este texto a tu asistente y pídele que guarde el diseño del panel.",
|
|
61874
|
+
"copy.copy": "Copiar",
|
|
61875
|
+
"copy.copied": "Copiado",
|
|
61876
|
+
"board.empty": "Esta pestaña no tiene widgets.",
|
|
61877
|
+
"board.emptyEdit": "Esta pestaña no tiene widgets: usa “Añadir widget” para poner alguno.",
|
|
61878
|
+
"widget.failed": "Este widget falló: {message}",
|
|
61879
|
+
"widget.unknown": "Widget desconocido: {id}",
|
|
61880
|
+
"empty.window": "No hay sesiones en las semanas elegidas.",
|
|
61881
|
+
"w.coachNotes": "Notas del entrenador",
|
|
61882
|
+
"notes.none": "Sin notas.",
|
|
61883
|
+
"notes.disclaimer": "Sugerencias de entrenamiento según los datos hasta el {date}. No es consejo médico.",
|
|
61884
|
+
"w.kpis": "Vista general",
|
|
61885
|
+
"kpi.totals": "{distance} · {time} · {n} sesión|{distance} · {time} · {n} sesiones",
|
|
61886
|
+
"kpi.distance": "Distancia · últimos 7 días",
|
|
61887
|
+
"kpi.time": "Tiempo · últimos 7 días",
|
|
61888
|
+
"kpi.vs4w": "frente a la media de 4 semanas",
|
|
61889
|
+
"delta.good": "mejor",
|
|
61890
|
+
"delta.bad": "peor",
|
|
61891
|
+
"kpi.ratio": "Ratio de carga 7:28",
|
|
61892
|
+
"kpi.band": "Rango 0,8-1,3",
|
|
61893
|
+
"kpi.ratioAll": "Usa todas las actividades, también con un filtro por tipo",
|
|
61894
|
+
"kpi.hrv": "HRV de anoche",
|
|
61895
|
+
"kpi.vsBaseline": "frente a la media de 7 días {v} ms",
|
|
61896
|
+
"w.readiness": "Preparación de hoy",
|
|
61897
|
+
"ready.easy": "Tómatelo con calma",
|
|
61898
|
+
"ready.push": "Listo para exigirte",
|
|
61899
|
+
"ready.normal": "Normal",
|
|
61900
|
+
"ready.hrv": "Estado de HRV",
|
|
61901
|
+
"ready.sleep": "Puntuación del sueño",
|
|
61902
|
+
"ready.rhr": "FC en reposo",
|
|
61903
|
+
"ready.rhrVs": "7 días antes: {avg} bpm",
|
|
61904
|
+
"ready.ratio": "Ratio de carga 7:28",
|
|
61905
|
+
"ready.note": "Una regla propia del panel, no la preparación de Garmin. Muestra hoy: el intervalo de semanas no la cambia.",
|
|
61906
|
+
"w.fitnessAge": "Edad física",
|
|
61907
|
+
"fa.age": "Edad física",
|
|
61908
|
+
"fa.actual": "Edad real",
|
|
61909
|
+
"fa.achievable": "Alcanzable",
|
|
61910
|
+
"fa.details": "Detalles",
|
|
61911
|
+
"fa.component": "Componente",
|
|
61912
|
+
"fa.value": "Ahora",
|
|
61913
|
+
"fa.target": "Objetivo de Garmin",
|
|
61914
|
+
"fa.potential": "Edad potencial",
|
|
61915
|
+
"fa.bodyFat": "Grasa corporal %",
|
|
61916
|
+
"fa.bmi": "IMC",
|
|
61917
|
+
"fa.rhr": "FC en reposo",
|
|
61918
|
+
"fa.vigorousDaysAvg": "Días intensos / semana",
|
|
61919
|
+
"fa.vigorousMinutesAvg": "Minutos intensos / semana",
|
|
61920
|
+
"today.note": "Muestra hoy: el intervalo de semanas no lo cambia.",
|
|
61921
|
+
"w.periodComparison": "Comparación de períodos",
|
|
61922
|
+
"pc.vs": "del {from} al {to} frente al período del {pfrom} al {pto}",
|
|
61923
|
+
"pc.partial": "parcial: estas semanas empiezan antes de los datos",
|
|
61924
|
+
"pc.type": "Tipo",
|
|
61925
|
+
"pc.sessions": "Sesiones",
|
|
61926
|
+
"pc.distance": "Distancia",
|
|
61927
|
+
"pc.time": "Tiempo",
|
|
61928
|
+
"pc.load": "Carga",
|
|
61929
|
+
"pc.total": "Total",
|
|
61930
|
+
"w.records": "Récords",
|
|
61931
|
+
"rec.best": "Mejor {d}",
|
|
61932
|
+
"d.1k": "1 km",
|
|
61933
|
+
"d.mile": "milla",
|
|
61934
|
+
"d.5k": "5 km",
|
|
61935
|
+
"d.10k": "10 km",
|
|
61936
|
+
"d.half": "media maratón",
|
|
61937
|
+
"d.marathon": "maratón",
|
|
61938
|
+
"rec.longestRun": "Carrera más larga",
|
|
61939
|
+
"rec.longestRide": "Salida en bici más larga",
|
|
61940
|
+
"rec.topSet": "Serie más pesada",
|
|
61941
|
+
"rec.e1rm": "Mejor 1RM estimado · {name}",
|
|
61942
|
+
"rec.none": "No hay récords en las semanas elegidas.",
|
|
61943
|
+
"rec.estimate": "Los valores de 1RM son estimaciones (Epley, series de 1-12 repeticiones).",
|
|
61944
|
+
"w.weeklyVolume": "Volumen semanal",
|
|
61945
|
+
"m.time": "Tiempo",
|
|
61946
|
+
"m.distance": "Distancia",
|
|
61947
|
+
"m.sessions": "Sesiones",
|
|
61948
|
+
"m.load": "Carga",
|
|
61949
|
+
"vol.avg": "Media de 4 semanas",
|
|
61950
|
+
"w.intensityMinutes": "Minutos de intensidad",
|
|
61951
|
+
"im.guide": "Recomendación 150 min",
|
|
61952
|
+
"im.note": "Minutos moderados + 2 x intensos solo de las actividades, no de todo el día.",
|
|
61953
|
+
"w.loadSafety": "Seguridad de la carga",
|
|
61954
|
+
"ls.weekLoad": "Carga · últimos 7 días",
|
|
61955
|
+
"ls.monotony": "Monotonía · 7 días",
|
|
61956
|
+
"ls.monotonyFlag": "Por encima de 2,0",
|
|
61957
|
+
"ls.strain": "Strain · 7 días",
|
|
61958
|
+
"ls.flagged": "Semanas por encima de +10%",
|
|
61959
|
+
"ls.load": "Carga semanal",
|
|
61960
|
+
"ls.ramp": "Cambio frente a la semana anterior (%)",
|
|
61961
|
+
"ls.note": "Todas las actividades. Las semanas en rojo subieron más del 10%; una semana en curso no se marca. La monotonía y el strain (Foster) son estimaciones.",
|
|
61962
|
+
"ls.estimated": "Algunas cargas son estimaciones: la actividad no tiene carga de Garmin.",
|
|
61963
|
+
"w.loadVsRecovery": "Carga y recuperación",
|
|
61964
|
+
"lvr.hrv": "HRV (ms)",
|
|
61965
|
+
"lvr.note": "Carga diaria por tipo (barras) y HRV nocturna (línea).",
|
|
61966
|
+
"w.calendar": "Constancia",
|
|
61967
|
+
"cal.minutes": "{n} min",
|
|
61968
|
+
"cal.rest": "Día de descanso",
|
|
61969
|
+
"cal.label": "Días de entrenamiento, por semana; muévete con las flechas",
|
|
61970
|
+
"cal.less": "Menos",
|
|
61971
|
+
"cal.more": "Más",
|
|
61972
|
+
"m.label": "Medida",
|
|
61973
|
+
skip: "Saltar a la pestaña",
|
|
61974
|
+
"w.hrv": "HRV",
|
|
61975
|
+
"hrv.low": "Mínimo del rango",
|
|
61976
|
+
"hrv.band": "Rango equilibrado",
|
|
61977
|
+
"hrv.nightly": "HRV nocturna",
|
|
61978
|
+
"hrv.avg": "Media de 7 días",
|
|
61979
|
+
rec14: "Últimos 14 días: el intervalo de semanas no los cambia.",
|
|
61980
|
+
"rec.board": "Los widgets de recuperación muestran los últimos 14 días: el intervalo de semanas no los cambia.",
|
|
61981
|
+
"w.restingHr": "FC en reposo",
|
|
61982
|
+
"w.sleep": "Sueño",
|
|
61983
|
+
"sl.deep": "Profundo",
|
|
61984
|
+
"sl.light": "Ligero",
|
|
61985
|
+
"sl.rem": "REM",
|
|
61986
|
+
"sl.awake": "Despierto",
|
|
61987
|
+
"sl.score": "Puntuación",
|
|
61988
|
+
"w.sleepNeed": "Necesidad de sueño",
|
|
61989
|
+
"sn.gap": "Por debajo de lo necesario · media",
|
|
61990
|
+
"sn.actual": "Dormido",
|
|
61991
|
+
"sn.need": "Necesario",
|
|
61992
|
+
"w.bedtime": "Hora de acostarse",
|
|
61993
|
+
"bt.spread": "Variación del horario",
|
|
61994
|
+
"w.bodyBattery": "Body Battery",
|
|
61995
|
+
"bb.high": "Máximo",
|
|
61996
|
+
"bb.low": "Mínimo",
|
|
61997
|
+
"w.activities": "Actividades",
|
|
61998
|
+
"col.date": "Fecha",
|
|
61999
|
+
"col.type": "Tipo",
|
|
62000
|
+
"col.name": "Nombre",
|
|
62001
|
+
"col.distance": "Distancia",
|
|
62002
|
+
"col.time": "Tiempo",
|
|
62003
|
+
"col.pace": "Ritmo / velocidad",
|
|
62004
|
+
"col.hr": "FC media",
|
|
62005
|
+
"col.ascent": "Ascenso",
|
|
62006
|
+
"col.race": "Competición",
|
|
62007
|
+
"laps.show": "vueltas",
|
|
62008
|
+
"lap.n": "Vuelta",
|
|
62009
|
+
"lap.intensity": "Intensidad",
|
|
62010
|
+
"act.more": "Mostrar las {n}",
|
|
62011
|
+
"act.less": "Mostrar menos",
|
|
62012
|
+
"one.session": "Solo una sesión por ahora: las tendencias empiezan con 2 sesiones.",
|
|
62013
|
+
"w.typeKpis": "Resumen",
|
|
62014
|
+
"tk.sessions": "Sesiones",
|
|
62015
|
+
"tk.time": "Tiempo",
|
|
62016
|
+
"tk.avg": "{v} por sesión",
|
|
62017
|
+
"tk.distance": "Distancia",
|
|
62018
|
+
"tk.speed": "Velocidad media",
|
|
62019
|
+
"tk.pace": "Ritmo medio",
|
|
62020
|
+
"tk.ascent": "Ascenso",
|
|
62021
|
+
"tk.hr": "FC media",
|
|
62022
|
+
"tk.peak": "Pico {v} bpm",
|
|
62023
|
+
"tk.load": "Carga de entrenamiento de Garmin",
|
|
62024
|
+
"tk.share": "{v}% de toda la carga",
|
|
62025
|
+
"w.typeWeekly": "Cada semana",
|
|
62026
|
+
"w.hrZones": "Tiempo en zonas de FC",
|
|
62027
|
+
"zones.approximate": "Aproximado: algunas actividades no tienen tiempo por zona, así que todos sus minutos cuentan en la zona de su FC media.",
|
|
62028
|
+
"form.cadence": "Cadencia",
|
|
62029
|
+
"form.stride": "Zancada",
|
|
62030
|
+
"form.gct": "Contacto con el suelo",
|
|
62031
|
+
"form.vertOsc": "Oscilación vertical",
|
|
62032
|
+
"form.vertRatio": "Relación vertical",
|
|
62033
|
+
"form.power": "Potencia",
|
|
62034
|
+
"form.normPower": "Potencia normalizada",
|
|
62035
|
+
"w.bestEfforts": "Mejores marcas",
|
|
62036
|
+
"be.distance": "Distancia",
|
|
62037
|
+
"be.activity": "Actividad",
|
|
62038
|
+
"w.races": "Competiciones",
|
|
62039
|
+
"races.none": "No hay competiciones en las semanas elegidas.",
|
|
62040
|
+
"te.aerobic": "TE aeróbico",
|
|
62041
|
+
"te.anaerobic": "TE anaeróbico",
|
|
62042
|
+
"w.aerobicFitness": "Forma aeróbica",
|
|
62043
|
+
"af.runs": "Carreras (las antiguas más claras)",
|
|
62044
|
+
"af.pace": "Ritmo (min{unit})",
|
|
62045
|
+
"af.weekPace": "Ritmo semanal",
|
|
62046
|
+
"af.efficiency": "Eficiencia",
|
|
62047
|
+
"af.effUnit": "m por latido",
|
|
62048
|
+
"w.intensityShare": "Carrera suave e intensa",
|
|
62049
|
+
"is.easy": "Z1-2 (suave)",
|
|
62050
|
+
"is.hard": "Z4-5 (intensa)",
|
|
62051
|
+
"w.runningForm": "Técnica de carrera",
|
|
62052
|
+
"w.runWalk": "Correr y caminar",
|
|
62053
|
+
"rw.none": "No hay sesiones de correr y caminar en las semanas elegidas.",
|
|
62054
|
+
"rw.runPace": "Ritmo de carrera",
|
|
62055
|
+
"rw.pace": "Ritmo total",
|
|
62056
|
+
"rw.walk": "Tiempo caminando",
|
|
62057
|
+
"w.trainingEffect": "Efecto del entrenamiento",
|
|
62058
|
+
"w.racePredictor": "Predicción de carrera",
|
|
62059
|
+
"rp.none": "No hay mejores marcas de 5 km o más en las semanas elegidas.",
|
|
62060
|
+
"rp.note": "Estimación (Riegel) a partir de {d} en {time} ({date}).",
|
|
62061
|
+
"w.longRuns": "Tiradas largas y reparto del ritmo",
|
|
62062
|
+
"lr.share": "Peso de la tirada más larga en la distancia semanal",
|
|
62063
|
+
"lr.ascent": "Ascenso semanal",
|
|
62064
|
+
"lr.note": "Semanas en rojo: la tirada más larga supera el 35% de la distancia de la semana. La tabla muestra las competiciones y las carreras de más de 15 km con vueltas.",
|
|
62065
|
+
"lr.first": "Primera mitad",
|
|
62066
|
+
"lr.second": "Segunda mitad",
|
|
62067
|
+
"lr.split": "Reparto",
|
|
62068
|
+
"lr.gap": "Ritmo ajustado a la pendiente",
|
|
62069
|
+
"lr.positive": "positivo",
|
|
62070
|
+
"lr.negative": "negativo",
|
|
62071
|
+
"lr.even": "regular",
|
|
62072
|
+
"w.setsPerCategory": "Series por movimiento",
|
|
62073
|
+
"ex.sets": "Series completadas",
|
|
62074
|
+
"w.muscleBalance": "Equilibrio de grupos musculares",
|
|
62075
|
+
"mb.pushPull": "Series de empuje : tirón",
|
|
62076
|
+
"mb.avg": "Media de series por semana",
|
|
62077
|
+
"mb.perWeek": "series por semana",
|
|
62078
|
+
"mb.note": "Una serie cuenta 1 por cada grupo muscular principal y 0,5 por cada secundario. Rango habitual: 10-20 series por semana (amarillo por debajo, rojo por encima).",
|
|
62079
|
+
"w.exercises": "Ejercicios",
|
|
62080
|
+
"ex.groups": "Grupos musculares",
|
|
62081
|
+
"ex.search": "Buscar ejercicios",
|
|
62082
|
+
"ex.name": "Ejercicio",
|
|
62083
|
+
"ex.sessions": "Sesiones",
|
|
62084
|
+
"ex.last": "Última",
|
|
62085
|
+
"ex.top": "Última serie máxima",
|
|
62086
|
+
"ex.best": "Mejor 1RM estimado",
|
|
62087
|
+
"ex.change": "Cambio",
|
|
62088
|
+
"ex.trend": "Tendencia",
|
|
62089
|
+
"ex.groupRow": "{n} ejercicio · {sets} series completadas|{n} ejercicios · {sets} series completadas",
|
|
62090
|
+
"ex.reps": "{n} repetición|{n} repeticiones",
|
|
62091
|
+
"ex.rest": "descanso mediano {v}",
|
|
62092
|
+
"ex.weight": "Serie máxima ({unit})",
|
|
62093
|
+
"ex.e1rm": "1RM estimado ({unit})",
|
|
62094
|
+
"ex.volume": "Volumen ({unit})",
|
|
62095
|
+
"ex.maxReps": "Repeticiones máximas",
|
|
62096
|
+
"ex.totalReps": "Repeticiones totales",
|
|
62097
|
+
"ex.longest": "Serie más larga (s)",
|
|
62098
|
+
"ex.restAfter": "descanso {v}",
|
|
62099
|
+
"ex.notDone": "no completada",
|
|
62100
|
+
"w.strengthSessions": "Sesiones",
|
|
62101
|
+
"ss.count": "{n} ejercicio|{n} ejercicios",
|
|
62102
|
+
"mg.chest": "Pecho",
|
|
62103
|
+
"mg.back": "Espalda",
|
|
62104
|
+
"mg.shoulders": "Hombros",
|
|
62105
|
+
"mg.biceps": "Bíceps",
|
|
62106
|
+
"mg.triceps": "Tríceps",
|
|
62107
|
+
"mg.forearms": "Antebrazos",
|
|
62108
|
+
"mg.quads": "Cuádriceps",
|
|
62109
|
+
"mg.hamstrings": "Isquiotibiales",
|
|
62110
|
+
"mg.glutes": "Glúteos",
|
|
62111
|
+
"mg.calves": "Gemelos",
|
|
62112
|
+
"mg.abs": "Abdominales",
|
|
62113
|
+
"mg.hips": "Caderas",
|
|
62114
|
+
"mg.other": "Otro",
|
|
62115
|
+
"w.walkDetails": "Ritmo y caminatas",
|
|
62116
|
+
"wd.steps": "Pasos",
|
|
62117
|
+
"wd.battery": "Cambio de Body Battery",
|
|
62118
|
+
"wd.goal": "Objetivo de pasos",
|
|
62119
|
+
"wd.floors": "Pisos",
|
|
62120
|
+
"w.steps": "Pasos diarios",
|
|
62121
|
+
"w.hikeVertical": "Velocidad vertical",
|
|
62122
|
+
"hv.vam": "Desnivel ({unit}) por hora",
|
|
62123
|
+
"w.bikeDetails": "Salidas en bici",
|
|
62124
|
+
"bd.outdoor": "Exterior",
|
|
62125
|
+
"bd.indoor": "Interior",
|
|
62126
|
+
"bd.rides": "{n} salida|{n} salidas",
|
|
62127
|
+
"bd.where": "Dónde",
|
|
62128
|
+
"w.swimDetails": "Sesiones de natación",
|
|
62129
|
+
"sw.pace": "Ritmo",
|
|
62130
|
+
"sw.lengths": "Largos",
|
|
62131
|
+
"sw.strokes": "Brazadas",
|
|
62132
|
+
"sw.pool": "Piscina",
|
|
62133
|
+
"w.racketDetails": "Sesiones e intensidad",
|
|
62134
|
+
"pd.peak": "FC máxima",
|
|
62135
|
+
"pd.z3": "Tiempo en Z3+",
|
|
62136
|
+
"pd.z45": "Proporción Z4-5",
|
|
62137
|
+
"pd.kcal": "kcal por hora",
|
|
62138
|
+
"pd.load": "Carga / carrera de igual duración",
|
|
62139
|
+
"pd.note": "La segunda carga es una estimación: la carga media de Garmin por minuto de tus carreras, por la duración de la sesión.",
|
|
62140
|
+
"col.sport": "Deporte",
|
|
62141
|
+
"sport.tennis": "Tenis",
|
|
62142
|
+
"sport.padel": "Pádel",
|
|
62143
|
+
"sport.pickleball": "Pickleball",
|
|
62144
|
+
"sport.racquetball": "Ráquetbol",
|
|
62145
|
+
"sport.squash": "Squash",
|
|
62146
|
+
"sport.badminton": "Bádminton",
|
|
62147
|
+
"sport.tableTennis": "Tenis de mesa",
|
|
62148
|
+
"sport.platformTennis": "Platform tennis"
|
|
62149
|
+
},
|
|
62150
|
+
pt: {
|
|
62151
|
+
title: "Treino + Recuperação",
|
|
62152
|
+
headline: "Carga de treino, recuperação e progresso do Garmin Connect",
|
|
62153
|
+
na: "n/d",
|
|
62154
|
+
noData: "Esta página ainda não tem dados. Peça ao seu assistente para escrever o painel.",
|
|
62155
|
+
period: "de {from} a {to}",
|
|
62156
|
+
inProgress: "* semana em andamento",
|
|
62157
|
+
"menu.settings": "Configurações",
|
|
62158
|
+
"menu.theme": "Tema",
|
|
62159
|
+
"footer.updated": "Dados do Garmin Connect, atualizados em {date}",
|
|
62160
|
+
"footer.failed": "Dias não lidos do Garmin: {n} (os valores deles aparecem como n/d)",
|
|
62161
|
+
"footer.license": "Licença MIT",
|
|
62162
|
+
"menu.language": "Idioma",
|
|
62163
|
+
"lang.auto": "Automático",
|
|
62164
|
+
"lang.missing": "{language} ainda não está nesta página. Peça ao seu assistente para adicionar o idioma.",
|
|
62165
|
+
"themeGroup.auto": "Automático",
|
|
62166
|
+
"themeGroup.dark": "Escuros",
|
|
62167
|
+
"themeGroup.light": "Claros",
|
|
62168
|
+
"themeGroup.accessible": "Acessíveis",
|
|
62169
|
+
"theme.system": "Sistema",
|
|
62170
|
+
"theme.midnight": "Meia-noite",
|
|
62171
|
+
"theme.graphite": "Grafite",
|
|
62172
|
+
"theme.ocean": "Oceano",
|
|
62173
|
+
"theme.forest": "Floresta",
|
|
62174
|
+
"theme.plum": "Ameixa",
|
|
62175
|
+
"theme.light": "Claro",
|
|
62176
|
+
"theme.paper": "Papel",
|
|
62177
|
+
"theme.mist": "Névoa",
|
|
62178
|
+
"theme.hcDark": "Alto contraste escuro",
|
|
62179
|
+
"theme.hcLight": "Alto contraste claro",
|
|
62180
|
+
"window.label": "Semanas",
|
|
62181
|
+
"window.last": "Últimas {n}",
|
|
62182
|
+
"window.all": "Todas",
|
|
62183
|
+
"window.from": "Da semana",
|
|
62184
|
+
"window.to": "Até a semana",
|
|
62185
|
+
"names.label": "Nomes das atividades",
|
|
62186
|
+
"names.all": "Todos",
|
|
62187
|
+
"names.count": "{n} escolhido|{n} escolhidos",
|
|
62188
|
+
"names.search": "Pesquisar nomes",
|
|
62189
|
+
"names.noMatch": "Nenhum nome corresponde.",
|
|
62190
|
+
"names.remove": "Remover {name}",
|
|
62191
|
+
"names.clear": "Mostrar todas",
|
|
62192
|
+
"type.all": "Todas",
|
|
62193
|
+
"type.recovery": "Recuperação",
|
|
62194
|
+
"type.running": "Corrida",
|
|
62195
|
+
"type.strengthTraining": "Força",
|
|
62196
|
+
"type.walking": "Caminhada",
|
|
62197
|
+
"type.hiking": "Trilha",
|
|
62198
|
+
"type.cycling": "Ciclismo",
|
|
62199
|
+
"type.swimming": "Natação",
|
|
62200
|
+
"type.racket": "Esportes de raquete",
|
|
62201
|
+
"type.generic": "Outros",
|
|
62202
|
+
"type.transition": "Transição",
|
|
62203
|
+
"type.fitnessEquipment": "Aparelhos de fitness",
|
|
62204
|
+
"type.basketball": "Basquete",
|
|
62205
|
+
"type.soccer": "Futebol",
|
|
62206
|
+
"type.tennis": "Tênis",
|
|
62207
|
+
"type.americanFootball": "Futebol americano",
|
|
62208
|
+
"type.training": "Treino",
|
|
62209
|
+
"type.crossCountrySkiing": "Esqui cross-country",
|
|
62210
|
+
"type.alpineSkiing": "Esqui alpino",
|
|
62211
|
+
"type.snowboarding": "Snowboard",
|
|
62212
|
+
"type.rowing": "Remo",
|
|
62213
|
+
"type.mountaineering": "Montanhismo",
|
|
62214
|
+
"type.multisport": "Multiesporte",
|
|
62215
|
+
"type.paddling": "Remada",
|
|
62216
|
+
"type.flying": "Voo",
|
|
62217
|
+
"type.eBiking": "E-bike",
|
|
62218
|
+
"type.motorcycling": "Motociclismo",
|
|
62219
|
+
"type.boating": "Navegação",
|
|
62220
|
+
"type.driving": "Direção",
|
|
62221
|
+
"type.golf": "Golfe",
|
|
62222
|
+
"type.hangGliding": "Asa-delta",
|
|
62223
|
+
"type.horsebackRiding": "Equitação",
|
|
62224
|
+
"type.hunting": "Caça",
|
|
62225
|
+
"type.fishing": "Pesca",
|
|
62226
|
+
"type.inlineSkating": "Patinação em linha",
|
|
62227
|
+
"type.rockClimbing": "Escalada",
|
|
62228
|
+
"type.sailing": "Vela",
|
|
62229
|
+
"type.iceSkating": "Patinação no gelo",
|
|
62230
|
+
"type.skyDiving": "Paraquedismo",
|
|
62231
|
+
"type.snowshoeing": "Caminhada com raquetes de neve",
|
|
62232
|
+
"type.snowmobiling": "Moto de neve",
|
|
62233
|
+
"type.standUpPaddleboarding": "Stand up paddle",
|
|
62234
|
+
"type.surfing": "Surfe",
|
|
62235
|
+
"type.wakeboarding": "Wakeboard",
|
|
62236
|
+
"type.waterSkiing": "Esqui aquático",
|
|
62237
|
+
"type.kayaking": "Caiaque",
|
|
62238
|
+
"type.rafting": "Rafting",
|
|
62239
|
+
"type.windsurfing": "Windsurf",
|
|
62240
|
+
"type.kitesurfing": "Kitesurf",
|
|
62241
|
+
"type.tactical": "Tático",
|
|
62242
|
+
"type.jumpmaster": "Jumpmaster",
|
|
62243
|
+
"type.boxing": "Boxe",
|
|
62244
|
+
"type.floorClimbing": "Subida de andares",
|
|
62245
|
+
"type.baseball": "Beisebol",
|
|
62246
|
+
"type.diving": "Mergulho",
|
|
62247
|
+
"type.shooting": "Tiro",
|
|
62248
|
+
"type.winterSport": "Esportes de inverno",
|
|
62249
|
+
"type.grinding": "Grinding",
|
|
62250
|
+
"type.hiit": "HIIT",
|
|
62251
|
+
"type.videoGaming": "Videogame",
|
|
62252
|
+
"type.wheelchairPushWalk": "Passeio em cadeira de rodas",
|
|
62253
|
+
"type.wheelchairPushRun": "Corrida em cadeira de rodas",
|
|
62254
|
+
"type.meditation": "Meditação",
|
|
62255
|
+
"type.paraSport": "Esportes paralímpicos",
|
|
62256
|
+
"type.discGolf": "Disc golf",
|
|
62257
|
+
"type.teamSport": "Esportes coletivos",
|
|
62258
|
+
"type.cricket": "Críquete",
|
|
62259
|
+
"type.rugby": "Rúgbi",
|
|
62260
|
+
"type.hockey": "Hóquei",
|
|
62261
|
+
"type.lacrosse": "Lacrosse",
|
|
62262
|
+
"type.volleyball": "Vôlei",
|
|
62263
|
+
"type.waterTubing": "Boia rebocada",
|
|
62264
|
+
"type.wakesurfing": "Wakesurf",
|
|
62265
|
+
"type.waterSport": "Esportes aquáticos",
|
|
62266
|
+
"type.archery": "Tiro com arco",
|
|
62267
|
+
"type.mixedMartialArts": "Artes marciais mistas",
|
|
62268
|
+
"type.motorSports": "Esportes a motor",
|
|
62269
|
+
"type.snorkeling": "Snorkel",
|
|
62270
|
+
"type.dance": "Dança",
|
|
62271
|
+
"type.jumpRope": "Pular corda",
|
|
62272
|
+
"type.poolApnea": "Apneia na piscina",
|
|
62273
|
+
"type.mobility": "Mobilidade",
|
|
62274
|
+
"type.geocaching": "Geocaching",
|
|
62275
|
+
"type.canoeing": "Canoagem",
|
|
62276
|
+
"tabs.label": "Tipos de atividade",
|
|
62277
|
+
"edit.start": "Editar layout",
|
|
62278
|
+
"edit.done": "Concluir",
|
|
62279
|
+
"edit.add": "Adicionar widget",
|
|
62280
|
+
"edit.reset": "Redefinir quadro",
|
|
62281
|
+
"edit.discard": "Descartar",
|
|
62282
|
+
"edit.bar": "Editar layout",
|
|
62283
|
+
"edit.editing": "Editando · {board}",
|
|
62284
|
+
"edit.unsaved": "Alterações não salvas",
|
|
62285
|
+
"discard.title": "Descartar as alterações?",
|
|
62286
|
+
"discard.text": "O layout volta a ser como era antes de você selecionar Editar layout, em todos os quadros.",
|
|
62287
|
+
"discard.keep": "Continuar editando",
|
|
62288
|
+
"discard.confirm": "Descartar alterações",
|
|
62289
|
+
"edit.copy": "Copiar layout",
|
|
62290
|
+
"edit.hide": "Ocultar {title}",
|
|
62291
|
+
"edit.show": "Mostrar {title}",
|
|
62292
|
+
"edit.remove": "Remover {title}",
|
|
62293
|
+
"edit.move": "Mover {title}",
|
|
62294
|
+
"edit.moveHint": "Arraste para mover ou pressione Espaço e depois as teclas de seta",
|
|
62295
|
+
"edit.width": "Largura de {title}",
|
|
62296
|
+
"edit.widthValue": "{n} de {cols} colunas",
|
|
62297
|
+
"edit.addHidden": "Adicionar widget · {n} ocultos",
|
|
62298
|
+
"add.addOne": "Adicionar {title}",
|
|
62299
|
+
"said.picked": "{title} selecionado, posição {n} de {count}. As setas o movem, Espaço o solta, Esc o devolve ao lugar.",
|
|
62300
|
+
"said.position": "Posição {n} de {count}",
|
|
62301
|
+
"said.cancelled": "{title} voltou ao seu lugar",
|
|
62302
|
+
"said.moved": "{title} movido para a posição {n} de {count}",
|
|
62303
|
+
"said.width": "{title}: {n} de {cols} colunas",
|
|
62304
|
+
"said.hidden": "{title} oculto",
|
|
62305
|
+
"said.shown": "{title} visível",
|
|
62306
|
+
"said.removed": "{title} removido",
|
|
62307
|
+
"said.added": "{title} adicionado",
|
|
62308
|
+
"said.saved": "Layout salvo",
|
|
62309
|
+
"said.unchanged": "Nenhuma alteração para salvar",
|
|
62310
|
+
"said.discarded": "Alterações descartadas",
|
|
62311
|
+
"stale.label": "Novo layout",
|
|
62312
|
+
"stale.text": "Seu assistente escreveu um novo layout para este painel. O layout que você fez neste navegador continua até você escolher.",
|
|
62313
|
+
"stale.keep": "Manter o meu",
|
|
62314
|
+
"stale.new": "Usar o novo",
|
|
62315
|
+
"stale.used": "O novo layout está em uso",
|
|
62316
|
+
"stale.kept": "Seu layout está em uso",
|
|
62317
|
+
"add.title": "Adicionar um widget",
|
|
62318
|
+
"add.type": "Tipo",
|
|
62319
|
+
"add.add": "Adicionar",
|
|
62320
|
+
"add.close": "Fechar",
|
|
62321
|
+
"add.hiddenGroup": "Ocultos neste quadro",
|
|
62322
|
+
"add.none": "Todos os widgets já estão neste quadro.",
|
|
62323
|
+
"add.search": "Pesquisar widgets",
|
|
62324
|
+
"add.noMatch": "Nenhum widget corresponde.",
|
|
62325
|
+
"copy.title": "Copiar layout",
|
|
62326
|
+
"copy.hint": "Este layout está salvo apenas neste navegador. Para mantê-lo na página, cole este texto na conversa com o seu assistente e peça que ele salve o layout do painel.",
|
|
62327
|
+
"copy.copy": "Copiar",
|
|
62328
|
+
"copy.copied": "Copiado",
|
|
62329
|
+
"board.empty": "Este quadro não tem widgets.",
|
|
62330
|
+
"board.emptyEdit": "Este quadro não tem widgets: use “Adicionar widget” para colocar alguns.",
|
|
62331
|
+
"widget.failed": "Este widget falhou: {message}",
|
|
62332
|
+
"widget.unknown": "Widget desconhecido: {id}",
|
|
62333
|
+
"empty.window": "Nenhuma sessão nas semanas selecionadas.",
|
|
62334
|
+
"w.coachNotes": "Notas do treinador",
|
|
62335
|
+
"notes.none": "Nenhuma nota.",
|
|
62336
|
+
"notes.disclaimer": "Sugestões de treino para os dados até {date}. Não é aconselhamento médico.",
|
|
62337
|
+
"w.kpis": "Visão geral",
|
|
62338
|
+
"kpi.totals": "{distance} · {time} · {n} sessão|{distance} · {time} · {n} sessões",
|
|
62339
|
+
"kpi.distance": "Distância · últimos 7 dias",
|
|
62340
|
+
"kpi.time": "Tempo · últimos 7 dias",
|
|
62341
|
+
"kpi.vs4w": "em relação à média de 4 semanas",
|
|
62342
|
+
"delta.good": "melhor",
|
|
62343
|
+
"delta.bad": "pior",
|
|
62344
|
+
"kpi.ratio": "Relação de carga 7:28",
|
|
62345
|
+
"kpi.band": "Faixa 0,8-1,3",
|
|
62346
|
+
"kpi.ratioAll": "Usa todas as atividades, mesmo com um filtro de tipo",
|
|
62347
|
+
"kpi.hrv": "HRV da última noite",
|
|
62348
|
+
"kpi.vsBaseline": "em relação à média de 7 dias {v} ms",
|
|
62349
|
+
"w.readiness": "Prontidão de hoje",
|
|
62350
|
+
"ready.easy": "Vá com calma",
|
|
62351
|
+
"ready.push": "Pronto para intensificar",
|
|
62352
|
+
"ready.normal": "Normal",
|
|
62353
|
+
"ready.hrv": "Status de HRV",
|
|
62354
|
+
"ready.sleep": "Pontuação do sono",
|
|
62355
|
+
"ready.rhr": "FC em repouso",
|
|
62356
|
+
"ready.rhrVs": "7 dias antes: {avg} bpm",
|
|
62357
|
+
"ready.ratio": "Relação de carga 7:28",
|
|
62358
|
+
"ready.note": "Uma regra do próprio painel, não a prontidão da Garmin. Mostra hoje: a janela de semanas não a altera.",
|
|
62359
|
+
"w.fitnessAge": "Idade física",
|
|
62360
|
+
"fa.age": "Idade física",
|
|
62361
|
+
"fa.actual": "Idade real",
|
|
62362
|
+
"fa.achievable": "Alcançável",
|
|
62363
|
+
"fa.details": "Detalhes",
|
|
62364
|
+
"fa.component": "Componente",
|
|
62365
|
+
"fa.value": "Agora",
|
|
62366
|
+
"fa.target": "Meta Garmin",
|
|
62367
|
+
"fa.potential": "Idade possível",
|
|
62368
|
+
"fa.bodyFat": "Gordura corporal %",
|
|
62369
|
+
"fa.bmi": "IMC",
|
|
62370
|
+
"fa.rhr": "FC em repouso",
|
|
62371
|
+
"fa.vigorousDaysAvg": "Dias vigorosos / semana",
|
|
62372
|
+
"fa.vigorousMinutesAvg": "Minutos vigorosos / semana",
|
|
62373
|
+
"today.note": "Mostra hoje: a janela de semanas não a altera.",
|
|
62374
|
+
"w.periodComparison": "Comparação de períodos",
|
|
62375
|
+
"pc.vs": "de {from} a {to} comparado com {pfrom} a {pto}",
|
|
62376
|
+
"pc.partial": "parcial: estas semanas começam antes dos dados",
|
|
62377
|
+
"pc.type": "Tipo",
|
|
62378
|
+
"pc.sessions": "Sessões",
|
|
62379
|
+
"pc.distance": "Distância",
|
|
62380
|
+
"pc.time": "Tempo",
|
|
62381
|
+
"pc.load": "Carga",
|
|
62382
|
+
"pc.total": "Total",
|
|
62383
|
+
"w.records": "Recordes",
|
|
62384
|
+
"rec.best": "Melhor tempo · {d}",
|
|
62385
|
+
"d.1k": "1 km",
|
|
62386
|
+
"d.mile": "milha",
|
|
62387
|
+
"d.5k": "5 km",
|
|
62388
|
+
"d.10k": "10 km",
|
|
62389
|
+
"d.half": "meia maratona",
|
|
62390
|
+
"d.marathon": "maratona",
|
|
62391
|
+
"rec.longestRun": "Corrida mais longa",
|
|
62392
|
+
"rec.longestRide": "Pedalada mais longa",
|
|
62393
|
+
"rec.topSet": "Série mais pesada",
|
|
62394
|
+
"rec.e1rm": "Melhor 1RM estimado · {name}",
|
|
62395
|
+
"rec.none": "Nenhum recorde nas semanas selecionadas.",
|
|
62396
|
+
"rec.estimate": "Os valores de 1RM são estimativas (Epley, séries de 1-12 repetições).",
|
|
62397
|
+
"w.weeklyVolume": "Volume semanal",
|
|
62398
|
+
"m.time": "Tempo",
|
|
62399
|
+
"m.distance": "Distância",
|
|
62400
|
+
"m.sessions": "Sessões",
|
|
62401
|
+
"m.load": "Carga",
|
|
62402
|
+
"vol.avg": "Média de 4 semanas",
|
|
62403
|
+
"w.intensityMinutes": "Minutos de intensidade",
|
|
62404
|
+
"im.guide": "Recomendação 150 min",
|
|
62405
|
+
"im.note": "Minutos moderados + 2 x vigorosos só das atividades, não do dia inteiro.",
|
|
62406
|
+
"w.loadSafety": "Segurança da carga",
|
|
62407
|
+
"ls.weekLoad": "Carga · últimos 7 dias",
|
|
62408
|
+
"ls.monotony": "Monotonia · 7 dias",
|
|
62409
|
+
"ls.monotonyFlag": "Acima de 2,0",
|
|
62410
|
+
"ls.strain": "Strain · 7 dias",
|
|
62411
|
+
"ls.flagged": "Semanas acima de +10%",
|
|
62412
|
+
"ls.load": "Carga semanal",
|
|
62413
|
+
"ls.ramp": "Variação em relação à semana anterior (%)",
|
|
62414
|
+
"ls.note": "Todas as atividades. As semanas vermelhas subiram mais de 10%; uma semana em andamento não é sinalizada. Monotonia e strain (Foster) são estimativas.",
|
|
62415
|
+
"ls.estimated": "Algumas cargas são estimativas: a atividade não tem carga Garmin.",
|
|
62416
|
+
"w.loadVsRecovery": "Carga e recuperação",
|
|
62417
|
+
"lvr.hrv": "HRV (ms)",
|
|
62418
|
+
"lvr.note": "Carga diária por tipo (barras) e HRV noturna (linha).",
|
|
62419
|
+
"w.calendar": "Consistência",
|
|
62420
|
+
"cal.minutes": "{n} min",
|
|
62421
|
+
"cal.rest": "Dia de descanso",
|
|
62422
|
+
"cal.label": "Dias de treino, por semana; as teclas de seta movem a seleção",
|
|
62423
|
+
"cal.less": "Menos",
|
|
62424
|
+
"cal.more": "Mais",
|
|
62425
|
+
"m.label": "Medida",
|
|
62426
|
+
skip: "Pular para o quadro",
|
|
62427
|
+
"w.hrv": "HRV",
|
|
62428
|
+
"hrv.low": "Limite inferior da faixa",
|
|
62429
|
+
"hrv.band": "Faixa equilibrada",
|
|
62430
|
+
"hrv.nightly": "HRV noturna",
|
|
62431
|
+
"hrv.avg": "Média de 7 dias",
|
|
62432
|
+
rec14: "Últimos 14 dias: a janela de semanas não os altera.",
|
|
62433
|
+
"rec.board": "Os widgets de recuperação mostram os últimos 14 dias: a janela de semanas não os altera.",
|
|
62434
|
+
"w.restingHr": "FC em repouso",
|
|
62435
|
+
"w.sleep": "Sono",
|
|
62436
|
+
"sl.deep": "Profundo",
|
|
62437
|
+
"sl.light": "Leve",
|
|
62438
|
+
"sl.rem": "REM",
|
|
62439
|
+
"sl.awake": "Acordado",
|
|
62440
|
+
"sl.score": "Pontuação",
|
|
62441
|
+
"w.sleepNeed": "Necessidade de sono",
|
|
62442
|
+
"sn.gap": "Abaixo da necessidade · média",
|
|
62443
|
+
"sn.actual": "Dormido",
|
|
62444
|
+
"sn.need": "Necessidade",
|
|
62445
|
+
"w.bedtime": "Hora de dormir",
|
|
62446
|
+
"bt.spread": "Variação do horário",
|
|
62447
|
+
"w.bodyBattery": "Body Battery",
|
|
62448
|
+
"bb.high": "Máximo",
|
|
62449
|
+
"bb.low": "Mínimo",
|
|
62450
|
+
"w.activities": "Atividades",
|
|
62451
|
+
"col.date": "Data",
|
|
62452
|
+
"col.type": "Tipo",
|
|
62453
|
+
"col.name": "Nome",
|
|
62454
|
+
"col.distance": "Distância",
|
|
62455
|
+
"col.time": "Tempo",
|
|
62456
|
+
"col.pace": "Ritmo / velocidade",
|
|
62457
|
+
"col.hr": "FC média",
|
|
62458
|
+
"col.ascent": "Subida",
|
|
62459
|
+
"col.race": "Prova",
|
|
62460
|
+
"laps.show": "voltas",
|
|
62461
|
+
"lap.n": "Volta",
|
|
62462
|
+
"lap.intensity": "Intensidade",
|
|
62463
|
+
"act.more": "Mostrar todas ({n})",
|
|
62464
|
+
"act.less": "Mostrar menos",
|
|
62465
|
+
"one.session": "Só uma sessão até agora: as tendências começam com 2 sessões.",
|
|
62466
|
+
"w.typeKpis": "Resumo",
|
|
62467
|
+
"tk.sessions": "Sessões",
|
|
62468
|
+
"tk.time": "Tempo",
|
|
62469
|
+
"tk.avg": "{v} por sessão",
|
|
62470
|
+
"tk.distance": "Distância",
|
|
62471
|
+
"tk.speed": "Velocidade média",
|
|
62472
|
+
"tk.pace": "Ritmo médio",
|
|
62473
|
+
"tk.ascent": "Subida",
|
|
62474
|
+
"tk.hr": "FC média",
|
|
62475
|
+
"tk.peak": "Pico {v} bpm",
|
|
62476
|
+
"tk.load": "Carga de treino Garmin",
|
|
62477
|
+
"tk.share": "{v}% de toda a carga",
|
|
62478
|
+
"w.typeWeekly": "Por semana",
|
|
62479
|
+
"w.hrZones": "Tempo nas zonas de FC",
|
|
62480
|
+
"zones.approximate": "Aproximado: algumas atividades não têm tempo por zona, então todos os seus minutos contam na zona da sua FC média.",
|
|
62481
|
+
"form.cadence": "Cadência",
|
|
62482
|
+
"form.stride": "Passada",
|
|
62483
|
+
"form.gct": "Contato com o solo",
|
|
62484
|
+
"form.vertOsc": "Oscilação vertical",
|
|
62485
|
+
"form.vertRatio": "Relação vertical",
|
|
62486
|
+
"form.power": "Potência",
|
|
62487
|
+
"form.normPower": "Potência normalizada",
|
|
62488
|
+
"w.bestEfforts": "Melhores esforços",
|
|
62489
|
+
"be.distance": "Distância",
|
|
62490
|
+
"be.activity": "Atividade",
|
|
62491
|
+
"w.races": "Provas",
|
|
62492
|
+
"races.none": "Nenhuma prova nas semanas selecionadas.",
|
|
62493
|
+
"te.aerobic": "TE aeróbico",
|
|
62494
|
+
"te.anaerobic": "TE anaeróbico",
|
|
62495
|
+
"w.aerobicFitness": "Condicionamento aeróbico",
|
|
62496
|
+
"af.runs": "Corridas (as mais antigas mais claras)",
|
|
62497
|
+
"af.pace": "Ritmo (min{unit})",
|
|
62498
|
+
"af.weekPace": "Ritmo semanal",
|
|
62499
|
+
"af.efficiency": "Eficiência",
|
|
62500
|
+
"af.effUnit": "m por batimento",
|
|
62501
|
+
"w.intensityShare": "Corrida leve e intensa",
|
|
62502
|
+
"is.easy": "Z1-2 (leve)",
|
|
62503
|
+
"is.hard": "Z4-5 (intensa)",
|
|
62504
|
+
"w.runningForm": "Técnica de corrida",
|
|
62505
|
+
"w.runWalk": "Corrida e caminhada",
|
|
62506
|
+
"rw.none": "Nenhuma sessão de corrida e caminhada nas semanas selecionadas.",
|
|
62507
|
+
"rw.runPace": "Ritmo de corrida",
|
|
62508
|
+
"rw.pace": "Ritmo geral",
|
|
62509
|
+
"rw.walk": "Tempo de caminhada",
|
|
62510
|
+
"w.trainingEffect": "Efeito do treino",
|
|
62511
|
+
"w.racePredictor": "Previsão de provas",
|
|
62512
|
+
"rp.none": "Nenhum melhor esforço de 5 km ou mais nas semanas selecionadas.",
|
|
62513
|
+
"rp.note": "Estimativa (Riegel) a partir de {d} em {time}, em {date}.",
|
|
62514
|
+
"w.longRuns": "Corridas longas e divisão do ritmo",
|
|
62515
|
+
"lr.share": "Parte da corrida mais longa na distância semanal",
|
|
62516
|
+
"lr.ascent": "Subida semanal",
|
|
62517
|
+
"lr.note": "Semanas vermelhas: a corrida mais longa passa de 35% da distância da semana. A tabela mostra provas e corridas acima de 15 km com voltas.",
|
|
62518
|
+
"lr.first": "Primeira metade",
|
|
62519
|
+
"lr.second": "Segunda metade",
|
|
62520
|
+
"lr.split": "Divisão",
|
|
62521
|
+
"lr.gap": "Ritmo ajustado à inclinação",
|
|
62522
|
+
"lr.positive": "positiva",
|
|
62523
|
+
"lr.negative": "negativa",
|
|
62524
|
+
"lr.even": "equilibrada",
|
|
62525
|
+
"w.setsPerCategory": "Séries por movimento",
|
|
62526
|
+
"ex.sets": "Séries concluídas",
|
|
62527
|
+
"w.muscleBalance": "Equilíbrio entre grupos musculares",
|
|
62528
|
+
"mb.pushPull": "Séries de empurrar : puxar",
|
|
62529
|
+
"mb.avg": "Média de séries por semana",
|
|
62530
|
+
"mb.perWeek": "séries por semana",
|
|
62531
|
+
"mb.note": "Uma série conta 1 para cada grupo muscular primário e 0,5 para cada secundário. Faixa usual: 10-20 séries por semana (amarelo abaixo, vermelho acima).",
|
|
62532
|
+
"w.exercises": "Exercícios",
|
|
62533
|
+
"ex.groups": "Grupos musculares",
|
|
62534
|
+
"ex.search": "Pesquisar exercícios",
|
|
62535
|
+
"ex.name": "Exercício",
|
|
62536
|
+
"ex.sessions": "Sessões",
|
|
62537
|
+
"ex.last": "Última",
|
|
62538
|
+
"ex.top": "Última série mais pesada",
|
|
62539
|
+
"ex.best": "Melhor 1RM estimado",
|
|
62540
|
+
"ex.change": "Variação",
|
|
62541
|
+
"ex.trend": "Tendência",
|
|
62542
|
+
"ex.groupRow": "{n} exercício · {sets} séries concluídas|{n} exercícios · {sets} séries concluídas",
|
|
62543
|
+
"ex.reps": "{n} repetição|{n} repetições",
|
|
62544
|
+
"ex.rest": "descanso mediano {v}",
|
|
62545
|
+
"ex.weight": "Série mais pesada ({unit})",
|
|
62546
|
+
"ex.e1rm": "1RM estimado ({unit})",
|
|
62547
|
+
"ex.volume": "Volume ({unit})",
|
|
62548
|
+
"ex.maxReps": "Máximo de repetições",
|
|
62549
|
+
"ex.totalReps": "Total de repetições",
|
|
62550
|
+
"ex.longest": "Série mais longa (s)",
|
|
62551
|
+
"ex.restAfter": "descanso {v}",
|
|
62552
|
+
"ex.notDone": "não feita",
|
|
62553
|
+
"w.strengthSessions": "Sessões",
|
|
62554
|
+
"ss.count": "{n} exercício|{n} exercícios",
|
|
62555
|
+
"mg.chest": "Peito",
|
|
62556
|
+
"mg.back": "Costas",
|
|
62557
|
+
"mg.shoulders": "Ombros",
|
|
62558
|
+
"mg.biceps": "Bíceps",
|
|
62559
|
+
"mg.triceps": "Tríceps",
|
|
62560
|
+
"mg.forearms": "Antebraços",
|
|
62561
|
+
"mg.quads": "Quadríceps",
|
|
62562
|
+
"mg.hamstrings": "Posteriores da coxa",
|
|
62563
|
+
"mg.glutes": "Glúteos",
|
|
62564
|
+
"mg.calves": "Panturrilhas",
|
|
62565
|
+
"mg.abs": "Abdominais",
|
|
62566
|
+
"mg.hips": "Quadris",
|
|
62567
|
+
"mg.other": "Outros",
|
|
62568
|
+
"w.walkDetails": "Ritmo e caminhadas",
|
|
62569
|
+
"wd.steps": "Passos",
|
|
62570
|
+
"wd.battery": "Variação do Body Battery",
|
|
62571
|
+
"wd.goal": "Meta de passos",
|
|
62572
|
+
"wd.floors": "Andares",
|
|
62573
|
+
"w.steps": "Passos diários",
|
|
62574
|
+
"w.hikeVertical": "Velocidade vertical",
|
|
62575
|
+
"hv.vam": "Subida ({unit}) por hora",
|
|
62576
|
+
"w.bikeDetails": "Pedaladas",
|
|
62577
|
+
"bd.outdoor": "Ao ar livre",
|
|
62578
|
+
"bd.indoor": "Indoor",
|
|
62579
|
+
"bd.rides": "{n} pedalada|{n} pedaladas",
|
|
62580
|
+
"bd.where": "Onde",
|
|
62581
|
+
"w.swimDetails": "Sessões de natação",
|
|
62582
|
+
"sw.pace": "Ritmo",
|
|
62583
|
+
"sw.lengths": "Comprimentos",
|
|
62584
|
+
"sw.strokes": "Braçadas",
|
|
62585
|
+
"sw.pool": "Piscina",
|
|
62586
|
+
"w.racketDetails": "Sessões e intensidade",
|
|
62587
|
+
"pd.peak": "FC de pico",
|
|
62588
|
+
"pd.z3": "Tempo em Z3+",
|
|
62589
|
+
"pd.z45": "Parte em Z4-5",
|
|
62590
|
+
"pd.kcal": "kcal por hora",
|
|
62591
|
+
"pd.load": "Carga / corrida de mesma duração",
|
|
62592
|
+
"pd.note": "A segunda carga é uma estimativa: a carga Garmin média por minuto das suas corridas, vezes a duração da sessão.",
|
|
62593
|
+
"col.sport": "Esporte",
|
|
62594
|
+
"sport.tennis": "Tênis",
|
|
62595
|
+
"sport.padel": "Padel",
|
|
62596
|
+
"sport.pickleball": "Pickleball",
|
|
62597
|
+
"sport.racquetball": "Raquetebol",
|
|
62598
|
+
"sport.squash": "Squash",
|
|
62599
|
+
"sport.badminton": "Badminton",
|
|
62600
|
+
"sport.tableTennis": "Tênis de mesa",
|
|
62601
|
+
"sport.platformTennis": "Platform tennis"
|
|
62602
|
+
}
|
|
62603
|
+
};
|
|
62604
|
+
// src/page/i18n/locales/en.json
|
|
62605
|
+
var en_default = {
|
|
60715
62606
|
title: "Training + Recovery",
|
|
60716
62607
|
headline: "Training load, recovery and progress from Garmin Connect",
|
|
60717
62608
|
na: "n/a",
|
|
@@ -60725,6 +62616,7 @@ const en = {
|
|
|
60725
62616
|
"footer.license": "MIT License",
|
|
60726
62617
|
"menu.language": "Language",
|
|
60727
62618
|
"lang.auto": "Auto",
|
|
62619
|
+
"lang.missing": "{language} is not in this page yet. Ask your assistant to add it.",
|
|
60728
62620
|
"themeGroup.auto": "Auto",
|
|
60729
62621
|
"themeGroup.dark": "Dark",
|
|
60730
62622
|
"themeGroup.light": "Light",
|
|
@@ -60745,6 +62637,13 @@ const en = {
|
|
|
60745
62637
|
"window.all": "All",
|
|
60746
62638
|
"window.from": "From week",
|
|
60747
62639
|
"window.to": "To week",
|
|
62640
|
+
"names.label": "Activity names",
|
|
62641
|
+
"names.all": "All",
|
|
62642
|
+
"names.count": "{n} chosen|{n} chosen",
|
|
62643
|
+
"names.search": "Search names",
|
|
62644
|
+
"names.noMatch": "No name matches.",
|
|
62645
|
+
"names.remove": "Remove {name}",
|
|
62646
|
+
"names.clear": "Show all",
|
|
60748
62647
|
"type.all": "All",
|
|
60749
62648
|
"type.recovery": "Recovery",
|
|
60750
62649
|
"type.running": "Run",
|
|
@@ -60876,6 +62775,8 @@ const en = {
|
|
|
60876
62775
|
"add.close": "Close",
|
|
60877
62776
|
"add.hiddenGroup": "Hidden on this board",
|
|
60878
62777
|
"add.none": "Every widget is on this board.",
|
|
62778
|
+
"add.search": "Search widgets",
|
|
62779
|
+
"add.noMatch": "No widget matches.",
|
|
60879
62780
|
"copy.title": "Copy layout",
|
|
60880
62781
|
"copy.hint": "This layout is saved in this browser only. To keep it in the page, paste this text to your assistant and ask it to save the dashboard layout.",
|
|
60881
62782
|
"copy.copy": "Copy",
|
|
@@ -60889,7 +62790,7 @@ const en = {
|
|
|
60889
62790
|
"notes.none": "No notes.",
|
|
60890
62791
|
"notes.disclaimer": "Training suggestions for the data up to {date}. Not medical advice.",
|
|
60891
62792
|
"w.kpis": "Overview",
|
|
60892
|
-
"kpi.totals": "{
|
|
62793
|
+
"kpi.totals": "{distance} · {time} · {n} session|{distance} · {time} · {n} sessions",
|
|
60893
62794
|
"kpi.distance": "Distance · last 7 days",
|
|
60894
62795
|
"kpi.time": "Time · last 7 days",
|
|
60895
62796
|
"kpi.vs4w": "vs 4-week average",
|
|
@@ -61048,7 +62949,7 @@ const en = {
|
|
|
61048
62949
|
"te.anaerobic": "Anaerobic TE",
|
|
61049
62950
|
"w.aerobicFitness": "Aerobic fitness",
|
|
61050
62951
|
"af.runs": "Runs (older paler)",
|
|
61051
|
-
"af.pace": "Pace (min
|
|
62952
|
+
"af.pace": "Pace (min{unit})",
|
|
61052
62953
|
"af.weekPace": "Weekly pace",
|
|
61053
62954
|
"af.efficiency": "Efficiency",
|
|
61054
62955
|
"af.effUnit": "m per heartbeat",
|
|
@@ -61066,9 +62967,9 @@ const en = {
|
|
|
61066
62967
|
"rp.none": "No best effort of 5 km or more in the selected weeks.",
|
|
61067
62968
|
"rp.note": "Estimate (Riegel) from the {d} in {time} on {date}.",
|
|
61068
62969
|
"w.longRuns": "Long runs and pace split",
|
|
61069
|
-
"lr.share": "Longest run share of weekly
|
|
62970
|
+
"lr.share": "Longest run share of weekly distance",
|
|
61070
62971
|
"lr.ascent": "Weekly ascent",
|
|
61071
|
-
"lr.note": "Red weeks: the longest run is more than 35% of the week's
|
|
62972
|
+
"lr.note": "Red weeks: the longest run is more than 35% of the week's distance. The table shows races and runs over 15 km with laps.",
|
|
61072
62973
|
"lr.first": "First half",
|
|
61073
62974
|
"lr.second": "Second half",
|
|
61074
62975
|
"lr.split": "Split",
|
|
@@ -61096,9 +62997,9 @@ const en = {
|
|
|
61096
62997
|
"ex.groupRow": "{n} exercise · {sets} completed sets|{n} exercises · {sets} completed sets",
|
|
61097
62998
|
"ex.reps": "{n} rep|{n} reps",
|
|
61098
62999
|
"ex.rest": "median rest {v}",
|
|
61099
|
-
"ex.weight": "Top set (
|
|
61100
|
-
"ex.e1rm": "Est. 1RM (
|
|
61101
|
-
"ex.volume": "Volume (
|
|
63000
|
+
"ex.weight": "Top set ({unit})",
|
|
63001
|
+
"ex.e1rm": "Est. 1RM ({unit})",
|
|
63002
|
+
"ex.volume": "Volume ({unit})",
|
|
61102
63003
|
"ex.maxReps": "Max reps",
|
|
61103
63004
|
"ex.totalReps": "Total reps",
|
|
61104
63005
|
"ex.longest": "Longest set (s)",
|
|
@@ -61126,7 +63027,7 @@ const en = {
|
|
|
61126
63027
|
"wd.floors": "Floors",
|
|
61127
63028
|
"w.steps": "Daily steps",
|
|
61128
63029
|
"w.hikeVertical": "Vertical speed",
|
|
61129
|
-
"hv.vam": "Vertical
|
|
63030
|
+
"hv.vam": "Vertical {unit} per hour",
|
|
61130
63031
|
"w.bikeDetails": "Rides",
|
|
61131
63032
|
"bd.outdoor": "Outdoor",
|
|
61132
63033
|
"bd.indoor": "Indoor",
|
|
@@ -61376,25 +63277,28 @@ const THEMES = [
|
|
|
61376
63277
|
const WIDGET_API = `A custom widget is the body of a JavaScript function (el, ctx). Draw into el. The page runs it like a built-in widget, inside try/catch: an error shows in its tile only.
|
|
61377
63278
|
|
|
61378
63279
|
ctx gives:
|
|
61379
|
-
- ctx.data: the getDashboardData result (activities, previousActivities, hrv, recovery, zones, fitnessAge, sync) and notes (plain text).
|
|
63280
|
+
- ctx.data: the getDashboardData result (activities, previousActivities, hrv, recovery, zones, fitnessAge, sync, units) and notes (plain text). The data is always metric (km, m, kg, seconds per km); units is the measurement system of the Garmin profile (metric, statute_us or statute_uk).
|
|
61380
63281
|
- ctx.type: "all" or the board the widget shows: running, strengthTraining, walking, hiking, cycling, swimming, racket, generic (Other), or another SDK sport name such as alpineSkiing. Each activity has type (the Garmin type key) and sport (the SDK sport name).
|
|
61381
63282
|
- ctx.from, ctx.to: Mondays of the first and last week of the window; ctx.weeks: every Monday in it.
|
|
61382
|
-
- ctx.activities: the activities of the window and of ctx.type. ctx.allActivities: every type. ctx.ofType(list): the same type filter for any list.
|
|
63283
|
+
- ctx.activities: the activities of the window and of ctx.type. ctx.allActivities: every type. ctx.ofType(list): the same type filter for any list. When the user chooses activity names on the page, ctx.activities and ctx.ofType keep only the activities with those names; ctx.allActivities and ctx.data keep every activity.
|
|
61383
63284
|
- ctx.t(key, params): a page text; ctx.lang: the page language.
|
|
61384
|
-
- ctx.
|
|
63285
|
+
- ctx.text(key, params): a text of the widget's own texts in the page language, else in English, with its {placeholders} filled from params.
|
|
63286
|
+
- ctx.fmt: num(v, digits), pct(v), day("yyyy-MM-dd"), longDay(d), hms(seconds), hours(seconds), na (the text for a missing value). In the profile's units, from metric data: dist(km, digits), pace(secondsPerKm), speed(kmh), elev(m), mass(kg), swimPace(secondsPer100m). ctx.fmt.to.dist/pace/speed/elev/mass/swim(value) give the number only, for sums and charts; ctx.fmt.unit.dist/pace/speed/elev/mass/swim give the unit names, for axis titles. Show every distance, pace, speed, elevation and weight with these, never with a fixed "km" or "kg".
|
|
61385
63287
|
- ctx.calc: groupOf(activity) (its board), sportOf(typeKey), seconds("1:02:03"), sessionSeconds(activity), dayOf(activity), mondayOf(day), weekly(activities, weeks, a => value), loadOf(activity, maxHR), loadRatio(activities, day, maxHR), dailyLoads(activities, from, to, maxHR), mean, sd, rolling(values, n), change(now, before), zonesOf(ctx.data.zones), zoneSeconds(activity, floors), paceSeconds(activity), efficiency(activity), epley(kg, reps), riegel(seconds, km, km), exerciseStats(activities), muscleSets(activities, weeks), swimPace(activity), verticalPerHour(activity).
|
|
61386
63288
|
- ctx.chart(el, config, heightPx): a Chart.js 4 chart that follows the theme, with tooltips. Chart types: bar, line, scatter, doughnut, pie, with the category and linear scales.
|
|
61387
63289
|
- ctx.color(token): a theme colour: text, muted, accent, good, bad, warn, line, panel, panel2, or a board (running, strengthTraining, walking, hiking, cycling, swimming, racket, generic); another sport gets the generic colour.
|
|
61388
63290
|
- ctx.empty(el, message): a short message in place of the content.
|
|
61389
63291
|
|
|
63292
|
+
Texts: give every text that the widget shows in its texts, by language code and key: { en: { title: "...", best: "Best: {value}" }, it: { ... } }. title is the widget's title. Write English and each language of pageLanguages (from editDashboard), with the same keys and {placeholders}. Draw texts with ctx.text(key, params), never as fixed strings in the code. A missing text shows in English.
|
|
63293
|
+
|
|
61390
63294
|
Rules: build elements with document.createElement and set text with textContent, never innerHTML with data. Use only values in ctx.data; show ctx.fmt.na for a missing value; label an estimate as one. Keep the code short. Requests from the page are blocked: fetch, XMLHttpRequest, and images, scripts and styles from the web.
|
|
61391
63295
|
|
|
61392
|
-
Example (the longest ride of the window):
|
|
63296
|
+
Example (the longest ride of the window), with texts { en: { title: "Longest ride", best: "{distance} on {day}" } }:
|
|
61393
63297
|
const rides = ctx.activities.filter((a) => ctx.calc.groupOf(a) === "cycling" && typeof a.km === "number");
|
|
61394
63298
|
if (rides.length === 0) return ctx.empty(el);
|
|
61395
63299
|
const best = rides.reduce((x, y) => (y.km > x.km ? y : x));
|
|
61396
63300
|
const p = document.createElement("p");
|
|
61397
|
-
p.textContent =
|
|
63301
|
+
p.textContent = ctx.text("best", { distance: ctx.fmt.dist(best.km), day: ctx.fmt.day(best.start.slice(0, 10)) });
|
|
61398
63302
|
el.append(p);`;
|
|
61399
63303
|
// src/prompts.ts
|
|
61400
63304
|
/**
|
|
@@ -61424,6 +63328,9 @@ function registerPrompts(server) {
|
|
|
61424
63328
|
const layout = layoutOf(arg(args.layout) === "" ? args.weeks : args.layout);
|
|
61425
63329
|
const text = layout === "freeform" ? renderDashboard(freeform, w) : render(renderDashboard(build, w), {
|
|
61426
63330
|
layout,
|
|
63331
|
+
title: en_default.title,
|
|
63332
|
+
headline: en_default.headline,
|
|
63333
|
+
builtIn: Object.keys(BUILT_IN).map((code) => `${languageName(code)} (${code})`).join(", "),
|
|
61427
63334
|
layoutNote: layout === "blank" ? "\nThe page is an empty grid: tell me to add widgets with \"Edit layout\" and \"Add widget\"." : ""
|
|
61428
63335
|
});
|
|
61429
63336
|
return result(dashboardDescription, text);
|
|
@@ -61472,10 +63379,10 @@ function registerPrompts(server) {
|
|
|
61472
63379
|
}, (args) => result(setupDescription, render(setupPrompt, {
|
|
61473
63380
|
weeks: String(weeks(args.weeks)),
|
|
61474
63381
|
today: localToday(),
|
|
61475
|
-
title:
|
|
61476
|
-
headline:
|
|
63382
|
+
title: en_default.title,
|
|
63383
|
+
headline: en_default.headline,
|
|
61477
63384
|
languages: ["en", ...Object.keys(BUILT_IN)].join(", "),
|
|
61478
|
-
languageNames: ["English", ...Object.keys(BUILT_IN).map(languageName)]
|
|
63385
|
+
languageNames: new Intl.ListFormat("en").format(["English", ...Object.keys(BUILT_IN).map(languageName)]),
|
|
61479
63386
|
themes: THEMES.map((t) => t.id).join(", ")
|
|
61480
63387
|
})));
|
|
61481
63388
|
const languageDescription = "Add a language to the training dashboard (the assistant translates its texts)";
|
|
@@ -61557,6 +63464,8 @@ function weeks(v) {
|
|
|
61557
63464
|
if (!/^[+-]?\d+$/.test(s)) return DEFAULT_WEEKS;
|
|
61558
63465
|
return Math.min(Math.max(Number(s), 1), MAX_WEEKS);
|
|
61559
63466
|
}
|
|
63467
|
+
/** The user's custom widgets, in the data dir: every page from the template gets them. */
|
|
63468
|
+
const LIBRARY_FILE = "widgets.json";
|
|
61560
63469
|
const MAX_NOTES = 5;
|
|
61561
63470
|
const REUSE_MS = 9e5;
|
|
61562
63471
|
const marker = (b) => new RegExp(`/\\*\\s*garmin-${b}:start\\s*\\*/([\\s\\S]*?)/\\*\\s*garmin-${b}:end\\s*\\*/`);
|
|
@@ -61632,8 +63541,12 @@ function filterData(data, s) {
|
|
|
61632
63541
|
if (no.has("zones")) delete out.zones;
|
|
61633
63542
|
return out;
|
|
61634
63543
|
}
|
|
61635
|
-
/** The built-in languages that the settings choose, by code. */
|
|
61636
|
-
|
|
63544
|
+
/** The built-in languages that the settings choose, by code; without a choice, the one of the profile locale. */
|
|
63545
|
+
function languagesOf(s, locale) {
|
|
63546
|
+
const own = typeof locale === "string" ? [locale, locale.slice(0, 2)] : [];
|
|
63547
|
+
const keep = s.languages ?? [own.find((code) => code in BUILT_IN)];
|
|
63548
|
+
return Object.fromEntries(Object.entries(BUILT_IN).filter(([code]) => keep.includes(code)));
|
|
63549
|
+
}
|
|
61637
63550
|
/**
|
|
61638
63551
|
* A page from the template with the settings: its settings block, its title, and the CSS of the chosen
|
|
61639
63552
|
* themes. Only rules for one theme alone go: a rule that lists more selectors is kept as it is.
|
|
@@ -61647,11 +63560,13 @@ function styled(page, s) {
|
|
|
61647
63560
|
}
|
|
61648
63561
|
/** The page with the built-in languages and the data that the settings keep, and the escaped notes. */
|
|
61649
63562
|
function withData(page, data, s, notes) {
|
|
61650
|
-
const next = writeBlock(page, "languages", languagesOf(s));
|
|
63563
|
+
const next = writeBlock(page, "languages", languagesOf(s, data.locale));
|
|
61651
63564
|
const kept = s.exclude?.includes("notes") ? [] : notes;
|
|
61652
63565
|
return writeBlock(next, "data", {
|
|
61653
63566
|
...filterData(data, s),
|
|
61654
63567
|
v: 3,
|
|
63568
|
+
server: version,
|
|
63569
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61655
63570
|
notes: kept.map(escapeHtml)
|
|
61656
63571
|
});
|
|
61657
63572
|
}
|
|
@@ -61732,13 +63647,14 @@ var DashboardWriter = class {
|
|
|
61732
63647
|
this.#queue = run.catch(() => {});
|
|
61733
63648
|
return run;
|
|
61734
63649
|
}
|
|
61735
|
-
async #write({ weeks, notes = [], layout, page, path, settings }) {
|
|
63650
|
+
async #write({ weeks, notes = [], layout, page, path, settings, onlyExisting }) {
|
|
61736
63651
|
if (layout && page) throw new Error("Give either `layout` (a page from the template) or `page` (your own page), not both");
|
|
61737
63652
|
if (page !== void 0 && !marker("data").test(page)) throw new Error("`page` has no /* garmin-data:start */ ... /* garmin-data:end */ block");
|
|
61738
63653
|
if (notes.length > MAX_NOTES) throw new Error(`At most ${MAX_NOTES} notes`);
|
|
61739
63654
|
const asked = settings === void 0 ? void 0 : checkSettings(settings);
|
|
61740
63655
|
const target = this.#target(path);
|
|
61741
63656
|
const old = await readExisting(target);
|
|
63657
|
+
if (onlyExisting && old === void 0) throw new Error(`${target} does not exist: make it first with the training-dashboard prompt`);
|
|
61742
63658
|
let oldSettings;
|
|
61743
63659
|
let unreadable = false;
|
|
61744
63660
|
try {
|
|
@@ -61773,6 +63689,7 @@ var DashboardWriter = class {
|
|
|
61773
63689
|
}
|
|
61774
63690
|
if (Object.keys(own).length > 0 && !marker("settings").test(next)) throw new Error("Settings need a page from the template; this page has no garmin-settings block");
|
|
61775
63691
|
if (page === void 0 && next !== old) next = styled(next, own);
|
|
63692
|
+
if (page === void 0) next = await this.#withLibrary(next, old);
|
|
61776
63693
|
const raw = JSON.parse(await this.#data(weeks ?? weeksOf(old)));
|
|
61777
63694
|
next = withData(next, raw, own, notes);
|
|
61778
63695
|
let backup;
|
|
@@ -61787,7 +63704,11 @@ var DashboardWriter = class {
|
|
|
61787
63704
|
...backup ? { backup } : {},
|
|
61788
63705
|
notes: own.exclude?.includes("notes") ? 0 : notes.length,
|
|
61789
63706
|
digest: digest(filterData(raw, own)),
|
|
61790
|
-
|
|
63707
|
+
pageLanguages: pageLanguages(next, own),
|
|
63708
|
+
settings: own,
|
|
63709
|
+
...hasLanguages(next) ? { languages: missingTexts(next) } : {},
|
|
63710
|
+
...page === void 0 && missingLanguage(next, raw.locale) ? { missingProfileLanguage: raw.locale } : {},
|
|
63711
|
+
...page === void 0 ? withMissing(widgetGaps(next, pageLanguages(next, own))) : {}
|
|
61791
63712
|
};
|
|
61792
63713
|
}
|
|
61793
63714
|
/** Changes the layout, the custom widgets or the languages of a page from the template; no options only reads. */
|
|
@@ -61803,6 +63724,7 @@ var DashboardWriter = class {
|
|
|
61803
63724
|
let page = old;
|
|
61804
63725
|
let layout;
|
|
61805
63726
|
let widgets;
|
|
63727
|
+
let library;
|
|
61806
63728
|
let texts;
|
|
61807
63729
|
let settings;
|
|
61808
63730
|
try {
|
|
@@ -61815,6 +63737,8 @@ var DashboardWriter = class {
|
|
|
61815
63737
|
} catch (e) {
|
|
61816
63738
|
throw new Error(`${e.message} in ${target}`, { cause: e });
|
|
61817
63739
|
}
|
|
63740
|
+
library = await this.#library(widgets);
|
|
63741
|
+
widgets = merged(widgets, library);
|
|
61818
63742
|
let layoutChanged = false;
|
|
61819
63743
|
if (o.layout !== void 0) {
|
|
61820
63744
|
layout = parseLayout(o.layout);
|
|
@@ -61823,8 +63747,26 @@ var DashboardWriter = class {
|
|
|
61823
63747
|
for (const w of o.widgets ?? []) {
|
|
61824
63748
|
const clean = checkWidget(w);
|
|
61825
63749
|
widgets = [...widgets.filter((x) => x.id !== clean.id), clean];
|
|
63750
|
+
library = [...library.filter((x) => x.id !== clean.id), clean];
|
|
63751
|
+
}
|
|
63752
|
+
for (const [id, byLang] of Object.entries(o.widgetTexts ?? {})) {
|
|
63753
|
+
const w = widgets.find((x) => x.id === id);
|
|
63754
|
+
if (!w) throw new Error(`Unknown custom widget ${id}`);
|
|
63755
|
+
const own = { ...w.texts };
|
|
63756
|
+
for (const [lang, entries] of Object.entries(byLang)) own[lang] = {
|
|
63757
|
+
...own[lang],
|
|
63758
|
+
...entries
|
|
63759
|
+
};
|
|
63760
|
+
const clean = checkWidget({
|
|
63761
|
+
...w,
|
|
63762
|
+
texts: own
|
|
63763
|
+
});
|
|
63764
|
+
widgets = widgets.map((x) => x.id === id ? clean : x);
|
|
63765
|
+
library = library.map((x) => x.id === id ? clean : x);
|
|
61826
63766
|
}
|
|
61827
|
-
|
|
63767
|
+
const forget = new Set(o.forgetWidgets);
|
|
63768
|
+
library = library.filter((x) => !forget.has(x.id));
|
|
63769
|
+
for (const id of [...o.removeWidgets ?? [], ...forget]) {
|
|
61828
63770
|
widgets = widgets.filter((x) => x.id !== id);
|
|
61829
63771
|
for (const [board, list] of Object.entries(layout.boards)) if (list.some((i) => i.widget === id)) {
|
|
61830
63772
|
layout = withBoard(layout, board, list.filter((i) => i.widget !== id));
|
|
@@ -61851,7 +63793,8 @@ var DashboardWriter = class {
|
|
|
61851
63793
|
if (!Object.hasOwn(texts, lang)) throw new Error(`The page has no added texts for ${lang}: built-in languages are chosen with writeDashboard settings.languages`);
|
|
61852
63794
|
delete texts[lang];
|
|
61853
63795
|
}
|
|
61854
|
-
const changed = layoutChanged || o.widgets !== void 0 || o.removeWidgets !== void 0 || o.languages !== void 0 || o.removeLanguages !== void 0;
|
|
63796
|
+
const changed = layoutChanged || o.widgets !== void 0 || o.removeWidgets !== void 0 || o.forgetWidgets !== void 0 || o.widgetTexts !== void 0 || o.languages !== void 0 || o.removeLanguages !== void 0;
|
|
63797
|
+
if (o.widgets !== void 0 || o.forgetWidgets !== void 0 || o.widgetTexts !== void 0) await this.#saveLibrary(library);
|
|
61855
63798
|
if (changed) {
|
|
61856
63799
|
if (layoutChanged) layout = {
|
|
61857
63800
|
...layout,
|
|
@@ -61866,24 +63809,59 @@ var DashboardWriter = class {
|
|
|
61866
63809
|
path: target,
|
|
61867
63810
|
changed,
|
|
61868
63811
|
layout,
|
|
61869
|
-
customWidgets: widgets.map((
|
|
61870
|
-
id,
|
|
61871
|
-
title,
|
|
61872
|
-
span: span
|
|
61873
|
-
...types ? { types } : {}
|
|
63812
|
+
customWidgets: widgets.map((w) => ({
|
|
63813
|
+
id: w.id,
|
|
63814
|
+
title: w.title,
|
|
63815
|
+
span: spanOf(w.span),
|
|
63816
|
+
...w.types ? { types: w.types } : {},
|
|
63817
|
+
...w.texts ? { texts: w.texts } : {},
|
|
63818
|
+
library: library.some((l) => l.id === w.id)
|
|
61874
63819
|
})),
|
|
63820
|
+
pageLanguages: pageLanguages(page, settings),
|
|
61875
63821
|
languages: missingTexts(page),
|
|
61876
63822
|
catalogue: CATALOGUE.filter((c) => !c.needs || !settings.exclude?.includes(c.needs)).map((c) => ({
|
|
61877
63823
|
id: c.id,
|
|
61878
|
-
title:
|
|
63824
|
+
title: en_default[c.title],
|
|
61879
63825
|
...c.types ? { types: c.types } : {},
|
|
61880
63826
|
span: c.span,
|
|
61881
63827
|
about: ABOUT[c.id]
|
|
61882
63828
|
})),
|
|
61883
63829
|
widgetApi: WIDGET_API,
|
|
61884
|
-
...o.englishTexts ? { englishTexts:
|
|
63830
|
+
...o.englishTexts ? { englishTexts: en_default } : {}
|
|
61885
63831
|
};
|
|
61886
63832
|
}
|
|
63833
|
+
/**
|
|
63834
|
+
* The widget library. Without a library file, the first page that has custom widgets starts it, so
|
|
63835
|
+
* the widgets made before the library existed join it.
|
|
63836
|
+
*/
|
|
63837
|
+
async #library(seed) {
|
|
63838
|
+
const file = resolve(this.dataDir, LIBRARY_FILE);
|
|
63839
|
+
let text;
|
|
63840
|
+
try {
|
|
63841
|
+
text = await readFile(file, "utf8");
|
|
63842
|
+
} catch (e) {
|
|
63843
|
+
if (e.code !== "ENOENT") throw e;
|
|
63844
|
+
if (seed.length > 0) await this.#saveLibrary(seed);
|
|
63845
|
+
return seed;
|
|
63846
|
+
}
|
|
63847
|
+
try {
|
|
63848
|
+
const list = JSON.parse(text);
|
|
63849
|
+
if (!Array.isArray(list)) throw new Error("not a list of widgets");
|
|
63850
|
+
return list.map((w) => checkWidget(w));
|
|
63851
|
+
} catch (e) {
|
|
63852
|
+
throw new Error(`${file}: ${e.message}; correct or delete the file`, { cause: e });
|
|
63853
|
+
}
|
|
63854
|
+
}
|
|
63855
|
+
#saveLibrary(widgets) {
|
|
63856
|
+
return writeAtomically(resolve(this.dataDir, LIBRARY_FILE), `${JSON.stringify(widgets, null, 2)}\n`);
|
|
63857
|
+
}
|
|
63858
|
+
/** The page with the library in its widgets block; `old` is the page that it replaces, to start the library. */
|
|
63859
|
+
async #withLibrary(page, old) {
|
|
63860
|
+
if (!marker("widgets").test(page)) return page;
|
|
63861
|
+
const own = readBlock(page, "widgets") ?? [];
|
|
63862
|
+
const before = old === void 0 ? [] : readBlock(old, "widgets") ?? [];
|
|
63863
|
+
return writeBlock(page, "widgets", merged(own, await this.#library(before)));
|
|
63864
|
+
}
|
|
61887
63865
|
/** The data for `weeks`; the same snapshot for 15 minutes, so the notes match the digest they come from. */
|
|
61888
63866
|
async #data(weeks) {
|
|
61889
63867
|
const covered = weeksCovered(weeks);
|
|
@@ -61899,11 +63877,15 @@ var DashboardWriter = class {
|
|
|
61899
63877
|
}
|
|
61900
63878
|
/** The page file: relative paths sit in the data dir; only .html files. */
|
|
61901
63879
|
#target(path) {
|
|
61902
|
-
|
|
61903
|
-
if (!/\.html$/i.test(target)) throw new Error(`The dashboard must be an .html file: ${target}`);
|
|
61904
|
-
return target;
|
|
63880
|
+
return pageTarget(this.dataDir, path);
|
|
61905
63881
|
}
|
|
61906
63882
|
};
|
|
63883
|
+
/** The absolute page file: a relative path is inside the data dir; only .html files. */
|
|
63884
|
+
function pageTarget(dataDir, path) {
|
|
63885
|
+
const target = resolve(dataDir, (path || "garmin-dashboard.html").replace(/^~(?=$|[/\\])/, homedir()));
|
|
63886
|
+
if (!/\.html$/i.test(target)) throw new Error(`The dashboard must be an .html file: ${target}`);
|
|
63887
|
+
return target;
|
|
63888
|
+
}
|
|
61907
63889
|
/** The boards: All, Recovery, the type boards, and a board for any other SDK sport. */
|
|
61908
63890
|
const TYPES = /* @__PURE__ */ new Set([
|
|
61909
63891
|
"all",
|
|
@@ -61912,6 +63894,8 @@ const TYPES = /* @__PURE__ */ new Set([
|
|
|
61912
63894
|
...Object.values(Profile.types.sport)
|
|
61913
63895
|
]);
|
|
61914
63896
|
const spanOf = (v) => Math.min(3, Math.max(1, Math.round(Number(v) || 1)));
|
|
63897
|
+
/** The page's custom widgets and the library; the library version wins on the same id. */
|
|
63898
|
+
const merged = (own, library) => [...own.filter((w) => !library.some((l) => l.id === w.id)), ...library];
|
|
61915
63899
|
/** A custom widget that the page can run; throws with what is wrong. */
|
|
61916
63900
|
function checkWidget(w) {
|
|
61917
63901
|
if (CATALOGUE.some((c) => c.id === w.id)) throw new Error(`${w.id} is a built-in widget: give the custom widget another id`);
|
|
@@ -61920,22 +63904,49 @@ function checkWidget(w) {
|
|
|
61920
63904
|
if (!w.code?.trim() || w.code.length > 2e4) throw new Error("A widget needs code, at most 20000 characters");
|
|
61921
63905
|
const unknown = (w.types ?? []).filter((t) => !TYPES.has(t));
|
|
61922
63906
|
if (unknown.length > 0) throw new Error(`Unknown widget types: ${unknown.join(", ")}`);
|
|
63907
|
+
const texts = checkWidgetTexts(w);
|
|
61923
63908
|
return {
|
|
61924
63909
|
id: w.id,
|
|
61925
63910
|
title: w.title.trim(),
|
|
61926
63911
|
code: w.code,
|
|
61927
63912
|
span: spanOf(w.span),
|
|
61928
|
-
...w.types?.length ? { types: w.types } : {}
|
|
63913
|
+
...w.types?.length ? { types: w.types } : {},
|
|
63914
|
+
...texts ? { texts } : {}
|
|
61929
63915
|
};
|
|
61930
63916
|
}
|
|
63917
|
+
/** The texts of a custom widget: language codes, short keys, and the placeholders of the English text. */
|
|
63918
|
+
function checkWidgetTexts(w) {
|
|
63919
|
+
const texts = w.texts;
|
|
63920
|
+
if (texts === void 0 || Object.keys(texts).length === 0) return void 0;
|
|
63921
|
+
if (JSON.stringify(texts).length > 2e4) throw new Error(`The texts of ${w.id} are too long: at most 20000 characters`);
|
|
63922
|
+
const problems = [];
|
|
63923
|
+
for (const [lang, entries] of Object.entries(texts)) {
|
|
63924
|
+
if (!/^[a-z]{2,3}(-[A-Z]{2})?$/.test(lang)) {
|
|
63925
|
+
problems.push(`bad language code ${lang}`);
|
|
63926
|
+
continue;
|
|
63927
|
+
}
|
|
63928
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
63929
|
+
problems.push(`${lang}: texts by key`);
|
|
63930
|
+
continue;
|
|
63931
|
+
}
|
|
63932
|
+
for (const [key, text] of Object.entries(entries)) {
|
|
63933
|
+
const english = texts.en?.[key];
|
|
63934
|
+
if (!/^[a-zA-Z][\w.-]{0,40}$/.test(key)) problems.push(`bad key ${key}`);
|
|
63935
|
+
else if (typeof text !== "string" || !text.trim() || text.length > 500) problems.push(`${lang}.${key}: 1 to 500 characters`);
|
|
63936
|
+
else if (english !== void 0 && placeholders(text) !== placeholders(english)) problems.push(`${lang}.${key}: keep the placeholders ${placeholders(english) || "(none)"}`);
|
|
63937
|
+
}
|
|
63938
|
+
}
|
|
63939
|
+
if (problems.length > 0) throw new Error(`Texts of ${w.id}: ${problems.join("; ")}`);
|
|
63940
|
+
return texts;
|
|
63941
|
+
}
|
|
61931
63942
|
const placeholders = (s) => [...new Set(s.match(/\{\w+\}/g) ?? [])].toSorted().join(" ");
|
|
61932
63943
|
/** Texts of a language: known keys only, strings, with the placeholders of the English text. */
|
|
61933
63944
|
function checkTexts(lang, entries) {
|
|
61934
63945
|
if (!/^[a-z]{2,3}(-[A-Z]{2})?$/.test(lang)) throw new Error(`Bad language code ${lang}: use a code such as de, fr or pt-BR`);
|
|
61935
63946
|
const problems = [];
|
|
61936
|
-
for (const [key, text] of Object.entries(entries)) if (!Object.hasOwn(
|
|
63947
|
+
for (const [key, text] of Object.entries(entries)) if (!Object.hasOwn(en_default, key)) problems.push(`unknown text ${key}`);
|
|
61937
63948
|
else if (typeof text !== "string" || !text.trim() || text.length > 500) problems.push(`${key}: 1 to 500 characters`);
|
|
61938
|
-
else if (placeholders(text) !== placeholders(
|
|
63949
|
+
else if (placeholders(text) !== placeholders(en_default[key])) problems.push(`${key}: keep the placeholders ${placeholders(en_default[key]) || "(none)"}`);
|
|
61939
63950
|
if (problems.length > 0) throw new Error(`Texts of ${lang}: ${problems.join("; ")}`);
|
|
61940
63951
|
return entries;
|
|
61941
63952
|
}
|
|
@@ -61943,10 +63954,43 @@ function hasLanguages(page) {
|
|
|
61943
63954
|
const t = readBlock(page, "i18n");
|
|
61944
63955
|
return !!t && typeof t === "object" && Object.keys(t).length > 0;
|
|
61945
63956
|
}
|
|
63957
|
+
/** English unless the settings leave it out, then the built-in and the added languages of the page. */
|
|
63958
|
+
function pageLanguages(page, s) {
|
|
63959
|
+
const codes = [
|
|
63960
|
+
...s.languages && !s.languages.includes("en") ? [] : ["en"],
|
|
63961
|
+
...Object.keys(readBlock(page, "languages") ?? {}),
|
|
63962
|
+
...Object.keys(readBlock(page, "i18n") ?? {})
|
|
63963
|
+
];
|
|
63964
|
+
return [...new Set(codes)];
|
|
63965
|
+
}
|
|
63966
|
+
/**
|
|
63967
|
+
* For each custom widget with texts, the page languages it has no texts in. A widget without texts
|
|
63968
|
+
* (written before widgets had them) shows fixed text, so there is nothing to add.
|
|
63969
|
+
*/
|
|
63970
|
+
function widgetGaps(page, languages) {
|
|
63971
|
+
const out = {};
|
|
63972
|
+
for (const w of readBlock(page, "widgets") ?? []) {
|
|
63973
|
+
if (!w.texts) continue;
|
|
63974
|
+
const gaps = languages.filter((code) => !w.texts[code] && !w.texts[code.slice(0, 2)]);
|
|
63975
|
+
if (gaps.length > 0) out[w.id] = gaps;
|
|
63976
|
+
}
|
|
63977
|
+
return out;
|
|
63978
|
+
}
|
|
63979
|
+
const withMissing = (gaps) => Object.keys(gaps).length > 0 ? { missingWidgetTexts: gaps } : {};
|
|
63980
|
+
/** True when the profile locale is not English, not built in and not added to the page. */
|
|
63981
|
+
function missingLanguage(page, locale) {
|
|
63982
|
+
if (typeof locale !== "string" || !locale) return false;
|
|
63983
|
+
const known = [
|
|
63984
|
+
"en",
|
|
63985
|
+
...Object.keys(BUILT_IN),
|
|
63986
|
+
...Object.keys(readBlock(page, "i18n") ?? {})
|
|
63987
|
+
];
|
|
63988
|
+
return ![locale, locale.slice(0, 2)].some((code) => known.includes(code));
|
|
63989
|
+
}
|
|
61946
63990
|
/** For each language of the page, the keys that it does not translate. */
|
|
61947
63991
|
function missingTexts(page) {
|
|
61948
63992
|
const t = readBlock(page, "i18n") ?? {};
|
|
61949
|
-
return Object.fromEntries(Object.entries(t).map(([lang, texts]) => [lang, { missing: Object.keys(
|
|
63993
|
+
return Object.fromEntries(Object.entries(t).map(([lang, texts]) => [lang, { missing: Object.keys(en_default).filter((k) => !(k in texts)) }]));
|
|
61950
63994
|
}
|
|
61951
63995
|
/** The page at `target`, or undefined when there is none; refuses a file that is not a dashboard. */
|
|
61952
63996
|
async function readExisting(target) {
|
|
@@ -61963,13 +64007,20 @@ function registerDashboardWriter(server, writer) {
|
|
|
61963
64007
|
"last `weeks` weeks, and returns its path and a short digest of the data (totals, weekly sums, load,",
|
|
61964
64008
|
"best efforts, races, strength top sets, HRV, recovery, fitness age, sync) for the coach notes.",
|
|
61965
64009
|
"An existing page keeps all its code, layout and widgets: only its data is replaced. A page from an",
|
|
61966
|
-
"older template gets the new template and keeps its layout and widgets.
|
|
64010
|
+
"older template gets the new template and keeps its layout and widgets. Every page from the template",
|
|
64011
|
+
"also gets the custom widgets of the widget library (<data-dir>/widgets.json), not placed on a board.",
|
|
64012
|
+
"`layout` writes a new page",
|
|
61967
64013
|
"from the template: `preset` (the default widgets) or `blank` (empty boards). `page` writes your own",
|
|
61968
64014
|
"page code instead; it must contain /* garmin-data:start */null/* garmin-data:end */. A replaced",
|
|
61969
64015
|
"page is kept as <file>.bak. `notes`: at most 5 plain-text coach notes; the server escapes them.",
|
|
61970
64016
|
"`settings` replace the page's settings (title, headline, sports, data to leave out, languages,",
|
|
61971
64017
|
"themes); without them the page keeps its own. The page holds only the data, languages and themes",
|
|
61972
|
-
"they keep. `
|
|
64018
|
+
"they keep. `pageLanguages` in the result lists the languages of the page's Language menu, and",
|
|
64019
|
+
"`settings` the settings that the page keeps. `missingProfileLanguage` is the language of the Garmin",
|
|
64020
|
+
"profile when the page has no texts in it (it shows English); editDashboard `languages` adds it.",
|
|
64021
|
+
"`missingWidgetTexts` lists the custom widgets that miss texts in a language of the page, with",
|
|
64022
|
+
"those languages; editDashboard `widgetTexts` adds them.",
|
|
64023
|
+
"`preview: true` writes nothing: it returns the digest and the activities of each board."
|
|
61973
64024
|
].join("\n"),
|
|
61974
64025
|
inputSchema: object({
|
|
61975
64026
|
weeks: number$1().int().describe("Weeks to cover, default 12, max 52").optional(),
|
|
@@ -61982,7 +64033,7 @@ function registerDashboardWriter(server, writer) {
|
|
|
61982
64033
|
headline: string().describe("A line under the title").optional(),
|
|
61983
64034
|
sports: array(string()).describe("The boards to keep, e.g. running, cycling; all when not set").optional(),
|
|
61984
64035
|
exclude: array(_enum(DATA_ITEMS)).describe("The data to leave out of the page").optional(),
|
|
61985
|
-
languages: array(string()).describe(`Languages to keep: en, ${Object.keys(BUILT_IN).join(", ")}`).optional(),
|
|
64036
|
+
languages: array(string()).describe(`Languages to keep: en, ${Object.keys(BUILT_IN).join(", ")}; when not set, English and the language of the Garmin profile`).optional(),
|
|
61986
64037
|
language: string().describe("Default language: auto or a code").optional(),
|
|
61987
64038
|
themes: array(string()).describe(`Themes to keep: ${THEMES.map((t) => t.id).join(", ")}`).optional(),
|
|
61988
64039
|
theme: string().describe("Default theme").optional()
|
|
@@ -62000,7 +64051,10 @@ function registerDashboardWriter(server, writer) {
|
|
|
62000
64051
|
"custom widgets, the missing texts of each language, the catalogue of built-in widgets and `widgetApi`,",
|
|
62001
64052
|
"the guide to writing a custom widget. `layout` replaces the whole layout (the text of the page's",
|
|
62002
64053
|
"'Copy layout'); `place` adds widgets at the end of a board; `widgets` adds or replaces custom widgets",
|
|
62003
|
-
"by id
|
|
64054
|
+
"by id and saves them in the widget library (<data-dir>/widgets.json), which every page from the",
|
|
64055
|
+
"template gets; `removeWidgets` removes them from this page and its boards, `forgetWidgets` also",
|
|
64056
|
+
"from the library. `widgetTexts` adds texts to custom widgets by id, language and key, e.g.",
|
|
64057
|
+
"{\"longest-ride\": {\"de\": {\"title\": \"Längste Fahrt\"}}}. `languages` adds page texts by key, e.g.",
|
|
62004
64058
|
"{\"de\": {\"title\": \"Training + Erholung\"}}: keys and {placeholders} as in `englishTexts: true`; a",
|
|
62005
64059
|
"text with \"|\" has a singular and a plural form. Missing texts show in English.",
|
|
62006
64060
|
"`removeLanguages` removes languages that `languages` added."
|
|
@@ -62018,9 +64072,12 @@ function registerDashboardWriter(server, writer) {
|
|
|
62018
64072
|
title: string(),
|
|
62019
64073
|
code: string().describe("The body of a function (el, ctx): see widgetApi"),
|
|
62020
64074
|
span: number$1().int().optional(),
|
|
62021
|
-
types: array(string()).describe("Types the widget can show").optional()
|
|
64075
|
+
types: array(string()).describe("Types the widget can show").optional(),
|
|
64076
|
+
texts: record(string(), record(string(), string())).describe("Texts by language code and key; title is the title: see widgetApi").optional()
|
|
62022
64077
|
})).optional(),
|
|
62023
|
-
removeWidgets: array(string()).describe("Ids of custom widgets").optional(),
|
|
64078
|
+
removeWidgets: array(string()).describe("Ids of custom widgets to remove from this page").optional(),
|
|
64079
|
+
forgetWidgets: array(string()).describe("Ids of custom widgets to remove from the library and from this page").optional(),
|
|
64080
|
+
widgetTexts: record(string(), record(string(), record(string(), string()))).describe("Texts of custom widgets by widget id, language code and key").optional(),
|
|
62024
64081
|
languages: record(string(), record(string(), string())).describe("Texts by language code and key").optional(),
|
|
62025
64082
|
removeLanguages: array(string()).describe("Codes of added languages, e.g. de").optional(),
|
|
62026
64083
|
englishTexts: boolean().describe("Also return the English texts to translate").optional(),
|
|
@@ -62031,24 +64088,584 @@ function registerDashboardWriter(server, writer) {
|
|
|
62031
64088
|
text: JSON.stringify(await writer.edit(a))
|
|
62032
64089
|
}] }));
|
|
62033
64090
|
}
|
|
64091
|
+
// src/schedule.ts
|
|
64092
|
+
/**
|
|
64093
|
+
* The automatic refresh of dashboard pages: one job in the scheduler of the operating system (launchd,
|
|
64094
|
+
* cron or Task Scheduler) runs `refresh --auto`, which updates the pages listed in
|
|
64095
|
+
* `<data-dir>/auto-refresh.json`. Nothing is installed until the user asks.
|
|
64096
|
+
*/
|
|
64097
|
+
const DAYS = [
|
|
64098
|
+
"sunday",
|
|
64099
|
+
"monday",
|
|
64100
|
+
"tuesday",
|
|
64101
|
+
"wednesday",
|
|
64102
|
+
"thursday",
|
|
64103
|
+
"friday",
|
|
64104
|
+
"saturday"
|
|
64105
|
+
];
|
|
64106
|
+
const MONTHS = [
|
|
64107
|
+
"January",
|
|
64108
|
+
"February",
|
|
64109
|
+
"March",
|
|
64110
|
+
"April",
|
|
64111
|
+
"May",
|
|
64112
|
+
"June",
|
|
64113
|
+
"July",
|
|
64114
|
+
"August",
|
|
64115
|
+
"September",
|
|
64116
|
+
"October",
|
|
64117
|
+
"November",
|
|
64118
|
+
"December"
|
|
64119
|
+
];
|
|
64120
|
+
/** Garmin gets about 8 requests per refresh: two runs must be at least this far apart. */
|
|
64121
|
+
const MIN_GAP_MINUTES = 60;
|
|
64122
|
+
/** Runs a command without a shell; its output never reaches our stdout. */
|
|
64123
|
+
const run = (cmd, args, input) => new Promise((ok, fail) => {
|
|
64124
|
+
const p = spawn(cmd, args, { stdio: "pipe" });
|
|
64125
|
+
let stdout = "";
|
|
64126
|
+
let stderr = "";
|
|
64127
|
+
p.stdout.on("data", (d) => stdout += d);
|
|
64128
|
+
p.stderr.on("data", (d) => stderr += d);
|
|
64129
|
+
p.on("error", fail);
|
|
64130
|
+
p.on("close", (code) => ok({
|
|
64131
|
+
code: code ?? 1,
|
|
64132
|
+
stdout,
|
|
64133
|
+
stderr
|
|
64134
|
+
}));
|
|
64135
|
+
p.stdin.end(input ?? "");
|
|
64136
|
+
});
|
|
64137
|
+
const STATE_FILE = "auto-refresh.json";
|
|
64138
|
+
const DEFAULT_CRON = "0 7 * * *";
|
|
64139
|
+
const LABEL = "com.indaco.garmin-personal-mcp.refresh";
|
|
64140
|
+
const TASK = "garmin-personal-mcp refresh";
|
|
64141
|
+
const MARK = "# garmin-personal-mcp refresh";
|
|
64142
|
+
const FIELDS = [
|
|
64143
|
+
[
|
|
64144
|
+
"minute",
|
|
64145
|
+
0,
|
|
64146
|
+
59
|
|
64147
|
+
],
|
|
64148
|
+
[
|
|
64149
|
+
"hour",
|
|
64150
|
+
0,
|
|
64151
|
+
23
|
|
64152
|
+
],
|
|
64153
|
+
[
|
|
64154
|
+
"day of month",
|
|
64155
|
+
1,
|
|
64156
|
+
31
|
|
64157
|
+
],
|
|
64158
|
+
[
|
|
64159
|
+
"month",
|
|
64160
|
+
1,
|
|
64161
|
+
12
|
|
64162
|
+
],
|
|
64163
|
+
[
|
|
64164
|
+
"weekday",
|
|
64165
|
+
0,
|
|
64166
|
+
7
|
|
64167
|
+
]
|
|
64168
|
+
];
|
|
64169
|
+
/** One cron field: *, n, a-b, with an optional /step, separated by commas. */
|
|
64170
|
+
function field(text, [label, min, max]) {
|
|
64171
|
+
const values = /* @__PURE__ */ new Set();
|
|
64172
|
+
for (const part of text.split(",")) {
|
|
64173
|
+
const m = /^(\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(part);
|
|
64174
|
+
const from = m?.[2] === void 0 ? min : Number(m[2]);
|
|
64175
|
+
const to = m?.[3] !== void 0 ? Number(m[3]) : m?.[2] === void 0 || m[4] ? max : from;
|
|
64176
|
+
const step = m?.[4] === void 0 ? 1 : Number(m[4]);
|
|
64177
|
+
if (!m || from < min || to > max || from > to || step < 1) throw new Error(`Cron ${label}: "${part}" is not ${min}-${max}, a list, a range or a /step`);
|
|
64178
|
+
for (let v = from; v <= to; v += step) values.add(label === "weekday" && v === 7 ? 0 : v);
|
|
64179
|
+
}
|
|
64180
|
+
return [...values].toSorted((a, b) => a - b);
|
|
64181
|
+
}
|
|
64182
|
+
/** Reads and checks a cron expression: numbers only, and at least an hour between two runs. */
|
|
64183
|
+
function parseCron(text) {
|
|
64184
|
+
const parts = text.trim().split(/\s+/);
|
|
64185
|
+
if (parts.length !== 5) throw new Error(`Cron: 5 fields, minute hour day-of-month month weekday (0 = Sunday), e.g. "0 7 * * 1"`);
|
|
64186
|
+
const [minutes, hours, days, months, weekdays] = parts.map((p, i) => field(p, FIELDS[i]));
|
|
64187
|
+
const times = hours.flatMap((h) => minutes.map((m) => h * 60 + m)).toSorted((a, b) => a - b);
|
|
64188
|
+
const gaps = times.map((t, i) => (i === 0 ? t + 1440 : t) - times[(i || times.length) - 1]);
|
|
64189
|
+
if (times.length > 1 && Math.min(...gaps) < MIN_GAP_MINUTES) throw new Error("Cron: the runs must be at least 1 hour apart");
|
|
64190
|
+
const cron = {
|
|
64191
|
+
text: parts.join(" "),
|
|
64192
|
+
times,
|
|
64193
|
+
...parts[2].startsWith("*") ? {} : { days },
|
|
64194
|
+
...parts[3].startsWith("*") ? {} : { months },
|
|
64195
|
+
...parts[4].startsWith("*") ? {} : { weekdays }
|
|
64196
|
+
};
|
|
64197
|
+
if (nextRuns(cron, /* @__PURE__ */ new Date(), 1).length === 0) throw new Error("Cron: this date never comes");
|
|
64198
|
+
return cron;
|
|
64199
|
+
}
|
|
64200
|
+
/** The CLI forms: daily [HH:MM], weekdays [HH:MM], weekly <day> [HH:MM], or a cron expression. */
|
|
64201
|
+
function cronOf(words) {
|
|
64202
|
+
if (words.length === 1 && words[0].trim().includes(" ")) return words[0];
|
|
64203
|
+
const [kind, ...rest] = words.map((w) => w.toLowerCase());
|
|
64204
|
+
const day = kind === "weekly" && rest[0] && !rest[0].includes(":") ? DAYS.indexOf(rest.shift()) : 1;
|
|
64205
|
+
const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(rest[0] ?? "07:00");
|
|
64206
|
+
const weekday = {
|
|
64207
|
+
daily: "*",
|
|
64208
|
+
weekdays: "1-5",
|
|
64209
|
+
weekly: String(day)
|
|
64210
|
+
}[kind ?? ""];
|
|
64211
|
+
if (!weekday || !m || rest.length > 1 || day < 0) throw new Error("Schedule: daily [HH:MM], weekdays [HH:MM], weekly <day> [HH:MM] or a cron expression in quotes, e.g. \"0 7 * * 1\"");
|
|
64212
|
+
return `${Number(m[2])} ${Number(m[1])} * * ${weekday}`;
|
|
64213
|
+
}
|
|
64214
|
+
/** Whether the job runs on that day. */
|
|
64215
|
+
function runsOn(c, d) {
|
|
64216
|
+
if (c.months && !c.months.includes(d.getMonth() + 1)) return false;
|
|
64217
|
+
if (c.days && c.weekdays) return c.days.includes(d.getDate()) || c.weekdays.includes(d.getDay());
|
|
64218
|
+
if (c.days) return c.days.includes(d.getDate());
|
|
64219
|
+
return c.weekdays?.includes(d.getDay()) ?? true;
|
|
64220
|
+
}
|
|
64221
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
64222
|
+
/** The next `count` runs after `from`, as local "YYYY-MM-DD HH:MM"; empty when the date never comes. */
|
|
64223
|
+
function nextRuns(c, from, count) {
|
|
64224
|
+
const runs = [];
|
|
64225
|
+
for (let i = 0; i < 2928 && runs.length < count; i++) {
|
|
64226
|
+
const d = new Date(from.getFullYear(), from.getMonth(), from.getDate() + i);
|
|
64227
|
+
if (!runsOn(c, d)) continue;
|
|
64228
|
+
for (const t of c.times) {
|
|
64229
|
+
const at = new Date(d.getFullYear(), d.getMonth(), d.getDate(), Math.floor(t / 60), t % 60);
|
|
64230
|
+
if (at > from && runs.length < count) runs.push(`${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad(at.getHours())}:${pad(at.getMinutes())}`);
|
|
64231
|
+
}
|
|
64232
|
+
}
|
|
64233
|
+
return runs;
|
|
64234
|
+
}
|
|
64235
|
+
const xml = (s) => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
64236
|
+
/** launchd has no cron syntax: each run time is a dict, and a run matches any dict of the array. */
|
|
64237
|
+
function calendar(c) {
|
|
64238
|
+
const months = c.months ?? [void 0];
|
|
64239
|
+
const days = c.days || c.weekdays ? [...(c.days ?? []).map((d) => ({ Day: d })), ...(c.weekdays ?? []).map((w) => ({ Weekday: w }))] : [{}];
|
|
64240
|
+
return c.times.flatMap((t) => months.flatMap((m) => days.map((d) => ({
|
|
64241
|
+
...m === void 0 ? {} : { Month: m },
|
|
64242
|
+
...d,
|
|
64243
|
+
Hour: Math.floor(t / 60),
|
|
64244
|
+
Minute: t % 60
|
|
64245
|
+
}))));
|
|
64246
|
+
}
|
|
64247
|
+
function plist(job, c) {
|
|
64248
|
+
const when = calendar(c).map((e) => ` <dict>${Object.entries(e).map(([k, v]) => `<key>${k}</key><integer>${v}</integer>`).join("")}</dict>`);
|
|
64249
|
+
const strings = (xs) => xs.map((x) => ` <string>${xml(x)}</string>`).join("\n");
|
|
64250
|
+
const env = Object.entries(job.env).map(([k, v]) => ` <key>${k}</key><string>${xml(v)}</string>`).join("\n");
|
|
64251
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
64252
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
64253
|
+
<plist version="1.0">
|
|
64254
|
+
<dict>
|
|
64255
|
+
<key>Label</key><string>${LABEL}</string>
|
|
64256
|
+
<key>ProgramArguments</key>
|
|
64257
|
+
<array>
|
|
64258
|
+
${strings([job.program, ...job.args])}
|
|
64259
|
+
</array>
|
|
64260
|
+
<key>EnvironmentVariables</key>
|
|
64261
|
+
<dict>
|
|
64262
|
+
${env}
|
|
64263
|
+
</dict>
|
|
64264
|
+
<key>StartCalendarInterval</key>
|
|
64265
|
+
<array>
|
|
64266
|
+
${when.join("\n")}
|
|
64267
|
+
</array>
|
|
64268
|
+
<key>StandardOutPath</key><string>${xml(job.log)}</string>
|
|
64269
|
+
<key>StandardErrorPath</key><string>${xml(job.log)}</string>
|
|
64270
|
+
</dict>
|
|
64271
|
+
</plist>
|
|
64272
|
+
`;
|
|
64273
|
+
}
|
|
64274
|
+
const sh = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
64275
|
+
/** The crontab line; % is a newline for cron, so it is escaped. */
|
|
64276
|
+
function cronLine(job, c) {
|
|
64277
|
+
const command = [
|
|
64278
|
+
...Object.entries(job.env).map(([k, v]) => `${k}=${sh(v)}`),
|
|
64279
|
+
sh(job.program),
|
|
64280
|
+
...job.args.map(sh),
|
|
64281
|
+
">>",
|
|
64282
|
+
sh(job.log),
|
|
64283
|
+
"2>&1"
|
|
64284
|
+
].join(" ");
|
|
64285
|
+
return `${c.text} ${command.replaceAll("%", "\\%")} ${MARK}`;
|
|
64286
|
+
}
|
|
64287
|
+
/** The crontab with our line replaced (or removed when `line` is undefined); other lines stay. */
|
|
64288
|
+
function withCronLine(crontab, line) {
|
|
64289
|
+
return [...crontab.split("\n").filter((l) => l.trim() !== "" && !l.endsWith(MARK)), ...line ? [line] : []].map((l) => `${l}\n`).join("");
|
|
64290
|
+
}
|
|
64291
|
+
/** The .cmd file the Windows task runs. */
|
|
64292
|
+
function windowsScript(job) {
|
|
64293
|
+
const env = Object.entries(job.env).map(([k, v]) => k === "PATH" ? `set "PATH=${v};%PATH%"` : `set "${k}=${v}"`);
|
|
64294
|
+
const command = [job.program, ...job.args].map((a) => `"${a}"`).join(" ");
|
|
64295
|
+
return [
|
|
64296
|
+
"@echo off",
|
|
64297
|
+
...env,
|
|
64298
|
+
`call ${command} >> "${job.log}" 2>&1`,
|
|
64299
|
+
""
|
|
64300
|
+
].join("\r\n");
|
|
64301
|
+
}
|
|
64302
|
+
const tags = (names) => names.map((n) => `<${n} />`).join("");
|
|
64303
|
+
/**
|
|
64304
|
+
* The Task Scheduler task: schtasks /SC cannot say most cron schedules, so each run time is a calendar
|
|
64305
|
+
* trigger. StartWhenAvailable runs a time that the computer missed, as launchd does.
|
|
64306
|
+
*/
|
|
64307
|
+
function taskXml(script, c) {
|
|
64308
|
+
const months = `<Months>${tags((c.months ?? MONTHS.map((_, i) => i + 1)).map((m) => MONTHS[m - 1]))}</Months>`;
|
|
64309
|
+
const weekdays = c.weekdays ? `<DaysOfWeek>${tags(c.weekdays.map((w) => DAYS[w][0].toUpperCase() + DAYS[w].slice(1)))}</DaysOfWeek>` : "";
|
|
64310
|
+
const byMonth = (days) => `<ScheduleByMonth><DaysOfMonth>${days.map((d) => `<Day>${d}</Day>`).join("")}</DaysOfMonth>${months}</ScheduleByMonth>`;
|
|
64311
|
+
const schedules = [
|
|
64312
|
+
...c.days ? [byMonth(c.days)] : [],
|
|
64313
|
+
...c.weekdays ? [c.months ? `<ScheduleByMonthDayOfWeek><Weeks><Week>1</Week><Week>2</Week><Week>3</Week><Week>4</Week><Week>Last</Week></Weeks>${weekdays}${months}</ScheduleByMonthDayOfWeek>` : `<ScheduleByWeek><WeeksInterval>1</WeeksInterval>${weekdays}</ScheduleByWeek>`] : [],
|
|
64314
|
+
...c.days || c.weekdays ? [] : [c.months ? byMonth(Array.from({ length: 31 }, (_, i) => i + 1)) : "<ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay>"]
|
|
64315
|
+
];
|
|
64316
|
+
const triggers = c.times.flatMap((t) => schedules.map((s) => ` <CalendarTrigger><StartBoundary>2026-01-01T${pad(Math.floor(t / 60))}:${pad(t % 60)}:00</StartBoundary>${s}</CalendarTrigger>`));
|
|
64317
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
64318
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
64319
|
+
<RegistrationInfo><Description>Updates the Garmin dashboard pages (${xml(c.text)})</Description></RegistrationInfo>
|
|
64320
|
+
<Triggers>
|
|
64321
|
+
${triggers.join("\n")}
|
|
64322
|
+
</Triggers>
|
|
64323
|
+
<Settings>
|
|
64324
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
64325
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
64326
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
64327
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
64328
|
+
<ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
|
|
64329
|
+
</Settings>
|
|
64330
|
+
<Actions><Exec><Command>${xml(script)}</Command></Exec></Actions>
|
|
64331
|
+
</Task>
|
|
64332
|
+
`;
|
|
64333
|
+
}
|
|
64334
|
+
var AutoRefresh = class {
|
|
64335
|
+
config;
|
|
64336
|
+
platform;
|
|
64337
|
+
home;
|
|
64338
|
+
exec;
|
|
64339
|
+
nodeDir;
|
|
64340
|
+
entry;
|
|
64341
|
+
constructor(config, platform = process.platform, home = homedir(), exec = run, nodeDir = dirname(process.execPath), entry = realEntry(process.argv[1])) {
|
|
64342
|
+
this.config = config;
|
|
64343
|
+
this.platform = platform;
|
|
64344
|
+
this.home = home;
|
|
64345
|
+
this.exec = exec;
|
|
64346
|
+
this.nodeDir = nodeDir;
|
|
64347
|
+
this.entry = entry;
|
|
64348
|
+
}
|
|
64349
|
+
get #stateFile() {
|
|
64350
|
+
return join(this.config.dataDir, STATE_FILE);
|
|
64351
|
+
}
|
|
64352
|
+
get #plist() {
|
|
64353
|
+
return join(this.home, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
64354
|
+
}
|
|
64355
|
+
get #script() {
|
|
64356
|
+
return join(this.config.dataDir, "auto-refresh.cmd");
|
|
64357
|
+
}
|
|
64358
|
+
get #taskFile() {
|
|
64359
|
+
return join(this.config.dataDir, "auto-refresh-task.xml");
|
|
64360
|
+
}
|
|
64361
|
+
get #log() {
|
|
64362
|
+
return join(this.home, ".cache/garmin-personal-mcp/refresh.log");
|
|
64363
|
+
}
|
|
64364
|
+
get #scheduler() {
|
|
64365
|
+
return {
|
|
64366
|
+
darwin: "launchd",
|
|
64367
|
+
linux: "cron",
|
|
64368
|
+
win32: "Task Scheduler"
|
|
64369
|
+
}[this.platform];
|
|
64370
|
+
}
|
|
64371
|
+
/** The pages that `refresh --auto` updates. */
|
|
64372
|
+
async pages() {
|
|
64373
|
+
return (await this.#state())?.pages ?? [];
|
|
64374
|
+
}
|
|
64375
|
+
async status() {
|
|
64376
|
+
const state = await this.#state();
|
|
64377
|
+
if (!state) return { scheduled: false };
|
|
64378
|
+
return {
|
|
64379
|
+
scheduled: true,
|
|
64380
|
+
cron: state.cron,
|
|
64381
|
+
next: nextRuns(parseCron(state.cron), /* @__PURE__ */ new Date(), 3),
|
|
64382
|
+
pages: state.pages,
|
|
64383
|
+
scheduler: this.#scheduler,
|
|
64384
|
+
installed: await this.#installed(),
|
|
64385
|
+
log: this.#log
|
|
64386
|
+
};
|
|
64387
|
+
}
|
|
64388
|
+
/** Adds the page (default: the main dashboard) and sets the cron, or keeps the current one. */
|
|
64389
|
+
async set(o) {
|
|
64390
|
+
if (!this.#scheduler) throw new Error("Automatic refresh works on macOS, Linux and Windows");
|
|
64391
|
+
if (!existsSync(this.config.apiKeyFile)) throw new Error(`Save the API key in ${this.config.apiKeyFile} first: a scheduled job cannot read the key from your MCP client`);
|
|
64392
|
+
const page = pageTarget(this.config.dataDir, o.path);
|
|
64393
|
+
if (!existsSync(page)) throw new Error(`${page} does not exist: make it first with the training-dashboard prompt`);
|
|
64394
|
+
const old = await this.#state();
|
|
64395
|
+
const cron = parseCron(o.cron ?? old?.cron ?? DEFAULT_CRON);
|
|
64396
|
+
const pages = [.../* @__PURE__ */ new Set([...old?.pages ?? [], page])];
|
|
64397
|
+
await this.#install(cron);
|
|
64398
|
+
await writeFile(this.#stateFile, `${JSON.stringify({
|
|
64399
|
+
cron: cron.text,
|
|
64400
|
+
pages
|
|
64401
|
+
}, null, 2)}\n`);
|
|
64402
|
+
return this.status();
|
|
64403
|
+
}
|
|
64404
|
+
/** Removes the page; without a page, or when no page is left, removes the job. */
|
|
64405
|
+
async off(path) {
|
|
64406
|
+
const old = await this.#state();
|
|
64407
|
+
if (!old) return { scheduled: false };
|
|
64408
|
+
const pages = path ? old.pages.filter((p) => p !== pageTarget(this.config.dataDir, path)) : [];
|
|
64409
|
+
if (pages.length > 0) {
|
|
64410
|
+
await writeFile(this.#stateFile, `${JSON.stringify({
|
|
64411
|
+
...old,
|
|
64412
|
+
pages
|
|
64413
|
+
}, null, 2)}\n`);
|
|
64414
|
+
return this.status();
|
|
64415
|
+
}
|
|
64416
|
+
await this.#uninstall();
|
|
64417
|
+
await rm(this.#stateFile, { force: true });
|
|
64418
|
+
return { scheduled: false };
|
|
64419
|
+
}
|
|
64420
|
+
#job() {
|
|
64421
|
+
const win = this.platform === "win32";
|
|
64422
|
+
const npx = this.entry === "" || /[/\\]_npx[/\\]/.test(this.entry);
|
|
64423
|
+
return {
|
|
64424
|
+
program: join(this.nodeDir, npx ? win ? "npx.cmd" : "npx" : win ? "node.exe" : "node"),
|
|
64425
|
+
args: [
|
|
64426
|
+
...npx ? ["-y", `${name}@latest`] : [this.entry],
|
|
64427
|
+
"refresh",
|
|
64428
|
+
"--auto"
|
|
64429
|
+
],
|
|
64430
|
+
env: {
|
|
64431
|
+
GARMIN_DATA_DIR: this.config.dataDir,
|
|
64432
|
+
GARMIN_API_KEY_FILE: this.config.apiKeyFile,
|
|
64433
|
+
GARMIN_BASE_URL: this.config.baseUrl,
|
|
64434
|
+
PATH: win ? this.nodeDir : [
|
|
64435
|
+
this.nodeDir,
|
|
64436
|
+
"/usr/bin",
|
|
64437
|
+
"/bin"
|
|
64438
|
+
].join(delimiter)
|
|
64439
|
+
},
|
|
64440
|
+
log: this.#log
|
|
64441
|
+
};
|
|
64442
|
+
}
|
|
64443
|
+
async #install(c) {
|
|
64444
|
+
await mkdir(dirname(this.#log), { recursive: true });
|
|
64445
|
+
const job = this.#job();
|
|
64446
|
+
if (this.platform === "darwin") {
|
|
64447
|
+
await mkdir(dirname(this.#plist), { recursive: true });
|
|
64448
|
+
await writeFile(this.#plist, plist(job, c));
|
|
64449
|
+
await this.exec("launchctl", [
|
|
64450
|
+
"bootout",
|
|
64451
|
+
this.#domain(),
|
|
64452
|
+
this.#plist
|
|
64453
|
+
]);
|
|
64454
|
+
await this.#must("launchctl", [
|
|
64455
|
+
"bootstrap",
|
|
64456
|
+
this.#domain(),
|
|
64457
|
+
this.#plist
|
|
64458
|
+
]);
|
|
64459
|
+
} else if (this.platform === "linux") await this.#must("crontab", ["-"], withCronLine(await this.#crontab(), cronLine(job, c)));
|
|
64460
|
+
else {
|
|
64461
|
+
await writeFile(this.#script, windowsScript(job));
|
|
64462
|
+
await writeFile(this.#taskFile, Buffer.from(`\ufeff${taskXml(this.#script, c)}`, "utf16le"));
|
|
64463
|
+
await this.#must("schtasks", [
|
|
64464
|
+
"/Create",
|
|
64465
|
+
"/F",
|
|
64466
|
+
"/TN",
|
|
64467
|
+
TASK,
|
|
64468
|
+
"/XML",
|
|
64469
|
+
this.#taskFile
|
|
64470
|
+
]);
|
|
64471
|
+
}
|
|
64472
|
+
}
|
|
64473
|
+
async #uninstall() {
|
|
64474
|
+
if (this.platform === "darwin") {
|
|
64475
|
+
await this.exec("launchctl", [
|
|
64476
|
+
"bootout",
|
|
64477
|
+
this.#domain(),
|
|
64478
|
+
this.#plist
|
|
64479
|
+
]);
|
|
64480
|
+
await rm(this.#plist, { force: true });
|
|
64481
|
+
} else if (this.platform === "linux") await this.#must("crontab", ["-"], withCronLine(await this.#crontab(), void 0));
|
|
64482
|
+
else {
|
|
64483
|
+
await this.exec("schtasks", [
|
|
64484
|
+
"/Delete",
|
|
64485
|
+
"/F",
|
|
64486
|
+
"/TN",
|
|
64487
|
+
TASK
|
|
64488
|
+
]);
|
|
64489
|
+
await rm(this.#script, { force: true });
|
|
64490
|
+
await rm(this.#taskFile, { force: true });
|
|
64491
|
+
}
|
|
64492
|
+
}
|
|
64493
|
+
async #installed() {
|
|
64494
|
+
if (this.platform === "darwin") return existsSync(this.#plist);
|
|
64495
|
+
if (this.platform === "linux") return (await this.#crontab()).includes(MARK);
|
|
64496
|
+
return (await this.exec("schtasks", [
|
|
64497
|
+
"/Query",
|
|
64498
|
+
"/TN",
|
|
64499
|
+
TASK
|
|
64500
|
+
])).code === 0;
|
|
64501
|
+
}
|
|
64502
|
+
#domain() {
|
|
64503
|
+
return `gui/${process.getuid()}`;
|
|
64504
|
+
}
|
|
64505
|
+
/** The user's crontab; "" when there is none (crontab -l then exits with 1). */
|
|
64506
|
+
async #crontab() {
|
|
64507
|
+
const r = await this.exec("crontab", ["-l"]);
|
|
64508
|
+
return r.code === 0 ? r.stdout : "";
|
|
64509
|
+
}
|
|
64510
|
+
async #must(cmd, args, input) {
|
|
64511
|
+
const r = await this.exec(cmd, args, input);
|
|
64512
|
+
if (r.code !== 0) throw new Error(`${cmd} failed (${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
64513
|
+
}
|
|
64514
|
+
async #state() {
|
|
64515
|
+
const text = await readFile(this.#stateFile, "utf8").catch(() => void 0);
|
|
64516
|
+
return text === void 0 ? void 0 : JSON.parse(text);
|
|
64517
|
+
}
|
|
64518
|
+
};
|
|
64519
|
+
/** The real path of the running script, so the job does not follow a link that can change. */
|
|
64520
|
+
function realEntry(path) {
|
|
64521
|
+
try {
|
|
64522
|
+
return path ? realpathSync(path) : "";
|
|
64523
|
+
} catch {
|
|
64524
|
+
return "";
|
|
64525
|
+
}
|
|
64526
|
+
}
|
|
64527
|
+
/** Updates each page; one failure does not stop the others. Returns one log line per page. */
|
|
64528
|
+
async function refreshPages(writer, pages) {
|
|
64529
|
+
const lines = [];
|
|
64530
|
+
let failed = false;
|
|
64531
|
+
for (const path of pages) {
|
|
64532
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
64533
|
+
try {
|
|
64534
|
+
await writer.write({
|
|
64535
|
+
path,
|
|
64536
|
+
onlyExisting: true
|
|
64537
|
+
});
|
|
64538
|
+
lines.push(`${at} updated ${path}`);
|
|
64539
|
+
} catch (e) {
|
|
64540
|
+
failed = true;
|
|
64541
|
+
lines.push(`${at} failed ${path}: ${e.message}`);
|
|
64542
|
+
}
|
|
64543
|
+
}
|
|
64544
|
+
return {
|
|
64545
|
+
lines,
|
|
64546
|
+
failed
|
|
64547
|
+
};
|
|
64548
|
+
}
|
|
64549
|
+
/** Registers scheduleDashboardRefresh. */
|
|
64550
|
+
function registerSchedule(server, auto) {
|
|
64551
|
+
server.registerTool("scheduleDashboardRefresh", {
|
|
64552
|
+
description: [
|
|
64553
|
+
"Turns on, changes or stops the automatic refresh of a dashboard page. Use it only when the user",
|
|
64554
|
+
"asks for it or says yes to the question of a dashboard prompt. A job in the scheduler of the",
|
|
64555
|
+
"computer (launchd, cron or Task Scheduler) replaces the data of the page at the given times, also",
|
|
64556
|
+
"with no assistant open. The refresh removes the coach notes, because old notes do not agree",
|
|
64557
|
+
"with new data. The computer must be on at that time. The page must exist (writeDashboard first).",
|
|
64558
|
+
"`cron`: convert what the user says, in any language, into a 5-field cron expression in local",
|
|
64559
|
+
"time: minute hour day-of-month month weekday, numbers only (0 = Sunday), e.g. \"0 7 * * 1-5\" for",
|
|
64560
|
+
"weekdays at 7. Two runs must be at least 1 hour apart. The cron is for all scheduled pages;",
|
|
64561
|
+
"`path` adds that page (default the main page); `off` stops the refresh of `path`, or of every",
|
|
64562
|
+
"page when no `path` is given. Without arguments it only reads. The reply has `next`, the next",
|
|
64563
|
+
"runs in local time: tell them to the user in their language, so they can check the schedule."
|
|
64564
|
+
].join("\n"),
|
|
64565
|
+
inputSchema: object({
|
|
64566
|
+
cron: string().describe("minute hour day-of-month month weekday, e.g. \"0 7 * * *\"; default 0 7 * * *").optional(),
|
|
64567
|
+
path: string().describe("The .html file; relative paths are inside the data dir").optional(),
|
|
64568
|
+
off: boolean().describe("Stop the refresh").optional()
|
|
64569
|
+
})
|
|
64570
|
+
}, async ({ cron, path, off }) => {
|
|
64571
|
+
const status = off ? await auto.off(path) : cron !== void 0 || path !== void 0 ? await auto.set({
|
|
64572
|
+
cron,
|
|
64573
|
+
path
|
|
64574
|
+
}) : await auto.status();
|
|
64575
|
+
return { content: [{
|
|
64576
|
+
type: "text",
|
|
64577
|
+
text: JSON.stringify(status)
|
|
64578
|
+
}] };
|
|
64579
|
+
});
|
|
64580
|
+
}
|
|
64581
|
+
// src/cli.ts
|
|
64582
|
+
/** The commands for a terminal or a scheduler; any other start is the MCP server. */
|
|
64583
|
+
const COMMANDS = ["refresh", "schedule"];
|
|
64584
|
+
/**
|
|
64585
|
+
* Runs `refresh [page.html | --auto]` or
|
|
64586
|
+
* `schedule [daily|weekdays|weekly <day>] [HH:MM] | "<cron>" | off`, each with an optional page.
|
|
64587
|
+
* Not MCP, so the result may go to stdout.
|
|
64588
|
+
*/
|
|
64589
|
+
async function cli(command, args, auto, writer) {
|
|
64590
|
+
try {
|
|
64591
|
+
if (command === "refresh") {
|
|
64592
|
+
const pages = args[0] === "--auto" ? await auto.pages() : [args[0] ?? ""];
|
|
64593
|
+
const { lines, failed } = await refreshPages(writer(), pages);
|
|
64594
|
+
return {
|
|
64595
|
+
code: failed ? 1 : 0,
|
|
64596
|
+
stdout: lines.join("\n")
|
|
64597
|
+
};
|
|
64598
|
+
}
|
|
64599
|
+
const rest = [...args];
|
|
64600
|
+
const path = rest.at(-1)?.toLowerCase().endsWith(".html") ? rest.pop() : void 0;
|
|
64601
|
+
return {
|
|
64602
|
+
code: 0,
|
|
64603
|
+
stdout: summary(rest[0] === "off" ? await auto.off(path) : rest.length === 0 && path === void 0 ? await auto.status() : await auto.set({
|
|
64604
|
+
...rest.length > 0 ? { cron: cronOf(rest) } : {},
|
|
64605
|
+
path
|
|
64606
|
+
}))
|
|
64607
|
+
};
|
|
64608
|
+
} catch (e) {
|
|
64609
|
+
return {
|
|
64610
|
+
code: 1,
|
|
64611
|
+
stderr: `${command} failed: ${e.message}`
|
|
64612
|
+
};
|
|
64613
|
+
}
|
|
64614
|
+
}
|
|
64615
|
+
function summary(s) {
|
|
64616
|
+
if (!s.scheduled) return "Automatic refresh is off.";
|
|
64617
|
+
return [
|
|
64618
|
+
`Automatic refresh "${s.cron}" with ${s.scheduler}, next: ${s.next.join(", ")}`,
|
|
64619
|
+
"Pages:",
|
|
64620
|
+
...s.pages.map((p) => ` ${p}`),
|
|
64621
|
+
...s.installed ? [] : [`The job is missing from ${s.scheduler}: run the schedule again.`],
|
|
64622
|
+
`Log: ${s.log}`
|
|
64623
|
+
].join("\n");
|
|
64624
|
+
}
|
|
64625
|
+
// src/config.ts
|
|
64626
|
+
/** Defaults reuse an existing ~/.config/garmin-mcp/api-key and ~/workspace/garmin-data. */
|
|
64627
|
+
function loadConfig(env = process.env) {
|
|
64628
|
+
return {
|
|
64629
|
+
baseUrl: env.GARMIN_BASE_URL || "https://personal.connectapi.garmin.com",
|
|
64630
|
+
apiKeyFile: env.GARMIN_API_KEY_FILE || join(homedir(), ".config/garmin-mcp/api-key"),
|
|
64631
|
+
dataDir: env.GARMIN_DATA_DIR || join(homedir(), "workspace/garmin-data")
|
|
64632
|
+
};
|
|
64633
|
+
}
|
|
62034
64634
|
// src/server.ts
|
|
62035
64635
|
/** The page template; the build writes it next to dist/index.js. */
|
|
62036
64636
|
const template = () => readFile(new URL("./dashboard.html", import.meta.url), "utf8");
|
|
62037
|
-
/** The
|
|
64637
|
+
/** The tools, the dashboard data and the page writer, wired to one API client. */
|
|
64638
|
+
function createParts(config) {
|
|
64639
|
+
const api = new GarminApiClient(config);
|
|
64640
|
+
const tools = new GarminTools(api, config.dataDir);
|
|
64641
|
+
const dashboard = new GarminDashboard(tools, api, config.dataDir);
|
|
64642
|
+
return {
|
|
64643
|
+
tools,
|
|
64644
|
+
dashboard,
|
|
64645
|
+
writer: new DashboardWriter((weeks) => dashboard.getDashboardData(weeks), config.dataDir, template)
|
|
64646
|
+
};
|
|
64647
|
+
}
|
|
64648
|
+
/** The garmin-personal MCP server with its tools and prompts. */
|
|
62038
64649
|
function createServer(config) {
|
|
62039
64650
|
const server = new McpServer({
|
|
62040
64651
|
name: "garmin-personal",
|
|
62041
64652
|
version
|
|
62042
64653
|
});
|
|
62043
|
-
const
|
|
62044
|
-
const tools = new GarminTools(api, config.dataDir);
|
|
64654
|
+
const { tools, dashboard, writer } = createParts(config);
|
|
62045
64655
|
registerTools(server, tools);
|
|
62046
|
-
const dashboard = new GarminDashboard(tools, api, config.dataDir);
|
|
62047
64656
|
registerDashboard(server, dashboard);
|
|
62048
|
-
registerDashboardWriter(server,
|
|
64657
|
+
registerDashboardWriter(server, writer);
|
|
64658
|
+
registerSchedule(server, new AutoRefresh(config));
|
|
62049
64659
|
registerPrompts(server);
|
|
62050
64660
|
return server;
|
|
62051
64661
|
}
|
|
62052
64662
|
// src/index.ts
|
|
62053
|
-
|
|
64663
|
+
const [command = "", ...args] = process.argv.slice(2);
|
|
64664
|
+
if (COMMANDS.includes(command)) {
|
|
64665
|
+
const config = loadConfig();
|
|
64666
|
+
const r = await cli(command, args, new AutoRefresh(config), () => createParts(config).writer);
|
|
64667
|
+
if (r.stdout) process.stdout.write(`${r.stdout}\n`);
|
|
64668
|
+
if (r.stderr) process.stderr.write(`${r.stderr}\n`);
|
|
64669
|
+
process.exitCode = r.code;
|
|
64670
|
+
} else serveStdio(() => createServer(loadConfig()));
|
|
62054
64671
|
export {};
|