feat(dashboard): show course progress in student sidebar (#761)
* feat(dashboard): show course progress in student sidebar Adds a progress ring with completed-lesson count to the course sidebar header, per-section completion percentages in the content tree, and a "Your progress" footer card with an overall progress bar plus a lessons/exercises breakdown. All indicators are student-only and derive from the existing per-item isComplete flags, so they update live when a lesson is marked complete. * feat(dashboard): show course progress ring in header on mobile Extracts the sidebar footer progress card into a reusable course-progress-card component and adds a course-progress-popover (progress ring trigger, mirroring the org nav setup ring) to the course header beside the Assistant button. Shown to students on mobile only, where the sidebar footer card is hidden; tapping the ring opens a popover with the linear progress bar and lessons/exercises breakdown. * fix(dashboard): align progress ring subtitle with ring scope The header ring percent covers lessons and exercises while the subtitle only counted lessons, which contradicted each other on courses with exercises. The subtitle now uses the same all-content counts. Also drops a duplicate useSidebar import in favor of the namespace export.
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
import { toggleAiAssistant } from '$features/ai-assistant/utils/store';
|
import { toggleAiAssistant } from '$features/ai-assistant/utils/store';
|
||||||
import { openCoursePreview } from '$features/course/utils/course-preview';
|
import { openCoursePreview } from '$features/course/utils/course-preview';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import CourseProgressPopover from './course-progress-popover.svelte';
|
||||||
import CoursePublishBadge from './course-publish-badge.svelte';
|
import CoursePublishBadge from './course-publish-badge.svelte';
|
||||||
import CoursePublicBadge from './course-public-badge.svelte';
|
import CoursePublicBadge from './course-public-badge.svelte';
|
||||||
import CourseContextMenuContent from './course-context-menu-content.svelte';
|
import CourseContextMenuContent from './course-context-menu-content.svelte';
|
||||||
@@ -85,6 +86,10 @@
|
|||||||
|
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
|
|
||||||
|
{#if $isStudentExperience}
|
||||||
|
<CourseProgressPopover class="md:hidden" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={toggleAiAssistant}
|
onclick={toggleAiAssistant}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Progress } from '@cio/ui/base/progress';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import type { ContentProgress } from '$features/course/utils/content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
progress: ContentProgress;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { progress, class: className = '' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={className}>
|
||||||
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
|
<span class="text-xs font-medium">{$t('course.sidebar.progress.title')}</span>
|
||||||
|
<span class="ui:text-primary text-xs font-semibold tabular-nums">{progress.percent}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={progress.percent} max={100} class="ui:h-1.5 mt-2" />
|
||||||
|
<p class="ui:text-muted-foreground mt-2 truncate text-[11px]">
|
||||||
|
{$t('course.sidebar.progress.lessons', {
|
||||||
|
completed: progress.lessonsComplete,
|
||||||
|
total: progress.lessonsTotal
|
||||||
|
})}{#if progress.exercisesTotal > 0}
|
||||||
|
· {$t('course.sidebar.progress.exercises', {
|
||||||
|
completed: progress.exercisesComplete,
|
||||||
|
total: progress.exercisesTotal
|
||||||
|
})}{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Popover from '@cio/ui/base/popover';
|
||||||
|
import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress';
|
||||||
|
import { courseApi } from '$features/course/api';
|
||||||
|
import { getCourseProgress } from '$features/course/utils/content';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import CourseProgressCard from './course-progress-card.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
const progress = $derived(getCourseProgress(courseApi.course));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if progress.total > 0}
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger class="shrink-0 {className}" aria-label={$t('course.sidebar.progress.title')}>
|
||||||
|
<PercentRingProgress value={progress.percent} size="small" />
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-72" align="end">
|
||||||
|
<CourseProgressCard {progress} />
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{/if}
|
||||||
+13
-1
@@ -8,7 +8,7 @@
|
|||||||
import * as Collapsible from '@cio/ui/base/collapsible';
|
import * as Collapsible from '@cio/ui/base/collapsible';
|
||||||
import { ContentType } from '@cio/utils/constants/content';
|
import { ContentType } from '@cio/utils/constants/content';
|
||||||
import { courseApi } from '$features/course/api';
|
import { courseApi } from '$features/course/api';
|
||||||
import { getContentRoute, getCourseContent } from '$features/course/utils/content';
|
import { getContentItemsProgress, getContentRoute, getCourseContent } from '$features/course/utils/content';
|
||||||
import { CircleCheckIcon } from '$features/ui/icons';
|
import { CircleCheckIcon } from '$features/ui/icons';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||||
@@ -55,6 +55,18 @@
|
|||||||
<span class="flex-1 truncate">{section.title}</span>
|
<span class="flex-1 truncate">{section.title}</span>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1">
|
<div class="ml-auto flex items-center gap-1">
|
||||||
|
{#if isStudent}
|
||||||
|
{@const sectionProgress = getContentItemsProgress(section.items)}
|
||||||
|
{#if sectionProgress.total > 0}
|
||||||
|
<span
|
||||||
|
class="shrink-0 text-[11px] font-medium tabular-nums {sectionProgress.percent === 100
|
||||||
|
? 'ui:text-green-600'
|
||||||
|
: 'ui:text-muted-foreground'}"
|
||||||
|
>
|
||||||
|
{sectionProgress.percent}%
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
{#if canOpenContentModal}
|
{#if canOpenContentModal}
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost-outline"
|
variant="ghost-outline"
|
||||||
|
|||||||
+30
-2
@@ -2,25 +2,53 @@
|
|||||||
import * as Avatar from '@cio/ui/base/avatar';
|
import * as Avatar from '@cio/ui/base/avatar';
|
||||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||||
|
import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { shortenName } from '$lib/utils/functions/string';
|
import { shortenName } from '$lib/utils/functions/string';
|
||||||
import { courseApi } from '$features/course/api';
|
import { courseApi } from '$features/course/api';
|
||||||
import { getNavItemRoute } from '$features/course/utils/functions';
|
import { getNavItemRoute } from '$features/course/utils/functions';
|
||||||
|
import { getCourseProgress } from '$features/course/utils/content';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isStudent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isStudent = false }: Props = $props();
|
||||||
|
|
||||||
|
const sidebar = Sidebar.useSidebar();
|
||||||
const courseTitle = $derived(courseApi.course?.title);
|
const courseTitle = $derived(courseApi.course?.title);
|
||||||
const courseId = $derived(courseApi.course?.id);
|
const courseId = $derived(courseApi.course?.id);
|
||||||
|
const progress = $derived(getCourseProgress(courseApi.course));
|
||||||
|
const isSidebarExpanded = $derived(sidebar.open || sidebar.isMobile);
|
||||||
|
const showProgressRing = $derived(isStudent && progress.total > 0 && isSidebarExpanded);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton
|
<Sidebar.MenuButton
|
||||||
size="sm"
|
size={showProgressRing ? 'lg' : 'sm'}
|
||||||
class="ui:data-[state=open]:bg-sidebar-accent ui:data-[state=open]:text-sidebar-accent-foreground"
|
class="ui:data-[state=open]:bg-sidebar-accent ui:data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href={courseId ? resolve(getNavItemRoute(courseId), {}) : '#'} {...props}>
|
<a href={courseId ? resolve(getNavItemRoute(courseId), {}) : '#'} {...props}>
|
||||||
{#if $currentOrg.name}
|
{#if showProgressRing}
|
||||||
|
<PercentRingProgress value={progress.percent} size="small" />
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
{#if courseTitle}
|
||||||
|
<span class="truncate text-sm font-medium">{courseTitle}</span>
|
||||||
|
{:else}
|
||||||
|
<Skeleton class="h-4 w-24" />
|
||||||
|
{/if}
|
||||||
|
<span class="ui:text-muted-foreground truncate text-xs">
|
||||||
|
{$t('course.sidebar.progress.completed', {
|
||||||
|
completed: progress.completed,
|
||||||
|
total: progress.total
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{:else if $currentOrg.name}
|
||||||
<Avatar.Root class="flex size-6! items-center justify-center rounded-md!">
|
<Avatar.Root class="flex size-6! items-center justify-center rounded-md!">
|
||||||
<Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} />
|
<Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} />
|
||||||
<Avatar.Fallback class="rounded-md! text-xs">{shortenName($currentOrg.name)}</Avatar.Fallback>
|
<Avatar.Fallback class="rounded-md! text-xs">{shortenName($currentOrg.name)}</Avatar.Fallback>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
import SidebarSkeleton from '$features/ui/sidebar/sidebar-skeleton.svelte';
|
import SidebarSkeleton from '$features/ui/sidebar/sidebar-skeleton.svelte';
|
||||||
import PoweredBy from '$features/ui/powered-by.svelte';
|
import PoweredBy from '$features/ui/powered-by.svelte';
|
||||||
import { courseApi } from '$features/course/api';
|
import { courseApi } from '$features/course/api';
|
||||||
|
import { getCourseProgress } from '$features/course/utils/content';
|
||||||
import { useSidebar } from '@cio/ui/base/sidebar';
|
import { useSidebar } from '@cio/ui/base/sidebar';
|
||||||
|
import CourseProgressCard from '$features/course/components/course-progress-card.svelte';
|
||||||
import { startResizablePanelDrag } from '$lib/utils/functions/resizable-panel';
|
import { startResizablePanelDrag } from '$lib/utils/functions/resizable-panel';
|
||||||
import { COURSE_SIDEBAR_DEFAULT_WIDTH, COURSE_SIDEBAR_MAX_WIDTH, COURSE_SIDEBAR_MIN_WIDTH } from './constants';
|
import { COURSE_SIDEBAR_DEFAULT_WIDTH, COURSE_SIDEBAR_MAX_WIDTH, COURSE_SIDEBAR_MIN_WIDTH } from './constants';
|
||||||
|
|
||||||
@@ -45,6 +47,10 @@
|
|||||||
let stopSidebarResize: (() => void) | null = null;
|
let stopSidebarResize: (() => void) | null = null;
|
||||||
|
|
||||||
const attributionCourseSlug = $derived(courseApi.course?.slug ?? null);
|
const attributionCourseSlug = $derived(courseApi.course?.slug ?? null);
|
||||||
|
const courseProgress = $derived(getCourseProgress(courseApi.course));
|
||||||
|
const showCourseProgress = $derived(
|
||||||
|
$isStudentExperience && isCourseReady && sidebar.open && !sidebar.isMobile && courseProgress.total > 0
|
||||||
|
);
|
||||||
|
|
||||||
function clampSidebarWidth(width: number) {
|
function clampSidebarWidth(width: number) {
|
||||||
return Math.min(COURSE_SIDEBAR_MAX_WIDTH, Math.max(COURSE_SIDEBAR_MIN_WIDTH, width));
|
return Math.min(COURSE_SIDEBAR_MAX_WIDTH, Math.max(COURSE_SIDEBAR_MIN_WIDTH, width));
|
||||||
@@ -114,7 +120,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Sidebar.Root collapsible="icon" class="z-app-bar-elevated!" mobileOverlayClass="z-app-bar-elevated!">
|
<Sidebar.Root collapsible="icon" class="z-app-bar-elevated!" mobileOverlayClass="z-app-bar-elevated!">
|
||||||
<Sidebar.Header>
|
<Sidebar.Header>
|
||||||
<CourseSidebarLogo />
|
<CourseSidebarLogo isStudent={$isStudentExperience} />
|
||||||
</Sidebar.Header>
|
</Sidebar.Header>
|
||||||
|
|
||||||
<Sidebar.Content>
|
<Sidebar.Content>
|
||||||
@@ -143,6 +149,12 @@
|
|||||||
<Sidebar.Rail onclick={handleRailClick} onpointerdown={handleRailPointerDown} />
|
<Sidebar.Rail onclick={handleRailClick} onpointerdown={handleRailPointerDown} />
|
||||||
|
|
||||||
<Sidebar.Footer>
|
<Sidebar.Footer>
|
||||||
|
{#if showCourseProgress}
|
||||||
|
<CourseProgressCard
|
||||||
|
progress={courseProgress}
|
||||||
|
class="ui:border-sidebar-border ui:bg-sidebar-accent/50 rounded-lg border p-3"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<PoweredBy
|
<PoweredBy
|
||||||
variant="sidebar"
|
variant="sidebar"
|
||||||
sidebarUtmSource="lms-course-sidebar"
|
sidebarUtmSource="lms-course-sidebar"
|
||||||
|
|||||||
@@ -56,6 +56,42 @@ export function getCourseContent(course: Course | null) {
|
|||||||
|
|
||||||
const NAVIGABLE_CONTENT_TYPES = [CourseContentType.Lesson, CourseContentType.Exercise] as const;
|
const NAVIGABLE_CONTENT_TYPES = [CourseContentType.Lesson, CourseContentType.Exercise] as const;
|
||||||
|
|
||||||
|
export type ContentProgress = {
|
||||||
|
lessonsTotal: number;
|
||||||
|
lessonsComplete: number;
|
||||||
|
exercisesTotal: number;
|
||||||
|
exercisesComplete: number;
|
||||||
|
total: number;
|
||||||
|
completed: number;
|
||||||
|
percent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getContentItemsProgress(items: ContentItem[]): ContentProgress {
|
||||||
|
const lessons = items.filter((item) => item.type === CourseContentType.Lesson);
|
||||||
|
const exercises = items.filter((item) => item.type === CourseContentType.Exercise);
|
||||||
|
const lessonsComplete = lessons.filter((item) => item.isComplete).length;
|
||||||
|
const exercisesComplete = exercises.filter((item) => item.isComplete).length;
|
||||||
|
const total = lessons.length + exercises.length;
|
||||||
|
const completed = lessonsComplete + exercisesComplete;
|
||||||
|
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lessonsTotal: lessons.length,
|
||||||
|
lessonsComplete,
|
||||||
|
exercisesTotal: exercises.length,
|
||||||
|
exercisesComplete,
|
||||||
|
total,
|
||||||
|
completed,
|
||||||
|
percent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCourseProgress(course: Course | null): ContentProgress {
|
||||||
|
const content = getCourseContent(course);
|
||||||
|
const items = content.grouped ? content.sections.flatMap((section) => section.items) : content.items;
|
||||||
|
return getContentItemsProgress(items);
|
||||||
|
}
|
||||||
|
|
||||||
export function getOrderedNavigableContent(course: Course | null): ContentItem[] {
|
export function getOrderedNavigableContent(course: Course | null): ContentItem[] {
|
||||||
const content = getCourseContent(course);
|
const content = getCourseContent(course);
|
||||||
const items = content.grouped ? content.sections.flatMap((s) => s.items) : content.items;
|
const items = content.grouped ? content.sections.flatMap((s) => s.items) : content.items;
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Overholdelse",
|
"title": "Overholdelse",
|
||||||
"due_date": "Forfalder",
|
"due_date": "Forfalder",
|
||||||
"valid_until": "Gælder indtil kl"
|
"valid_until": "Gælder indtil kl"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Dine fremskridt",
|
||||||
|
"completed": "{completed} af {total} fuldført",
|
||||||
|
"lessons": "{completed} af {total} lektioner",
|
||||||
|
"exercises": "{completed} af {total} øvelser"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Compliance",
|
"title": "Compliance",
|
||||||
"due_date": "Fällig",
|
"due_date": "Fällig",
|
||||||
"valid_until": "Gültig bis"
|
"valid_until": "Gültig bis"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Ihr Fortschritt",
|
||||||
|
"completed": "{completed} von {total} abgeschlossen",
|
||||||
|
"lessons": "{completed} von {total} Lektionen",
|
||||||
|
"exercises": "{completed} von {total} Übungen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2005,6 +2005,12 @@
|
|||||||
"content_count": {
|
"content_count": {
|
||||||
"lessons": "{count} lessons",
|
"lessons": "{count} lessons",
|
||||||
"exercises": "{count} exercises"
|
"exercises": "{count} exercises"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Your progress",
|
||||||
|
"completed": "{completed} of {total} completed",
|
||||||
|
"lessons": "{completed} of {total} lessons",
|
||||||
|
"exercises": "{completed} of {total} exercises"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Cumplimiento",
|
"title": "Cumplimiento",
|
||||||
"due_date": "debido",
|
"due_date": "debido",
|
||||||
"valid_until": "Válido hasta"
|
"valid_until": "Válido hasta"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Tu progreso",
|
||||||
|
"completed": "{completed} de {total} completados",
|
||||||
|
"lessons": "{completed} de {total} lecciones",
|
||||||
|
"exercises": "{completed} de {total} ejercicios"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Conformité",
|
"title": "Conformité",
|
||||||
"due_date": "Due",
|
"due_date": "Due",
|
||||||
"valid_until": "Valable jusqu'au"
|
"valid_until": "Valable jusqu'au"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Votre progression",
|
||||||
|
"completed": "{completed} sur {total} terminés",
|
||||||
|
"lessons": "{completed} sur {total} leçons",
|
||||||
|
"exercises": "{completed} sur {total} exercices"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "अनुपालन",
|
"title": "अनुपालन",
|
||||||
"due_date": "देय",
|
"due_date": "देय",
|
||||||
"valid_until": "तक वैध है"
|
"valid_until": "तक वैध है"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "आपकी प्रगति",
|
||||||
|
"completed": "{total} में से {completed} पूर्ण",
|
||||||
|
"lessons": "{total} में से {completed} पाठ",
|
||||||
|
"exercises": "{total} में से {completed} अभ्यास"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Zgodność",
|
"title": "Zgodność",
|
||||||
"due_date": "Termin",
|
"due_date": "Termin",
|
||||||
"valid_until": "Ważne do"
|
"valid_until": "Ważne do"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Twój postęp",
|
||||||
|
"completed": "Ukończono {completed} z {total}",
|
||||||
|
"lessons": "{completed} z {total} lekcji",
|
||||||
|
"exercises": "{completed} z {total} ćwiczeń"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Conformidade",
|
"title": "Conformidade",
|
||||||
"due_date": "Devido",
|
"due_date": "Devido",
|
||||||
"valid_until": "Válido até"
|
"valid_until": "Válido até"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Seu progresso",
|
||||||
|
"completed": "{completed} de {total} concluídos",
|
||||||
|
"lessons": "{completed} de {total} aulas",
|
||||||
|
"exercises": "{completed} de {total} exercícios"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Соответствие",
|
"title": "Соответствие",
|
||||||
"due_date": "Срок погашения",
|
"due_date": "Срок погашения",
|
||||||
"valid_until": "Действительно до"
|
"valid_until": "Действительно до"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Ваш прогресс",
|
||||||
|
"completed": "Завершено {completed} из {total}",
|
||||||
|
"lessons": "{completed} из {total} уроков",
|
||||||
|
"exercises": "{completed} из {total} упражнений"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
@@ -2370,6 +2370,12 @@
|
|||||||
"title": "Tuân thủ",
|
"title": "Tuân thủ",
|
||||||
"due_date": "Đến hạn",
|
"due_date": "Đến hạn",
|
||||||
"valid_until": "Có hiệu lực cho đến khi"
|
"valid_until": "Có hiệu lực cho đến khi"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "Tiến độ của bạn",
|
||||||
|
"completed": "Đã hoàn thành {completed} trong số {total}",
|
||||||
|
"lessons": "{completed} trong số {total} bài học",
|
||||||
|
"exercises": "{completed} trong số {total} bài tập"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navItems": {
|
"navItems": {
|
||||||
|
|||||||
Reference in New Issue
Block a user