This commit is contained in:
Timothy Jaeryang Baek 2026-09-03 20:22:32 -04:00
parent ca26632a2a
commit f677fdbf50
59 changed files with 285 additions and 217 deletions

View file

@ -4,9 +4,11 @@
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, getContext, onMount } from 'svelte';
import XMark from '$lib/components/icons/XMark.svelte';
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher();
export let onClick: Function = () => {};
@ -103,7 +105,7 @@
bind:this={closeButtonElement}
class="absolute -top-0.5 -left-0.5 p-0.5 rounded-full opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-opacity z-10"
on:click|stopPropagation={closeHandler}
aria-label="Dismiss notification"
aria-label={$i18n.t('Dismiss notification')}
>
<XMark className="size-3" />
</button>

View file

@ -14,7 +14,7 @@
export let endDate: number | null = null;
export let onClose: () => void = () => {};
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
type Tab = 'overview' | 'chats';
type ChatSortKey = 'title' | 'updated_at' | 'user_name';
@ -25,7 +25,7 @@
const TIME_RANGES: { key: TimeRange; label: string; days: number }[] = [
{ key: '30d', label: '30D', days: 30 },
{ key: '1y', label: '1Y', days: 365 },
{ key: 'all', label: 'All', days: 0 }
{ key: 'all', label: $i18n.t('All'), days: 0 }
];
let selectedRange: TimeRange = '30d';
let history: Array<{ date: string; won: number; lost: number }> = [];
@ -187,7 +187,7 @@
<button
class="self-center rounded-lg p-1 text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
on:click={close}
aria-label="Close"
aria-label={$i18n.t('Close')}
>
<XMark className={'size-4'} />
</button>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import Modal from '$lib/components/common/Modal.svelte';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import XMark from '$lib/components/icons/XMark.svelte';
import { getFeedbackById } from '$lib/apis/evaluations';
import { toast } from 'svelte-sonner';
@ -49,7 +49,7 @@
<button
class="self-center rounded-lg p-1 text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
on:click={close}
aria-label="Close"
aria-label={$i18n.t('Close')}
>
<XMark className={'size-4'} />
</button>

View file

