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

@@ -12,6 +12,7 @@
import { slide } from 'svelte/transition';
import OIDCClientForm from './oidc-client-form.svelte';
import OIDCClientList from './oidc-client-list.svelte';
import { m } from '$lib/paraglide/messages';
let { data } = $props();
let clients = $state(data.clients);
@@ -29,7 +30,7 @@
const clientSecret = await oidcService.createClientSecret(createdClient.id);
clientSecretStore.set(clientSecret);
goto(`/settings/admin/oidc-clients/${createdClient.id}`);
toast.success('OIDC client created successfully');
toast.success(m.oidc_client_created_successfully());
return true;
} catch (e) {
axiosErrorToast(e);
@@ -39,18 +40,18 @@
</script>
<svelte:head>
<title>OIDC Clients</title>
<title>{m.oidc_clients()}</title>
</svelte:head>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Create OIDC Client</Card.Title>
<Card.Description>Add a new OIDC client to {$appConfigStore.appName}.</Card.Description>
<Card.Title>{m.create_oidc_client()}</Card.Title>
<Card.Description>{m.add_a_new_oidc_client_to_appname({ appName: $appConfigStore.appName})}</Card.Description>
</div>
{#if !expandAddClient}
<Button on:click={() => (expandAddClient = true)}>Add OIDC Client</Button>
<Button on:click={() => (expandAddClient = true)}>{m.add_oidc_client()}</Button>
{:else}
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddClient = false)}>
<LucideMinus class="h-5 w-5" />
@@ -69,7 +70,7 @@
<Card.Root>
<Card.Header>
<Card.Title>Manage OIDC Clients</Card.Title>
<Card.Title>{m.manage_oidc_clients()}</Card.Title>
</Card.Header>
<Card.Content>
<OIDCClientList {clients} requestOptions={clientsRequestOptions} />

View File

