feat: automatically detect language on server and translate UI (#530)

* feat: detect language on the server and translate page

* translate auth pages

* PR reviews
This commit is contained in:
Rotimi Best
2024-12-28 23:11:55 +01:00
committed by GitHub
parent 1f1ee8afd3
commit 381776f388
24 changed files with 431 additions and 232 deletions
+12 -29
View File
@@ -1,13 +1,12 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
import { dev, browser } from '$app/environment';
import { redirect } from '@sveltejs/kit';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { getCurrentOrg } from '$lib/utils/services/org';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { loadTranslations } from '$lib/utils/functions/translations';
import type { CurrentOrg } from '$lib/utils/types/org';
import { IS_SELFHOSTED } from '$env/static/private';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
import { IS_SELFHOSTED } from '$env/static/private';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { getCurrentOrg } from '$lib/utils/services/org';
import type { CurrentOrg } from '$lib/utils/types/org';
import { redirect } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
if (!supabase) {
getSupabase();
@@ -19,19 +18,19 @@ interface LoadOutput {
skipAuth: boolean;
org: CurrentOrg | null;
baseMetaTags: MetaTagsProps;
serverLang: string;
}
export const load = async ({ url, cookies }): Promise<LoadOutput> => {
export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
const response: LoadOutput = {
orgSiteName: '',
isOrgSite: false,
skipAuth: false,
org: null,
baseMetaTags: getBaseMetaTags(url)
baseMetaTags: getBaseMetaTags(url),
serverLang: request.headers?.get('accept-language') || ''
};
console.log('IS_SELFHOSTED', IS_SELFHOSTED);
// Selfhosted usecase would be here
if (IS_SELFHOSTED === 'true') {
const subdomain = getSubdomain(url);
@@ -105,11 +104,6 @@ export const load = async ({ url, cookies }): Promise<LoadOutput> => {
throw redirect(307, 'https://app.classroomio.com');
}
// Load translations
const { pathname } = url;
const initLocale = getInitialLocale();
await loadTranslations(initLocale, pathname);
return response;
};
@@ -151,18 +145,7 @@ function getBaseMetaTags(url: URL) {
});
}
// Define getInitialLocale function
function getInitialLocale(): string {
if (browser) {
try {
return window.navigator.language.split('-')[0];
} catch (e) {
return 'en';
}
}
return 'en';
}
function getSubdomain(url: URL) {
const host = url.host.replace('www.', '');
+7 -6
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { browser } from '$app/environment';
import { page } from '$app/stores';
import debounce from 'lodash/debounce';
import Apps from '$lib/components/Apps/index.svelte';
import { course } from '$lib/components/Course/store';
import OrgNavigation from '$lib/components/Navigation/app.svelte';
@@ -22,7 +24,6 @@
import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org';
import { isMobile } from '$lib/utils/store/useMobile';
import { user } from '$lib/utils/store/user';
import { Theme } from 'carbon-components-svelte';
import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte';
import merge from 'lodash/merge';
@@ -42,6 +43,8 @@
isMobile.update(() => window.innerWidth <= 760);
}
const getProfileDebounced = debounce(getProfile, 1000);
onMount(() => {
console.log(
'Welcome to ClassroomIO, we are grateful you chose us.',
@@ -77,18 +80,15 @@
}
// Skip Authentication
if (data.skipAuth || $user.fetchingUser) return;
if (data.skipAuth) return;
// Authentication Steps
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') {
$user.fetchingUser = true;
getProfile({
getProfileDebounced({
path,
queryParam,
isOrgSite: data.isOrgSite,
orgSiteName: data.orgSiteName
}).then(() => {
$user.fetchingUser = false;
});
}
// else if (!['TOKEN_REFRESHED'].includes(event)) {
@@ -125,6 +125,7 @@
<Theme bind:theme={carbonTheme} />
<UpgradeModal />
<Snackbar />
{#if data.org?.is_restricted || $currentOrg.is_restricted}
+28
View File
@@ -0,0 +1,28 @@
import { config, loadTranslations } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user';
import { get } from 'svelte/store';
const SUPPORTED_LANGUAGES = config?.loaders?.map((loader) => loader.locale) || [];
export const load = async ({ url, data }) => {
const { pathname } = url;
const profileStore = get(profile);
const serverLang = data.serverLang.split('-')[0];
const userLocale = profileStore.id ? profileStore.locale : getInitialLocale(serverLang);
const initLocale = getInitialLocale(userLocale);
await loadTranslations(initLocale, pathname); // keep this just before the `return`
return data;
};
function getInitialLocale(lang: string): string {
const locale = lang.split('-')[0];
if (SUPPORTED_LANGUAGES.includes(locale)) return locale;
return 'en';
}
@@ -1,21 +1,22 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import type { Profile } from '$lib/components/Course/components/People/types';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import { logout } from '$lib/utils/functions/logout';
import { getSupabase } from '$lib/utils/functions/supabase';
import { setTheme } from '$lib/utils/functions/theme';
import { t } from '$lib/utils/functions/translations';
import {
authValidation,
getConfirmPasswordError,
getDisableSubmit
} from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { setTheme } from '$lib/utils/functions/theme';
import type { CurrentOrg } from '$lib/utils/types/org';
import { onMount } from 'svelte';
import type { Profile } from '$lib/components/Course/components/People/types';
import { logout } from '$lib/utils/functions/logout';
export let data;
@@ -61,7 +62,7 @@
if (error) throw error;
if (!signupData.user) {
throw 'User cannot be created ';
throw $t('login.validation.user_cannot_be_created');
}
// Insert profile
@@ -86,7 +87,7 @@
}
if (!profile?.id) {
throw 'Unable to create profile';
throw $t('login.validation.unable_to_create_profile');
}
const res = await supabase.auth.signInWithPassword({
@@ -142,9 +143,6 @@
$: disableSubmit = getDisableSubmit(fields);
$: console.log('data', data.invite);
$: console.log('org', $currentOrg);
$: console.log('path', $currentOrgPath);
</script>
<svelte:head>
@@ -153,15 +151,15 @@
<AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} showLogo={true} bind:formRef>
<div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">
<p class="mb-6 text-lg font-semibold dark:text-white">
{#if data.invite.profile}
Log in to join
{$t('login.login_to_join')}
{:else}
Create a free account to join
{$t('login.create_to_join')}
{/if}
</p>
<TextField
label="Your Email"
label={$t('login.fields.email')}
value={data.invite.email}
type="email"
placeholder="you@domain.com"
@@ -171,7 +169,7 @@
/>
{#if !data.invite.profile}
<TextField
label="Full Name"
label={$t('login.fields.full_name')}
bind:value={fields.name}
type="text"
autoFocus={true}
@@ -184,7 +182,7 @@
/>
{/if}
<TextField
label="Your Password"
label={$t('login.fields.password')}
bind:value={fields.password}
type="password"
placeholder="************"
@@ -192,12 +190,12 @@
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
helperMessage={$t('login.fields.password_helper_message')}
isRequired
/>
{#if !data.invite.profile}
<TextField
label="Confirm Password"
label={$t('login.fields.confirm_password')}
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
@@ -213,7 +211,7 @@
{/if}
</div>
<div class="my-4 w-full flex justify-end items-center">
<div class="my-4 flex w-full items-center justify-end">
<PrimaryButton
label="Accept Invite"
type="submit"
+10 -14
View File
@@ -1,15 +1,14 @@
<script lang="ts">
import { page } from '$app/stores';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import { authValidation } from '$lib/utils/functions/validator';
import { LOGIN_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { authValidation } from '$lib/utils/functions/validator';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org';
let formRef: HTMLFormElement;
let supabase = getSupabase();
@@ -18,9 +17,6 @@
let loading = false;
let errors = Object.assign({}, LOGIN_FIELDS);
let query = new URLSearchParams($page.url.search);
let redirect = query.get('redirect');
async function handleSubmit() {
const validationRes = authValidation(fields);
console.log('validationRes', validationRes);
@@ -48,7 +44,7 @@
email: fields.email
});
}
} catch (error) {
} catch (error: any) {
submitError = error.error_description || error.message;
loading = false;
}
@@ -61,7 +57,7 @@
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
<div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">{$t('login.welcome')}</p>
<p class="mb-6 text-lg font-semibold dark:text-white">{$t('login.welcome')}</p>
<TextField
label={$t('login.email')}
bind:value={fields.email}
@@ -71,7 +67,7 @@
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
errorMessage={$t(errors.email)}
/>
<TextField
label={$t('login.password')}
@@ -81,7 +77,7 @@
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
errorMessage={$t(errors.password)}
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
@@ -91,7 +87,7 @@
</div>
</div>
<div class="my-4 w-full flex justify-end items-center">
<div class="my-4 flex w-full items-center justify-end">
<!-- <a href="/login" class="text-primary-700 text-sm">Create an account</a> -->
<PrimaryButton
label={$t('login.login')}
@@ -9,6 +9,23 @@ async function getGenericData(orgSlug: string): Promise<{
orgHasAvatarUrl: boolean;
courseData: any;
}> {
const { data: publishedCourse } = await supabase
.from('course')
.select(
`
is_published,
group:group_id!inner(
organization!inner(
siteName
)
)
`
)
.eq('is_published', true)
.eq('group.organization.siteName', orgSlug)
.limit(1)
.returns<{ is_published: boolean }>();
const { data } = await supabase
.from('course')
.select(
@@ -23,14 +40,13 @@ async function getGenericData(orgSlug: string): Promise<{
)
`
)
.eq('group.organization.siteName', orgSlug);
.eq('group.organization.siteName', orgSlug)
.limit(1);
const result = data || [];
console.log('courseData', result);
return {
isCoursePublished: result.some((c) => c.is_published === true),
isCoursePublished: publishedCourse?.is_published === true,
isCourseCreated: result.length > 0,
orgHasAvatarUrl: !!get(result[0], 'group.organization.avatar_url', ''),
courseData: result
@@ -54,7 +70,8 @@ async function getIsLessonCreated(
)
`
)
.eq('course.group.organization.siteName', orgSlug);
.eq('course.group.organization.siteName', orgSlug)
.limit(1);
console.log('lessonsData', data);
return {
@@ -80,7 +97,8 @@ async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
)
`
)
.eq('lesson.course.group.organization.siteName', orgSlug);
.eq('lesson.course.group.organization.siteName', orgSlug)
.limit(1);
return Array.isArray(data) ? data?.length > 0 : false;
}
+16 -15
View File
@@ -1,21 +1,22 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import SenjaEmbed from '$lib/components/Senja/Embed.svelte';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import {
authValidation,
getConfirmPasswordError,
getDisableSubmit
} from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { globalStore } from '$lib/utils/store/app';
import SenjaEmbed from '$lib/components/Senja/Embed.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
let supabase = getSupabase();
let fields = Object.assign({}, SIGNUP_FIELDS);
@@ -112,7 +113,7 @@
formRef?.reset();
success = true;
fields = Object.assign({}, SIGNUP_FIELDS);
} catch (error) {
} catch (error: any) {
submitError = error?.error_description || error?.message;
loading = false;
}
@@ -130,7 +131,7 @@
<AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} bind:formRef>
<div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">Create a free account</p>
<p class="mb-6 text-lg font-semibold dark:text-white">Create a free account</p>
<!-- <TextField
label="Full Name"
bind:value={fields.name}
@@ -144,30 +145,30 @@
isRequired
/> -->
<TextField
label="Your Email"
label={$t('login.fields.email')}
bind:value={fields.email}
type="email"
placeholder="you@domain.com"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
errorMessage={$t(errors.email ?? '')}
isRequired
/>
<TextField
label="Your Password"
label={$t('login.fields.password')}
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
errorMessage={$t(errors.password ?? '')}
helperMessage={$t('login.fields.password_helper_message')}
isRequired
/>
<TextField
label="Confirm Password"
label={$t('login.fields.confirm_password')}
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
@@ -182,9 +183,9 @@
{/if}
</div>
<div class="my-4 w-full flex justify-end items-center">
<div class="my-4 flex w-full items-center justify-end">
<PrimaryButton
label="Create Account"
label={$t('login.create_account')}
type="submit"
className="sm:w-full w-full"
isDisabled={disableSubmit || loading}