refactor(workout): extract shared WorkoutSummary component from PostSwim and SessionDetail screens
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
import { View, Text, ScrollView, Pressable, type ReactNode } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatTime, formatPace } from "@utils/formatTime";
|
||||||
|
import { formatDistance } from "@utils/formatDistance";
|
||||||
|
import { STROKE_COLORS } from "@constants/strokes";
|
||||||
|
|
||||||
|
type Set = {
|
||||||
|
stroke: string;
|
||||||
|
distancePerRep: number;
|
||||||
|
repCount: number;
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Lap = {
|
||||||
|
setPosition: number;
|
||||||
|
repNumber: number;
|
||||||
|
distance: number;
|
||||||
|
duration: number;
|
||||||
|
heartRate: number | null;
|
||||||
|
swolf: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name?: string | null;
|
||||||
|
date?: string;
|
||||||
|
totalDistance: number;
|
||||||
|
totalDuration: number;
|
||||||
|
calories?: number | null;
|
||||||
|
avgHeartRate?: number | null;
|
||||||
|
poolLength: number;
|
||||||
|
sets: Set[];
|
||||||
|
laps: Lap[];
|
||||||
|
avgSwolf: number | null;
|
||||||
|
onDone?: () => void;
|
||||||
|
rightAction?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkoutSummary({
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
totalDistance,
|
||||||
|
totalDuration,
|
||||||
|
calories,
|
||||||
|
avgHeartRate,
|
||||||
|
poolLength,
|
||||||
|
sets,
|
||||||
|
laps,
|
||||||
|
avgSwolf,
|
||||||
|
onDone,
|
||||||
|
rightAction,
|
||||||
|
}: Props) {
|
||||||
|
const { t } = useTranslation("active-workout");
|
||||||
|
const { t: tc } = useTranslation("common");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView className="flex-1 bg-white">
|
||||||
|
<View className="p-4">
|
||||||
|
{/* Header */}
|
||||||
|
<View className="flex-row justify-between items-start mb-6">
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-2xl font-bold text-slate-900 mb-1">
|
||||||
|
{name || tc("swimSession")}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-slate-500 text-sm">
|
||||||
|
{date || tc("pool", { length: poolLength })}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{rightAction}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Stats grid */}
|
||||||
|
<View className="flex-row gap-3 mb-4">
|
||||||
|
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
||||||
|
<Text className="text-2xl font-bold text-sky-600">
|
||||||
|
{formatDistance(totalDistance)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-slate-500 mt-1">{tc("distance")}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
||||||
|
<Text className="text-2xl font-bold text-sky-600">
|
||||||
|
{formatTime(totalDuration)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-slate-500 mt-1">{tc("duration")}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-row gap-3 mb-4">
|
||||||
|
{calories != null && calories > 0 && (
|
||||||
|
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
||||||
|
<Text className="text-2xl font-bold text-sky-600">
|
||||||
|
{Math.round(calories)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-slate-500 mt-1">{t("calories")}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{avgHeartRate != null && avgHeartRate > 0 && (
|
||||||
|
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
||||||
|
<Text className="text-2xl font-bold text-sky-600">{avgHeartRate}</Text>
|
||||||
|
<Text className="text-xs text-slate-500 mt-1">{t("avgHeartRate")}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* SWOLF */}
|
||||||
|
{avgSwolf != null && avgSwolf > 0 && (
|
||||||
|
<View className="bg-slate-50 rounded-2xl p-4 mb-4 flex-row justify-between items-center">
|
||||||
|
<Text className="text-sm font-semibold text-slate-700">{tc("avgSwolf")}</Text>
|
||||||
|
<Text className="text-xl font-bold text-slate-900">{avgSwolf}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sets */}
|
||||||
|
{sets.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
||||||
|
{t("sets")}
|
||||||
|
</Text>
|
||||||
|
{sets.map((set, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
className="flex-row items-center justify-between py-3 border-b border-slate-100"
|
||||||
|
>
|
||||||
|
<View className="flex-row items-center gap-2">
|
||||||
|
<View
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: STROKE_COLORS[set.stroke] ?? "#94a3b8" }}
|
||||||
|
/>
|
||||||
|
<Text className="text-slate-700 font-medium">
|
||||||
|
{tc(`stroke.${set.stroke.toLowerCase()}`)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-slate-600">
|
||||||
|
{set.repCount}×{set.distancePerRep}m
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Laps */}
|
||||||
|
{laps.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text className="text-sm font-semibold text-slate-500 uppercase tracking-wide mt-6 mb-2">
|
||||||
|
{t("lapsDetail")}
|
||||||
|
</Text>
|
||||||
|
{laps.map((lap, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
className="flex-row items-center justify-between py-2.5 border-b border-slate-100"
|
||||||
|
>
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-slate-700 text-sm">
|
||||||
|
{t("setRep", { set: lap.setPosition, rep: lap.repNumber })}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-slate-400 text-xs">{lap.distance}m</Text>
|
||||||
|
</View>
|
||||||
|
<View className="items-end">
|
||||||
|
<Text className="text-slate-900 font-medium">{formatTime(lap.duration)}</Text>
|
||||||
|
<Text className="text-slate-400 text-xs">
|
||||||
|
{formatPace(lap.distance, lap.duration)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{lap.heartRate != null && lap.heartRate > 0 && (
|
||||||
|
<View className="ml-3 items-end">
|
||||||
|
<Text className="text-red-500 text-xs">♥ {lap.heartRate}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Done button */}
|
||||||
|
{onDone && (
|
||||||
|
<View className="p-4 pb-8">
|
||||||
|
<Pressable
|
||||||
|
onPress={onDone}
|
||||||
|
className="bg-slate-900 rounded-2xl py-4 items-center"
|
||||||
|
>
|
||||||
|
<Text className="text-white font-bold text-lg">{tc("done")}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { View, Text, Pressable } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import type { RootStackParamList } from "@navigation/types";
|
|
||||||
import type { RouteProp } from "@react-navigation/native";
|
import type { RouteProp } from "@react-navigation/native";
|
||||||
|
import type { RootStackParamList } from "@navigation/types";
|
||||||
import { getWorkoutWithSets, getWorkoutLaps } from "@db/operations";
|
import { getWorkoutWithSets, getWorkoutLaps } from "@db/operations";
|
||||||
import { formatTime } from "@utils/formatTime";
|
import { WorkoutSummary } from "../components/WorkoutSummary";
|
||||||
import { formatDistance } from "@utils/formatDistance";
|
|
||||||
|
type Set = {
|
||||||
|
stroke: string;
|
||||||
|
distancePerRep: number;
|
||||||
|
repCount: number;
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Lap = {
|
||||||
|
setPosition: number;
|
||||||
|
repNumber: number;
|
||||||
|
distance: number;
|
||||||
|
duration: number;
|
||||||
|
heartRate: number | null;
|
||||||
|
swolf: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export function PostSwimScreen() {
|
export function PostSwimScreen() {
|
||||||
const { t } = useTranslation("active-workout");
|
const { t } = useTranslation("active-workout");
|
||||||
const { t: tc } = useTranslation("common");
|
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||||
const route = useRoute<RouteProp<RootStackParamList, "PostSwim">>();
|
const route = useRoute<RouteProp<RootStackParamList, "PostSwim">>();
|
||||||
const [summary, setSummary] = useState<{
|
const [data, setData] = useState<{
|
||||||
distance: number;
|
name: string | null;
|
||||||
duration: number;
|
poolLength: number;
|
||||||
laps: number;
|
totalDistance: number;
|
||||||
|
totalDuration: number;
|
||||||
|
calories: number | null;
|
||||||
|
avgHeartRate: number | null;
|
||||||
|
sets: Set[];
|
||||||
|
laps: Lap[];
|
||||||
avgSwolf: number | null;
|
avgSwolf: number | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
@@ -37,59 +56,39 @@ export function PostSwimScreen() {
|
|||||||
? Math.round(laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / laps.length)
|
? Math.round(laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / laps.length)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
setSummary({
|
setData({
|
||||||
distance: workout.totalDistance ?? 0,
|
name: workout.name,
|
||||||
duration: totalDuration,
|
poolLength: workout.poolLength,
|
||||||
laps: laps.length,
|
totalDistance: workout.totalDistance ?? 0,
|
||||||
|
totalDuration,
|
||||||
|
calories: workout.calories,
|
||||||
|
avgHeartRate: workout.avgHeartRate,
|
||||||
|
sets: workout.sets ?? [],
|
||||||
|
laps,
|
||||||
avgSwolf,
|
avgSwolf,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-white p-4">
|
<View className="flex-1 bg-white items-center justify-center">
|
||||||
<View className="items-center mb-8">
|
|
||||||
<Text className="text-3xl mb-2">🏊</Text>
|
|
||||||
<Text className="text-2xl font-bold text-slate-900 mb-1">{t("swimComplete")}</Text>
|
|
||||||
<Text className="text-slate-500">{t("greatJob")}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{summary ? (
|
|
||||||
<View className="gap-3 mb-8">
|
|
||||||
<View className="bg-sky-50 rounded-2xl p-5 flex-row justify-between items-center">
|
|
||||||
<Text className="text-slate-600">{tc("distance")}</Text>
|
|
||||||
<Text className="text-xl font-bold text-sky-600">
|
|
||||||
{formatDistance(summary.distance)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="bg-sky-50 rounded-2xl p-5 flex-row justify-between items-center">
|
|
||||||
<Text className="text-slate-600">{tc("duration")}</Text>
|
|
||||||
<Text className="text-xl font-bold text-sky-600">
|
|
||||||
{formatTime(summary.duration)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="bg-sky-50 rounded-2xl p-5 flex-row justify-between items-center">
|
|
||||||
<Text className="text-slate-600">{tc("laps")}</Text>
|
|
||||||
<Text className="text-xl font-bold text-sky-600">{summary.laps}</Text>
|
|
||||||
</View>
|
|
||||||
{summary.avgSwolf && (
|
|
||||||
<View className="bg-sky-50 rounded-2xl p-5 flex-row justify-between items-center">
|
|
||||||
<Text className="text-slate-600">{tc("avgSwolf")}</Text>
|
|
||||||
<Text className="text-xl font-bold text-sky-600">{summary.avgSwolf}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View className="py-8 items-center">
|
|
||||||
<Text className="text-slate-400">{t("loadingSummary")}</Text>
|
<Text className="text-slate-400">{t("loadingSummary")}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<Pressable
|
return (
|
||||||
onPress={() => navigation.popToTop()}
|
<WorkoutSummary
|
||||||
className="bg-slate-900 rounded-2xl py-4 items-center"
|
name={data.name}
|
||||||
>
|
totalDistance={data.totalDistance}
|
||||||
<Text className="text-white font-bold text-lg">{tc("done")}</Text>
|
totalDuration={data.totalDuration}
|
||||||
</Pressable>
|
calories={data.calories}
|
||||||
</View>
|
avgHeartRate={data.avgHeartRate}
|
||||||
|
poolLength={data.poolLength}
|
||||||
|
sets={data.sets}
|
||||||
|
laps={data.laps}
|
||||||
|
avgSwolf={data.avgSwolf}
|
||||||
|
onDone={() => navigation.popToTop()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { View, Text, ScrollView, Pressable, Alert } from "react-native";
|
import { View, Text, Pressable, Alert } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import type { HistoryStackProps } from "@navigation/types";
|
import type { HistoryStackProps } from "@navigation/types";
|
||||||
import { getWorkoutWithSets, deleteWorkout } from "@db/operations";
|
import { getWorkoutWithSets, deleteWorkout } from "@db/operations";
|
||||||
import { formatTime } from "@utils/formatTime";
|
import { WorkoutSummary } from "../../active-workout/components/WorkoutSummary";
|
||||||
import { formatDistance } from "@utils/formatDistance";
|
|
||||||
import { STROKE_COLORS } from "@constants/strokes";
|
|
||||||
|
|
||||||
type WorkoutDetail = {
|
type WorkoutDetail = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,14 +27,12 @@ type WorkoutDetail = {
|
|||||||
repNumber: number;
|
repNumber: number;
|
||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
strokeCount: number | null;
|
|
||||||
heartRate: number | null;
|
heartRate: number | null;
|
||||||
swolf: number | null;
|
swolf: number | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) {
|
export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) {
|
||||||
const { t } = useTranslation("history");
|
|
||||||
const { t: tc } = useTranslation("common");
|
const { t: tc } = useTranslation("common");
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const [workout, setWorkout] = useState<WorkoutDetail | null>(null);
|
const [workout, setWorkout] = useState<WorkoutDetail | null>(null);
|
||||||
@@ -80,95 +76,22 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView className="flex-1 bg-white p-4">
|
<WorkoutSummary
|
||||||
<View className="flex-row justify-between items-start mb-6">
|
name={workout.name}
|
||||||
<View className="flex-1">
|
date={new Date(workout.startedAt).toLocaleDateString()}
|
||||||
<Text className="text-2xl font-bold text-slate-900 mb-1">
|
totalDistance={workout.totalDistance ?? 0}
|
||||||
{workout.name || tc("swimSession")}
|
totalDuration={workout.totalDuration ?? 0}
|
||||||
</Text>
|
calories={workout.calories}
|
||||||
<Text className="text-slate-500">
|
avgHeartRate={workout.avgHeartRate}
|
||||||
{new Date(workout.startedAt).toLocaleDateString()}
|
poolLength={workout.poolLength}
|
||||||
</Text>
|
sets={workout.sets}
|
||||||
</View>
|
laps={workout.laps}
|
||||||
<Pressable
|
avgSwolf={avgSwolf}
|
||||||
onPress={handleDelete}
|
rightAction={
|
||||||
className="bg-red-50 rounded-xl p-2.5"
|
<Pressable onPress={handleDelete} className="bg-red-50 rounded-xl p-2.5">
|
||||||
>
|
|
||||||
<Text className="text-red-500 text-lg">🗑</Text>
|
<Text className="text-red-500 text-lg">🗑</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
}
|
||||||
|
|
||||||
<View className="flex-row gap-3 mb-6">
|
|
||||||
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
|
||||||
<Text className="text-2xl font-bold text-sky-600">
|
|
||||||
{formatDistance(workout.totalDistance ?? 0)}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-slate-500 mt-1">{tc("distance")}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
|
||||||
<Text className="text-2xl font-bold text-sky-600">
|
|
||||||
{formatTime(workout.totalDuration ?? 0)}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-slate-500 mt-1">{tc("duration")}</Text>
|
|
||||||
</View>
|
|
||||||
{workout.calories && (
|
|
||||||
<View className="flex-1 bg-sky-50 rounded-2xl p-4 items-center">
|
|
||||||
<Text className="text-2xl font-bold text-sky-600">
|
|
||||||
{Math.round(workout.calories)}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-slate-500 mt-1">kcal</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{avgSwolf && (
|
|
||||||
<View className="bg-slate-50 rounded-2xl p-4 mb-4">
|
|
||||||
<Text className="text-sm font-semibold text-slate-700">{tc("avgSwolf")}</Text>
|
|
||||||
<Text className="text-xl font-bold text-slate-900">{avgSwolf}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
|
||||||
{tc("sets")}
|
|
||||||
</Text>
|
|
||||||
{workout.sets.map((set, i) => (
|
|
||||||
<View
|
|
||||||
key={i}
|
|
||||||
className="flex-row items-center justify-between py-3 border-b border-slate-100"
|
|
||||||
>
|
|
||||||
<View className="flex-row items-center gap-2">
|
|
||||||
<View
|
|
||||||
className="w-3 h-3 rounded-full"
|
|
||||||
style={{ backgroundColor: STROKE_COLORS[set.stroke] ?? "#94a3b8" }}
|
|
||||||
/>
|
/>
|
||||||
<Text className="text-slate-700 font-medium">{tc(`stroke.${set.stroke.toLowerCase()}`)}</Text>
|
|
||||||
</View>
|
|
||||||
<Text className="text-slate-600">
|
|
||||||
{set.repCount}×{set.distancePerRep}m
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{workout.laps.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Text className="text-sm font-semibold text-slate-500 uppercase tracking-wide mt-6 mb-2">
|
|
||||||
{tc("laps")}
|
|
||||||
</Text>
|
|
||||||
{workout.laps.map((lap, i) => (
|
|
||||||
<View
|
|
||||||
key={i}
|
|
||||||
className="flex-row justify-between py-2 border-b border-slate-100"
|
|
||||||
>
|
|
||||||
<Text className="text-slate-600">
|
|
||||||
{t("lap", { number: i + 1 })} · {lap.distance}m
|
|
||||||
</Text>
|
|
||||||
<Text className="text-slate-600 font-medium">
|
|
||||||
{formatTime(lap.duration)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Schwimmen abgeschlossen!",
|
"swimComplete": "Schwimmen abgeschlossen!",
|
||||||
"greatJob": "Gut gemacht! Du bist heute ins Wasser gegangen!",
|
"greatJob": "Gut gemacht! Du bist heute ins Wasser gegangen!",
|
||||||
"loadingSummary": "Zusammenfassung laden...",
|
"loadingSummary": "Zusammenfassung laden...",
|
||||||
|
"sets": "Sets",
|
||||||
|
"lapsDetail": "Bahnen",
|
||||||
|
"avgHeartRate": "Durchschn. Herzfrequenz",
|
||||||
|
"calories": "Kalorien",
|
||||||
|
"pace": "Tempo",
|
||||||
|
"setRep": "Set {{set}} · Wiederholung {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch bereit",
|
"ready": "Watch bereit",
|
||||||
"pairedNotInRange": "Watch gekoppelt, aber nicht in Reichweite",
|
"pairedNotInRange": "Watch gekoppelt, aber nicht in Reichweite",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Svømning fuldført!",
|
"swimComplete": "Svømning fuldført!",
|
||||||
"greatJob": "Godt gået! Du var i vandet i dag!",
|
"greatJob": "Godt gået! Du var i vandet i dag!",
|
||||||
"loadingSummary": "Indlæser opsummering...",
|
"loadingSummary": "Indlæser opsummering...",
|
||||||
|
"sets": "Sæt",
|
||||||
|
"lapsDetail": "Baner",
|
||||||
|
"avgHeartRate": "Gns. puls",
|
||||||
|
"calories": "Kalorier",
|
||||||
|
"pace": "Tempo",
|
||||||
|
"setRep": "Sæt {{set}} · Gentagelse {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch klar",
|
"ready": "Watch klar",
|
||||||
"pairedNotInRange": "Watch parret, men ikke inden for rækkevidde",
|
"pairedNotInRange": "Watch parret, men ikke inden for rækkevidde",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Swim Complete!",
|
"swimComplete": "Swim Complete!",
|
||||||
"greatJob": "Great job getting in the water today!",
|
"greatJob": "Great job getting in the water today!",
|
||||||
"loadingSummary": "Loading summary...",
|
"loadingSummary": "Loading summary...",
|
||||||
|
"sets": "Sets",
|
||||||
|
"lapsDetail": "Laps",
|
||||||
|
"avgHeartRate": "Avg Heart Rate",
|
||||||
|
"calories": "Calories",
|
||||||
|
"pace": "Pace",
|
||||||
|
"setRep": "Set {{set}} · Rep {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch ready",
|
"ready": "Watch ready",
|
||||||
"pairedNotInRange": "Watch paired but not in range",
|
"pairedNotInRange": "Watch paired but not in range",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "¡Natación completada!",
|
"swimComplete": "¡Natación completada!",
|
||||||
"greatJob": "¡Buen trabajo! ¡Hoy has estado en el agua!",
|
"greatJob": "¡Buen trabajo! ¡Hoy has estado en el agua!",
|
||||||
"loadingSummary": "Cargando resumen...",
|
"loadingSummary": "Cargando resumen...",
|
||||||
|
"sets": "Series",
|
||||||
|
"lapsDetail": "Largos",
|
||||||
|
"avgHeartRate": "Freq. card. media",
|
||||||
|
"calories": "Calorías",
|
||||||
|
"pace": "Ritmo",
|
||||||
|
"setRep": "Serie {{set}} · Repetición {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch lista",
|
"ready": "Watch lista",
|
||||||
"pairedNotInRange": "Watch emparejada pero fuera de alcance",
|
"pairedNotInRange": "Watch emparejada pero fuera de alcance",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Nage terminée !",
|
"swimComplete": "Nage terminée !",
|
||||||
"greatJob": "Bravo ! Vous êtes allé à l'eau aujourd'hui !",
|
"greatJob": "Bravo ! Vous êtes allé à l'eau aujourd'hui !",
|
||||||
"loadingSummary": "Chargement du résumé...",
|
"loadingSummary": "Chargement du résumé...",
|
||||||
|
"sets": "Séries",
|
||||||
|
"lapsDetail": "Longueurs",
|
||||||
|
"avgHeartRate": "Fréq. cardiaque moy.",
|
||||||
|
"calories": "Calories",
|
||||||
|
"pace": "Rythme",
|
||||||
|
"setRep": "Série {{set}} · Répétition {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch prête",
|
"ready": "Watch prête",
|
||||||
"pairedNotInRange": "Watch appariée mais hors de portée",
|
"pairedNotInRange": "Watch appariée mais hors de portée",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Nuoto completato!",
|
"swimComplete": "Nuoto completato!",
|
||||||
"greatJob": "Ottimo lavoro! Sei entrato in acqua oggi!",
|
"greatJob": "Ottimo lavoro! Sei entrato in acqua oggi!",
|
||||||
"loadingSummary": "Caricamento riepilogo...",
|
"loadingSummary": "Caricamento riepilogo...",
|
||||||
|
"sets": "Serie",
|
||||||
|
"lapsDetail": "Lunghezze",
|
||||||
|
"avgHeartRate": "Freq. card. media",
|
||||||
|
"calories": "Calorie",
|
||||||
|
"pace": "Ritmo",
|
||||||
|
"setRep": "Serie {{set}} · Ripetizione {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch pronta",
|
"ready": "Watch pronta",
|
||||||
"pairedNotInRange": "Watch associata ma fuori portata",
|
"pairedNotInRange": "Watch associata ma fuori portata",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Zwemmen voltooid!",
|
"swimComplete": "Zwemmen voltooid!",
|
||||||
"greatJob": "Goed gedaan! Je bent vandaag het water in geweest!",
|
"greatJob": "Goed gedaan! Je bent vandaag het water in geweest!",
|
||||||
"loadingSummary": "Samenvatting laden...",
|
"loadingSummary": "Samenvatting laden...",
|
||||||
|
"sets": "Sets",
|
||||||
|
"lapsDetail": "Banen",
|
||||||
|
"avgHeartRate": "Gem. hartslag",
|
||||||
|
"calories": "Calorieën",
|
||||||
|
"pace": "Tempo",
|
||||||
|
"setRep": "Set {{set}} · Herhaling {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch gereed",
|
"ready": "Watch gereed",
|
||||||
"pairedNotInRange": "Watch gekoppeld maar niet in bereik",
|
"pairedNotInRange": "Watch gekoppeld maar niet in bereik",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Pływanie zakończone!",
|
"swimComplete": "Pływanie zakończone!",
|
||||||
"greatJob": "Świetna robota! Dziś byłeś w wodzie!",
|
"greatJob": "Świetna robota! Dziś byłeś w wodzie!",
|
||||||
"loadingSummary": "Ładowanie podsumowania...",
|
"loadingSummary": "Ładowanie podsumowania...",
|
||||||
|
"sets": "Serie",
|
||||||
|
"lapsDetail": "Długości",
|
||||||
|
"avgHeartRate": "Śr. tętno",
|
||||||
|
"calories": "Kalorie",
|
||||||
|
"pace": "Tempo",
|
||||||
|
"setRep": "Serie {{set}} · Powtórzenie {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch gotowa",
|
"ready": "Watch gotowa",
|
||||||
"pairedNotInRange": "Watch sparowana, ale poza zasięgiem",
|
"pairedNotInRange": "Watch sparowana, ale poza zasięgiem",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"swimComplete": "Natação concluída!",
|
"swimComplete": "Natação concluída!",
|
||||||
"greatJob": "Bom trabalho! Esteve na água hoje!",
|
"greatJob": "Bom trabalho! Esteve na água hoje!",
|
||||||
"loadingSummary": "A carregar resumo...",
|
"loadingSummary": "A carregar resumo...",
|
||||||
|
"sets": "Séries",
|
||||||
|
"lapsDetail": "Comprimentos",
|
||||||
|
"avgHeartRate": "Freq. card. média",
|
||||||
|
"calories": "Calorias",
|
||||||
|
"pace": "Ritmo",
|
||||||
|
"setRep": "Série {{set}} · Repetição {{rep}}",
|
||||||
"watchStatus": {
|
"watchStatus": {
|
||||||
"ready": "Watch pronta",
|
"ready": "Watch pronta",
|
||||||
"pairedNotInRange": "Watch associada, mas fora de alcance",
|
"pairedNotInRange": "Watch associada, mas fora de alcance",
|
||||||
|
|||||||
Reference in New Issue
Block a user