mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-05 09:19:04 +02:00
refactor: use paraglide markup instead of custom implementation
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
import { LucideChevronDown, type Icon as IconType } from '@lucide/svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import FormattedMessage from './formatted-message.svelte';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
|
||||
@@ -71,7 +70,7 @@
|
||||
{title}
|
||||
</Card.Title>
|
||||
{#if description}
|
||||
<Card.Description><FormattedMessage m={description} /></Card.Description>
|
||||
<Card.Description>{description}</Card.Description>
|
||||
{/if}
|
||||
</div>
|
||||
{#if button}
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>{$confirmDialogStore.title}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
<FormattedMessage m={$confirmDialogStore.message} />
|
||||
{#if typeof $confirmDialogStore.message === 'string'}
|
||||
{$confirmDialogStore.message}
|
||||
{:else}
|
||||
<FormattedMessage {...$confirmDialogStore.message} />
|
||||
{/if}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { AnyFormattedMessage, AnyMessage, FormattedMessageProps } from '../formatted-message';
|
||||
import ConfirmDialog from './confirm-dialog.svelte';
|
||||
|
||||
export const confirmDialogStore = writable({
|
||||
interface ConfirmDialogState {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string | AnyFormattedMessage;
|
||||
confirm: {
|
||||
label: string;
|
||||
destructive: boolean;
|
||||
action: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const confirmDialogStore = writable<ConfirmDialogState>({
|
||||
open: false,
|
||||
title: '',
|
||||
message: '',
|
||||
@@ -13,13 +25,13 @@ export const confirmDialogStore = writable({
|
||||
}
|
||||
});
|
||||
|
||||
function openConfirmDialog({
|
||||
function openConfirmDialog<TMessage extends AnyMessage = AnyMessage>({
|
||||
title,
|
||||
message,
|
||||
confirm
|
||||
}: {
|
||||
title: string;
|
||||
message: string;
|
||||
message: string | FormattedMessageProps<TMessage>;
|
||||
confirm: {
|
||||
label?: string;
|
||||
destructive?: boolean;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { LucideExternalLink } from '@lucide/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import FormattedMessage from '../formatted-message.svelte';
|
||||
import { cn } from '$lib/utils/style';
|
||||
|
||||
type WithoutChildren = {
|
||||
@@ -38,7 +37,7 @@
|
||||
}: HTMLAttributes<HTMLDivElement> &
|
||||
(WithChildren | WithoutChildren) & {
|
||||
label?: string;
|
||||
description?: string;
|
||||
description?: string | Snippet;
|
||||
docsLink?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
@@ -63,7 +62,11 @@
|
||||
{/if}
|
||||
{#if description}
|
||||
<Field.Description>
|
||||
<FormattedMessage m={description} />
|
||||
{#if typeof description === 'string'}
|
||||
{description}
|
||||
{:else}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{#if docsLink}
|
||||
<a
|
||||
class="relative text-black after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:-translate-y-px after:bg-white dark:text-white"
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
{/if}
|
||||
|
||||
<p class="text-muted-foreground mt-2 text-xs">
|
||||
<FormattedMessage m={m.logo_from_url_description()} />
|
||||
<FormattedMessage message={m.logo_from_url_description} />
|
||||
</p>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -1,100 +1,49 @@
|
||||
<!-- Component to display messages from Paraglide with support for links in the format <link href="url">text</link>. -->
|
||||
<!-- This gets redundant in the future, because the library will support this natively. https://github.com/opral/inlang-sdk/issues/240 -->
|
||||
<script lang="ts">
|
||||
let {
|
||||
m
|
||||
}: {
|
||||
m: string;
|
||||
} = $props();
|
||||
<script lang="ts" generics="TMessage extends AnyMessage">
|
||||
import {
|
||||
ParaglideMessage,
|
||||
type MarkupRendererProps,
|
||||
type MessageLike,
|
||||
type MessageOptions
|
||||
} from '@inlang/paraglide-js-svelte';
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import type { AnyMessage, FormattedMessageProps } from './formatted-message';
|
||||
|
||||
interface MessagePart {
|
||||
type: 'text' | 'link' | 'bold';
|
||||
content: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
// Extracts attribute value from a tag's attribute string
|
||||
function getAttr(attrs: string, name: string): string | undefined {
|
||||
const re = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, 'i');
|
||||
const m = re.exec(attrs ?? '');
|
||||
return m?.[2];
|
||||
}
|
||||
|
||||
const handlers: Record<string, (attrs: string, inner: string) => MessagePart | null> = {
|
||||
link: (attrs, inner) => {
|
||||
const href = getAttr(attrs, 'href');
|
||||
if (!href) return { type: 'text', content: inner };
|
||||
return { type: 'link', content: inner, href };
|
||||
},
|
||||
b: (_attrs, inner) => ({ type: 'bold', content: inner })
|
||||
type BoldTag = {
|
||||
options: Record<string, never>;
|
||||
attributes: Record<string, never>;
|
||||
children: true;
|
||||
};
|
||||
|
||||
function buildTokenRegex(): RegExp {
|
||||
const keys = Object.keys(handlers).join('|');
|
||||
// Matches: <tag attrs>inner</tag> for allowed tags only
|
||||
return new RegExp(`<(${keys})\\b([^>]*)>(.*?)<\\/\\1>`, 'g');
|
||||
}
|
||||
type LinkTag = {
|
||||
options: { href: string };
|
||||
attributes: Record<string, never>;
|
||||
children: true;
|
||||
};
|
||||
|
||||
function parseMessage(content: string): MessagePart[] | string {
|
||||
const tokenRegex = buildTokenRegex();
|
||||
if (!tokenRegex.test(content)) return content;
|
||||
// Reset lastIndex for reuse
|
||||
tokenRegex.lastIndex = 0;
|
||||
type NativeMessageProps = {
|
||||
message: MessageLike<any, any, any>;
|
||||
inputs?: unknown;
|
||||
b: Snippet<[MarkupRendererProps<unknown, MessageOptions, BoldTag>]>;
|
||||
link: Snippet<[MarkupRendererProps<unknown, MessageOptions, LinkTag>]>;
|
||||
};
|
||||
|
||||
const parts: MessagePart[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
const NativeParaglideMessage = ParaglideMessage as unknown as Component<NativeMessageProps>;
|
||||
|
||||
while ((match = tokenRegex.exec(content)) !== null) {
|
||||
// Add text before the matched token
|
||||
if (match.index > lastIndex) {
|
||||
const textContent = content.slice(lastIndex, match.index);
|
||||
if (textContent) parts.push({ type: 'text', content: textContent });
|
||||
}
|
||||
|
||||
const tag = match[1];
|
||||
const attrs = match[2] ?? '';
|
||||
const inner = match[3] ?? '';
|
||||
const handler = handlers[tag];
|
||||
const part: MessagePart | null = handler
|
||||
? handler(attrs, inner)
|
||||
: { type: 'text', content: inner };
|
||||
if (part) parts.push(part);
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Add remaining text after the last token
|
||||
if (lastIndex < content.length) {
|
||||
const remainingText = content.slice(lastIndex);
|
||||
if (remainingText) parts.push({ type: 'text', content: remainingText });
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
const parsedContent = parseMessage(m);
|
||||
let { message, inputs }: FormattedMessageProps<TMessage> = $props();
|
||||
</script>
|
||||
|
||||
{#if typeof parsedContent === 'string'}
|
||||
{parsedContent}
|
||||
{:else}
|
||||
{#each parsedContent as part, i (i)}
|
||||
{#if part.type === 'text'}
|
||||
{part.content}
|
||||
{:else if part.type === 'bold'}
|
||||
<b>
|
||||
{part.content}
|
||||
</b>
|
||||
{:else if part.type === 'link'}
|
||||
<a
|
||||
class="text-black underline dark:text-white"
|
||||
href={part.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{part.content}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
<NativeParaglideMessage {message} {inputs}>
|
||||
{#snippet b({ children })}
|
||||
<b>{@render children?.()}</b>
|
||||
{/snippet}
|
||||
{#snippet link({ children, options })}
|
||||
<a
|
||||
class="text-black underline dark:text-white"
|
||||
href={options.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/snippet}
|
||||
</NativeParaglideMessage>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { MessageLike } from '@inlang/paraglide-js-svelte';
|
||||
|
||||
export type AnyMessage = MessageLike<any, any, any>;
|
||||
|
||||
type MessageInputs<TMessage extends AnyMessage> = Parameters<TMessage>[0];
|
||||
|
||||
type MessageInputsProp<TInputs> = undefined extends TInputs
|
||||
? { inputs?: Exclude<NoInfer<TInputs>, undefined> }
|
||||
: { inputs: NoInfer<TInputs> };
|
||||
|
||||
export type FormattedMessageProps<TMessage extends AnyMessage> = {
|
||||
message: TMessage;
|
||||
} & MessageInputsProp<MessageInputs<TMessage>>;
|
||||
|
||||
export type AnyFormattedMessage = {
|
||||
message: AnyMessage;
|
||||
inputs?: unknown;
|
||||
};
|
||||
@@ -207,10 +207,11 @@
|
||||
{:else if reauthenticationRequired && deviceInfo?.client}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
<FormattedMessage
|
||||
m={m.do_you_want_to_sign_in_to_client_with_your_app_name_account({
|
||||
message={m.do_you_want_to_sign_in_to_client_with_your_app_name_account}
|
||||
inputs={{
|
||||
client: deviceInfo.client.name,
|
||||
appName: $appConfigStore.appName
|
||||
})}
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{:else if authorizationRequired}
|
||||
@@ -219,9 +220,10 @@
|
||||
<Card.Header>
|
||||
<Card.Description class="text-start">
|
||||
<FormattedMessage
|
||||
m={m.client_wants_to_access_the_following_information({
|
||||
message={m.client_wants_to_access_the_following_information}
|
||||
inputs={{
|
||||
client: deviceInfo!.client.name
|
||||
})}
|
||||
}}
|
||||
/>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
@@ -120,14 +120,16 @@
|
||||
{errorMessage}.
|
||||
{:else if currentStep == 'select_account' && $userStore}
|
||||
<FormattedMessage
|
||||
m={m.account_selection_signin_confirmation({ name: interactionSession.client.name })}
|
||||
message={m.account_selection_signin_confirmation}
|
||||
inputs={{ name: interactionSession.client.name }}
|
||||
/>
|
||||
{:else}
|
||||
<FormattedMessage
|
||||
m={m.do_you_want_to_sign_in_to_client_with_your_app_name_account({
|
||||
message={m.do_you_want_to_sign_in_to_client_with_your_app_name_account}
|
||||
inputs={{
|
||||
client: interactionSession.client.name,
|
||||
appName: $appConfigStore.appName
|
||||
})}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</p>
|
||||
@@ -174,9 +176,10 @@
|
||||
<Card.Header>
|
||||
<p class="text-muted-foreground text-start">
|
||||
<FormattedMessage
|
||||
m={m.client_wants_to_access_the_following_information({
|
||||
message={m.client_wants_to_access_the_following_information}
|
||||
inputs={{
|
||||
client: interactionSession.client.name
|
||||
})}
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Card.Header>
|
||||
|
||||
@@ -38,10 +38,11 @@
|
||||
|
||||
<p class="text-muted-foreground mt-2">
|
||||
<FormattedMessage
|
||||
m={m.do_you_want_to_sign_out_of_pocketid_with_the_account({
|
||||
message={m.do_you_want_to_sign_out_of_pocketid_with_the_account}
|
||||
inputs={{
|
||||
username: $userStore?.username ?? '',
|
||||
appName: $appConfigStore.appName
|
||||
})}
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<Item.Description>
|
||||
{m.select_the_language_you_want_to_use()}
|
||||
<br />
|
||||
<FormattedMessage m={m.contribute_to_translation()} />
|
||||
<FormattedMessage message={m.contribute_to_translation} />
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
<ShieldPlus class="text-primary/80 size-5" />
|
||||
{m.create_api_key()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage m={m.add_a_new_api_key_for_programmatic_access()} /></Card.Description
|
||||
>
|
||||
<Card.Description>
|
||||
<FormattedMessage message={m.add_a_new_api_key_for_programmatic_access} />
|
||||
</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button class="w-full md:w-auto" onclick={() => (expandAddApiKey = true)}
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
<LucidePlus class="text-primary/80 size-5" />
|
||||
{m.create_api()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage m={m.create_a_new_api_description()} /></Card.Description
|
||||
>
|
||||
<Card.Description>
|
||||
<FormattedMessage message={m.create_a_new_api_description} />
|
||||
</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddApi}
|
||||
<Button class="w-full md:w-auto" onclick={() => (expandAddApi = true)}
|
||||
|
||||
@@ -127,7 +127,10 @@
|
||||
function disableGroupRestriction() {
|
||||
openConfirmDialog({
|
||||
title: m.unrestrict_oidc_client({ clientName: client.name }),
|
||||
message: m.confirm_unrestrict_oidc_client_description({ clientName: client.name }),
|
||||
message: {
|
||||
message: m.confirm_unrestrict_oidc_client_description,
|
||||
inputs: { clientName: client.name }
|
||||
},
|
||||
confirm: {
|
||||
label: m.unrestrict(),
|
||||
destructive: true,
|
||||
@@ -373,7 +376,7 @@
|
||||
<Card.Header>
|
||||
<Card.Title>{m.scim_provisioning()}</Card.Title>
|
||||
<Card.Description>
|
||||
<FormattedMessage m={m.scim_provisioning_description()} />
|
||||
<FormattedMessage message={m.scim_provisioning_description} />
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
|
||||
+4
-3
@@ -50,9 +50,10 @@
|
||||
async function onDisable() {
|
||||
openConfirmDialog({
|
||||
title: m.disable_scim_provisioning(),
|
||||
message: m.disable_scim_provisioning_confirm_description({
|
||||
clientName: existingProvider!.oidcClient.name
|
||||
}),
|
||||
message: {
|
||||
message: m.disable_scim_provisioning_confirm_description,
|
||||
inputs: { clientName: existingProvider!.oidcClient.name }
|
||||
},
|
||||
confirm: {
|
||||
label: m.disable(),
|
||||
destructive: true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
label: string;
|
||||
description: string;
|
||||
description: string | Snippet;
|
||||
callbackURLs: string[];
|
||||
error?: string | null;
|
||||
children?: Snippet;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
@@ -186,6 +187,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet callbackUrlDescription()}
|
||||
<FormattedMessage message={m.callback_url_description} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet logoutCallbackUrlDescription()}
|
||||
<FormattedMessage message={m.logout_callback_url_description} />
|
||||
{/snippet}
|
||||
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<div class="grid grid-cols-1 gap-x-3 gap-y-7 sm:flex-row md:grid-cols-2">
|
||||
<FormInput
|
||||
@@ -209,14 +218,14 @@
|
||||
/>
|
||||
<OidcCallbackUrlInput
|
||||
label={m.callback_urls()}
|
||||
description={m.callback_url_description()}
|
||||
description={callbackUrlDescription}
|
||||
class="w-full"
|
||||
bind:callbackURLs={$inputs.callbackURLs.value}
|
||||
bind:error={$inputs.callbackURLs.error}
|
||||
/>
|
||||
<OidcCallbackUrlInput
|
||||
label={m.logout_callback_urls()}
|
||||
description={m.logout_callback_url_description()}
|
||||
description={logoutCallbackUrlDescription}
|
||||
class="w-full"
|
||||
bind:callbackURLs={$inputs.logoutCallbackURLs.value}
|
||||
bind:error={$inputs.logoutCallbackURLs.error}
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
async function revokeAuthorizedClient(client: OidcClientMetaData) {
|
||||
openConfirmDialog({
|
||||
title: m.revoke_access(),
|
||||
message: m.revoke_access_description({
|
||||
clientName: client.name
|
||||
}),
|
||||
message: {
|
||||
message: m.revoke_access_description,
|
||||
inputs: { clientName: client.name }
|
||||
},
|
||||
confirm: {
|
||||
label: m.revoke(),
|
||||
destructive: true,
|
||||
|
||||
Reference in New Issue
Block a user