mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-31 19:56:35 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
39
frontend/src/routes/settings/account/locale-picker.svelte
Normal file
39
frontend/src/routes/settings/account/locale-picker.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user