feat(train): Halkyon Train's shell, Todo's copied and renamed (ADR 0030, D12)
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 10:26:49 +01:00
{
"@@locale": "en",
"appTitle": "Halkyon Train",
"@appTitle": {"description": "The app's name: the window's title, and where halkyon_app's texts name the app."},
"launchProduct": "Train",
"@launchProduct": {"description": "The app's word in its wordmark, under \"Halkyon\"."},
"instanceFirstStartSubtitle": "Choose the instance to keep your workouts on. Someone runs each one; you can add others later.",
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
"homeEmptyTitle": "No workouts yet",
"homeEmptyHint": "Workouts recorded on your phone show here.",
feat(train): Halkyon Train's shell, Todo's copied and renamed (ADR 0030, D12)
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 10:26:49 +01:00
"accountTitle": "Account",
"accountDeleteMessage": "Your account is deleted on the server and on all your devices. This can't be undone.",
feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 13:24:56 +01:00
"aboutTitle": "About",
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
"homeRecordHint": "Record one with the button below.",
feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 13:24:56 +01:00
"recordAction": "Record",
"recordBack": "Back to the recording",
"recordSheetTitle": "Record a workout",
"activityRunning": "Running",
"activityWalking": "Walking",
"activityHiking": "Hiking",
"activityCycling": "Cycling",
"activityIndoorRowing": "Rowing indoors",
feat(train): the Loop's training sessions as workouts (ADR 0030, D39)
Each read of the band lists the sessions it holds, reads those from the
read's first day on (`LoopDays.kt`, one line per exercise: its start
with the band's offset, length, Polar's sport ID, figures and heart
rate at its interval), and deletes those up to the day kept, as the
days. The app keeps each as a workout of origin Polar Loop with its
heart rate (`takeLoopSessions`), Polar's running and cycling as
Train's and every other sport as the new `Activity.other`, which a
workout's edit sheet now changes; a session overlapping a workout Train
recorded gives it its heart rate where it has none instead. The
sessions taken are remembered in `loop.json`, so one deleted in Train
isn't brought back. Health Connect writes the Loop's workouts as
Train's own.
The SDK gives sessions as its protobuf messages: protobuf-javalite
3.20.0, the SDK's own, is named for compiling only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 20:22:23 +01:00
"activityOther": "Other workout",
feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 13:24:56 +01:00
"recordAutoPause": "Pause by itself",
"@recordAutoPause": {"description": "The switch for auto-pause, as the recording starts."},
"recordAutoPauseHint": "When you stand still, and going on as you move again",
"recordStart": "Start",
feat(train): a recording runs an interval plan, with cues and Lap (ADR 0030, D42)
The plan chosen on the Record sheet goes to the recording service spelled
out: each step's end, its target (zones as the heart rates they span for
the profile's highest rate) and its spoken line in the app's language.
PlanRunner.kt moves through the steps on the moving time and distance,
counts a timed step's last seconds down and cues off target from a 30 s
window; PlanCues.kt gives a tone, a vibration and the voice, ducking
music. The file's plan and step lines bring it back, and the recording
screen shows the step, what is left of it, its target, the next step and
Lap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 22:59:48 +01:00
"recordPlan": "Plan",
"recordNoPlan": "None",
"recordingLap": "Lap",
"recordingPlanDone": "Plan done",
"recordingStepOf": "Step {index} of {count}",
"@recordingStepOf": {"placeholders": {"index": {"type": "int"}, "count": {"type": "int"}}},
"recordingNextStep": "Next: {step}",
"@recordingNextStep": {"placeholders": {"step": {"type": "String"}}},
"recordingStepTapLap": "Until you tap Lap",
feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 13:24:56 +01:00
"recordLocationRefused": "Train needs your location to record this. You can allow it in the system's settings.",
"recordingWaitingForGps": "Waiting for GPS",
"recordingWaitingForGpsAccuracy": "Waiting for GPS · ±{metres} m",
"@recordingWaitingForGpsAccuracy": {"description": "Before a fix precise enough: how far off the last one may be.", "placeholders": {"metres": {"type": "int"}}},
"recordingGpsAccuracy": "GPS ±{metres} m",
"@recordingGpsAccuracy": {"description": "How far off the last fix may be.", "placeholders": {"metres": {"type": "int"}}},
"recordingMovingTime": "moving time",
"@recordingMovingTime": {"description": "Under the time, lower case: the time moved, pauses left out."},
"recordingDistance": "distance",
"recordingPace": "pace",
"recordingSpeed": "speed",
"recordingDistanceValue": "{km} km",
"@recordingDistanceValue": {"placeholders": {"km": {"type": "String", "example": "5.12"}}},
"recordingPaceValue": "{pace} /km",
"@recordingPaceValue": {"description": "Minutes and seconds a kilometre takes.", "placeholders": {"pace": {"type": "String", "example": "6:18"}}},
"recordingSpeedValue": "{kmh} km/h",
"@recordingSpeedValue": {"placeholders": {"kmh": {"type": "String", "example": "24.3"}}},
"recordingPausedAuto": "Paused: not moving",
"@recordingPausedAuto": {"description": "Train paused the recording by itself (auto-pause)."},
"recordingPausedByHand": "Paused",
"recordingPause": "Pause",
"recordingResume": "Resume",
"recordingStop": "Stop",
"recordingHoldToStop": "Hold Stop to end the workout",
"recordingStoppedTitle": "Save this workout?",
"recordingSave": "Save",
"recordingDiscard": "Discard",
"recordingDiscardTitle": "Discard this workout?",
"recordingDiscardMessage": "Its time and track are deleted from this device. This can't be undone.",
"recordingSaveFailed": "It can't be saved on this device, which doesn't hold your account's keys. It stays here until it can be.",
"recordingCutOffTitle": "A recording was cut off",
"recordingCutOffMessage": "A recording from {start} was cut off. Save it up to {end}, or discard it?",
"@recordingCutOffMessage": {"description": "A recording found unfinished as the app opened: when it started and its last sample.", "placeholders": {"start": {"type": "String", "example": "07:30"}, "end": {"type": "String", "example": "08:02"}}},
"recordingNotificationChannel": "Recording",
"@recordingNotificationChannel": {"description": "The name of the recording's notification channel, in the system's settings."},
"recordingNotificationPaused": "{activity} · paused",
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
"@recordingNotificationPaused": {"description": "The notification's title while paused.", "placeholders": {"activity": {"type": "String", "example": "Running"}}},
"workoutName": "{when, select, morning_running{Morning run} morning_walking{Morning walk} morning_hiking{Morning hike} morning_cycling{Morning ride} morning_indoorRowing{Morning row} afternoon_running{Afternoon run} afternoon_walking{Afternoon walk} afternoon_hiking{Afternoon hike} afternoon_cycling{Afternoon ride} afternoon_indoorRowing{Afternoon row} evening_running{Evening run} evening_walking{Evening walk} evening_hiking{Evening hike} evening_cycling{Evening ride} evening_indoorRowing{Evening row} night_running{Night run} night_walking{Night walk} night_hiking{Night hike} night_cycling{Night ride} night_indoorRowing{Night row} other{Workout}}",
"@workoutName": {"description": "A workout's name where the person gave none: the time of day it started (morning 5– 12, afternoon 12– 17, evening 17– 22, night) and its activity, as Strava names them.", "placeholders": {"when": {"type": "String"}}},
"workoutLocked": "Locked workout",
"workoutLockedHint": "This device doesn't hold the key it's sealed with.",
"@workoutLockedHint": {"description": "Under a workout this device can't open (its key is missing here)."},
"workoutEdit": "Edit",
"workoutEditTitle": "Edit workout",
"workoutNameLabel": "Name",
feat(train): the Loop's training sessions as workouts (ADR 0030, D39)
Each read of the band lists the sessions it holds, reads those from the
read's first day on (`LoopDays.kt`, one line per exercise: its start
with the band's offset, length, Polar's sport ID, figures and heart
rate at its interval), and deletes those up to the day kept, as the
days. The app keeps each as a workout of origin Polar Loop with its
heart rate (`takeLoopSessions`), Polar's running and cycling as
Train's and every other sport as the new `Activity.other`, which a
workout's edit sheet now changes; a session overlapping a workout Train
recorded gives it its heart rate where it has none instead. The
sessions taken are remembered in `loop.json`, so one deleted in Train
isn't brought back. Health Connect writes the Loop's workouts as
Train's own.
The SDK gives sessions as its protobuf messages: protobuf-javalite
3.20.0, the SDK's own, is named for compiling only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 20:22:23 +01:00
"workoutActivityLabel": "Activity",
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
"workoutNotesLabel": "Notes",
"workoutSave": "Save",
"workoutDelete": "Delete",
"workoutDeleteTitle": "Delete this workout?",
"workoutDeleteMessage": "It's deleted on all your devices.",
"workoutDeleted": "Workout deleted",
"workoutMovingTime": "moving time",
"workoutTotalTime": "whole time",
"@workoutTotalTime": {"description": "Lower case, under a figure: from start to end, pauses included."},
"workoutDistance": "distance",
"workoutPace": "pace",
"workoutSpeed": "speed",
"workoutClimb": "climbed",
"@workoutClimb": {"description": "Lower case, under a figure: the elevation gained."},
"workoutClimbValue": "{metres} m",
"@workoutClimbValue": {"placeholders": {"metres": {"type": "int"}}},
"workoutSplits": "Splits",
feat(train): a workout keeps its plan's steps as laps, shown on its page (ADR 0030, D43)
As a recording with a plan is saved, each step run becomes a lap in the
workout's sealed payload: the step, its round, when it started and ended,
its moving time and distance, with the highest heart rate the zones ran
with. The page lists the steps with their pace, speed or split, stroke
rate, watts and heart rate, marks whether each held its target, and the
charts shade the work steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 23:12:34 +01:00
"workoutSteps": "Steps of {plan}",
"@workoutSteps": {"placeholders": {"plan": {"type": "String"}}},
"workoutOnTarget": "On target",
"workoutOffTarget": "Off target",
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
"workoutSplitKm": "km {number}",
"@workoutSplitKm": {"description": "A kilometre's split, its number.", "placeholders": {"number": {"type": "int"}}},
"workoutSplitFiveKm": "{from}– {to} km",
"@workoutSplitFiveKm": {"description": "A cycling split of 5 km: where it starts and ends.", "placeholders": {"from": {"type": "int"}, "to": {"type": "int"}}},
"workoutPaceChart": "Pace",
"workoutSpeedChart": "Speed",
"workoutElevationChart": "Elevation",
"workoutMapSource": "Map by {name}: it sees your device's address and the area shown, not your track.",
"@workoutMapSource": {"description": "Under a workout's map: what the map's source sees.", "placeholders": {"name": {"type": "String", "example": "OpenFreeMap"}}},
"workoutMapSourceAbout": "What {name} keeps",
"@workoutMapSourceAbout": {"description": "A link to the map source's page about what it keeps.", "placeholders": {"name": {"type": "String", "example": "OpenFreeMap"}}},
"workoutMapUnavailable": "The map can't be loaded now.",
feat(train): the heart-rate strap, recorded by the service itself (ADR 0030, D25-D27)
The Record sheet gains a Heart rate row: a sheet looks for straps
advertising the Heart Rate Service, the nearest first, once nearby
devices are allowed, and the one chosen is remembered in the person's
folder; it records with every workout unless switched off for one.
The recorder service connects to it with Android's own Bluetooth API,
no package (D26), parses Heart Rate Measurement in Kotlin (tested on
the JVM), writes each rate and the strap's state to the recording
file, and asks for a lost strap again for as long as it records. The
recording screen shows the heart rate, and "Strap lost" while it is
gone; a workout's page shows the heart's average and highest rate,
each split's average and its chart.
iOS's recording waits for a Mac (D25), after the rower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 15:50:36 +01:00
"workoutNotFound": "This workout is gone.",
"recordHeartRate": "Heart rate",
"recordNoStrap": "No strap",
2026-10-28 21:32:13 +01:00
"recordHeartLoop": "From the Polar Loop",
"recordHeartStrapThenLoop": "{strap}, the Loop while it is lost",
"@recordHeartStrapThenLoop": {"placeholders": {"strap": {"type": "String", "example": "Polar H10"}}},
feat(train): the heart-rate strap, recorded by the service itself (ADR 0030, D25-D27)
The Record sheet gains a Heart rate row: a sheet looks for straps
advertising the Heart Rate Service, the nearest first, once nearby
devices are allowed, and the one chosen is remembered in the person's
folder; it records with every workout unless switched off for one.
The recorder service connects to it with Android's own Bluetooth API,
no package (D26), parses Heart Rate Measurement in Kotlin (tested on
the JVM), writes each rate and the strap's state to the recording
file, and asks for a lost strap again for as long as it records. The
recording screen shows the heart rate, and "Strap lost" while it is
gone; a workout's page shows the heart's average and highest rate,
each split's average and its chart.
iOS's recording waits for a Mac (D25), after the rower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 15:50:36 +01:00
"@recordNoStrap": {"description": "On the Record sheet, under Heart rate, where no strap is chosen."},
"strapSheetTitle": "Heart-rate strap",
"strapSearching": "Looking for straps nearby…",
"strapSearchingHint": "Put the strap on: most wake up only once they touch skin.",
"strapNear": "close by",
"@strapNear": {"description": "Under a strap found: its signal is strong."},
"strapFar": "further off",
"@strapFar": {"description": "Under a strap found: its signal is weak."},
"strapUnnamed": "Unnamed strap",
"@strapUnnamed": {"description": "A strap that sends no name."},
"strapForget": "Forget strap",
"strapRefused": "Train needs nearby devices to find your strap. You can allow it in the system's settings.",
feat(train): rowing on the WP 5000 over FTMS (ADR 0030, D28)
Rowing indoors is offered again, with the rower chosen on the Record
sheet: a sheet looks for Fitness Machine Service rowers, and the one
chosen is remembered in the person's folder. The recorder service
connects to it as to the strap (one NotifyLink for both), parses
FTMS's Rower Data in Kotlin (tested on the JVM on frames written from
the specification and D8's), counts its totals from its first frame,
pauses 5 s after the last stroke, and gets the connectedDevice type. A
row's screen shows its metres, split per 500 m, stroke rate and watts;
its page its average split, stroke rate and power, the highest power,
its strokes, splits per 500 m and charts of the split, stroke rate and
power. Not yet tried on the WP 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 16:20:37 +01:00
"strapBluetoothOff": "Bluetooth is off. Turn it on to look for devices nearby.",
feat(train): the heart-rate strap, recorded by the service itself (ADR 0030, D25-D27)
The Record sheet gains a Heart rate row: a sheet looks for straps
advertising the Heart Rate Service, the nearest first, once nearby
devices are allowed, and the one chosen is remembered in the person's
folder; it records with every workout unless switched off for one.
The recorder service connects to it with Android's own Bluetooth API,
no package (D26), parses Heart Rate Measurement in Kotlin (tested on
the JVM), writes each rate and the strap's state to the recording
file, and asks for a lost strap again for as long as it records. The
recording screen shows the heart rate, and "Strap lost" while it is
gone; a workout's page shows the heart's average and highest rate,
each split's average and its chart.
iOS's recording waits for a Mac (D25), after the rower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 15:50:36 +01:00
"recordingHeartRate": "heart rate",
"@recordingHeartRate": {"description": "Under the heart rate, lower case."},
"recordingStrapLost": "Strap lost",
"@recordingStrapLost": {"description": "While recording: the strap is out of reach or off; Train keeps asking for it."},
"workoutAverageHeartRate": "average heart rate",
"@workoutAverageHeartRate": {"description": "Lower case, under a figure."},
"workoutMaxHeartRate": "highest heart rate",
"@workoutMaxHeartRate": {"description": "Lower case, under a figure."},
"workoutBpmValue": "{bpm} bpm",
"@workoutBpmValue": {"placeholders": {"bpm": {"type": "int"}}},
feat(train): rowing on the WP 5000 over FTMS (ADR 0030, D28)
Rowing indoors is offered again, with the rower chosen on the Record
sheet: a sheet looks for Fitness Machine Service rowers, and the one
chosen is remembered in the person's folder. The recorder service
connects to it as to the strap (one NotifyLink for both), parses
FTMS's Rower Data in Kotlin (tested on the JVM on frames written from
the specification and D8's), counts its totals from its first frame,
pauses 5 s after the last stroke, and gets the connectedDevice type. A
row's screen shows its metres, split per 500 m, stroke rate and watts;
its page its average split, stroke rate and power, the highest power,
its strokes, splits per 500 m and charts of the split, stroke rate and
power. Not yet tried on the WP 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 16:20:37 +01:00
"workoutHeartRateChart": "Heart rate",
"recordRower": "Rowing machine",
"recordNoRower": "Choose your rowing machine",
"recordRowerRefused": "Train needs nearby devices to record from your rowing machine. You can allow it in the system's settings.",
"rowerSheetTitle": "Rowing machine",
"rowerSearching": "Looking for rowing machines nearby…",
"rowerSearchingHint": "Wake the console: most show once you pull or press a button. One another app holds (Christopeit Connect, Kinomap) can't be found: close that app first.",
"rowerUnnamed": "Unnamed rowing machine",
"rowerForget": "Forget rowing machine",
"rowerRefused": "Train needs nearby devices to find your rowing machine. You can allow it in the system's settings.",
"recordingRowerConnecting": "Looking for the rowing machine",
"@recordingRowerConnecting": {"description": "While recording a row: the rower isn't connected yet. Another app may hold it."},
"recordingRowerLost": "Rowing machine lost",
"@recordingRowerLost": {"description": "While recording a row: the rower is out of reach or off; Train keeps asking for it."},
"recordingSplit": "split",
"@recordingSplit": {"description": "Lower case, under a figure: how long 500 m take now."},
"recordingStrokeRate": "strokes a minute",
"@recordingStrokeRate": {"description": "Lower case, under a figure."},
"recordingWatts": "watts",
"@recordingWatts": {"description": "Lower case, under a figure."},
"workoutSplitValue": "{split} /500 m",
"@workoutSplitValue": {"description": "Minutes and seconds 500 m take.", "placeholders": {"split": {"type": "String", "example": "2:04"}}},
"workoutAverageSplit": "average split",
"@workoutAverageSplit": {"description": "Lower case, under a figure."},
"workoutAverageStrokeRate": "average stroke rate",
"@workoutAverageStrokeRate": {"description": "Lower case, under a figure."},
"workoutStrokeRateValue": "{rate} /min",
"@workoutStrokeRateValue": {"description": "Strokes a minute.", "placeholders": {"rate": {"type": "String", "example": "24"}}},
"workoutAveragePower": "average power",
"@workoutAveragePower": {"description": "Lower case, under a figure."},
"workoutMaxPower": "highest power",
"@workoutMaxPower": {"description": "Lower case, under a figure."},
"workoutWattsValue": "{watts} W",
"@workoutWattsValue": {"placeholders": {"watts": {"type": "int"}}},
"workoutStrokes": "strokes",
"@workoutStrokes": {"description": "Lower case, under a figure: how many strokes."},
"workoutSplitMetres": "{from}– {to} m",
"@workoutSplitMetres": {"description": "A rowing split of 500 m: where it starts and ends.", "placeholders": {"from": {"type": "int"}, "to": {"type": "int"}}},
"workoutSplitChart": "Split",
"workoutStrokeRateChart": "Stroke rate",
"workoutPowerChart": "Power",
"recordAutoPauseRowingHint": "When you stop rowing, going on with the next stroke",
feat(train): workouts written to Health Connect (ADR 0030, D31)
A Health Connect row in the account sheet opens its screen: Write my
workouts, off until turned on, Health Connect asking as it is. Then
every workout this phone holds is written there (its session with its
pauses and route, heart rate, speed and the rower's watts as samples,
distance and elevation gained), and written again or deleted as it
changes, here or through a sync, and as the app opens. Client IDs come
from the workout's ID, a version counts up with each change; what was
written is kept in the person's health_connect.json, never synced. A
permission taken back in Health Connect turns writing off and says so;
Health Connect's link to Train's rationale opens a screen of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 17:06:31 +01:00
"recordingMetres": "metres",
"healthTitle": "Health Connect",
"healthHint": "Share workouts with other apps on this phone",
"@healthHint": {"description": "Under Health Connect in the account sheet."},
"healthUnavailable": "Health Connect isn't on this phone. Android 14 and later have it built in; before 14, it's an app of its own.",
"healthNeedsUpdate": "Update the Health Connect app to use it with Train.",
"healthWrite": "Write my workouts",
"healthWriteHint": "Every workout on this phone, kept up to date as you edit or delete it",
feat(train): other apps' workouts read from Health Connect (ADR 0030, D31)
Read from other apps, off until turned on, asks for reading with the
history; the apps that wrote sessions are listed, each off. One turned
on brings its sessions, earlier ones at once and later ones through the
changes token as the app opens and comes to the front: each a workout
of Train's own store, sealed and synced, its origin in its payload, its
heart rate, pauses and a row's power as series, named by its app in the
list and on its page. A double of a workout Train has brings at most its
heart rate; one deleted in Train stays out; a track comes only when
asked for on the page, Health Connect asking for consent. What came from
Health Connect is never written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 17:17:59 +01:00
"healthTakenBack": "Health Connect took this back. Allow it there to turn it on again.",
feat(train): workouts written to Health Connect (ADR 0030, D31)
A Health Connect row in the account sheet opens its screen: Write my
workouts, off until turned on, Health Connect asking as it is. Then
every workout this phone holds is written there (its session with its
pauses and route, heart rate, speed and the rower's watts as samples,
distance and elevation gained), and written again or deleted as it
changes, here or through a sync, and as the app opens. Client IDs come
from the workout's ID, a version counts up with each change; what was
written is kept in the person's health_connect.json, never synced. A
permission taken back in Health Connect turns writing off and says so;
Health Connect's link to Train's rationale opens a screen of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 17:06:31 +01:00
"healthRefused": "Health Connect didn't allow it. You can allow it in its settings.",
"healthOpenSettings": "Open Health Connect",
"healthWhy": "Why Train asks",
"healthRationaleTitle": "Train and Health Connect",
"healthRationaleWrite": "Train writes your workouts to Health Connect only once you turn it on: each one with its route, heart rate, speed, power, distance and climb, so that other apps on this phone can use them. A workout edited or deleted in Train is edited or deleted there too.",
"healthRationaleRead": "Train reads other apps' workouts only once you turn it on, and only from the apps you choose: their heart rate, distance and power come into Train as workouts, sealed with your key before they're synced to your instance, which can't read them.",
feat(train): other apps' workouts read from Health Connect (ADR 0030, D31)
Read from other apps, off until turned on, asks for reading with the
history; the apps that wrote sessions are listed, each off. One turned
on brings its sessions, earlier ones at once and later ones through the
changes token as the app opens and comes to the front: each a workout
of Train's own store, sealed and synced, its origin in its payload, its
heart rate, pauses and a row's power as series, named by its app in the
list and on its page. A double of a workout Train has brings at most its
heart rate; one deleted in Train stays out; a track comes only when
asked for on the page, Health Connect asking for consent. What came from
Health Connect is never written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 17:17:59 +01:00
"healthRationaleNothingElse": "What Train reads there is shown to you and nobody else, and isn't used for anything else. Turning a switch off, or taking a permission back in Health Connect, stops it.",
"healthRead": "Read from other apps",
"healthReadHint": "Their workouts come into Train, from the apps you choose below",
"healthApps": "Apps",
"@healthApps": {"description": "Over the apps that wrote workouts to Health Connect, each with its switch."},
"healthAppsEmpty": "No app has written workouts to Health Connect yet.",
"workoutFromList": "from {app}",
"@workoutFromList": {"description": "In the list, after when an imported workout was: the app it came from.", "placeholders": {"app": {"type": "String", "example": "Gadgetbridge"}}},
"workoutFrom": "From {app}, through Health Connect",
"@workoutFrom": {"description": "On an imported workout's page.", "placeholders": {"app": {"type": "String", "example": "Gadgetbridge"}}},
"workoutBringTrack": "Bring in the track",
"@workoutBringTrack": {"description": "On an imported workout's page: asks Health Connect for its route."},
feat(train): the body's figures in a sealed, synced profile (ADR 0030, D33, D34)
A person's profile, one row per person (its ID derived from theirs),
sealed like a workout: sex, birth date, height, weight, resting and
maximum heart rate, VO2max, training background, typical day and sleep
goal, each optional. "Your body" in the account sheet edits it; the
Loop's setup is to reuse its form. The migration only adds the synced
table `profile`, deleted with the person's sync space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 18:02:04 +01:00
"workoutNoTrack": "Health Connect gave no track for it.",
"profileTitle": "Your body",
"profileHint": "What your band and your heart-rate zones work from",
"@profileHint": {"description": "Under Your body in the account sheet."},
"profileIntro": "Each figure is optional and sealed with your key, as your workouts are; your instance can't read them.",
"profileLocked": "Your profile is sealed with a key this device doesn't hold.",
"profileSex": "Sex",
"profileSexHint": "As Polar's formulas take it: the one your body's figures follow",
"profileSexFemale": "Female",
"profileSexMale": "Male",
"profileNotGiven": "Not given",
"profileBirthDate": "Birth date",
"profileHeight": "Height",
"profileHeightUnit": "cm",
"profileWeight": "Weight",
"profileWeightUnit": "kg",
"profileRestingHeartRate": "Resting heart rate",
"profileMaxHeartRate": "Highest heart rate",
"profileBpmUnit": "bpm",
"profileVo2Max": "VO2max",
"profileVo2MaxUnit": "ml/kg/min",
"profileTrainingBackground": "Training background",
"trainingOccasional": "Occasional",
"trainingRegular": "Regular",
"trainingFrequent": "Frequent",
"trainingHeavy": "Heavy",
"trainingSemiPro": "Semi-pro",
"trainingPro": "Pro",
"profileTypicalDay": "Typical day",
"typicalDaySitting": "Mostly sitting",
"typicalDayStanding": "Mostly standing",
"typicalDayMoving": "Mostly moving",
"profileSleepGoal": "Sleep goal",
"profileSave": "Save",
"profileSaved": "Saved",
feat(train): setting up and keeping the Polar Loop (ADR 0030, D34–D36)
On Android, the Loop through Polar's BLE SDK 8.3.0 (from JitPack, asked
for Polar's group alone, its files pinned by SHA-256 in Gradle's
verification metadata): LoopBridge.kt over app.halkyon.train/loop, the
first-time use's figures mapped and checked in LoopSetup.kt, tested on
the JVM. PolarLoop per person remembers the band in loop.json, connects
while the app is open, sets its clock at each connection, keeps what it
tells (battery, charging, firmware), offers a changed profile and sends
it only when asked (D35: first-time use writes a new user identifier
each time), and lets the band go by a factory reset after asking. The
setup sheet finds Loops nearest first and asks only for what the
profile lacks; Polar's defaults (60, 220 less the age, a VO2max from
Polar's fitness tables) go to the band alone.
Polar's SDK is not free software (D36, correcting D32): the app's AGPL
gains a section 7 permission for linking it, LICENSE.md names it, and
its licence and bundled notices join the licence list on Android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 18:58:02 +01:00
"profileInvalid": "Check the figures marked.",
"profileNeeded": "Needed for your band",
"loopTitle": "Polar Loop",
"loopHint": "Your band: its setup, battery and clock",
"@loopHint": {"description": "Under Polar Loop in the account sheet."},
"loopNone": "Train sets up a Polar Loop and keeps it itself, without Polar Flow or a Polar account. A band used with Polar Flow needs a factory reset first.",
"loopSetUp": "Set up a Loop",
"loopSetUpAgain": "Set up again",
"loopLetGo": "Let the band go",
"loopLetGoTitle": "Let the band go?",
"loopLetGoText": "The band is reset to the factory's state, and what it holds that hasn't reached Train is lost. Afterwards it can be paired with another phone, or with Polar Flow.",
"loopLetGoConfirm": "Reset the band",
"loopConnected": "Connected",
"loopLastSeen": "Last seen {when}",
"@loopLastSeen": {"placeholders": {"when": {"type": "String", "example": "28 Oct 2026, 18:30"}}},
"loopNotInReach": "Not in reach",
"loopBattery": "Battery {level} %",
"@loopBattery": {"placeholders": {"level": {"type": "int"}}},
"loopCharging": "Battery {level} %, charging",
"@loopCharging": {"placeholders": {"level": {"type": "int"}}},
"loopFirmware": "Firmware {version}",
"@loopFirmware": {"placeholders": {"version": {"type": "String", "example": "1.2.3"}}},
feat(train): the Loop's firmware updates, directly or through the instance (ADR 0030, D10, D40)
Train's server relays Polar's firmware service for the people signed in
(`firmware`: offered, check, download), a fetched file kept in memory a
day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app
asks on the day's first connection and on the band's screen, from Polar
or the instance as the person chose, never Polar in the instance's
place; an update runs only after asking, reads and keeps the band
first, and sets it up again with its figures and clock after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 20:52:53 +01:00
"loopFirmwareCheck": "Check for a firmware update",
"loopFirmwareCurrent": "The band's firmware is up to date.",
"loopFirmwareOffered": "Firmware {version} available",
"@loopFirmwareOffered": {"placeholders": {"version": {"type": "String", "example": "2.1.0"}}},
"loopFirmwareViaPolar": "Fetched from Polar directly: Polar learns this phone's address (or its VPN's), with the band's firmware version and hardware code.",
"loopFirmwareViaInstance": "Fetched through your instance: Polar learns the instance's address, not this phone's, with the band's firmware version and hardware code.",
"loopFirmwareErases": "The update erases the band. Train first keeps what is on it, then sets it up again with your profile.",
"loopFirmwareUpdate": "Update",
"loopFirmwareUpdateTitle": "Update the band's firmware?",
"loopFirmwareUpdateText": "Train first keeps what is on the band, as the update erases it. The band restarts, which takes a few minutes, and Train then sets it up again with your profile. Keep it near the phone until then.",
"loopFirmwareStage": "{stage, select, reading{Keeping what is on the band…} fetching{Fetching the firmware…} preparing{Preparing the band…} writing{Writing the firmware…} finalizing{Finishing the update…} done{Written} restarting{The band restarts…} other{Updating…}}",
"@loopFirmwareStage": {"placeholders": {"stage": {"type": "String"}}},
"loopFirmwareStageHint": "Keep the band near the phone, and Train open.",
"loopFirmwareUpdated": "Firmware updated; the band is set up again.",
"loopFirmwareThroughInstance": "Firmware through your instance",
"loopFirmwareThroughInstanceHint": "Polar then learns the instance's address rather than this phone's. Off, the phone asks Polar directly.",
feat(train): setting up and keeping the Polar Loop (ADR 0030, D34–D36)
On Android, the Loop through Polar's BLE SDK 8.3.0 (from JitPack, asked
for Polar's group alone, its files pinned by SHA-256 in Gradle's
verification metadata): LoopBridge.kt over app.halkyon.train/loop, the
first-time use's figures mapped and checked in LoopSetup.kt, tested on
the JVM. PolarLoop per person remembers the band in loop.json, connects
while the app is open, sets its clock at each connection, keeps what it
tells (battery, charging, firmware), offers a changed profile and sends
it only when asked (D35: first-time use writes a new user identifier
each time), and lets the band go by a factory reset after asking. The
setup sheet finds Loops nearest first and asks only for what the
profile lacks; Polar's defaults (60, 220 less the age, a VO2max from
Polar's fitness tables) go to the band alone.
Polar's SDK is not free software (D36, correcting D32): the app's AGPL
gains a section 7 permission for linking it, LICENSE.md names it, and
its licence and bundled notices join the licence list on Android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 18:58:02 +01:00
"loopProfileChanged": "Your body changed since the band was set up",
"loopProfileChangedHint": "Sending sets the band up again with your figures as they are now.",
"loopSend": "Send to the band",
"loopSent": "Sent to the band.",
"loopFailed": "The band didn't answer. Keep it close, and try again.",
"loopSearching": "Looking for Loops nearby",
"loopSearchingHint": "Keep the band close; tap it to wake it up.",
"loopRefused": "Train can't look for your band without nearby devices. Allow them in Android's settings.",
"loopUnnamed": "Polar Loop",
"loopSettingUp": "Setting up the band…",
"loopSettingUpHint": "Android may ask you to pair it.",
"loopSetUpDone": "The band is set up.",
"loopFormTitle": "For your band",
"loopFormIntro": "Your band needs these. They're kept in Your body, sealed with your key. For a resting or highest heart rate or a VO2max you haven't given, the band takes Polar's defaults, and Your body stays as it is.",
feat(train): Days, the Loop's days and nights kept sealed (ADR 0030, D37, D38)
What the band read brings, from the bridge or the background job's
file (`LoopLines`), is kept by `DayRepository`: one sealed `day` and
`night` a date and the day's heart rate in chunks of three hours, every
ID made from the person and the date, a later read replacing what it
brought and keeping the rest, nothing rewritten unchanged; versioned,
gzipped JSON (`day_payload.dart`). `PolarLoop` reads the band as each
connection is ready (every half hour at most, or when asked), takes in
the background file as the app opens, and remembers up to which day
the days are sealed, which the next read then deletes on the band;
it keeps the background job on the band and from the right day, and
`Let the band go` stops it.
Home gains a Days tab, each day with its steps, active time, sleep and
Nightly Recharge, pulled to read the band; a day's page shows its
figures, its time in each class, its heart rate, the night's phases
and rating, Nightly Recharge and skin temperature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 19:53:12 +01:00
"loopFormSave": "Set up the band",
"homeWorkouts": "Workouts",
"homeDays": "Days",
feat(train): interval plans, synced and sealed, with an editor and templates (ADR 0030, D42)
Train's server gains the synced `plan` table, a sealed payload alone;
the migration `plans` only adds it, its space index and the foreign key
to its sync space, and a person's deletion takes it with their space.
The app keeps plans as `plan` rows sealed for their row: a name, what
they are for, whether a step off target is told, and blocks of steps
repeated, each ending after a time, a distance or a lap tap and aiming
at a pace, speed, heart rate or zone, or the rower's split, watts or
stroke rate. Home's Plans tab lists them, a new one starts from nothing
or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes
steps on a sheet, and a deletion asks first and can be undone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 22:35:22 +01:00
"homePlans": "Plans",
"plansNew": "New plan",
"plansNewEmpty": "From nothing",
"plansEmptyTitle": "No plans yet",
"plansEmptyHint": "A plan's steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.",
"planLocked": "A plan that doesn't open here",
"planTemplateFourByFour": "4× 4 minutes",
"planTemplateTabata": "Tabata",
"planTemplateFourHundreds": "10× 400 m",
"planTitle": "Plan",
"planNewTitle": "New plan",
"planName": "Name",
"planActivity": "For",
"planActivityAny": "Any activity",
"planOffTarget": "Tell me when I'm off target",
"planOffTargetHint": "A short cue at most every 30 seconds.",
"planSave": "Save",
"planNotSaved": "The plan wasn't saved.",
"planAddStep": "Add a step",
"planAddRepeat": "Add a repeat",
"planRepeat": "{count}× ",
"@planRepeat": {"placeholders": {"count": {"type": "int"}}},
"planFewer": "Fewer rounds",
"planMore": "More rounds",
"planBlockMenu": "More",
"planMoveUp": "Move up",
"planMoveDown": "Move down",
"planRemove": "Remove",
"planDelete": "Delete",
"planDeleteTitle": "Delete this plan?",
"planDeleteMessage": "It's deleted on all your devices; workouts recorded with it keep their steps.",
"planDeleted": "Plan deleted",
"planSteps": "{count, plural, =1{1 step} other{{count} steps}}",
"@planSteps": {"placeholders": {"count": {"type": "int"}}},
feat(train): a recording runs an interval plan, with cues and Lap (ADR 0030, D42)
The plan chosen on the Record sheet goes to the recording service spelled
out: each step's end, its target (zones as the heart rates they span for
the profile's highest rate) and its spoken line in the app's language.
PlanRunner.kt moves through the steps on the moving time and distance,
counts a timed step's last seconds down and cues off target from a 30 s
window; PlanCues.kt gives a tone, a vibration and the voice, ducking
music. The file's plan and step lines bring it back, and the recording
screen shows the step, what is left of it, its target, the next step and
Lap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 22:59:48 +01:00
"planSayRound": "{round} of {rounds}",
"@planSayRound": {"placeholders": {"round": {"type": "int"}, "rounds": {"type": "int"}}},
"planSayMinutes": "{count, plural, =1{1 minute} other{{count} minutes}}",
"@planSayMinutes": {"placeholders": {"count": {"type": "int"}}},
"planSaySeconds": "{count, plural, =1{1 second} other{{count} seconds}}",
"@planSaySeconds": {"placeholders": {"count": {"type": "int"}}},
"planSayMetres": "{count, plural, =1{1 metre} other{{count} metres}}",
"@planSayMetres": {"placeholders": {"count": {"type": "int"}}},
"planSayKilometres": "{km} kilometres",
"@planSayKilometres": {"placeholders": {"km": {"type": "String"}}},
"planSayUntilLap": "until you tap Lap",
"planSayPace": "{from} to {to} a kilometre",
"@planSayPace": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSaySpeed": "{from} to {to} kilometres an hour",
"@planSaySpeed": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSayHeartRate": "{from} to {to} beats a minute",
"@planSayHeartRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSayZone": "zone {zone}",
"@planSayZone": {"placeholders": {"zone": {"type": "int"}}},
"planSaySplit": "{from} to {to} per 500 metres",
"@planSaySplit": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSayWatts": "{from} to {to} watts",
"@planSayWatts": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSayStrokeRate": "{from} to {to} strokes a minute",
"@planSayStrokeRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"planSayHarder": "Push a little",
"planSayEasier": "Ease off a little",
"planSayDone": "Plan done",
feat(train): interval plans, synced and sealed, with an editor and templates (ADR 0030, D42)
Train's server gains the synced `plan` table, a sealed payload alone;
the migration `plans` only adds it, its space index and the foreign key
to its sync space, and a person's deletion takes it with their space.
The app keeps plans as `plan` rows sealed for their row: a name, what
they are for, whether a step off target is told, and blocks of steps
repeated, each ending after a time, a distance or a lap tap and aiming
at a pace, speed, heart rate or zone, or the rower's split, watts or
stroke rate. Home's Plans tab lists them, a new one starts from nothing
or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes
steps on a sheet, and a deletion asks first and can be undone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 22:35:22 +01:00
"stepTitle": "Step",
"stepKind": "Kind",
"stepWarmUp": "Warm-up",
"stepWork": "Work",
"stepRecovery": "Recovery",
"stepCoolDown": "Cool-down",
"stepEnds": "Ends",
"stepEndTime": "After a time",
"stepEndDistance": "After a distance",
"stepEndLap": "With a lap tap",
"stepTimeLabel": "Time (min:s)",
"stepMetresLabel": "Metres",
"stepTarget": "Target",
"stepZone": "Zone",
"stepDone": "Done",
"stepMinutes": "{minutes} min",
"@stepMinutes": {"placeholders": {"minutes": {"type": "int"}}},
"stepSeconds": "{seconds} s",
"@stepSeconds": {"placeholders": {"seconds": {"type": "int"}}},
"stepMinutesSeconds": "{minutes} min {seconds} s",
"@stepMinutesSeconds": {"placeholders": {"minutes": {"type": "int"}, "seconds": {"type": "int"}}},
"stepMetres": "{metres} m",
"@stepMetres": {"placeholders": {"metres": {"type": "int"}}},
"stepKilometres": "{km} km",
"@stepKilometres": {"placeholders": {"km": {"type": "String"}}},
"stepUntilLap": "until a lap tap",
"targetNone": "None",
"targetFrom": "From",
"targetTo": "To",
"targetKindPace": "Pace (per km)",
"targetKindSpeed": "Speed (km/h)",
"targetKindHeartRate": "Heart rate (bpm)",
"targetKindHeartZone": "Heart-rate zone",
"targetKindSplit": "Split (per 500 m)",
"targetKindWatts": "Power (W)",
"targetKindStrokeRate": "Stroke rate (per min)",
"targetPace": "{from}– {to} per km",
"@targetPace": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetSpeed": "{from}– {to} km/h",
"@targetSpeed": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetHeartRate": "{from}– {to} bpm",
"@targetHeartRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetZone": "Zone {zone}",
"@targetZone": {"placeholders": {"zone": {"type": "int"}}},
"targetSplit": "{from}– {to} per 500 m",
"@targetSplit": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetWatts": "{from}– {to} W",
"@targetWatts": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetStrokeRate": "{from}– {to} strokes/min",
"@targetStrokeRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
feat(train): Days, the Loop's days and nights kept sealed (ADR 0030, D37, D38)
What the band read brings, from the bridge or the background job's
file (`LoopLines`), is kept by `DayRepository`: one sealed `day` and
`night` a date and the day's heart rate in chunks of three hours, every
ID made from the person and the date, a later read replacing what it
brought and keeping the rest, nothing rewritten unchanged; versioned,
gzipped JSON (`day_payload.dart`). `PolarLoop` reads the band as each
connection is ready (every half hour at most, or when asked), takes in
the background file as the app opens, and remembers up to which day
the days are sealed, which the next read then deletes on the band;
it keeps the background job on the band and from the right day, and
`Let the band go` stops it.
Home gains a Days tab, each day with its steps, active time, sleep and
Nightly Recharge, pulled to read the band; a day's page shows its
figures, its time in each class, its heart rate, the night's phases
and rating, Nightly Recharge and skin temperature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 19:53:12 +01:00
"daysEmptyTitle": "No days yet",
"daysEmptyHint": "The days and nights your Polar Loop counts show here.",
"daysSteps": "{steps, plural, =1{1 step} other{{steps} steps}}",
"@daysSteps": {"placeholders": {"steps": {"type": "int"}}},
"daysActive": "{time} active",
"@daysActive": {"placeholders": {"time": {"type": "String", "example": "1 h 20 min"}}},
"daysAsleep": "{time} asleep",
"@daysAsleep": {"placeholders": {"time": {"type": "String", "example": "7 h 10 min"}}},
"daysRecharge": "Recharge {level} of 6",
"@daysRecharge": {"placeholders": {"level": {"type": "int"}}},
"dayHoursMinutes": "{hours} h {minutes} min",
"@dayHoursMinutes": {"placeholders": {"hours": {"type": "int"}, "minutes": {"type": "int"}}},
"dayMinutes": "{minutes} min",
"@dayMinutes": {"placeholders": {"minutes": {"type": "int"}}},
"dayCalories": "{calories} kcal",
"@dayCalories": {"placeholders": {"calories": {"type": "int"}}},
"dayGoal": "{percent} % of the day's goal",
"@dayGoal": {"placeholders": {"percent": {"type": "int"}}},
"dayNothing": "The band has given nothing for this day yet.",
"dayTimes": "The day's time",
"dayTimeNonWear": "Not worn",
"dayTimeSleep": "Asleep",
"dayTimeSedentary": "Sitting still",
"dayTimeLight": "Light activity",
"dayTimeModerate": "Moderate activity",
"dayTimeVigorous": "Vigorous activity",
"dayHeartRate": "Heart rate",
"dayBpm": "{bpm} bpm",
"@dayBpm": {"placeholders": {"bpm": {"type": "int"}}},
"dayNight": "The night",
"dayNightSpan": "{from} – {to}",
"@dayNightSpan": {"placeholders": {"from": {"type": "String", "example": "23:15"}, "to": {"type": "String", "example": "07:05"}}},
"daySleepWake": "Awake",
"daySleepRem": "REM",
"daySleepLight": "Light",
"daySleepDeep": "Deep",
"dayRatingPoorly": "You slept poorly",
"dayRatingSomewhatPoorly": "You slept somewhat poorly",
"dayRatingNeither": "You slept neither poorly nor well",
"dayRatingSomewhatWell": "You slept somewhat well",
"dayRatingWell": "You slept well",
"dayRecharge": "Nightly Recharge",
"dayRechargeLevel": "Recovery {level} of 6",
"@dayRechargeLevel": {"placeholders": {"level": {"type": "int"}}},
"dayAnsStatus": "Autonomic nervous system {value}, from − 10 to +10",
"@dayAnsStatus": {"placeholders": {"value": {"type": "String", "example": "+2.5"}}},
"dayNightHeart": "Heart rate {bpm} bpm, your usual {usual} bpm",
"@dayNightHeart": {"placeholders": {"bpm": {"type": "int"}, "usual": {"type": "int"}}},
"dayHrv": "Heart rate variability {ms} ms, your usual {usual} ms",
"@dayHrv": {"placeholders": {"ms": {"type": "int"}, "usual": {"type": "int"}}},
"dayBreathing": "Breathing {rate} a minute",
"@dayBreathing": {"placeholders": {"rate": {"type": "String", "example": "14.5"}}},
"daySkin": "Skin temperature",
"dayCelsius": "{value} °C",
"@dayCelsius": {"placeholders": {"value": {"type": "String", "example": "33.4"}}},
"daySkinDeviation": "{value} °C from your usual at night",
feat(train): routes kept as sealed rows, planned on the map (ADR 0030, D44)
Routes are synced `route` rows (the model `SavedRoute`, as Flutter has a
`Route`), sealed as plans are; the migration `routes` only creates the
table, its space index and its cascade to the sync engine's spaces. A
route's payload holds its name, activity, profile, waypoints and the way
planned, points and elevations delta-coded and gzipped.
Home gets a fourth tab, Routes, and the planner: waypoints tapped on the
map, held and dragged, the last undone, a new route's first where the
phone is (GPS through the recorder's channel), the way planned by the
instance's BRouter after each change, an earlier answer dropped; its
distance, climb, descent, elevation and surfaces under the map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 00:37:44 +01:00
"@daySkinDeviation": {"placeholders": {"value": {"type": "String", "example": "− 0.2"}}},
"homeRoutes": "Routes",
"routesNew": "New route",
"routesEmptyTitle": "No routes yet",
"routesEmptyHint": "Tap the map to set waypoints, and your instance plans the way between them on paths and roads, with its climb and surfaces.",
"routeLocked": "A route that doesn't open here",
"routeTitle": "Route",
"routeNewTitle": "New route",
"routeName": "Name",
"routeActivity": "For",
"routeActivityAny": "Any activity",
"routeProfile": "Planned for",
"routeProfileRun": "Running",
"routeProfileHike": "Hiking",
"routeProfileRoadBike": "Road bike",
"routeProfileTrekkingBike": "Trekking bike",
"routeProfileGravel": "Gravel bike",
"routeProfileMtb": "Mountain bike",
"routeTapHint": "Tap the map to add a waypoint; hold one and drag it to move it.",
"routeUndo": "Take the last waypoint off",
"routePlanning": "Planning…",
"routeRefusedInvalid": "A waypoint too many, or one off the map.",
"routeRefusedTooOften": "Planned too often: try again in a minute.",
"routeRefusedNoRoute": "No way found there: a waypoint may be off the paths, or outside the region your instance plans in.",
"routeRefusedUnreachable": "Your instance can't be reached to plan the way.",
"routeNotOffered": "Your instance doesn't plan routes.",
"routeAscent": "{metres} m up",
"@routeAscent": {"placeholders": {"metres": {"type": "int"}}},
"routeDescent": "{metres} m down",
"@routeDescent": {"placeholders": {"metres": {"type": "int"}}},
"routeSummary": "{distance} · {ascent}",
"@routeSummary": {"description": "A route in the list: its distance and climb.", "placeholders": {"distance": {"type": "String", "example": "12.3 km"}, "ascent": {"type": "String", "example": "120 m up"}}},
2026-10-29 10:44:47 +01:00
"routesBringIn": "Bring in a GPX file",
"@routesBringIn": {"description": "In the Routes tab: a route brought in from a GPX file (ADR 0030, D45)."},
"routeBringInTitle": "Bring in {name}",
"@routeBringInTitle": {"placeholders": {"name": {"type": "String", "example": "Isar loop"}}},
"routeBringInSnap": "Plan it along the paths",
"routeBringInSnapHint": "Through its points, with the profile you choose; its turns told",
"routeBringInDrawn": "Keep it as drawn",
"routeBringInDrawnHint": "Its way as the file has it, no turns told",
"routeNotGpx": "This file holds no route Train can read",
"routeShareGpx": "Share as GPX",
"routeSaveGpx": "Save as GPX",
2026-10-29 10:28:36 +01:00
"routeOffline": "Keep the map offline",
"@routeOffline": {"description": "A switch on a saved route: its map kept on this device (ADR 0030, D45)."},
"routeOfflineOff": "Its map along the way, 2 km either side, on this device for when there's no network",
"routeOfflineLoading": "Keeping the map: {done} of {total} tiles",
"@routeOfflineLoading": {"placeholders": {"done": {"type": "int"}, "total": {"type": "int"}}},
"routeOfflineKept": "Kept on this device: {size} MB",
"@routeOfflineKept": {"placeholders": {"size": {"type": "String", "example": "12.4"}}},
"routeOfflineMissing": "{missing} of {total} tiles didn't come: asked for again when the app next opens online",
"@routeOfflineMissing": {"placeholders": {"missing": {"type": "int"}, "total": {"type": "int"}}},
"routeOfflineNoMap": "The map can't be reached now: try again online",
"routeOfflineSaveFirst": "Save the route to keep its map offline",
"routeKeptOffline": "Map kept offline",
feat(train): routes kept as sealed rows, planned on the map (ADR 0030, D44)
Routes are synced `route` rows (the model `SavedRoute`, as Flutter has a
`Route`), sealed as plans are; the migration `routes` only creates the
table, its space index and its cascade to the sync engine's spaces. A
route's payload holds its name, activity, profile, waypoints and the way
planned, points and elevations delta-coded and gzipped.
Home gets a fourth tab, Routes, and the planner: waypoints tapped on the
map, held and dragged, the last undone, a new route's first where the
phone is (GPS through the recorder's channel), the way planned by the
instance's BRouter after each change, an earlier answer dropped; its
distance, climb, descent, elevation and surfaces under the map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 00:37:44 +01:00
"routeSurfaces": "Surfaces",
"surfacePaved": "Paved",
"surfaceCompacted": "Compacted",
"surfaceGravel": "Gravel",
"surfaceNatural": "Natural ground",
"surfaceUnknown": "Not known",
"routeSave": "Save",
"routeNotSaved": "The route wasn't saved.",
"routeDelete": "Delete",
"routeDeleteTitle": "Delete this route?",
"routeDeleteMessage": "It's deleted on all your devices.",
2026-10-29 08:27:27 +01:00
"routeDeleted": "Route deleted",
"turnStraight": "Go straight on",
"turnLeft": "Turn left",
"turnSlightLeft": "Bear left",
"turnSharpLeft": "Turn sharp left",
"turnRight": "Turn right",
"turnSlightRight": "Bear right",
"turnSharpRight": "Turn sharp right",
"turnKeepLeft": "Keep left",
"turnKeepRight": "Keep right",
"turnAround": "Turn around",
"turnRoundabout": "At the roundabout, take exit {exit}",
"@turnRoundabout": {"placeholders": {"exit": {"type": "int"}}},
"turnExitLeft": "Take the exit on the left",
"turnExitRight": "Take the exit on the right",
"routeSayIn": "In {metres} metres: {turn}",
"@routeSayIn": {"description": "Spoken before a turn while following a route.", "placeholders": {"metres": {"type": "int"}, "turn": {"type": "String", "example": "Turn left"}}},
"routeSayOff": "Off the route. It lies {metres} metres {direction}.",
"@routeSayOff": {"description": "Spoken off the route; the recorder puts the figures in.", "placeholders": {"metres": {"type": "String", "example": "80"}, "direction": {"type": "String", "example": "to the north-east"}}},
"routeSayOn": "Back on the route",
"routeSayArrived": "End of the route",
"directionNorth": "to the north",
"directionNorthEast": "to the north-east",
"directionEast": "to the east",
"directionSouthEast": "to the south-east",
"directionSouth": "to the south",
"directionSouthWest": "to the south-west",
"directionWest": "to the west",
"directionNorthWest": "to the north-west",
"recordRoute": "Route",
"recordNoRoute": "None",
"recordingNextTurn": "In {distance}",
"@recordingNextTurn": {"placeholders": {"distance": {"type": "String", "example": "320 m"}}},
"recordingOffRoute": "Off the route: {distance} {direction}",
"@recordingOffRoute": {"placeholders": {"distance": {"type": "String", "example": "80 m"}, "direction": {"type": "String", "example": "to the north-east"}}},
"recordingRouteEnd": "End of the route",
"recordingFollowRoute": "Following {name}",
"@recordingFollowRoute": {"placeholders": {"name": {"type": "String"}}},
"recordingMapHint": "Swipe for the map",
"recordingFiguresHint": "Swipe back for the figures",
"recordingNorthUp": "North up",
2026-10-29 12:36:32 +01:00
"recordingHeadingUp": "Heading up",
"musicTitle": "Music",
"@musicTitle": {"description": "The music screen's title, and its tile in the account sheet."},
"musicHint": "Jellyfin playlists to run to",
"@musicHint": {"description": "Under Music in the account sheet."},
"musicIntro": "Train plays music from your Jellyfin server, choosing tracks whose beat fits your steps. Your account and music stay on this phone.",
"musicServer": "Jellyfin server",
"@musicServer": {"description": "The label of the server's address."},
"musicConnect": "Connect",
"musicBadAddress": "Enter the server's address, such as jellyfin.example.org.",
"musicUnreachable": "The server can't be reached. Check the address and the connection.",
"musicNotJellyfin": "There is no Jellyfin server at this address.",
"musicRefused": "Jellyfin refused. Check the user name and password.",
"musicCodeExpired": "The code has expired.",
"musicFailed": "Jellyfin answered with an error. Try again later.",
"musicQuickConnect": "To sign in to {server}, enter this code in Jellyfin on another device, under Settings, Quick Connect.",
"@musicQuickConnect": {"placeholders": {"server": {"type": "String"}}},
"musicNewCode": "New code",
"musicBack": "Back",
"musicUsePassword": "Use a password",
"musicPasswordIntro": "Sign in to {server} with your Jellyfin user name and password.",
"@musicPasswordIntro": {"placeholders": {"server": {"type": "String"}}},
"musicUserName": "User name",
"musicPassword": "Password",
"musicSignIn": "Sign in",
"musicSignedInAs": "Signed in as {name}",
"@musicSignedInAs": {"placeholders": {"name": {"type": "String"}}},
"musicSignOut": "Sign out",
"musicPlaylists": "Playlists for running",
"musicPlaylistsHint": "Train chooses from the tracks of the playlists you mark, and keeps them on this phone.",
"musicNoPlaylists": "There are no music playlists on this server yet.",
"musicRetry": "Try again",
"musicTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
2026-10-29 12:48:53 +01:00
"@musicTracks": {"placeholders": {"count": {"type": "int"}}},
"musicKept": "On this phone",
"musicKeptWaiting": "{done} of {total} tracks: the rest waits for Wi-Fi",
"@musicKeptWaiting": {"placeholders": {"done": {"type": "int"}, "total": {"type": "int"}}},
"musicKeptLoading": "Downloading: {done} of {total} tracks",
"@musicKeptLoading": {"placeholders": {"done": {"type": "int"}, "total": {"type": "int"}}},
"musicKeptDone": "{count} tracks, {size} MB, {tempo} with a tempo",
"@musicKeptDone": {"placeholders": {"count": {"type": "int"}, "size": {"type": "String", "example": "230.4"}, "tempo": {"type": "int"}}},
"musicKeptDelete": "Delete",
"musicKeptDeleteTitle": "Delete the music on this phone?",
feat(train): music by cadence in a recording, or the metronome (ADR 0030, D48)
A workout keeps its cadence as a series of its own (steps a minute, every
10 s while moving). The Record sheet offers Music, Metronome or Off on
foot, remembered; Music once a kept track has a tempo. The recorder
measures the cadence over the last 30 s and plays a fitting track (its
tempo, half or double, within 6 %) through Media3's ExoPlayer, retuning
the track playing where it still fits, else crossfading over 3 s; a
plan's new step picks anew. A run starts at the cadence learnt from the
person's runs, which also gives a plan's steps their cadence.
Media3 1.10.1, pinned by Gradle's checksums (trusted on first download
from Google's Maven, 29 Oct 2026), for review:
media3-common aar 39686f54ff6baf8cbc841199afd55619533eaef2ab6cd0efc32b8a3c55dd88c3
module a81b2f0ebc256fe0e09f05d93c5259ade5bf44707748b9295343bee67575d9c2
media3-container aar a198b62c577fa434941282fbfa31eb66ff0ff308a6573edf0b5dd01f4e38445d
module e2a1c62c7e5d1013b906bc6ff5056db3e797cda4505e2d7d229524b24ca67a0e
media3-database aar 520e6f814e42a7d428db187ac073b453c0fcba0f4f5f4edd1cf33fafd2cfb740
module c380fea161e80d8074215a20e86e3dc2c2d825de964ca87aee66c8331f5ae252
media3-datasource aar e22f9f779047ce951152c793af094593e7cbb52475a82d56fa4f869fd319e700
module 9d32d2ec6a983ee9f418bcc05777aa23eee4db37bf794f44fb66a05d6f22e963
media3-decoder aar a9fb2b8fa1d54b4a8d6bda98a1c655b04d59c74b3ace7e2503e9e39830f13aee
module 388d5226366edd1facf8eec42cb5420871a46032d545286e90eaea8454789580
media3-exoplayer aar 19090f829afbbb595d67618f59c212f72b433ac01dc72b47594cf42552c8ed61
module ba0b9ef325fa114baa0f2fbdb4caf54029606924be1999f4af02bed5f079756b
media3-extractor aar 3c823279356b64f4215a8c6cb1e6bfa60c533351a651786ad6fa69ad001ef1b1
module b3eb755d7373e4fd199ff80814005f3b303693dfb8ace16b5b9482655d49d493
Not tried on a phone yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 13:31:41 +01:00
"musicKeptDeleteText": "Every playlist is unmarked, and its tracks deleted from this phone. They stay on your Jellyfin server.",
"recordMusic": "Music",
"recordMusicOn": "Music",
"@recordMusicOn": {"description": "The segment playing the kept tracks by cadence."},
"recordMetronome": "Metronome",
"recordMusicOff": "Off"
feat(train): Halkyon Train's shell, Todo's copied and renamed (ADR 0030, D12)
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 10:26:49 +01:00
}