feat: add support for translations (#349)

Co-authored-by: Kyle Mendell <kmendell@outlook.com>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Jonas Claes
2025-03-20 19:57:41 +01:00
committed by GitHub
parent 041c565dc1
commit 269b5a3c92
83 changed files with 1567 additions and 453 deletions

View File

@@ -2,6 +2,7 @@
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service';
import appConfigStore from '$lib/stores/application-configuration-store';
@@ -13,6 +14,7 @@
import { toast } from 'svelte-sonner';
import ProfilePictureSettings from '../../../lib/components/form/profile-picture-settings.svelte';
import AccountForm from './account-form.svelte';
import LocalePicker from './locale-picker.svelte';
import LoginCodeModal from './login-code-modal.svelte';
import PasskeyList from './passkey-list.svelte';
import RenamePasskeyModal from './rename-passkey-modal.svelte';
@@ -39,7 +41,7 @@
let success = true;
await userService
.updateCurrent(user)
.then(() => toast.success('Account details updated successfully'))
.then(() => toast.success(m.account_details_updated_successfully()))
.catch((e) => {
axiosErrorToast(e);
success = false;
@@ -51,9 +53,7 @@
async function updateProfilePicture(image: File) {
await userService
.updateCurrentUsersProfilePicture(image)
.then(() =>
toast.success('Profile picture updated successfully. It may take a few minutes to update.')
)
.then(() => toast.success(m.profile_picture_updated_successfully()))
.catch(axiosErrorToast);
}
@@ -72,24 +72,22 @@
</script>
<svelte:head>
<title>Account Settings</title>
<title>{m.account_settings()}</title>
</svelte:head>
{#if passkeys.length == 0}
<Alert.Root variant="warning">
<LucideAlertTriangle class="size-4" />
<Alert.Title>Passkey missing</Alert.Title>
<Alert.Title>{m.passkey_missing()}</Alert.Title>
<Alert.Description
>Please add a passkey to prevent losing access to your account.</Alert.Description
>{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()}</Alert.Description
>
</Alert.Root>
{:else if passkeys.length == 1}
<Alert.Root variant="warning" dismissibleId="single-passkey">
<LucideAlertTriangle class="size-4" />
<Alert.Title>Single Passkey Configured</Alert.Title>
<Alert.Description
>It is recommended to add more than one passkey to avoid losing access to your account.</Alert.Description
>
<Alert.Title>{m.single_passkey_configured()}</Alert.Title>
<Alert.Description>{m.it_is_recommended_to_add_more_than_one_passkey()}</Alert.Description>
</Alert.Root>
{/if}
@@ -99,7 +97,7 @@
>
<Card.Root>
<Card.Header>
<Card.Title>Account Details</Card.Title>
<Card.Title>{m.account_details()}</Card.Title>
</Card.Header>
<Card.Content>
<AccountForm {account} callback={updateAccount} />
@@ -122,12 +120,12 @@
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Passkeys</Card.Title>
<Card.Title>{m.passkeys()}</Card.Title>
<Card.Description class="mt-1">
Manage your passkeys that you can use to authenticate yourself.
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
</Card.Description>
</div>
<Button size="sm" class="ml-3" on:click={createPasskey}>Add Passkey</Button>
<Button size="sm" class="ml-3" on:click={createPasskey}>{m.add_passkey()}</Button>
</div>
</Card.Header>
{#if passkeys.length != 0}
@@ -141,12 +139,28 @@
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Login Code</Card.Title>
<Card.Title>{m.login_code()}</Card.Title>
<Card.Description class="mt-1">
Create a one-time login code to sign in from a different device without a passkey.
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
</Card.Description>
</div>
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}>Create</Button>
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}
>{m.create()}</Button
>
</div>
</Card.Header>
</Card.Root>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>{m.language()}</Card.Title>
<Card.Description class="mt-1">
{m.select_the_language_you_want_to_use()}
</Card.Description>
</div>
<LocalePicker />
</div>
</Card.Header>
</Card.Root>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import { m } from '$lib/paraglide/messages';
import type { UserCreate } from '$lib/types/user.type';
import { createForm } from '$lib/utils/form-util';
import { z } from 'zod';
@@ -24,7 +25,7 @@
.max(30)
.regex(
/^[a-z0-9_@.-]+$/,
"Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols"
m.username_can_only_contain()
),
email: z.string().email(),
isAdmin: z.boolean()
@@ -36,7 +37,7 @@
const data = form.validate();
if (!data) return;
isLoading = true;
const success = await callback(data);
await callback(data);
// Reset form if user was successfully created
isLoading = false;
}
@@ -45,21 +46,21 @@
<form onsubmit={onSubmit}>
<div class="flex flex-col gap-3 sm:flex-row">
<div class="w-full">
<FormInput label="First name" bind:input={$inputs.firstName} />
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
</div>
<div class="w-full">
<FormInput label="Last name" bind:input={$inputs.lastName} />
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
</div>
</div>
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
<div class="w-full">
<FormInput label="Email" bind:input={$inputs.email} />
<FormInput label={m.email()} bind:input={$inputs.email} />
</div>
<div class="w-full">
<FormInput label="Username" bind:input={$inputs.username} />
<FormInput label={m.username()} bind:input={$inputs.username} />
</div>
</div>
<div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">Save</Button>
<Button {isLoading} type="submit">{m.save()}</Button>
</div>
</form>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { getLocale, setLocale, type Locale } from '$lib/paraglide/runtime';
import UserService from '$lib/services/user-service';
import userStore from '$lib/stores/user-store';
const userService = new UserService();
const currentLocale = getLocale();
const locales = {
en: 'English',
nl: 'Nederlands'
};
function updateLocale(locale: Locale) {
setLocale(locale);
userService.updateCurrent({
...$userStore!,
locale
});
}
</script>
<Select.Root
selected={{
label: locales[currentLocale],
value: currentLocale
}}
onSelectedChange={(v) => updateLocale(v!.value)}
>
<Select.Trigger class="h-9 max-w-[200px]" aria-label="Select locale">
<Select.Value>{locales[currentLocale]}</Select.Value>
</Select.Trigger>
<Select.Content>
{#each Object.entries(locales) as [value, label]}
<Select.Item {value}>{label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>

View File

@@ -3,6 +3,7 @@
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
import * as Dialog from '$lib/components/ui/dialog';
import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service';
import { axiosErrorToast } from '$lib/utils/error-util';
@@ -37,9 +38,9 @@
<Dialog.Root open={!!code} {onOpenChange}>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>Login Code</Dialog.Title>
<Dialog.Title>{m.login_code()}</Dialog.Title>
<Dialog.Description
>Sign in using the following code. The code will expire in 15 minutes.
>{m.sign_in_using_the_following_code_the_code_will_expire_in_minutes()}
</Dialog.Description>
</Dialog.Header>
@@ -49,7 +50,7 @@
</CopyToClipboard>
<div class="text-muted-foreground flex items-center justify-center gap-3">
<Separator />
<p class="text-nowrap text-xs">or visit</p>
<p class="text-nowrap text-xs">{m.or_visit()}</p>
<Separator />
</div>
<div>

View File

@@ -8,6 +8,7 @@
import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import RenamePasskeyModal from './rename-passkey-modal.svelte';
import { m } from '$lib/paraglide/messages';
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
@@ -17,16 +18,16 @@
async function deletePasskey(passkey: Passkey) {
openConfirmDialog({
title: `Delete ${passkey.name}`,
message: 'Are you sure you want to delete this passkey?',
title: m.delete_passkey_name({ passkeyName: passkey.name }),
message: m.are_you_sure_you_want_to_delete_this_passkey(),
confirm: {
label: 'Delete',
label: m.delete(),
destructive: true,
action: async () => {
try {
await webauthnService.removeCredential(passkey.id);
passkeys = await webauthnService.listCredentials();
toast.success('Passkey deleted successfully');
toast.success(m.passkey_deleted_successfully());
} catch (e) {
axiosErrorToast(e);
}
@@ -44,7 +45,7 @@
<div>
<p>{passkey.name}</p>
<p class="text-xs text-muted-foreground">
Added on {new Date(passkey.createdAt).toLocaleDateString()}
{m.added_on()} {new Date(passkey.createdAt).toLocaleDateString()}
</p>
</div>
</div>
@@ -53,13 +54,13 @@
on:click={() => (passkeyToRename = passkey)}
size="sm"
variant="outline"
aria-label="Rename"><LucidePencil class="h-3 w-3" /></Button
aria-label={m.rename()}><LucidePencil class="h-3 w-3" /></Button
>
<Button
on:click={() => deletePasskey(passkey)}
size="sm"
variant="outline"
aria-label="Delete"><LucideTrash class="h-3 w-3 text-red-500" /></Button
aria-label={m.delete()}><LucideTrash class="h-3 w-3 text-red-500" /></Button
>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { m } from '$lib/paraglide/messages';
import WebAuthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type';
import { axiosErrorToast } from '$lib/utils/error-util';
@@ -35,7 +36,7 @@
.updateCredentialName(passkey!.id, name)
.then(() => {
passkey = null;
toast.success('Passkey name updated successfully');
toast.success(m.passkey_name_updated_successfully());
callback?.();
})
.catch(axiosErrorToast);
@@ -45,16 +46,16 @@
<Dialog.Root open={!!passkey} {onOpenChange}>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>Name Passkey</Dialog.Title>
<Dialog.Description>Name your passkey to easily identify it later.</Dialog.Description>
<Dialog.Title>{m.name_passkey()}</Dialog.Title>
<Dialog.Description>{m.name_your_passkey_to_easily_identify_it_later()}</Dialog.Description>
</Dialog.Header>
<form onsubmit={onSubmit}>
<div class="grid items-center gap-4 sm:grid-cols-4">
<Label for="name" class="sm:text-right">Name</Label>
<Label for="name" class="sm:text-right">{m.name()}</Label>
<Input id="name" bind:value={name} class="col-span-3" />
</div>
<Dialog.Footer class="mt-4">
<Button type="submit">Save</Button>
<Button type="submit">{m.save()}</Button>
</Dialog.Footer>
</form>
</Dialog.Content>