feat: multi language support (#254)

* feat: multitranslational support

* - completion of settings page
- completion of sidebar

* sidebar rerendering bug fix

* completion of the lesson navigation & materials page

* completion of the course navigation

* completion of requested languages

* - Implementation of Language Picker
- Rename Bug Fixes

* checkpoint save

* - addition of the language picker to the onboarding.
- addition of the language switcher for the lesson notes.

* merge completion

* initialization of the new branch

* add transition.svelte file

* lesson note bug fix

* feat: add translation migration

* completion of translation

* fix height issue of dropdown

* refactoring

* fix: improve code with LOCALE from db

* finally get it fully functional

* connect with AI

* backward compatibility for lesson note

* preserve old content, when generating AI content

* undo translations in upgrade modal

* fix duplicate key issue from codacy trigger

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2024-04-19 05:45:48 +01:00
committed by GitHub
co-authored by Rotimi Best
parent ff7d2cee35
commit d75e2f258e
153 changed files with 11264 additions and 1130 deletions
@@ -4,6 +4,7 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { handleOpenWidget } from '../../store';
import { t } from '$lib/utils/functions/translations';
export let course = {};
@@ -12,10 +13,15 @@
}
</script>
<TextField className="mt-5" labelClassName="font-bold" label="Title" bind:value={course.title} />
<TextField
className="mt-5"
labelClassName="font-bold"
label={$t('course.navItem.landing_page.editor.header_form.title')}
bind:value={course.title}
/>
<TextArea
label="Description"
label={$t('course.navItem.landing_page.editor.header_form.description')}
bind:value={course.description}
rows="6"
className="mt-5"
@@ -25,13 +31,17 @@
<TextField
className="mt-5"
labelClassName="font-bold"
label="Short Video"
label={$t('course.navItem.landing_page.editor.header_form.short_video')}
placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
helperMessage="Enter a link to a youtube video"
type="text"
bind:value={course.metadata.videoUrl}
/>
<div class="mt-7">
<p class="font-bold mb-3">Replace cover image</p>
<PrimaryButton label="Replace" variant={VARIANTS.OUTLINED} onClick={widgetControl} />
<p class="font-bold mb-3">{$t('course.navItem.landing_page.editor.header_form.replace_cover')}</p>
<PrimaryButton
label={$t('course.navItem.landing_page.editor.header_form.replace')}
variant={VARIANTS.OUTLINED}
onClick={widgetControl}
/>
</div>
@@ -8,6 +8,7 @@
import UploadImage from '$lib/components/UploadImage/index.svelte';
import { uploadAvatar } from '$lib/utils/services/courses';
import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course;
let name: string | undefined;
@@ -60,29 +61,31 @@
</script>
<div class="mt-5">
<label for="upload" class="font-bold">Instructor Logo</label>
<label for="upload" class="font-bold"
>{$t('course.navItem.landing_page.editor.instructure_form.upload')}</label
>
<UploadImage bind:avatar src={imgUrl} />
</div>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor name"
placeholder="Your company name"
label={$t('course.navItem.landing_page.editor.instructure_form.name')}
placeholder={$t('course.navItem.landing_page.editor.instructure_form.name_placeholder')}
bind:value={name}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor Role"
label={$t('course.navItem.landing_page.editor.instructure_form.role')}
placeholder="e.g Software developer"
bind:value={role}
/>
<TextArea
label="About instructor"
placeholder="A short background about the instructor"
label={$t('course.navItem.landing_page.editor.instructure_form.about')}
placeholder={$t('course.navItem.landing_page.editor.instructure_form.about_placeholder')}
rows={6}
className="mt-5"
labelClassName="font-bold"
@@ -92,7 +95,7 @@
<TextField
className="mt-5"
labelClassName="font-bold"
label="Total number of courses"
label={$t('course.navItem.landing_page.editor.instructure_form.total')}
type="number"
bind:value={courseNo}
/>
@@ -9,6 +9,7 @@
import TextEditor from '$lib/components/TextEditor/index.svelte';
import { isCourseFree } from '$lib/utils/functions/course';
import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course;
@@ -53,7 +54,10 @@
</script>
{#if typeof course !== 'undefined'}
<Select labelText="Currency" bind:selected={course.currency}>
<Select
labelText={$t('course.navItem.landing_page.editor.pricing_form.currency')}
bind:selected={course.currency}
>
<SelectItem value="NGN" />
<SelectItem value="USD" />
</Select>
@@ -61,7 +65,7 @@
<TextField
className="mt-5"
labelClassName="font-bold"
label="Cost"
label={$t('course.navItem.landing_page.editor.pricing_form.cost')}
type="number"
bind:value={course.cost}
/>
@@ -70,7 +74,7 @@
<TextField
className="mt-5"
labelClassName="font-bold"
label="Payment Link"
label={$t('course.navItem.landing_page.editor.pricing_form.payment')}
helperMessage="Stripe, Lemon Squeezy or any payment link"
bind:value={paymentLink}
/>
@@ -78,9 +82,12 @@
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
<Toggle
labelText={$t('course.navItem.landing_page.editor.pricing_form.discount')}
bind:toggled={showDiscount}
>
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
</Toggle>
</div>
@@ -88,7 +95,7 @@
<TextField
className="mt-5"
labelClassName="font-bold"
label="Discout Percent"
label={$t('course.navItem.landing_page.editor.pricing_form.percent')}
type="number"
bind:value={discount}
helperMessage="In Percentage %"
@@ -98,13 +105,15 @@
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
</Toggle>
</div>
{#if giftToggled}
<p class="dark:text-white font-bold mt-5">About Gift</p>
<p class="dark:text-white font-bold mt-5">
{$t('course.navItem.landing_page.editor.pricing_form.gift')}
</p>
<div class="h-2/5">
<TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} />
@@ -7,6 +7,7 @@
import TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { uploadAvatar } from '$lib/utils/services/courses';
import { t } from '$lib/utils/functions/translations';
export let courseId;
export let reviews = [];
@@ -42,7 +43,7 @@
<TextField
className="mt-2 w-full"
labelClassName="font-normal"
label="Full Name"
label={$t('course.navItem.landing_page.editor.reviews_form.fullname')}
placeholder=""
type="text"
bind:value={review.name}
@@ -51,7 +52,7 @@
<!-- Description -->
<TextArea
label="Description"
label={$t('course.navItem.landing_page.editor.reviews_form.description')}
rows="4"
className="mt-2 w-full"
labelClassName="font-normal"
@@ -68,7 +69,7 @@
<TextField
className="mt-2 w-20"
labelClassName="font-normal"
label="Rating"
label={$t('course.navItem.landing_page.editor.reviews_form.rating')}
type="number"
min={1}
max={5}
@@ -78,7 +79,11 @@
<!-- Toggle -->
<div class="w-24">
<Toggle labelText="Hide Review" size="sm" bind:toggled={review.hide} />
<Toggle
labelText={$t('course.navItem.landing_page.editor.reviews_form.hide')}
size="sm"
bind:toggled={review.hide}
/>
</div>
</div>
@@ -25,6 +25,7 @@
import { isMobile } from '$lib/utils/store/useMobile';
import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course;
export let courseId: string;
@@ -46,43 +47,43 @@
{
key: 1,
path: '',
title: 'Header'
title: $t('course.navItem.landing_page.editor.title.1')
},
{
key: 2,
path: 'metadata.requirements',
title: 'Requirement',
title: $t('course.navItem.landing_page.editor.title.2'),
enableAIWriter: true,
initPrompt: 'Please write few requirements needed to take this course:'
},
{
key: 3,
path: 'metadata.description',
title: 'Description',
title: $t('course.navItem.landing_page.editor.title.3'),
enableAIWriter: true,
initPrompt: 'Please write a detailed course description for this course:'
},
{
key: 4,
path: 'metadata.goals',
title: 'Goals',
title: $t('course.navItem.landing_page.editor.title.4'),
enableAIWriter: true,
initPrompt: 'What should a student expect to learn from this course:'
},
{
key: 5,
path: '',
title: 'Reviews'
title: $t('course.navItem.landing_page.editor.title.5')
},
{
key: 6,
path: '',
title: 'Instructor'
title: $t('course.navItem.landing_page.editor.title.6')
},
{
key: 7,
path: '',
title: 'Pricing'
title: $t('course.navItem.landing_page.editor.title.7')
}
];
let selectedSection: Section | null = null;
@@ -168,7 +169,7 @@
<CloseButton onClick={handleClose} />
<div class="flex items-center">
<PrimaryButton
label="Save"
label={$t('course.navItem.landing_page.editor.save')}
type="button"
className="mr-1"
variant={VARIANTS.OUTLINED}
@@ -181,7 +182,7 @@
</div>
</div>
<div class="flex justify-between items-center px-2 w-full mb-2">
<h3 class="dark:text-white">Page Builder</h3>
<h3 class="dark:text-white">{$t('course.navItem.landing_page.editor.page_builder')}</h3>
</div>
{#each sections as section, index}
<button
@@ -189,7 +190,10 @@
sections.length && 'border-b-0'} border-gray-300"
on:click={handleSectionSelect(section.key)}
>
<p class="dark:text-white mr-2">{section.title} section</p>
<p class="dark:text-white mr-2">
{section.title}
{$t('course.navItem.landing_page.editor.section')}
</p>
<ChevronRightIcon size={24} class="carbon-class" />
</button>
{/each}
@@ -10,6 +10,7 @@
import type { Course } from '$lib/utils/types';
import { ROLE } from '$lib/utils/constants/roles';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { t } from '$lib/utils/functions/translations';
export let className = '';
export let editMode = false;
@@ -92,12 +93,14 @@
<p class="dark:text-white font-medium text-sm flex items-center gap-1">
{formatter?.format(calculatedCost) || calculatedCost}
{#if isFree}
<span class="text-xs">(Free)</span>
<span class="text-xs"
>({$t('course.navItem.landing_page.pricing_section.free')})</span
>
{/if}
</p>
{#if courseData?.metadata?.showDiscount}
<p class="dark:text-white font-light text-sm text-gray-500">
{discount}% Discount.
{discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
<span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
>
@@ -105,7 +108,7 @@
{/if}
{:else}
<p class="dark:text-white text-lg">
This course is currently not accepting new students.
{$t('course.navItem.landing_page.pricing_section.not_accepting')}
</p>
{/if}
</div>
@@ -113,7 +116,9 @@
<!-- Call To Action Buttons -->
<div class="flex flex-col w-full h-full items-center">
<PrimaryButton
label={isFree ? 'Enroll Now' : 'Buy Now'}
label={isFree
? $t('course.navItem.landing_page.pricing_section.enroll')
: $t('course.navItem.landing_page.pricing_section.buy')}
className="w-full sm:w-full h-[40px]"
onClick={handleJoinCourse}
isDisabled={!courseData.metadata.allowNewStudent}
@@ -135,12 +140,13 @@
<p class="dark:text-white font-medium text-lg">
{formatter?.format(calculatedCost) || calculatedCost}
{#if isFree}
<span class="text-sm">(Free)</span>
<span class="text-sm">({$t('course.navItem.landing_page.pricing_section.free')})</span
>
{/if}
</p>
{#if courseData?.metadata?.showDiscount}
<p class="dark:text-white font-light text-sm text-gray-500">
{discount}% Discount.
{discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
<span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
>
@@ -148,7 +154,7 @@
{/if}
{:else}
<p class="dark:text-white text-lg">
This course is currently not accepting new students.
{$t('course.navItem.landing_page.pricing_section.not_accepting')}
</p>
{/if}
</div>
@@ -156,13 +162,15 @@
<!-- Call To Action Buttons -->
<div class="flex flex-col w-full items-center">
<PrimaryButton
label={isFree ? 'Enroll Now' : 'Buy Now'}
label={isFree
? $t('course.navItem.landing_page.pricing_section.enroll')
: $t('course.navItem.landing_page.pricing_section.buy')}
className="w-full sm:w-full py-3 mb-3"
onClick={handleJoinCourse}
isDisabled={!courseData.metadata.allowNewStudent}
/>
{#if courseData?.metadata?.showDiscount && courseData.metadata.allowNewStudent}
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
<p class="dark:text-white font-light text-sm text-gray-500">{$t('course.navItem.landing_page.pricing_section.bird')}</p>
{/if}
</div>
</div>
@@ -1,3 +1,5 @@
// import { t } from '$lib/utils/functions/translations';
export const SECTION_KEYS = {
HEADER: 'header',
REQUIREMENT: '#requirement',
@@ -11,26 +13,26 @@ export const SECTION_KEYS = {
export const NAV_ITEMS = [
{
key: SECTION_KEYS.REQUIREMENT,
label: 'Requirement'
label: 'course.navItem.landing_page.editor.title.2'
},
{
key: SECTION_KEYS.DESCRIPTION,
label: 'Description'
label: 'course.navItem.landing_page.editor.title.3'
},
{
key: SECTION_KEYS.GOALS,
label: 'Goals'
label: 'course.navItem.landing_page.editor.title.4'
},
{
key: SECTION_KEYS.LESSONS,
label: 'Lessons'
label: 'course.navItems.2'
},
{
key: SECTION_KEYS.REVIEWS,
label: 'Reviews'
label: 'course.navItem.landing_page.editor.title.5'
},
{
key: SECTION_KEYS.INSTRUCTOR,
label: 'Instructor'
label: 'course.navItem.landing_page.editor.title.6'
}
];
@@ -26,6 +26,7 @@
import { currentOrg } from '$lib/utils/store/org';
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { course } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';
export let editMode = false;
export let courseData: Course;
@@ -124,7 +125,7 @@
{get(courseData, 'metadata.instructor.name', '')}
</p>
<PrimaryButton
label="Start Course"
label={$t('course.navItem.landing_page.start_course')}
className="px-6 py-5 mt-6 sm:w-fit hidden md:block"
onClick={() => {
if (editMode) return;
@@ -207,14 +208,17 @@
class="{navItem.key === activeNav &&
'active text-primary-700'} z-0 mr-6 rounded-lg px-2 font-normal text-slate-700 hover:bg-gray-200 dark:text-white dark:hover:text-slate-900"
>
{navItem.label}
{$t(navItem.label)}
</a>
{/each}
</nav>
<!-- Sections - Requirement -->
<section id="requirement" class="mt-8 border-b border-gray-300 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Requirement</h3>
<h3 class="mb-3 mt-0 text-2xl font-bold">
{$t('course.navItem.landing_page.requirement')}
</h3>
<ul class="list font-light">
<HtmlRender content={get(courseData, 'metadata.requirements', '')} />
</ul>
@@ -222,7 +226,10 @@
<!-- Sections - Course Description -->
<section id="description" class="mt-8 border-b border-gray-300 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Course Description</h3>
<h3 class="mb-3 mt-0 text-2xl font-bold">
{$t('course.navItem.landing_page.description')}
</h3>
<HtmlRender
className="dark:text-white text-sm font-light"
content={get(courseData, 'metadata.description', '')}
@@ -231,27 +238,28 @@
<!-- Sections - Goal -->
<section id="goals" class="mt-8 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">What you'll learn</h3>
<h3 class="text-2xl font-bold mt-0 mb-3">{$t('course.navItem.landing_page.learn')}</h3>
<ul class="list font-light">
<HtmlRender content={get(courseData, 'metadata.goals', '')} />
</ul>
</section>
<!-- Sections - Certificate -->
<section class="mt-8 border-b border-gray-300 pb-10">
<h3 class="mt-0 text-2xl font-bold">Certificate</h3>
<p class="mb-3 text-sm font-light dark:text-white">
When you complete all of the courses in the program, you'll earn a Certificate to share
with your professional network.
<section class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0">{$t('course.navItem.landing_page.certificate')}</h3>
<p class="dark:text-white text-sm font-light mb-3">
{$t('course.navItem.landing_page.certificate_text')}
</p>
<img src="/images/certificate-template.svg" alt="certificate template" />
</section>
<!-- Sections - Lessons -->
<section id="lessons" class="mt-8 border-b border-gray-300 pb-10">
<div class="mb-3 flex w-full items-center justify-between">
<h3 class="mb-3 mt-0 text-2xl font-bold">Course Content</h3>
<p class="text-sm font-light dark:text-white">
<section id="lessons" class="border-b border-gray-300 mt-8 pb-10">
<div class="flex items-center justify-between w-full mb-3">
<h3 class="text-2xl font-bold mt-0 mb-3">
{$t('course.navItem.landing_page.content')}
</h3>
<p class="dark:text-white text-sm font-light">
<!-- {lessons.length} lessons -->
{pluralize('lesson', lessons.length, true)}
</p>
@@ -271,23 +279,25 @@
<div class="w-2/4">
<div class="flex items-center">
{#if lesson.slide_url}
<span class="flex w-2/4 text-sm font-light"
><PresentationFile size={16} class="mr-1" />1 slide</span
<span class="text-sm font-light flex w-2/4"
><PresentationFile size={16} class="mr-1" />{$t(
'course.navItem.landing_page.slide'
)}</span
>
{/if}
{#if lesson.note}
<span class="flex w-2/4 text-sm font-light"
><Notebook size={16} class="mr-1" />note</span
<span class="text-sm font-light flex w-2/4"
><Notebook size={16} class="mr-1" />{$t(
'course.navItem.landing_page.note'
)}</span
>
{/if}
</div>
<div class="flex items-center">
{#if lesson.videos}
<span class="flex w-2/4 text-sm font-light"
><Video size={16} class="mr-1" />{lesson.videos.length} video{lesson.videos
.length > 1
? 's'
: ''}
<span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />{lesson.videos.length}
{$t('course.navItem.landing_page.video')}{lesson.videos.length > 1 ? 's' : ''}
</span>
{/if}
{#if get(lesson, 'totalExercises[0].count')}
@@ -308,7 +318,9 @@
<!-- Sections - Reviews -->
{#if reviews && reviews.length > 0}
<section id="reviews">
<h2 class="my-16 mb-6 ml-0 mr-0 font-semibold">Reviews</h2>
<h2 class="my-16 mr-0 mb-6 ml-0 font-semibold">
{$t('course.navItem.landing_page.reviews')}
</h2>
<div class="flex flex-wrap">
{#each reviews.slice(0, 4) as review, id}
{#if !review.hide}
@@ -355,7 +367,7 @@
</div>
{#if reviews.length > 4}
<PrimaryButton
label="See All"
label={$t('course.navItem.landing_page.see_all')}
className="w-3/12 p-4 mt-2"
variant={VARIANTS.OUTLINED}
onClick={() => ($reviewsModalStore.open = true)}
@@ -367,16 +379,18 @@
onClose={() => ($reviewsModalStore.open = false)}
bind:open={$reviewsModalStore.open}
width="w-9/12"
modalHeading="Reviews"
modalHeading={$t('course.navItem.landing_page.reviews_modal.title')}
>
<div class="flex">
<!-- ratings -->
<div class="w-1/3">
<h2 class="text-xl font-bold">
{averageRating} Rating
{averageRating}
{$t('course.navItem.landing_page.reviews_modal.rating')}
</h2>
<h2 class="mt-2 text-lg font-semibold">
{reviews.length} Ratings
<h2 class="text-lg font-semibold mt-2">
{reviews.length}
{$t('course.navItem.landing_page.reviews_modal.ratings')}
</h2>
</div>
<!-- reviews -->
@@ -418,8 +432,10 @@
<!-- Sections - Instructor -->
<section id="instructor" class="mt-8 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Instructor</h3>
<div class="mb-4 flex items-center">
<h3 class="text-2xl font-bold mt-0 mb-3">
{$t('course.navItem.landing_page.instructor')}
</h3>
<div class="flex items-center mb-4">
<img
alt="Author Avatar"
class="mr-3 block h-20 w-20 rounded-full"
@@ -434,7 +450,10 @@
</p>
<p class="text-md flex items-center font-light dark:text-white">
<PlayFilled size={16} class="text-primary-700" />
<span class="ml-1">{get(instructor, 'courseNo', 10)} courses</span>
<span class="ml-1"
>{get(instructor, 'courseNo', '')}
{$t('course.navItem.landing_page.courses')}</span
>
</p>
</div>
</div>