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:
@@ -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.', '');
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user