@@ -16,6 +16,7 @@
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
import OidcForm from '../oidc-client-form.svelte';
import { m } from '$lib/paraglide/messages';
let { data } = $props();
let client = $state({
@@ -27,13 +28,13 @@
const oidcService = new OidcService();
const setupDetails = $state({
'Authorization URL': `https://${$page.url.hostname}/authorize`,
'OIDC Discovery URL': `https://${$page.url.hostname}/.well-known/openid-configuration`,
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
'Logout URL': `https://${$page.url.hostname}/api/oidc/end-session`,
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
PKCE: client.pkceEnabled ? 'Enabled' : 'Disabled'
[m.authorization_url()]: `https://${$page.url.hostname}/authorize`,
[m.oidc_discovery_url()]: `https://${$page.url.hostname}/.well-known/openid-configuration`,
[m.token_url()]: `https://${$page.url.hostname}/api/oidc/token`,
[m.userinfo_url()]: `https://${$page.url.hostname}/api/oidc/userinfo`,
[m.logout_url()]: `https://${$page.url.hostname}/api/oidc/end-session`,
[m.certificate_url()]: `https://${$page.url.hostname}/.well-known/jwks.json`,
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled()
});
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
@@ -45,11 +46,11 @@
: Promise.resolve();
client.isPublic = updatedClient.isPublic;
setupDetails.PKCE = updatedClient.pkceEnabled ? 'Enabled' : 'Disabled';
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
await Promise.all([dataPromise, imagePromise])
.then(() => {
toast.success('OIDC client updated successfully');
toast.success(m.oidc_client_updated_successfully());
})
.catch((e) => {
axiosErrorToast(e);
@@ -61,17 +62,17 @@
async function createClientSecret() {
openConfirmDialog({
title: 'Create new client secret',
title: m.create_new_client_secret(),
message:
'Are you sure you want to create a new client secret? The old one will be invalidated.',
m.are_you_sure_you_want_to_create_a_new_client_secret(),
confirm: {
label: 'Generate',
label: m.generate(),
destructive: true,
action: async () => {
try {
const clientSecret = await oidcService.createClientSecret(client.id);
clientSecretStore.set(clientSecret);
toast.success('New client secret created successfully');
toast.success(m.new_client_secret_created_successfully());
} catch (e) {
axiosErrorToast(e);
}
@@ -84,7 +85,7 @@
await oidcService
.updateAllowedUserGroups(client.id, allowedGroups)
.then(() => {
toast.success('Allowed user groups updated successfully');
toast.success(m.allowed_user_groups_updated_successfully());
})
.catch((e) => {
axiosErrorToast(e);
@@ -97,12 +98,12 @@
</script>
<svelte:head>
<title>OIDC Client {client.name}</title>
<title>{m.oidc_client_name({ name: client.name })}</title>
</svelte:head>
<div>
<a class="text-muted-foreground flex text-sm" href="/settings/admin/oidc-clients"
><LucideChevronLeft class="h-5 w-5" /> Back</a
><LucideChevronLeft class="h-5 w-5" /> {m.back()}</a
>
</div>
<Card.Root>
@@ -112,14 +113,14 @@
<Card.Content>
<div class="flex flex-col">
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">Client ID</Label>
<Label class="mb-0 w-44">{m.client_id()}</Label>
<CopyToClipboard value={client.id}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard>
</div>
{#if !client.isPublic}
<div class="mb-2 mt-1 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">Client secret</Label>
<Label class="mb-0 w-44">{m.client_secret()}</Label>
{#if $clientSecretStore}
<CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -158,7 +159,7 @@
{#if !showAllDetails}
<div class="mt-4 flex justify-center">
<Button on:click={() => (showAllDetails = true)} size="sm" variant="ghost"
>Show more details</Button
>{m.show_more_details()}</Button
>
</div>
{/if}
@@ -172,11 +173,11 @@
</Card.Root>
<CollapsibleCard
id="allowed-user-groups"
title="Allowed User Groups"
description="Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client."
title={m.allowed_user_groups()}
description={m.add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups()}
>
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
<div class="mt-5 flex justify-end">
<Button on:click={() => updateUserGroupClients(client.allowedUserGroupIds)}>Save</Button>
<Button on:click={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
</div>
</CollapsibleCard>

View File

@@ -2,6 +2,7 @@
import * as Dialog from '$lib/components/ui/dialog';
import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import { m } from '$lib/paraglide/messages';
let {
oneTimeLink = $bindable()
@@ -19,13 +20,12 @@
<Dialog.Root open={!!oneTimeLink} {onOpenChange}>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>One Time Link</Dialog.Title>
<Dialog.Title>{m.one_time_link()}</Dialog.Title>
<Dialog.Description
>Use this link to sign in once. This is needed for users who haven't added a passkey yet or
have lost it.</Dialog.Description
>{m.use_this_link_to_sign_in_once()}</Dialog.Description
>
</Dialog.Header>
<Label for="one-time-link">One Time Link</Label>
<Label for="one-time-link">{m.one_time_link()}</Label>
<Input id="one-time-link" value={oneTimeLink} readonly />
</Dialog.Content>
</Dialog.Root>

View File

@@ -2,6 +2,7 @@
import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { m } from '$lib/paraglide/messages';
import { LucideMinus, LucidePlus } from 'lucide-svelte';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
@@ -53,7 +54,7 @@
on:click={() => (callbackURLs = [...callbackURLs, ''])}
>
<LucidePlus class="mr-1 h-4 w-4" />
{callbackURLs.length === 0 ? 'Add' : 'Add another'}
{callbackURLs.length === 0 ? m.add() : m.add_another()}
</Button>
{/if}
</div>

View File

@@ -12,6 +12,7 @@
import { createForm } from '$lib/utils/form-util';
import { z } from 'zod';
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
import { m } from '$lib/paraglide/messages';
let {
callback,
@@ -79,16 +80,16 @@
<form onsubmit={onSubmit}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-7 sm:flex-row">
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
<FormInput label={m.name()} class="w-full" bind:input={$inputs.name} />
<div></div>
<OidcCallbackUrlInput
label="Callback URLs"
label={m.callback_urls()}
class="w-full"
bind:callbackURLs={$inputs.callbackURLs.value}
bind:error={$inputs.callbackURLs.error}
/>
<OidcCallbackUrlInput
label="Logout Callback URLs"
label={m.logout_callback_urls()}
class="w-full"
allowEmpty
bind:callbackURLs={$inputs.logoutCallbackURLs.value}
@@ -96,8 +97,8 @@
/>
<CheckboxWithLabel
id="public-client"
label="Public Client"
description="Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app."
label={m.public_client()}
description={m.public_clients_do_not_have_a_client_secret_and_use_pkce_instead()}
onCheckedChange={(v) => {
if (v == true) form.setValue('pkceEnabled', true);
}}
@@ -105,21 +106,21 @@
/>
<CheckboxWithLabel
id="pkce"
label="PKCE"
description="Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks."
label={m.pkce()}
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
disabled={$inputs.isPublic.value}
bind:checked={$inputs.pkceEnabled.value}
/>
</div>
<div class="mt-8">
<Label for="logo">Logo</Label>
<Label for="logo">{m.logo()}</Label>
<div class="mt-2 flex items-end gap-3">
{#if logoDataURL}
<div class="bg-muted h-32 w-32 rounded-2xl p-3">
<img
class="m-auto max-h-full max-w-full object-contain"
src={logoDataURL}
alt={`${$inputs.name.value} logo`}
alt={m.name_logo({name: $inputs.name.value})}
/>
</div>
{/if}
@@ -131,17 +132,17 @@
onchange={onLogoChange}
>
<Button variant="secondary">
{logoDataURL ? 'Change Logo' : 'Upload Logo'}
{logoDataURL ? m.change_logo() : m.upload_logo()}
</Button>
</FileInput>
{#if logoDataURL}
<Button variant="outline" on:click={resetLogo}>Remove Logo</Button>
<Button variant="outline" on:click={resetLogo}>{m.remove_logo()}</Button>
{/if}
</div>
</div>
</div>
<div class="w-full"></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

@@ -10,6 +10,7 @@
import { LucidePencil, LucideTrash } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import OneTimeLinkModal from './client-secret.svelte';
import { m } from '$lib/paraglide/messages';
let {
clients = $bindable(),
@@ -25,16 +26,16 @@
async function deleteClient(client: OidcClient) {
openConfirmDialog({
title: `Delete ${client.name}`,
message: 'Are you sure you want to delete this OIDC client?',
title: m.delete_name({name: client.name}),
message: m.are_you_sure_you_want_to_delete_this_oidc_client(),
confirm: {
label: 'Delete',
label: m.delete(),
destructive: true,
action: async () => {
try {
await oidcService.removeClient(client.id);
clients = await oidcService.listClients(requestOptions!);
toast.success('OIDC client deleted successfully');
toast.success(m.oidc_client_deleted_successfully());
} catch (e) {
axiosErrorToast(e);
}
@@ -49,9 +50,9 @@
{requestOptions}
onRefresh={async (o) => (clients = await oidcService.listClients(o))}
columns={[
{ label: 'Logo' },
{ label: 'Name', sortColumn: 'name' },
{ label: 'Actions', hidden: true }
{ label: m.logo() },
{ label: m.name(), sortColumn: 'name' },
{ label: m.actions(), hidden: true }
]}
>
{#snippet rows({ item })}
@@ -61,7 +62,7 @@
<img
class="m-auto max-h-full max-w-full object-contain"
src="/api/oidc/clients/{item.id}/logo"
alt="{item.name} logo"
alt={m.name_logo({name: item.name})}
/>
</div>
{/if}
@@ -72,9 +73,9 @@
href="/settings/admin/oidc-clients/{item.id}"
size="sm"
variant="outline"
aria-label="Edit"><LucidePencil class="h-3 w-3 " /></Button
aria-label={m.edit()}><LucidePencil class="h-3 w-3 " /></Button
>
<Button on:click={() => deleteClient(item)} size="sm" variant="outline" aria-label="Delete"
<Button on:click={() => deleteClient(item)} size="sm" variant="outline" aria-label={m.delete()}
><LucideTrash class="h-3 w-3 text-red-500" /></Button
>
</Table.Cell>