@ -11,7 +11,7 @@
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let rankedModels = [];
let query = '';
@ -174,7 +174,7 @@
>
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
<tr class="border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
{#each [{ key: 'rating', label: 'RK', class: 'w-3' }, { key: 'name', label: 'Model', class: '' }, { key: 'rating', label: 'Rating', class: 'text-right w-fit' }, { key: 'won', label: 'Won', class: 'text-right w-5' }, { key: 'lost', label: 'Lost', class: 'text-right w-5' }] as col}
{#each [{ key: 'rating', label: 'RK', class: 'w-3' }, { key: 'name', label: $i18n.t('Model'), class: '' }, { key: 'rating', label: $i18n.t('Rating'), class: 'text-right w-fit' }, { key: 'won', label: $i18n.t('Won'), class: 'text-right w-5' }, { key: 'lost', label: $i18n.t('Lost'), class: 'text-right w-5' }] as col}
<th
scope="col"
class="px-2.5 py-2 font-normal cursor-pointer select-none {col.class}"

View file

@ -10,13 +10,13 @@
export let model = null;
export let onClose: () => void = () => {};
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
type TimeRange = '30d' | '1y' | 'all';
const TIME_RANGES: { key: TimeRange; label: string; days: number }[] = [
{ key: '30d', label: '30D', days: 30 },
{ key: '1y', label: '1Y', days: 365 },
{ key: 'all', label: 'All', days: 0 } // 0 = all time, starts from first feedback
{ key: 'all', label: $i18n.t('All'), days: 0 } // 0 = all time, starts from first feedback
];
let selectedRange: TimeRange = '30d';
@ -70,7 +70,7 @@
<button
class="self-center rounded-lg p-1 text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
on:click={close}
aria-label="Close"
aria-label={$i18n.t('Close')}
>
<XMark className={'size-4'} />
</button>

View file

@ -3,7 +3,7 @@
import { getContext } from 'svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let history: Array<{ date: string; won: number; lost: number }> = [];
export let loading = false;
@ -76,7 +76,7 @@
labels,
datasets: [
{
label: 'Won',
label: $i18n.t('Won'),
data: wonData,
backgroundColor: '#5ba3c8',
borderRadius: 2,
@ -84,7 +84,7 @@
categoryPercentage
},
{
label: 'Lost',
label: $i18n.t('Lost'),
data: lostData,
backgroundColor: '#d97c5a',
borderRadius: 2,

View file

@ -159,7 +159,7 @@
bind:value={adminConfig.DEFAULT_GROUP_ID}
placeholder={$i18n.t('Select a group')}
>
<option value={''}>None</option>
<option value={''}>{$i18n.t('None')}</option>
{#each groups as group}
<option value={group.id}>{group.name}</option>
{/each}
@ -707,7 +707,7 @@
>
<input
class={inputClass}
placeholder="* (all domains)"
placeholder={$i18n.t('* (all domains)')}
bind:value={oauthConfig.OAUTH_ALLOWED_DOMAINS}
/>
</AdminSettingField>

View file

@ -205,7 +205,7 @@
<span class="capitalize text-black dark:text-white"
>{$config?.license_metadata?.type} license</span
>
registered to
{$i18n.t('registered to')}
<span class="capitalize text-black dark:text-white"
>{$config?.license_metadata?.organization_name}</span
>

View file

@ -437,7 +437,7 @@
<button
class="shrink-0 text-gray-400 transition-colors hover:text-gray-900 dark:text-gray-600 dark:hover:text-white"
type="button"
aria-label="verify connection"
aria-label={$i18n.t('verify connection')}
on:click={async () => {
await updateConfigHandler();
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
@ -503,7 +503,7 @@
<button
class="shrink-0 text-gray-400 transition-colors hover:text-gray-900 dark:text-gray-600 dark:hover:text-white"
type="button"
aria-label="verify connection"
aria-label={$i18n.t('verify connection')}
on:click={async () => {
await updateConfigHandler();
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
@ -801,7 +801,7 @@
<button
class="shrink-0 text-gray-400 transition-colors hover:text-gray-900 dark:text-gray-600 dark:hover:text-white"
type="button"
aria-label="verify connection"
aria-label={$i18n.t('verify connection')}
on:click={async () => {
await updateConfigHandler();
const res = await verifyConfigUrl(localStorage.token).catch((error) => {

View file

@ -5,7 +5,7 @@
const { saveAs } = fileSaver;
import { onMount, onDestroy, getContext, tick } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import {
config,
@ -966,7 +966,7 @@
>
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt="modelfile profile"
alt={$i18n.t('modelfile profile')}
class=" rounded-xl size-7 object-cover"
loading="lazy"
decoding="async"

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config, settings } from '$lib/stores';
import { splitStream } from '$lib/utils';
@ -623,7 +623,7 @@
</div>
<div>
<Tooltip content="Update All Models" placement="top">
<Tooltip content={$i18n.t('Update All Models')} placement="top">
<button
class="flex gap-2 items-center bg-transparent rounded-lg transition"
aria-label={$i18n.t('Update All Models')}
@ -726,7 +726,10 @@
{#if updateModelId}
<div class="text-xs flex justify-between items-center">
<div>Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}</div>
<div>
{$i18n.t('Updating "{{modelId}}"', { modelId: updateModelId })}
{updateProgress ? `(${updateProgress}%)` : ''}
</div>
<Tooltip content={$i18n.t('Cancel')}>
<button

View file

@ -306,7 +306,7 @@
}}
>
{#if pipelineFiles}
{pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected
{$i18n.t('{{count}} pipeline(s) selected', { count: pipelineFiles.length })}
{:else}
{$i18n.t('Select a .py file')}
{/if}

View file

@ -35,7 +35,7 @@
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
import UserPreviewModal from '$lib/components/admin/UserPreviewModal.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let page = 1;
@ -166,9 +166,10 @@
className="mx-0"
banner={{
type: 'error',
title: 'License Error',
content:
title: $i18n.t('License Error'),
content: $i18n.t(
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
)
}}
/>
</div>

View file

@ -12,7 +12,7 @@
import XMark from '$lib/components/icons/XMark.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
@ -275,7 +275,7 @@
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{$i18n.t('{{count}} document(s) selected.', { count: inputFiles.length })}
{:else}
{$i18n.t('Click here to select a csv file.')}
{/if}

View file

@ -1,13 +1,16 @@
<script lang="ts">
import { getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
const i18n: any = getContext('i18n');
let selected = '';
</script>
<nav
aria-label="App navigation"
aria-label={$i18n.t('App navigation')}
class="min-w-[4.5rem] bg-gray-50 dark:bg-gray-950 flex gap-2.5 flex-col pt-8"
>
<div class="flex justify-center relative">
@ -17,9 +20,9 @@
</div>
{/if}
<Tooltip content="Home" placement="right">
<Tooltip content={$i18n.t('Home')} placement="right">
<button
aria-label="Home"
aria-label={$i18n.t('Home')}
class=" cursor-pointer {selected === 'home' ? 'rounded-2xl' : 'rounded-full'}"
on:click={() => {
selected = 'home';
@ -51,7 +54,7 @@
</div>
{/if}
<button
aria-label="Chat"
aria-label={$i18n.t('Chat')}
class=" cursor-pointer bg-transparent"
on:click={() => {
selected = '';

View file

@ -4,7 +4,7 @@
import { tick, getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import { config, mobile, settings, socket, user } from '$lib/stores';
import {
@ -997,7 +997,7 @@
id="input-menu-button"
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-[1.875rem] flex justify-center items-center outline-hidden focus:outline-hidden shrink-0"
type="button"
aria-label="More"
aria-label={$i18n.t('More')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -1048,7 +1048,7 @@
toast.error($i18n.t('Permission denied when accessing microphone'));
}
}}
aria-label="Voice Input"
aria-label={$i18n.t('Voice Input')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -20,7 +20,7 @@
import Pin from '../icons/Pin.svelte';
import PinnedMessagesModal from './PinnedMessagesModal.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let showChannelPinnedMessagesModal = false;
let showChannelInfoModal = false;
@ -159,7 +159,7 @@
<Tooltip content={$i18n.t('Pinned Messages')}>
<button
class=" flex cursor-pointer py-1.5 px-1.5 border dark:border-gray-850 border-gray-50 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
aria-label="Pinned Messages"
aria-label={$i18n.t('Pinned Messages')}
type="button"
on:click={() => {
showChannelPinnedMessagesModal = true;
@ -175,7 +175,7 @@
<Tooltip content={$i18n.t('Users')}>
<button
class=" flex cursor-pointer shrink-0 py-1 px-1.5 border dark:border-gray-850 border-gray-50 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
aria-label="User Count"
aria-label={$i18n.t('User Count')}
type="button"
on:click={() => {
showChannelInfoModal = true;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext, createEventDispatcher } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher();
import {
@ -245,7 +245,7 @@
{#if contents[selectedContentIdx].type === 'iframe'}
<iframe
bind:this={iframeElement}
title="Content"
title={$i18n.t('Content')}
srcdoc={injectCsp(
contents[selectedContentIdx].content,
$config?.ui?.iframe_csp ?? ''

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { getContext } from 'svelte';
import { embed, showControls, showEmbeds } from '$lib/stores';
import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte';
@ -6,6 +7,8 @@
export let overlay = false;
const i18n: any = getContext('i18n');
const getSrcUrl = (url: string, chatId?: string, messageId?: string, sourceId: string) => {
try {
const parsed = new URL(url);
@ -57,7 +60,7 @@
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
aria-label="Close embed"
aria-label={$i18n.t('Close embed')}
on:click={() => {
showControls.set(false);
showEmbeds.set(false);

View file

@ -13,7 +13,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let modelIds = [];
export let models = [];
@ -112,7 +112,7 @@
</div>
{#if models[selectedModelIdx]?.info?.meta?.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
By
{$i18n.t('By')}
{#if models[selectedModelIdx]?.info?.meta?.user.community}
<a
href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user

View file

@ -58,7 +58,7 @@
import PortPreview from './FileNav/PortPreview.svelte';
import XTerminal from './XTerminal.svelte';
const i18n: any = getContext('i18n');
const i18n = getContext('i18n');
export let overlay = false;
export let chatId: string | null = null;
@ -937,7 +937,9 @@
}
} catch (e) {
console.error('Failed to render Office file:', e);
fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`;
fileContent = $i18n.t('Error previewing file: {{error}}', {
error: e instanceof Error ? e.message : $i18n.t('Unknown error')
});
}
} else {
fileContent = await readFile(terminal.url, terminal.key, filePath, chatId ?? undefined);

View file

@ -318,7 +318,7 @@
</span>
{/if}
{#if !writable && !renaming}
<span class="text-[0.625rem] text-gray-400 shrink-0">Read-only</span>
<span class="text-[0.625rem] text-gray-400 shrink-0">{$i18n.t('Read-only')}</span>
{/if}
{#if entry.type === 'file' && entry.size !== undefined && !renaming}
{#if showDate && entry.modified}

View file

@ -135,7 +135,9 @@
{/if}
</div>
{#if !writable}
<span class="text-[0.625rem] text-gray-400 dark:text-gray-500 shrink-0"> Read-only </span>
<span class="text-[0.625rem] text-gray-400 dark:text-gray-500 shrink-0">
{$i18n.t('Read-only')}
</span>
{/if}
<Tooltip content={$i18n.t('Refresh')}>

View file

@ -19,7 +19,7 @@
let pdfPagesPreviewRef: PdfPagesPreview;
let fileCodeEditorRef: FileCodeEditor;
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let selectedFile: string | null = null;
export let fileLoading = false;
@ -242,7 +242,7 @@
jsonError = null;
} catch (e) {
parsedJson = undefined;
jsonError = e instanceof Error ? e.message : 'Invalid JSON';
jsonError = e instanceof Error ? e.message : $i18n.t('Invalid JSON');
}
} else {
parsedJson = undefined;
@ -393,7 +393,7 @@
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
class="w-full h-full border-none bg-white"
title="HTML Preview"
title={$i18n.t('HTML Preview')}
/>
{:else if isHtml && !showRaw}
{#if overlay}
@ -409,7 +409,7 @@
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
class="w-full h-full border-none bg-white"
title="HTML Preview"
title={$i18n.t('HTML Preview')}
/>
{:else if isHtml && showRaw}
<div class="absolute inset-0">
@ -470,7 +470,9 @@
</div>
{:else if isJson && !showRaw && jsonError}
<div class="p-3 text-xs">
<div class="text-red-500 mb-2">JSON parse error: {jsonError}</div>
<div class="text-red-500 mb-2">
{$i18n.t('JSON parse error: {{error}}', { error: jsonError })}
</div>
<pre
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed">{fileContent}</pre>
</div>

View file

@ -12,7 +12,7 @@
import Tooltip from '../../common/Tooltip.svelte';
import CellEditor from './CellEditor.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let notebook: Record<string, unknown>;
export let filePath: string = '';
@ -309,7 +309,7 @@
class="nb-run-btn"
on:click={() => runCell(i)}
disabled={runningCell !== null}
title="Run cell (⌘+Enter)"
title={$i18n.t('Run cell (⌘+Enter)')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -2,7 +2,7 @@
import { getContext, onDestroy } from 'svelte';
import Spinner from '../../common/Spinner.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let data: ArrayBuffer;
@ -46,7 +46,7 @@
loading = false;
} catch (e) {
loading = false;
error = e instanceof Error ? e.message : 'Failed to open database';
error = e instanceof Error ? e.message : $i18n.t('Failed to open database');
}
};
@ -82,7 +82,7 @@
rows = [];
}
} catch (e) {
error = e instanceof Error ? e.message : 'Query failed';
error = e instanceof Error ? e.message : $i18n.t('Query failed');
}
};
@ -99,7 +99,7 @@
queryRows = [];
}
} catch (e) {
queryError = e instanceof Error ? e.message : 'Query failed';
queryError = e instanceof Error ? e.message : $i18n.t('Query failed');
queryColumns = [];
queryRows = [];
}
@ -275,7 +275,13 @@
/>
</svg>
</button>
<span>{page + 1} / {totalPages} ({totalRows.toLocaleString()} rows)</span>
<span>
{$i18n.t('{{page}} / {{totalPages}} ({{rows}} rows)', {
page: page + 1,
totalPages,
rows: totalRows.toLocaleString()
})}
</span>
<button
aria-label={$i18n.t('Next page')}
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"

View file

@ -113,7 +113,7 @@
import QueuedMessageItem from './MessageInput/QueuedMessageItem.svelte';
import TaskList from './Messages/ResponseMessage/TaskList.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
type AskUserPrompt = {
show: boolean;
@ -1780,7 +1780,7 @@
<div class="mx-1 rounded-2xl bg-white text-xs dark:bg-gray-900">
<div class="flex items-center justify-between px-3 py-1.5">
<div class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300">
<span>Status</span>
<span>{$i18n.t('Status')}</span>
</div>
<button
@ -1790,14 +1790,14 @@
showStatusPanel = false;
}}
>
Close
{$i18n.t('Close')}
</button>
</div>
<div class="space-y-0.5 px-3 pb-2">
<div class="rounded-xl py-0.5 text-gray-600 dark:text-gray-400">
<div class="flex min-h-4 items-center gap-3">
<span class="min-w-0 flex-1 truncate">Context usage</span>
<span class="min-w-0 flex-1 truncate">{$i18n.t('Context usage')}</span>
<span
class="shrink-0 font-mono text-[0.625rem] text-gray-400 dark:text-gray-600"
>
@ -1818,7 +1818,7 @@
{#if messageQueue.length}
<div class="flex min-h-5 items-center gap-3 text-gray-600 dark:text-gray-400">
<span class="min-w-0 flex-1 truncate">Queued messages</span>
<span class="min-w-0 flex-1 truncate">{$i18n.t('Queued messages')}</span>
<span class="font-mono text-[0.625rem] text-gray-400 dark:text-gray-600">
{messageQueue.length}
</span>
@ -1827,7 +1827,7 @@
{#if chatTasks.length}
<div class="flex min-h-5 items-center gap-3 text-gray-600 dark:text-gray-400">
<span class="min-w-0 flex-1 truncate">Tasks</span>
<span class="min-w-0 flex-1 truncate">{$i18n.t('Tasks')}</span>
<span class="font-mono text-[0.625rem] text-gray-400 dark:text-gray-600">
{chatTasks.length}
</span>
@ -1835,7 +1835,7 @@
{/if}
<div class="flex min-h-5 items-center gap-3 text-gray-600 dark:text-gray-400">
<span class="min-w-0 flex-1 truncate">Chat ID</span>
<span class="min-w-0 flex-1 truncate">{$i18n.t('Chat ID')}</span>
{#if chatId}
<button
type="button"
@ -1869,7 +1869,7 @@
<div class="flex items-center justify-between w-full">
<div class="pl-[0.0625rem] flex items-center gap-2 text-sm dark:text-gray-500">
<img
alt="model profile"
alt={$i18n.t('model profile')}
class="size-3.5 max-w-[1.75rem] object-cover rounded-full"
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${$models.find((model) => model.id === atSelectedModel.id).id}&lang=${$i18n.language}`}
/>
@ -1991,7 +1991,7 @@
<button
type="button"
class="absolute top-2.5 right-3 z-20 p-1 rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-800/50"
aria-label="Expand input"
aria-label={$i18n.t('Expand input')}
on:click={() => {
showInputModal = true;
}}
@ -2330,7 +2330,7 @@
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label="Available Tools"
aria-label={$i18n.t('Available Tools')}
type="button"
on:click={() => {
showTools = !showTools;
@ -2353,7 +2353,7 @@
>
<button
class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
aria-label="Available Skills"
aria-label={$i18n.t('Available Skills')}
type="button"
on:click={() => {
showSkills = !showSkills;
@ -2607,7 +2607,7 @@
toast.error($i18n.t('Permission denied when accessing microphone'));
}
}}
aria-label="Voice Input"
aria-label={$i18n.t('Voice Input')}
>
<Mic className="size-[1.125rem]" />
</button>

View file

@ -15,7 +15,7 @@
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let eventTarget: EventTarget;
export let submitPrompt: Function;
@ -57,7 +57,7 @@
...videoInputDevices,
{
deviceId: 'screen',
label: 'Screen Share'
label: $i18n.t('Screen Share')
}
];
}

View file

@ -9,7 +9,7 @@
import Knobs from '$lib/components/icons/Knobs.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let query = '';
export let onSelect = (e) => {};
@ -142,10 +142,10 @@
{#each commandItems as item, commandIdx}
{#if item.data.id === 'temporary'}
<Tooltip content="Toggle temporary chat for this new chat." placement="top">
<Tooltip content={$i18n.t('Toggle temporary chat for this new chat.')} placement="top">
<button
type="button"
aria-label="Temporary: toggle temporary chat for this new chat."
aria-label={$i18n.t('Temporary: toggle temporary chat for this new chat.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''}"
on:mousedown={(e) => e.preventDefault()}
@ -166,18 +166,21 @@
{/if}
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Temporary</span>
<span class="truncate">{$i18n.t('Temporary')}</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">
{temporaryEnabled ? 'On' : 'Off'}
{temporaryEnabled ? $i18n.t('On') : $i18n.t('Off')}
</span>
</span>
</button>
</Tooltip>
{:else if item.data.id === 'compact'}
<Tooltip content="Shorten older messages so this chat can keep going." placement="top">
<Tooltip
content={$i18n.t('Shorten older messages so this chat can keep going.')}
placement="top"
>
<button
type="button"
aria-label="Compact: shorten older messages so this chat can keep going."
aria-label={$i18n.t('Compact: shorten older messages so this chat can keep going.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50"
disabled={compactDisabled}
@ -220,20 +223,20 @@
{/if}
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Compact</span>
<span class="truncate">{$i18n.t('Compact')}</span>
{#if contextHasThreshold}
<span class="app-muted text-[0.625rem] truncate shrink-0">
{contextCirclePercent}% full
{$i18n.t('{{percent}}% full', { percent: contextCirclePercent })}
</span>
{/if}
</span>
</button>
</Tooltip>
{:else if item.data.id === 'fork'}
<Tooltip content="Fork the current chat branch into a new chat." placement="top">
<Tooltip content={$i18n.t('Fork the current chat branch into a new chat.')} placement="top">
<button
type="button"
aria-label="Fork: fork the current chat branch into a new chat."
aria-label={$i18n.t('Fork: fork the current chat branch into a new chat.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50"
disabled={forkDisabled}
@ -268,16 +271,18 @@
</svg>
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Fork</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">Current branch</span>
<span class="truncate">{$i18n.t('Fork')}</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">
{$i18n.t('Current branch')}
</span>
</span>
</button>
</Tooltip>
{:else if item.data.id === 'status'}
<Tooltip content="Check what is running in this chat." placement="top">
<Tooltip content={$i18n.t('Check what is running in this chat.')} placement="top">
<button
type="button"
aria-label="Status: check what is running in this chat."
aria-label={$i18n.t('Status: check what is running in this chat.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''}"
on:mousedown={(e) => e.preventDefault()}
@ -306,18 +311,18 @@
</svg>
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Status</span>
<span class="truncate">{$i18n.t('Status')}</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">
Check what is running in this chat.
{$i18n.t('Check what is running in this chat.')}
</span>
</span>
</button>
</Tooltip>
{:else if item.data.id === 'model'}
<Tooltip content="Show or switch the current model." placement="top">
<Tooltip content={$i18n.t('Show or switch the current model.')} placement="top">
<button
type="button"
aria-label="Model: show or switch the current model."
aria-label={$i18n.t('Model: show or switch the current model.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''}"
on:mousedown={(e) => e.preventDefault()}
@ -334,16 +339,16 @@
<Sparkles className="size-3.5" />
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Model</span>
<span class="truncate">{$i18n.t('Model')}</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">/model</span>
</span>
</button>
</Tooltip>
{:else if item.data.id === 'settings'}
<Tooltip content="Open settings." placement="top">
<Tooltip content={$i18n.t('Open settings.')} placement="top">
<button
type="button"
aria-label="Settings: open settings."
aria-label={$i18n.t('Settings: open settings.')}
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''}"
on:mousedown={(e) => e.preventDefault()}
@ -360,7 +365,7 @@
<Knobs className="size-3.5" />
</span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Settings</span>
<span class="truncate">{$i18n.t('Settings')}</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">/settings</span>
</span>
</button>

View file

@ -1,6 +1,9 @@
<script lang="ts">
import { getContext } from 'svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
const i18n: any = getContext('i18n');
export let show = false;
</script>
@ -9,7 +12,7 @@
class="absolute inset-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
aria-label={$i18n.t('Drag and Drop Container')}
>
<div
class="absolute w-full h-full backdrop-blur-sm bg-gray-100/50 dark:bg-gray-900/80 flex justify-center"

View file

@ -28,7 +28,7 @@
import AttachWebpageModal from './AttachWebpageModal.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let files = [];
@ -54,13 +54,13 @@
const toolApprovalModes = [
{
value: 'full',
label: 'Full access',
description: 'Run tools without asking for approval.'
label: $i18n.t('Full access'),
description: $i18n.t('Run tools without asking for approval.')
},
{
value: 'ask',
label: 'Ask for approval',
description: 'Stop before each tool call until you allow or deny it.'
label: $i18n.t('Ask for approval'),
description: $i18n.t('Stop before each tool call until you allow or deny it.')
}
];

View file

@ -1,10 +1,13 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import type { Token } from 'marked';
import { getContext } from 'svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { settings } from '$lib/stores';
const i18n: any = getContext('i18n');
export let id: string;
export let token: Token;
@ -23,7 +26,7 @@
<video
class="w-full my-2"
src={videoSrc.replaceAll('&amp;', '&')}
title="Video player"
title={$i18n.t('Video player')}
frameborder="0"
referrerpolicy="strict-origin-when-cross-origin"
controls
@ -40,7 +43,7 @@
<audio
class="w-full my-2"
src={audioSrc.replaceAll('&amp;', '&')}
title="Audio player"
title={$i18n.t('Audio player')}
controls
></audio>
{:else}
@ -55,7 +58,7 @@
<iframe
class="w-full aspect-video my-2"
src={`https://www.youtube.com/embed/${ytId}`}
title="YouTube video player"
title={$i18n.t('YouTube video player')}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
@ -70,7 +73,7 @@
<iframe
class="w-full my-2"
src={iframeSrc}
title="Embedded content"
title={$i18n.t('Embedded content')}
frameborder="0"
sandbox
on:load={(e) => {
@ -107,7 +110,7 @@
<iframe
class="w-full my-2"
src={`${WEBUI_BASE_URL}/api/v1/files/${fileId}/content/html`}
title="Content"
title={$i18n.t('Content')}
frameborder="0"
sandbox="{($settings?.iframeSandboxAllowScripts ?? true)
? 'allow-scripts'

View file

@ -1498,9 +1498,9 @@
{/each}
{#if message.done && !readOnly && forkHandler && ($user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true))}
<Tooltip content="Fork chat" placement="bottom">
<Tooltip content={$i18n.t('Fork chat')} placement="bottom">
<button
aria-label="Fork chat"
aria-label={$i18n.t('Fork chat')}
class="{isLastMessage || ($settings?.highContrastMode ?? false)
? 'visible'
: 'hover-reveal'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"

View file

@ -48,7 +48,7 @@
import ModelItem from './ModelItem.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher();
export let id = '';
@ -1296,8 +1296,10 @@
>
<Spinner className="size-3 shrink-0 text-gray-400 dark:text-gray-500" />
<div class="min-w-0 flex-1 truncate">
Downloading "{downloadName}"{download?.providerLabel
? ` from ${download.providerLabel}`
{$i18n.t('Downloading "{{name}}"', {
name: downloadName
})}{download?.providerLabel
? ` ${$i18n.t('from {{provider}}', { provider: download.providerLabel })}`
: ''}
</div>
{#if 'pullProgress' in download}

View file

@ -37,7 +37,7 @@
import Knobs from '../icons/Knobs.svelte';
import { isTemporaryChatId } from '$lib/utils/chatId';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let initNewChat: Function;
export let readOnly: boolean = false;
@ -75,7 +75,7 @@
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
aria-label={$i18n.t('New Chat')}
/>
<nav
@ -223,7 +223,7 @@
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
aria-label={$i18n.t('New Chat')}
>
<ChatPlus className="size-4.5" strokeWidth="1.5" />
</button>
@ -237,7 +237,7 @@
on:click={async () => {
await showControls.set(!$showControls);
}}
aria-label="Controls"
aria-label={$i18n.t('Controls')}
>
<Knobs className="size-5" strokeWidth="1" />
</button>
@ -264,7 +264,7 @@
<Banner
banner={{
type: 'info',
title: 'Trial License',
title: $i18n.t('Trial License'),
content: $i18n.t(
'You are currently using a trial license. Please contact support to upgrade your license.'
)
@ -276,7 +276,7 @@
<Banner
banner={{
type: 'error',
title: 'License Error',
title: $i18n.t('License Error'),
content: $i18n.t(
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
)

View file

@ -56,10 +56,16 @@
<PinSlash />
{/if}
</ControlButton>
<ControlButton on:click={() => setLayoutDirection('vertical')} title="Vertical Layout">
<ControlButton
on:click={() => setLayoutDirection('vertical')}
title={$i18n.t('Vertical Layout')}
>
<AlignVertical className="size-4" />
</ControlButton>
<ControlButton on:click={() => setLayoutDirection('horizontal')} title="Horizontal Layout">
<ControlButton
on:click={() => setLayoutDirection('horizontal')}
title={$i18n.t('Horizontal Layout')}
>
<AlignHorizontal className="size-4" />
</ControlButton>
</Controls>

View file

@ -28,7 +28,7 @@
import FolderPlaceholder from './Placeholder/FolderPlaceholder.svelte';
import FolderTitle from './Placeholder/FolderTitle.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let createMessagePair: Function;
export let stopResponse: Function;
@ -209,7 +209,7 @@
{#if models[selectedModelIdx]?.info?.meta?.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
By
{$i18n.t('By')}
{#if models[selectedModelIdx]?.info?.meta?.user.community}
<a
href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user

View file

@ -10,7 +10,7 @@
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSection from './UserSettingSection.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let ollamaVersion = '';
@ -120,7 +120,8 @@
<span>{$WEBUI_NAME}</span> -
{/if}
<span class="capitalize">{$config?.license_metadata?.type}</span> license purchased by
<span class="capitalize">{$config?.license_metadata?.type}</span>
{$i18n.t('license purchased by')}
<span class="capitalize">{$config?.license_metadata?.organization_name}</span>
</div>
{:else}
@ -144,8 +145,10 @@
{/if}
<div class="text-xs text-gray-400 dark:text-gray-500">
Emoji graphics provided by
<a href="https://github.com/jdecked/twemoji" target="_blank">Twemoji</a>, licensed under
{$i18n.t('Emoji graphics provided by')}
<a href="https://github.com/jdecked/twemoji" target="_blank">Twemoji</a>, {$i18n.t(
'licensed under'
)}
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC-BY 4.0</a>.
</div>
@ -153,10 +156,11 @@
<!-- LICENSE covers this Open WebUI copyright attribution.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
https://docs.openwebui.com/license. -->
Copyright (c) {new Date().getFullYear()}
{$i18n.t('Copyright (c)')}
{new Date().getFullYear()}
<a href="https://openwebui.com" target="_blank" class="underline">Open WebUI Inc.</a>
<a href="https://github.com/open-webui/open-webui/blob/main/LICENSE" target="_blank"
>All rights reserved.</a
>{$i18n.t('All rights reserved.')}</a
>
</div>

View file

@ -5,7 +5,7 @@
import { config, models, settings, theme, user } from '$lib/stores';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import AdvancedParams from './Advanced/AdvancedParams.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
@ -240,7 +240,7 @@
</UserSettingRow>
{#if $i18n.language === 'en-US' && !($config?.license_metadata ?? false)}
<div class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
Couldn't find your language?
{$i18n.t("Couldn't find your language?")}
<a
class="font-normal underline text-gray-400 dark:text-gray-600"
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
@ -249,7 +249,7 @@
<!-- LICENSE covers this Open WebUI wordmark.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
https://docs.openwebui.com/license. -->
Help us translate Open WebUI!
{$i18n.t('Help us translate Open WebUI!')}
</a>
</div>
{/if}

View file

@ -28,10 +28,14 @@
let events: { event: string; label: string; description?: string }[] = [
{
event: 'chat.finished',
label: 'Chat finished',
description: 'A chat run finished successfully.'
label: $i18n.t('Chat finished'),
description: $i18n.t('A chat run finished successfully.')
},
{ event: 'chat.failed', label: 'Chat failed', description: 'A chat run failed.' }
{
event: 'chat.failed',
label: $i18n.t('Chat failed'),
description: $i18n.t('A chat run failed.')
}
];
let loadingTargets = false;
let savingTarget = false;

View file

@ -123,9 +123,9 @@
];
const heatmapModes: Array<{ value: HeatmapMode; label: string }> = [
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'cumulative', label: 'Cumulative' }
{ value: 'daily', label: $i18n.t('Daily') },
{ value: 'weekly', label: $i18n.t('Weekly') },
{ value: 'cumulative', label: $i18n.t('Cumulative') }
];
const loadUsage = async () => {

View file

@ -183,7 +183,7 @@
const allSettings: SettingsTab[] = [
{
id: 'general',
title: 'General',
title: $i18n.t('General'),
keywords: [
'advancedparams',
'advancedparameters',
@ -214,7 +214,7 @@
},
{
id: 'interface',
title: 'Interface',
title: $i18n.t('Interface'),
keywords: [
'allow user location',
'allow voice interruption in call',
@ -350,7 +350,7 @@
},
{
id: 'notifications',
title: 'Notifications',
title: $i18n.t('Notifications'),
keywords: [
'browser notifications',
'browsernotifications',
@ -366,7 +366,7 @@
},
{
id: 'shortcuts',
title: 'Keyboard',
title: $i18n.t('Keyboard'),
keywords: [
'commands',
'hotkeys',
@ -381,7 +381,7 @@
},
{
id: 'connections',
title: 'Connections',
title: $i18n.t('Connections'),
keywords: [
'addconnection',
'add connection',
@ -394,7 +394,7 @@
},
{
id: 'tools',
title: 'Integrations',
title: $i18n.t('Integrations'),
keywords: [
'addconnection',
'add connection',
@ -412,7 +412,7 @@
{
id: 'personalization',
title: 'Personalization',
title: $i18n.t('Personalization'),
keywords: [
'account preferences',
'account settings',
@ -434,7 +434,7 @@
},
{
id: 'audio',
title: 'Audio',
title: $i18n.t('Audio'),
keywords: [
'audio config',
'audio control',
@ -510,7 +510,7 @@
},
{
id: 'data_controls',
title: 'Data Controls',
title: $i18n.t('Data Controls'),
keywords: [
'archive all chats',
'archive chats',
@ -545,7 +545,7 @@
},
{
id: 'usage',
title: 'Usage',
title: $i18n.t('Usage'),
keywords: [
'activity',
'activity heatmap',
@ -563,7 +563,7 @@
},
{
id: 'archived_chats',
title: 'Archived Chats',
title: $i18n.t('Archived Chats'),
keywords: [
'archive',
'archive chat',
@ -582,7 +582,7 @@
},
{
id: 'account',
title: 'Account',
title: $i18n.t('Account'),
keywords: [
'account preferences',
'account settings',
@ -632,7 +632,7 @@
},
{
id: 'about',
title: 'About',
title: $i18n.t('About'),
keywords: [
'about app',
'about me',
@ -683,12 +683,12 @@
const adminSettings: SettingsTab[] = [
{
id: 'admin:general',
title: 'General',
title: $i18n.t('General'),
keywords: ['general', 'admin', 'settings', 'version', 'update', 'community', 'channels']
},
{
id: 'admin:authentication',
title: 'Authentication',
title: $i18n.t('Authentication'),
keywords: [
'authentication',
'auth',
@ -703,7 +703,7 @@
},
{
id: 'admin:connections',
title: 'Connections',
title: $i18n.t('Connections'),
keywords: [
'connections',
'ollama',
@ -716,7 +716,7 @@
},
{
id: 'admin:models',
title: 'Models',
title: $i18n.t('Models'),
keywords: [
'models',
'pull',
@ -731,63 +731,63 @@
},
{
id: 'admin:subagents',
title: 'Sub-agents',
title: $i18n.t('Sub-agents'),
keywords: ['sub-agents', 'subagents', 'delegation', 'background', 'agents']
},
{
id: 'admin:interface',
title: 'Interface',
title: $i18n.t('Interface'),
keywords: ['interface', 'ui', 'appearance', 'banners', 'tasks', 'prompt suggestions', 'tags']
},
{
id: 'admin:audio',
title: 'Audio',
title: $i18n.t('Audio'),
keywords: ['audio', 'voice', 'speech', 'tts', 'stt', 'whisper', 'deepgram', 'azure']
},
{
id: 'admin:images',
title: 'Images',
title: $i18n.t('Images'),
keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111']
},
{
id: 'admin:evaluations',
title: 'Evaluations',
title: $i18n.t('Evaluations'),
keywords: ['evaluations', 'feedback', 'rating', 'arena', 'leaderboard', 'preference']
},
{
id: 'admin:analytics',
title: 'Analytics',
title: $i18n.t('Analytics'),
keywords: ['analytics', 'usage', 'stats', 'dashboard', 'models', 'users', 'messages']
},
{
id: 'admin:integrations',
title: 'Integrations',
title: $i18n.t('Integrations'),
keywords: ['tools', 'integrations', 'plugins', 'extensions', 'functions', 'openapi', 'server']
},
{
id: 'admin:documents',
title: 'Documents',
title: $i18n.t('Documents'),
keywords: ['documents', 'files', 'rag', 'knowledge', 'upload', 'embedding', 'vector db']
},
{
id: 'admin:web',
title: 'Web Search',
title: $i18n.t('Web Search'),
keywords: ['web search', 'google', 'bing', 'duckduckgo', 'serp', 'searxng', 'tavily', 'exa']
},
{
id: 'admin:code-execution',
title: 'Code Execution',
title: $i18n.t('Code Execution'),
keywords: ['code execution', 'python', 'sandbox', 'compiler', 'jupyter', 'interpreter']
},
{
id: 'admin:pipelines',
title: 'Pipelines',
title: $i18n.t('Pipelines'),
keywords: ['pipelines', 'workflows', 'filters', 'valves', 'middleware']
},
{
id: 'admin:db',
title: 'Database',
title: $i18n.t('Database'),
keywords: ['database', 'export', 'import', 'backup', 'chats', 'users']
}
];
@ -1189,7 +1189,7 @@
}}
>
<AdminTabIcon id={adminTabSegment(tab.id)} className="size-3.5" strokeWidth="2" />
<span>{$i18n.t(tab.title)}</span>
<span>{tab.title}</span>
</button>
{/if}
{/each}

View file

@ -4,7 +4,7 @@
import CodeEditor from './CodeEditor.svelte';
import Drawer from './Drawer.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let {
show = $bindable(),
@ -36,7 +36,7 @@
</div>
<button
class="self-center"
aria-label="Close"
aria-label={$i18n.t('Close')}
onclick={() => {
show = false;
}}

View file

@ -205,7 +205,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
on:click={zoomOut}
aria-label="Zoom out"
aria-label={$i18n.t('Zoom out')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -224,7 +224,7 @@
type="button"
class="shrink-0 min-w-12 h-7 px-1.5 py-1 text-center text-[0.6875rem] font-normal text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition tabular-nums"
on:click={resetView}
aria-label="Reset zoom"
aria-label={$i18n.t('Reset zoom')}
>
{Math.round(zoomLevel * 100)}%
</button>
@ -232,7 +232,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
on:click={zoomIn}
aria-label="Zoom in"
aria-label={$i18n.t('Zoom in')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -13,7 +13,7 @@
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
const CONTENT_PREVIEW_LIMIT = 10000;
let expandedContent = false;
@ -545,7 +545,7 @@
{@html excelHtml}
</div>
{:else}
<div class="text-gray-500 text-sm p-4">No content available</div>
<div class="text-gray-500 text-sm p-4">{$i18n.t('No content available')}</div>
{/if}
{/if}
{:else if isCode}
@ -571,7 +571,7 @@
{:else if docxData}
<DocxPreview data={docxData} className="h-[60vh]" />
{:else}
<div class="text-gray-500 text-sm p-4">No content available</div>
<div class="text-gray-500 text-sm p-4">{$i18n.t('No content available')}</div>
{/if}
{:else if isPptx}
{#if pptxError}
@ -583,7 +583,7 @@
className="h-[60vh]"
/>
{:else}
<div class="text-gray-500 text-sm p-4">No content available</div>
<div class="text-gray-500 text-sm p-4">{$i18n.t('No content available')}</div>
{/if}
{:else}
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">

View file

@ -5,7 +5,7 @@
import Drawer from './Drawer.svelte';
import RichTextInput from './RichTextInput.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let id = 'input-modal';
@ -32,7 +32,7 @@
</div>
<button
class="self-center"
aria-label="Close"
aria-label={$i18n.t('Close')}
onclick={() => {
show = false;
onClose();

View file

@ -1,10 +1,12 @@
<script lang="ts">
import { onMount, onDestroy, tick } from 'svelte';
import { getContext, onMount, onDestroy, tick } from 'svelte';
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
import panzoom, { type PanZoom } from 'panzoom';
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
import Spinner from './Spinner.svelte';
const i18n: any = getContext('i18n');
export let url: string | null = null;
export let data: ArrayBuffer | Uint8Array | null = null;
export let className = 'w-full h-[70vh]';
@ -548,7 +550,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 pointer-coarse:hidden"
on:click={zoomOut}
aria-label="Zoom out"
aria-label={$i18n.t('Zoom out')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -567,7 +569,7 @@
type="button"
class="shrink-0 min-w-12 h-7 px-1.5 py-1 text-center text-[0.6875rem] font-normal text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition tabular-nums"
on:click={resetView}
aria-label="Reset zoom"
aria-label={$i18n.t('Reset zoom')}
>
{Math.round(zoomLevel * 100)}%
</button>
@ -575,7 +577,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 pointer-coarse:hidden"
on:click={zoomIn}
aria-label="Zoom in"
aria-label={$i18n.t('Zoom in')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -1,8 +1,10 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte';
import { getContext, onDestroy, onMount, tick } from 'svelte';
import panzoom, { type PanZoom } from 'panzoom';
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
const i18n: any = getContext('i18n');
export let slides: string[] = [];
export let currentSlide = 0;
export let className = '';
@ -356,7 +358,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 pointer-coarse:hidden"
on:click={zoomOut}
aria-label="Zoom out"
aria-label={$i18n.t('Zoom out')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -375,7 +377,7 @@
type="button"
class="shrink-0 min-w-12 h-7 px-1.5 py-1 text-center text-[0.6875rem] font-normal text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition tabular-nums"
on:click={resetView}
aria-label="Reset zoom"
aria-label={$i18n.t('Reset zoom')}
>
{Math.round(zoomLevel * 100)}%
</button>
@ -383,7 +385,7 @@
type="button"
class="shrink-0 min-w-7 h-7 inline-flex items-center justify-center p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400 pointer-coarse:hidden"
on:click={zoomIn}
aria-label="Zoom in"
aria-label={$i18n.t('Zoom in')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -1,5 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { getContext, onDestroy, onMount } from 'svelte';
const i18n: any = getContext('i18n');
export let open = false;
export let side: 'left' | 'right' = 'right';
@ -145,7 +147,7 @@
on:keydown={resizeKeyHandler}
role="separator"
tabindex="0"
aria-label="Resize panel"
aria-label={$i18n.t('Resize panel')}
aria-orientation="vertical"
>
<span
@ -168,7 +170,7 @@
on:keydown={resizeKeyHandler}
role="separator"
tabindex="0"
aria-label="Resize panel"
aria-label={$i18n.t('Resize panel')}
aria-orientation="vertical"
>
<span

View file

@ -24,7 +24,7 @@
{valvesSpec.properties[property].title}
{#if (valvesSpec?.required ?? []).includes(property)}
<span class=" text-gray-500">*required</span>
<span class=" text-gray-500">{$i18n.t('*required')}</span>
{/if}
</div>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onDestroy, onMount, tick } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
import Modal from '$lib/components/common/Modal.svelte';
import SearchInput from './Sidebar/SearchInput.svelte';
@ -845,7 +845,7 @@
}}
>
<button
aria-label="Chat Menu"
aria-label={$i18n.t('Chat Menu')}
class="self-center dark:hover:text-white transition"
>
<svg

View file

@ -37,7 +37,7 @@
} from '$lib/stores/chatList';
import { onMount, getContext, tick, onDestroy } from 'svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
$: canImportChats = $user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true);
@ -189,11 +189,11 @@
const getMenuItemMeta = (id) => {
const items = {
notes: { label: 'Notes', href: '/notes', iconType: 'note' },
workspace: { label: 'Workspace', href: '/workspace', iconType: 'workspace' },
automations: { label: 'Automations', href: '/automations', iconType: 'automations' },
calendar: { label: 'Calendar', href: '/calendar', iconType: 'calendar' },
playground: { label: 'Playground', href: '/playground', iconType: 'playground' }
notes: { label: $i18n.t('Notes'), href: '/notes', iconType: 'note' },
workspace: { label: $i18n.t('Workspace'), href: '/workspace', iconType: 'workspace' },
automations: { label: $i18n.t('Automations'), href: '/automations', iconType: 'automations' },
calendar: { label: $i18n.t('Calendar'), href: '/calendar', iconType: 'calendar' },
playground: { label: $i18n.t('Playground'), href: '/playground', iconType: 'playground' }
};
return items[id];
};

View file

@ -768,7 +768,7 @@
>
<button
type="button"
aria-label="Chat Menu"
aria-label={$i18n.t('Chat Menu')}
class="flex size-5 items-center justify-center self-center dark:hover:text-white transition m-0"
>
<MoreHorizontalIcon className="size-3.5" strokeWidth="2" />

View file

@ -931,7 +931,7 @@
{/if}
{#if chats === null && chatsLoading}
<div class="flex gap-1 px-2 py-1.5" aria-label="Loading">
<div class="flex gap-1 px-2 py-1.5" aria-label={$i18n.t('Loading')}>
<span class="size-1 rounded-full bg-gray-400 animate-pulse dark:bg-gray-600"></span>
<span
class="size-1 rounded-full bg-gray-400 animate-pulse [animation-delay:150ms] dark:bg-gray-600"
@ -974,7 +974,7 @@
on:click={() => setFolderItems(true)}
>
{#if chatsLoading}
<div class="flex gap-1 px-2 py-1.5" aria-label="Loading">
<div class="flex gap-1 px-2 py-1.5" aria-label={$i18n.t('Loading')}>
<span class="size-1 rounded-full bg-gray-400 animate-pulse dark:bg-gray-600"
></span>
<span

View file

@ -98,7 +98,10 @@
{/if}
{#if !isWritable}
<div class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600" title="Read only">
<div
class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600"
title={$i18n.t('Read only')}
>
<Eye className="size-3" />
</div>
{/if}
@ -108,7 +111,7 @@
{#if expanded}
<div class="pl-3">
{#if loading && !loaded}
<div class="flex gap-1 px-2 py-1.5" aria-label="Loading">
<div class="flex gap-1 px-2 py-1.5" aria-label={$i18n.t('Loading')}>
<span class="size-1 rounded-full bg-gray-400 animate-pulse dark:bg-gray-600"></span>
<span
class="size-1 rounded-full bg-gray-400 animate-pulse [animation-delay:150ms] dark:bg-gray-600"
@ -143,7 +146,7 @@
on:click={() => loadChats(true)}
>
{#if loading}
<div class="flex gap-1 px-2 py-1.5" aria-label="Loading">
<div class="flex gap-1 px-2 py-1.5" aria-label={$i18n.t('Loading')}>
<span class="size-1 rounded-full bg-gray-400 animate-pulse dark:bg-gray-600"></span>
<span
class="size-1 rounded-full bg-gray-400 animate-pulse [animation-delay:150ms] dark:bg-gray-600"

View file

@ -37,7 +37,7 @@
import Download from '../icons/Download.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
let loaded = false;
@ -261,7 +261,7 @@
const exportData = {
chat: {
title: 'Playground Chat',
title: $i18n.t('Playground Chat'),
models: [selectedModelId],
params: system ? { system } : {},
history: {

View file

@ -35,7 +35,7 @@
import AccessButton from '$lib/components/common/AccessButton.svelte';
import { extractInputVariables } from '$lib/utils';
const i18n = getContext('i18n');
const i18n: any = getContext('i18n');
export let onSubmit: Function;
export let onBack: null | Function = null;
@ -622,13 +622,13 @@
{#if info.meta.profile_image_url}
<img
src={info.meta.profile_image_url}
alt="model profile"
alt={$i18n.t('model profile')}
class="size-full object-cover"
/>
{:else}
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
alt="model profile"
alt={$i18n.t('model profile')}
class="size-full object-cover"
/>
{/if}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, tick } from 'svelte';
import { createEventDispatcher, getContext, tick } from 'svelte';
type Voice = {
id: string;
@ -17,6 +17,8 @@
export let className = 'w-full';
export let selectedIds: string[] | null = null;
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher<{
select: Voice;
enableall: Voice[];
@ -153,7 +155,9 @@
enableVoices();
}}
>
<span class="truncate">Enable all ({matchedVoices.length})</span>
<span class="truncate"
>{$i18n.t('Enable all ({{count}})', { count: matchedVoices.length })}</span
>
</button>
{/if}
{/if}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, tick } from 'svelte';
import { createEventDispatcher, getContext, tick } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Search from '$lib/components/icons/Search.svelte';
import TTSVoiceInput from './TTSVoiceInput.svelte';
@ -22,6 +22,8 @@
export let selectedIds: string[] | null = null;
export let variant: 'inline' | 'dropdown' = 'inline';
const i18n: any = getContext('i18n');
const dispatch = createEventDispatcher<{
select: Item;
enableall: Item[];
@ -101,7 +103,9 @@
class="h-[1.6875rem] w-full rounded-xl px-2 text-left text-[0.8125rem] text-gray-700 transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
on:click={enableItems}
>
<span class="truncate">Enable all ({matchedItems.length})</span>
<span class="truncate"
>{$i18n.t('Enable all ({{count}})', { count: matchedItems.length })}</span
>
</button>
{/if}