🌐 Added i18next

This commit is contained in:
Mawoka
2022-03-25 18:23:41 +01:00
parent 326780c8d7
commit 8f8e152c32
16 changed files with 4457 additions and 2495 deletions
@@ -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: '.'
}
};
+7 -2
View File
@@ -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"
}
}
+1973 -298
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -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);
}
}
+36
View File
@@ -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);
};
+75
View File
@@ -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);
});
}
}
+9
View File
@@ -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!'
}
}
};
+2
View File
@@ -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({
+22 -20
View File
@@ -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>
+21 -15
View File
@@ -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>
+6 -3
View File
@@ -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.
+9 -4
View File
@@ -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>
+11 -11
View File
@@ -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>
+14 -12
View File
@@ -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