This commit is contained in:
Timothy Jaeryang Baek 2026-10-11 19:19:14 +04:00
parent 42dc6f3d09
commit 352e1a12cc
2 changed files with 104 additions and 79 deletions

View file

@ -512,6 +512,7 @@ class Pipe:
{:else}
<LocalizedField
placeholder={$i18n.t('Function Name')}
showControls={false}
bind:value={name}
bind:translations={meta.i18n}
{locale}
@ -562,6 +563,7 @@ class Pipe:
{:else}
<LocalizedField
placeholder={$i18n.t('Function Description')}
showControls={false}
bind:value={meta.description}
bind:translations={meta.i18n}
{locale}

View file

@ -10,6 +10,7 @@
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Search from '$lib/components/icons/Search.svelte';
const i18n = getContext<any>('i18n');
export let value: Record<string, string> = {};
export let source: Record<string, string> = {};
@ -86,32 +87,44 @@
};
</script>
<div class="flex min-h-0 flex-1 flex-col gap-2">
<div class="flex flex-wrap items-center gap-3 text-xs">
<slot name="language" />
<input
bind:this={fileInput}
type="file"
accept="application/json,.json"
hidden
on:change={importFile}
/>
<button type="button" disabled={readOnly} on:click={() => fileInput.click()}
>{$i18n.t('Import')}</button
>
<button type="button" on:click={exportFile}>{$i18n.t('Export')}</button>
{#if allowNewKeys && !readOnly}<Tooltip content={$i18n.t('Add translation')}
><button
class="flex size-6 items-center justify-center"
type="button"
aria-label={$i18n.t('Add translation')}
on:click={() => (adding = !adding)}><Plus className="size-3.5" /></button
></Tooltip
>{/if}
<div class="flex min-h-0 flex-1 flex-col gap-1.5">
<div class="flex min-h-7 flex-wrap items-center justify-between gap-x-2 gap-y-1">
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Translations')}</div>
<div class="ms-auto flex shrink-0 items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<slot name="language" />
<input
bind:this={fileInput}
type="file"
accept="application/json,.json"
hidden
on:change={importFile}
/>
<button
type="button"
class="hover:text-gray-900 disabled:opacity-40 dark:hover:text-gray-100"
disabled={readOnly}
on:click={() => fileInput.click()}>{$i18n.t('Import')}</button
>
<button
type="button"
class="hover:text-gray-900 disabled:opacity-40 dark:hover:text-gray-100"
on:click={exportFile}>{$i18n.t('Export')}</button
>
{#if allowNewKeys && !readOnly}
<Tooltip content={$i18n.t('Add translation')}>
<button
class="flex size-6 items-center justify-center text-gray-400 dark:text-gray-600"
type="button"
aria-label={$i18n.t('Add translation')}
on:click={() => (adding = !adding)}><Plus className="size-3.5" /></button
>
</Tooltip>
{/if}
</div>
</div>
{#if adding && !readOnly}<div class="flex items-center gap-2 text-xs">
<input
class="min-w-0 flex-1 rounded-lg border border-gray-200 bg-transparent px-2 py-1.5 dark:border-gray-800"
class="min-w-0 flex-1 bg-transparent px-2 py-1 text-[0.8125rem] outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"
aria-label={$i18n.t('Translation key')}
placeholder={$i18n.t('Translation key')}
bind:value={newKey}
@ -128,9 +141,10 @@
on:click={addKey}><Plus className="size-4" /></button
>
</div>{/if}
<div class="flex items-center gap-3 text-xs">
<div class="flex items-center gap-2 px-2 py-1 text-xs text-gray-500 dark:text-gray-400">
<Search className="size-3.5 shrink-0 text-gray-500" />
<input
class="min-w-0 flex-1 rounded-lg border border-gray-200 bg-transparent px-2 py-1.5 outline-hidden dark:border-gray-800"
class="min-w-0 w-full bg-transparent py-0.5 text-xs placeholder:text-gray-500 dark:placeholder:text-gray-400 outline-hidden"
bind:value={query}
placeholder={$i18n.t('Search')}
aria-label={$i18n.t('Search translations')}
@ -140,60 +154,69 @@
>
</div>
<div class="min-h-0 flex-1 overflow-y-auto">
<div
class="sticky top-0 z-10 hidden grid-cols-[minmax(0,1fr)_minmax(0,1fr)_1.5rem] gap-3 border-b border-gray-100 bg-white py-2 text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-900 sm:grid"
>
<span>{$i18n.t('Original')}</span><span>{$i18n.t('Translation')}</span>
</div>
{#each keys.slice(page * 50, (page + 1) * 50) as key (key)}
<div
class="grid grid-cols-[minmax(0,1fr)_1.5rem] items-start gap-2 border-b border-gray-100 py-2 dark:border-gray-800 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_1.5rem] sm:gap-3"
>
<div class="col-span-2 min-w-0 text-xs sm:col-span-1">
<div class="break-words">{key}</div>
{#if source[key] && source[key] !== key}<div class="mt-1 break-words text-gray-500">
{source[key]}
</div>{/if}
<div class="flex flex-col gap-1.5">
{#each keys.slice(page * 50, (page + 1) * 50) as key (key)}
<div class="flex items-end gap-2 px-2">
<label class="min-w-0 flex-1 space-y-1">
<Tooltip
as="span"
className="block min-w-0"
content={key}
allowHTML={false}
placement="top-start"
>
<span
class="block whitespace-pre-wrap break-words text-xs text-gray-600 dark:text-gray-400"
>{source[key] || key}</span
>
</Tooltip>
<textarea
rows="1"
readonly={readOnly}
class="block min-h-7 max-h-40 min-w-0 w-full resize-none rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-500 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-400 dark:focus:border-blue-500 [field-sizing:content]"
aria-label={key}
placeholder={$i18n.t('Translation')}
value={value?.[key] ?? ''}
on:input={(event) => update(key, event.currentTarget.value)}
></textarea>
</label>
<div class="mb-0.5 w-6 shrink-0">
{#if !readOnly && Object.hasOwn(value ?? {}, key)}
<Tooltip content={$i18n.t('Use default')}>
<button
type="button"
class="flex size-6 items-center justify-center text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
aria-label={`${$i18n.t('Use default')}: ${key}`}
on:click={() => reset(key)}><ArrowUturnLeft className="size-3.5" /></button
>
</Tooltip>
{/if}
</div>
</div>
<textarea
rows="2"
readonly={readOnly}
class="w-full min-w-0 resize-y rounded-md border border-gray-100 bg-transparent px-2 py-1 text-xs outline-hidden focus:border-gray-400 dark:border-gray-800"
aria-label={key}
placeholder={source[key] || key}
value={value?.[key] ?? ''}
on:input={(event) => update(key, event.currentTarget.value)}
></textarea>
<Tooltip content={$i18n.t('Use default')}
><button
type="button"
class="flex size-6 items-center justify-center disabled:opacity-30"
aria-label={`${$i18n.t('Use default')}: ${key}`}
disabled={readOnly || !Object.hasOwn(value ?? {}, key)}
on:click={() => reset(key)}><ArrowUturnLeft className="size-3.5" /></button
></Tooltip
>
</div>
{/each}
{#if !keys.length}<div class="py-6 text-center text-xs text-gray-500">
{$i18n.t('No results found')}
</div>{/if}
</div>
<div class="flex items-center justify-end gap-3 text-xs text-gray-500">
<span>{page + 1} / {pages}</span>
<button
type="button"
class="flex size-7 items-center justify-center disabled:opacity-30"
aria-label={$i18n.t('Previous')}
disabled={page === 0}
on:click={() => page--}><ChevronLeft className="size-4" /></button
>
<button
type="button"
class="flex size-7 items-center justify-center disabled:opacity-30"
aria-label={$i18n.t('Next')}
disabled={page + 1 >= pages}
on:click={() => page++}><ChevronRight className="size-4" /></button
>
{:else}
<div class="px-2 py-1 text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('No results found')}
</div>
{/each}
</div>
</div>
{#if pages > 1}
<div class="flex items-center justify-end gap-3 text-xs text-gray-500">
<span>{page + 1} / {pages}</span>
<button
type="button"
class="flex size-7 items-center justify-center disabled:opacity-30"
aria-label={$i18n.t('Previous')}
disabled={page === 0}
on:click={() => page--}><ChevronLeft className="size-4" /></button
>
<button
type="button"
class="flex size-7 items-center justify-center disabled:opacity-30"
aria-label={$i18n.t('Next')}
disabled={page + 1 >= pages}
on:click={() => page++}><ChevronRight className="size-4" /></button
>
</div>
{/if}
</div>