🌐 Added i18next
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
module.exports = {
|
||||
options: {
|
||||
debug: true,
|
||||
// read strings from functions: IllegalMoveError('KEY') or t('KEY')
|
||||
func: {
|
||||
list: ['IllegalMoveError', 't'],
|
||||
extensions: ['.js', '.svelte']
|
||||
},
|
||||
|
||||
trans: false,
|
||||
|
||||
// Create and update files `en.json`, `fr.json`, `es.json`
|
||||
lngs: ['en', 'de'],
|
||||
|
||||
ns: [
|
||||
// The namespace I use
|
||||
'translation'
|
||||
],
|
||||
|
||||
defaultLng: 'en',
|
||||
defaultNs: 'translation',
|
||||
|
||||
// Put a blank string as initial translation
|
||||
// (useful for Weblate be marked as 'not yet translated', see later)
|
||||
defaultValue: (lng, ns, key) => '',
|
||||
|
||||
// Location of translation files
|
||||
resource: {
|
||||
loadPath: 'src/lib/i18n/locales/{{lng}}.json',
|
||||
savePath: 'src/lib/i18n/locales/{{lng}}.json',
|
||||
jsonIndent: 4
|
||||
},
|
||||
|
||||
nsSeparator: ':',
|
||||
keySeparator: '.'
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,8 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .",
|
||||
"run:prod": "node index.js"
|
||||
"run:prod": "node index.js",
|
||||
"translations-scan": "i18next-scanner --config i18next-scanner.config.engine.cjs src/routes/*/*.svelte"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@felte/validator-yup": "^1.0.1",
|
||||
@@ -32,6 +33,7 @@
|
||||
"eslint-plugin-svelte3": "^3.2.1",
|
||||
"felte": "^1.0.1",
|
||||
"highlight.js": "^11.4.0",
|
||||
"i18next-scanner": "^3.1.0",
|
||||
"ioredis": "^4.28.5",
|
||||
"js-cookie": "^3.0.1",
|
||||
"luxon": "^2.3.1",
|
||||
@@ -51,5 +53,8 @@
|
||||
"typescript": "~4.5.4",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"i18next": "^21.6.14"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1973
-298
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
import i18next from 'i18next';
|
||||
import translations from './translations';
|
||||
import en from './locales/en.json';
|
||||
|
||||
import type { i18n, Resource } from 'i18next';
|
||||
|
||||
const INITIAL_LANGUAGE = 'en';
|
||||
|
||||
export class I18nService {
|
||||
// expose i18next
|
||||
i18n: i18n;
|
||||
constructor() {
|
||||
this.i18n = i18next;
|
||||
this.initialize();
|
||||
this.changeLanguage(INITIAL_LANGUAGE);
|
||||
}
|
||||
|
||||
// Our translation function
|
||||
t(key: string, replacements?: Record<string, unknown>): string {
|
||||
return this.i18n.t(key, replacements);
|
||||
}
|
||||
|
||||
// Initializing i18n
|
||||
initialize() {
|
||||
this.i18n.init({
|
||||
lng: INITIAL_LANGUAGE,
|
||||
fallbackLng: 'en',
|
||||
debug: false,
|
||||
defaultNS: 'translation',
|
||||
fallbackNS: 'common',
|
||||
resources: translations as Resource,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
detection: {
|
||||
order: ['querystring', 'navigator'],
|
||||
lookupQuerystring: 'lng'
|
||||
}
|
||||
});
|
||||
this.i18n.addResourceBundle('en', 'translation', en);
|
||||
}
|
||||
|
||||
changeLanguage(language: string) {
|
||||
this.i18n.changeLanguage(language);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { I18nService } from './i18n-service';
|
||||
import { I18NextTranslationService } from './translation-service';
|
||||
import type { TType } from './translation-service';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
export const initLocalizationContext = () => {
|
||||
// Initialize our services
|
||||
const i18n = new I18nService();
|
||||
const tranlator = new I18NextTranslationService(i18n);
|
||||
|
||||
// Setting the Svelte context
|
||||
setLocalization({
|
||||
t: tranlator.translate,
|
||||
currentLanguage: tranlator.locale
|
||||
});
|
||||
|
||||
return {
|
||||
i18n
|
||||
};
|
||||
};
|
||||
const CONTEXT_KEY = 't';
|
||||
|
||||
export type I18nContext = {
|
||||
t: Readable<TType>;
|
||||
currentLanguage: Writable<string>;
|
||||
};
|
||||
|
||||
export const setLocalization = (context: I18nContext) => {
|
||||
return setContext<I18nContext>(CONTEXT_KEY, context);
|
||||
};
|
||||
|
||||
// To make retrieving the t function easier.
|
||||
export const getLocalization = () => {
|
||||
return getContext<I18nContext>(CONTEXT_KEY);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"index_page": {
|
||||
"slogan": "The open-source quiz-platform!",
|
||||
"meta": {
|
||||
"description": "ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use",
|
||||
"title": "Home"
|
||||
},
|
||||
"stats": "Stats",
|
||||
"features": "Features",
|
||||
"features_description": {
|
||||
"1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.",
|
||||
"2": "The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes."
|
||||
}
|
||||
},
|
||||
"overview_page": {
|
||||
"create": "Create",
|
||||
"import": "Import",
|
||||
"logout": "Logout",
|
||||
"title": "Title",
|
||||
"created_at": "Created at",
|
||||
"question_count": "Question count",
|
||||
"play": "Play",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"public": "Public",
|
||||
"start": "Start"
|
||||
},
|
||||
"edit_page": {
|
||||
"save": "Save",
|
||||
"success_update_title": "Successfully updated quiz!",
|
||||
"success_update_body": "Updated the quiz successfully!",
|
||||
"close": "Close"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"title": "Successfully created quiz!",
|
||||
"body": "Created the quiz successfully!"
|
||||
},
|
||||
"close": "Close"
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Nice to meet you!",
|
||||
"create_account": "Create account",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"forgot_password?": "Forgot password?",
|
||||
"register": "Register",
|
||||
"already_have_account?": "Already have an account?",
|
||||
"login": "Login"
|
||||
},
|
||||
"login_page": {
|
||||
"welcome_back": "Welcome back!",
|
||||
"login_or_create_account": "Login or create an account",
|
||||
"already_have_account": "Don't have an account?",
|
||||
"modal": {
|
||||
"success": {
|
||||
"success_check_mail": "Login Succeded! Check your mailbox!",
|
||||
"success": "Login Succeded!",
|
||||
"description": {
|
||||
"success_check_mail": "Please check your mailbox, since you should have received a mail, with a link you can click to login.",
|
||||
"success": "You've successfully logged in!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Wrong email or password!",
|
||||
"unexpected": "Unexpected error!",
|
||||
"description": {
|
||||
"wrong_creds": "Please make sure that your password and your email are correct!",
|
||||
"unexpected": "There was the good old unexpected error!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { I18nService } from './i18n-service';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
|
||||
export type TType = (text: string, replacements?: Record<string, unknown>) => string;
|
||||
|
||||
export interface TranslationService {
|
||||
locale: Writable<string>;
|
||||
translate: Readable<TType>;
|
||||
}
|
||||
|
||||
export class I18NextTranslationService implements TranslationService {
|
||||
public locale: Writable<string>;
|
||||
public translate: Readable<TType>;
|
||||
|
||||
constructor(i18n: I18nService) {
|
||||
i18n.initialize();
|
||||
this.locale = this.createLocale(i18n);
|
||||
this.translate = this.createTranslate(i18n);
|
||||
}
|
||||
|
||||
// Locale initialization.
|
||||
// 1. Create a writable store
|
||||
// 2. Create a new set function that changes the i18n locale.
|
||||
// 3. Create a new update function that changes the i18n locale.
|
||||
// 4. Return modified writable.
|
||||
private createLocale(i18n: I18nService) {
|
||||
const { subscribe, set, update } = writable<string>(i18n.i18n.language);
|
||||
|
||||
const setLocale = (newLocale: string) => {
|
||||
i18n.changeLanguage(newLocale);
|
||||
set(newLocale);
|
||||
};
|
||||
|
||||
const updateLocale = (updater: (value: string) => string) => {
|
||||
update((currentValue) => {
|
||||
const nextLocale = updater(currentValue);
|
||||
i18n.changeLanguage(nextLocale);
|
||||
return nextLocale;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
update: updateLocale,
|
||||
set: setLocale
|
||||
};
|
||||
}
|
||||
|
||||
// Create a translate function.
|
||||
// It is derived from the "locale" writable.
|
||||
// This means it will be updated every time the locale changes.
|
||||
private createTranslate(i18n: I18nService) {
|
||||
return derived([this.locale], () => {
|
||||
return (key: string, replacements?: Record<string, unknown>) =>
|
||||
i18n.t(key, replacements);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
en: {
|
||||
common: {
|
||||
'ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use.':
|
||||
'ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use.',
|
||||
'The open-source quiz-platform!': 'The open-source quiz-platform!'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,8 @@
|
||||
import { navbarVisible } from '$lib/stores.ts';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import { initLocalizationContext } from '$lib/i18n';
|
||||
initLocalizationContext();
|
||||
|
||||
if (import.meta.env.VITE_SENTRY !== null) {
|
||||
Sentry.init({
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<script lang="ts">
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
import { browser } from '$app/env';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
@@ -84,11 +87,11 @@
|
||||
</h2>
|
||||
|
||||
<h3 class="mt-1 text-xl font-medium text-center text-gray-600 dark:text-gray-200">
|
||||
Welcome Back
|
||||
{$t('login_page.welcome_back')}
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-center text-gray-500 dark:text-gray-400">
|
||||
Login or create account
|
||||
{$t('login_page.login_or_create_account')}
|
||||
</p>
|
||||
|
||||
<form on:submit|preventDefault={login}>
|
||||
@@ -101,13 +104,13 @@
|
||||
name="email"
|
||||
type="email"
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Email"
|
||||
placeholder={$t('register_page.email')}
|
||||
/>
|
||||
<label
|
||||
for="email"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-700 dark:text-white bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Email
|
||||
{$t('register_page.email')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,13 +122,13 @@
|
||||
type="password"
|
||||
bind:value={loginData.password}
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Password"
|
||||
placeholder={$t('register_page.password')}
|
||||
/>
|
||||
<label
|
||||
for="password"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-500 bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Password
|
||||
{$t('register_page.password')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +137,7 @@
|
||||
<a
|
||||
href="/account/reset-password"
|
||||
class="text-sm text-gray-600 dark:text-gray-200 hover:text-gray-500"
|
||||
>Forgot Password?</a
|
||||
>{$t('register_page.forgot_password?')}</a
|
||||
>
|
||||
|
||||
<button
|
||||
@@ -157,7 +160,7 @@
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
Login
|
||||
{$t('register_page.login')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -169,13 +172,13 @@
|
||||
class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700"
|
||||
>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-200"
|
||||
>Don't have an account?
|
||||
>{$t('login_page.already_have_account')}
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="/account/register"
|
||||
class="mx-2 text-sm font-bold text-blue-500 dark:text-blue-400 hover:underline"
|
||||
>Register</a
|
||||
>{$t('register_page.register')}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,13 +270,13 @@
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
{#if responseData.data === 'magic'}
|
||||
Login Succeded! Check your mailbox!
|
||||
{$t('login_page.modal.success.success_check_mail')}
|
||||
{:else if responseData.data === 'password'}
|
||||
Login Succeded!
|
||||
{$t('login_page.modal.success.success')}
|
||||
{:else if responseData.data === '404'}
|
||||
Wrong email or password!
|
||||
{$t('login_page.modal.error.wrong_creds')}
|
||||
{:else if responseData.data === 'error'}
|
||||
unexpected error!
|
||||
{$t('login_page.modal.error.unexpected')}
|
||||
{:else}
|
||||
You stupid Mawoka!
|
||||
{/if}
|
||||
@@ -281,14 +284,13 @@
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
{#if responseData.data === 'magic'}
|
||||
Please check your mailbox, since you should have received a
|
||||
Mail, with a link you can click to login.
|
||||
{$t('login_page.modal.success.description.success_check_mail')}
|
||||
{:else if responseData.data === 'password'}
|
||||
You've successfully logged in!
|
||||
{$t('login_page.modal.success.description.success')}
|
||||
{:else if responseData.data === '404'}
|
||||
Please make sure that your password and your email are correct!
|
||||
{$t('login_page.modal.error.description.wrong_creds')}
|
||||
{:else if responseData.data === 'error'}
|
||||
There was the good old unexpected error!
|
||||
{$t('login_page.modal.error.description.unexpected')}
|
||||
{:else}
|
||||
You stupid Mawoka!
|
||||
{/if}
|
||||
@@ -307,7 +309,7 @@
|
||||
}
|
||||
}}
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>Close
|
||||
>{$t('create_page.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { createForm } from 'felte';
|
||||
// import reporter from '@felte/reporter-tippy';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { validateSchema } from '@felte/validator-yup';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
const { t } = getLocalization();
|
||||
// import reporter from '@felte/reporter-tippy';
|
||||
|
||||
navbarVisible.set(true);
|
||||
import * as yup from 'yup';
|
||||
|
||||
@@ -83,10 +87,12 @@
|
||||
</h2>
|
||||
|
||||
<h3 class="mt-1 text-xl font-medium text-center text-gray-600 dark:text-gray-200">
|
||||
Nice to meet you!
|
||||
{$t('register_page.greeting')}
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-center text-gray-500 dark:text-gray-400">Create account</p>
|
||||
<p class="mt-1 text-center text-gray-500 dark:text-gray-400">
|
||||
{$t('register_page.create_account')}
|
||||
</p>
|
||||
|
||||
<form use:form>
|
||||
<div class="w-full mt-4">
|
||||
@@ -97,7 +103,7 @@
|
||||
name="email"
|
||||
type="email"
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Email"
|
||||
placeholder={$t('register_page.email')}
|
||||
class:ring-red-700={$errors.email !== null}
|
||||
class:ring-green-600={$touched.email === true &&
|
||||
$errors.email === null}
|
||||
@@ -106,7 +112,7 @@
|
||||
for="email"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-700 dark:text-white bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Email
|
||||
{$t('register_page.email')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,7 +123,7 @@
|
||||
name="username"
|
||||
type="text"
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Username"
|
||||
placeholder={$t('register_page.username')}
|
||||
class:ring-red-700={$errors.username !== null}
|
||||
class:ring-green-600={$touched.username === true &&
|
||||
$errors.username === null}
|
||||
@@ -126,7 +132,7 @@
|
||||
for="username"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-700 dark:text-white bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Username
|
||||
{$t('register_page.username')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,13 +146,13 @@
|
||||
class:ring-green-600={$touched.password1 === true &&
|
||||
$errors.password1 === null}
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Password"
|
||||
placeholder={$t('register_page.password')}
|
||||
/>
|
||||
<label
|
||||
for="password1"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-500 bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Password
|
||||
{$t('register_page.password')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +163,7 @@
|
||||
name="password2"
|
||||
type="password"
|
||||
class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
|
||||
placeholder="Password"
|
||||
placeholder={$t('register_page.password')}
|
||||
class:ring-red-700={$errors.password2 !== null}
|
||||
class:ring-green-600={$touched.password2 === true &&
|
||||
$errors.password2 === null}
|
||||
@@ -166,7 +172,7 @@
|
||||
for="password2"
|
||||
class="absolute cursor-text left-0 -top-3 text-sm text-gray-500 bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
|
||||
>
|
||||
Password
|
||||
{$t('register_page.password')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,7 +181,7 @@
|
||||
<a
|
||||
href="/account/reset-password"
|
||||
class="text-sm text-gray-600 dark:text-gray-200 hover:text-gray-500"
|
||||
>Forgot Password?</a
|
||||
>{$t('register_page.forgot_password?')}</a
|
||||
>
|
||||
|
||||
<button
|
||||
@@ -197,7 +203,7 @@
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
Register
|
||||
{$t('register_page.register')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -209,13 +215,13 @@
|
||||
class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700"
|
||||
>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-200"
|
||||
>Already have an account?
|
||||
>{$t('register_page.already_have_account?')}
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="/account/login"
|
||||
class="mx-2 text-sm font-bold text-blue-500 dark:text-blue-400 hover:underline"
|
||||
>Login</a
|
||||
>{$t('register_page.login')}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Editor from '$lib/editor.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
interface Data {
|
||||
public: boolean;
|
||||
@@ -134,10 +137,10 @@
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Successfully created quiz!
|
||||
{$t('create_page.success.title')}
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">Created the quiz successfully!</p>
|
||||
<p class="text-sm text-gray-500">{$t('create_page.success.body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +152,7 @@
|
||||
window.location.href = '/overview';
|
||||
}}
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>Close
|
||||
>{$t('create_page.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
href="https://create.kahoot.it/discover"
|
||||
rel="nofollow"
|
||||
target="_blank">create.kahoot.it/discover</a
|
||||
>, find the quiz you want to import, click on it and copy the URL into your clipboard.
|
||||
Then go to
|
||||
>, find the quiz you want to import, click on it and copy the URL into your clipboard. Then
|
||||
go to
|
||||
<a href="https://classquiz.mawoka.eu/import" rel="nofollow" target="_blank"
|
||||
>classquiz.mawoka.eu/import</a
|
||||
> and paste the url into the text field.
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Editor from '$lib/editor.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
let responseData = {
|
||||
open: false,
|
||||
@@ -88,7 +91,7 @@
|
||||
{:then lol}
|
||||
{#if data !== undefined}
|
||||
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
|
||||
<Editor bind:data submit_button_text={'Save'} />
|
||||
<Editor bind:data submit_button_text={$t('edit_page.save')} />
|
||||
</form>
|
||||
{/if}
|
||||
{:catch err}
|
||||
@@ -141,10 +144,12 @@
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Successfully updated quiz!
|
||||
{$t('edit_page.success_update_title')}
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">Updated the quiz successfully!</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
{$t('edit_page.success_update_body')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +161,7 @@
|
||||
window.location.href = '/overview';
|
||||
}}
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>Close
|
||||
>{$t('edit_page.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
@@ -15,26 +18,23 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>ClassQuiz - Home</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use."
|
||||
/>
|
||||
<title>ClassQuiz - {$t('index_page.meta.title')}</title>
|
||||
<meta name="description" content={$t('index_page.meta.description')} />
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<div class="pt-12 text-center">
|
||||
<h1 class="sm:text-8xl text-6xl mt-6 marck-script">ClassQuiz</h1>
|
||||
<p class="text-xl mt-4">The open-source quiz-platform!</p>
|
||||
<p class="text-xl mt-4">{$t('index_page.slogan')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="stats">
|
||||
<div class="text-center pt-48 snap-y">
|
||||
<h1 class="sm:text-6xl text-4xl">Stats</h1>
|
||||
<h1 class="sm:text-6xl text-4xl">{$t('index_page.stats')}</h1>
|
||||
<p class="text-xl pt-4">
|
||||
{#await getStats() then stats}
|
||||
There are already <span class="underline">{stats.user_count}</span> users and
|
||||
There are already <span class="underline">{stats.user_count}</span> users and <!-- TODO: Add translation -->
|
||||
<span class="underline">{stats.quiz_count}</span> quizzes on ClassQuiz.
|
||||
{/await}
|
||||
</p>
|
||||
@@ -42,11 +42,11 @@
|
||||
</section>
|
||||
<section id="features">
|
||||
<div class="text-center pt-24 snap-y">
|
||||
<h1 class="sm:text-6xl text-4xl">Features</h1>
|
||||
<h1 class="sm:text-6xl text-4xl">{$t('index_page.features')}</h1>
|
||||
<p class="text-xl pt-4">
|
||||
ClassQuiz is a quiz-platform that allows you to create and manage quizzes.
|
||||
{$t('index_page.features_description.1')}
|
||||
<br />
|
||||
The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes.
|
||||
{$t('index_page.features_description.2')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<script lang="ts">
|
||||
import type { QuizData } from '../app';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
const { t } = getLocalization();
|
||||
|
||||
const getData = async (): Promise<Array<QuizData>> => {
|
||||
const res = await fetch('/api/v1/quiz/list');
|
||||
@@ -76,17 +78,17 @@
|
||||
<a
|
||||
href="/create"
|
||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>Create
|
||||
>{$t('overview_page.create')}
|
||||
</a>
|
||||
<a
|
||||
href="/import"
|
||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>Import
|
||||
>{$t('overview_page.import')}
|
||||
</a>
|
||||
<a
|
||||
href="/api/v1/users/logout"
|
||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>Logout
|
||||
>{$t('overview_page.logout')}
|
||||
</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto sm:-mx-8 lg:-mx-8">
|
||||
@@ -99,43 +101,43 @@
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Title
|
||||
{$t('overview_page.title')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Created at
|
||||
{$t('overview_page.created_at')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Question count
|
||||
{$t('overview_page.question_count')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Play
|
||||
{$t('overview_page.play')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Edit
|
||||
{$t('overview_page.edit')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Delete
|
||||
{$t('overview_page.delete')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
Public
|
||||
{$t('overview_page.public')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -167,7 +169,7 @@
|
||||
}}
|
||||
class="border border-green-600"
|
||||
>
|
||||
Start
|
||||
{$t('overview_page.start')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
@@ -184,7 +186,7 @@
|
||||
}}
|
||||
class="border border-green-600"
|
||||
>
|
||||
Delete
|
||||
{$t('overview_page.delete')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
|
||||
Reference in New Issue
Block a user