This commit is contained in:
Timothy Jaeryang Baek 2026-10-08 23:49:38 +04:00
parent 7b7dba6eec
commit 7d205a86dc
76 changed files with 557 additions and 500 deletions

View file

@ -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: $i18n.t('All'), days: 0 }
{ key: 'all', label: 'All', days: 0 }
];
let selectedRange: TimeRange = '30d';
let history: Array<{ date: string; won: number; lost: number }> = [];
@ -239,7 +239,7 @@
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => selectRange(range.key)}
>
{range.label}
{range.key === 'all' ? $i18n.t('All') : range.label}
</button>
{/each}
</div>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext<any>('i18n');
import dayjs from 'dayjs';
interface Props {
@ -95,7 +97,7 @@
{@const idx = i === labelCount - 1 ? data.length - 1 : Math.min(i * step, data.length - 1)}
{#if data[idx]}
<span class={i === 0 ? 'text-left' : i === labelCount - 1 ? 'text-right' : 'text-center'}
>{dayjs(data[idx].date).format(dateFormat)}</span
>{dayjs(data[idx].date).locale($i18n.language).format(dateFormat)}</span
>
{/if}
{/each}
@ -112,9 +114,9 @@
>
<div class="mb-1.5 text-[0.625rem] text-gray-400">
{#if hovered.date?.includes(':')}
{dayjs(hovered.date).format('MMM D, h A')}
{dayjs(hovered.date).locale($i18n.language).format('MMM D, h A')}
{:else}
{dayjs(hovered.date).format('MMM D, YYYY')}
{dayjs(hovered.date).locale($i18n.language).format('MMM D, YYYY')}
{/if}
</div>
{#each Object.entries(hovered.models || {})

View file

@ -8,7 +8,7 @@
dayjs.extend(relativeTime);
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import {
deleteFeedbackById,
@ -492,7 +492,9 @@
{/if}
<td class=" px-3 py-1 text-right font-normal">
{dayjs(feedback.updated_at * 1000).fromNow()}
{dayjs(feedback.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</td>
<td class=" px-3 py-1 text-right font-normal" on:click={(e) => e.stopPropagation()}>

View file

@ -16,7 +16,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: $i18n.t('All'), days: 0 } // 0 = all time, starts from first feedback
{ key: 'all', label: 'All', days: 0 } // 0 = all time, starts from first feedback
];
let selectedRange: TimeRange = '30d';
@ -94,7 +94,7 @@
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => selectRange(range.key)}
>
{range.label}
{range.key === 'all' ? $i18n.t('All') : range.label}
</button>
{/each}
</div>

View file

@ -57,9 +57,9 @@
const labels = chartData.map((h) => {
const date = new Date('startDate' in h ? h.startDate : h.date);
if (aggregateWeekly) {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return date.toLocaleDateString($i18n.language, { month: 'short', day: 'numeric' });
}
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return date.toLocaleDateString($i18n.language, { month: 'short', day: 'numeric' });
});
// Diverging chart: wins go UP (positive), losses go DOWN (negative)
@ -166,7 +166,7 @@
});
};
$: if (chartCanvas && history.length && !loading && aggregateWeekly !== undefined) {
$: if (chartCanvas && history.length && !loading && aggregateWeekly !== undefined && $i18n) {
createChart();
}

View file

@ -47,7 +47,7 @@
import ChevronDown from '../icons/ChevronDown.svelte';
import ChevronUp from '../icons/ChevronUp.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
let shiftKey = false;
@ -538,11 +538,17 @@
</div>
{/if}
<Tooltip content={dayjs(func.updated_at * 1000).format('LLLL')}>
<Tooltip
content={dayjs(func.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs(func.updated_at * 1000).fromNow()}
{dayjs(func.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
</div>

View file

@ -5,14 +5,14 @@
import Check from '$lib/components/icons/Check.svelte';
import Select from '$lib/components/common/Select.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let value = '';
export let placeholder = $i18n.t('Select view');
export let placeholder: string | undefined = undefined;
export let align = 'start';
export let onChange: (value: string) => void = () => {};
const items = [
$: items = [
{ value: '', label: $i18n.t('All') },
{ value: 'available', label: $i18n.t('Available') },
{ value: 'unavailable', label: $i18n.t('Unavailable') },
@ -32,7 +32,7 @@
<Select
bind:value
{items}
{placeholder}
placeholder={placeholder ?? $i18n.t('Select view')}
{align}
triggerClass="relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[0.8125rem] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
labelClass="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"

View file

@ -20,6 +20,7 @@
import { createNewGroup, getGroups, updateGroupById } from '$lib/apis/groups';
import { getUserDefaultPermissions, updateUserDefaultPermissions } from '$lib/apis/users';
/** @type {import('svelte/store').Writable<import('i18next').i18n>} */
const i18n = getContext('i18n');
let loaded = false;
@ -35,7 +36,7 @@
let dropTarget = '';
let moving = false;
const sortItems = [
$: sortItems = [
{ value: 'members', label: $i18n.t('Members') },
{ value: 'name', label: $i18n.t('Name') }
];

View file

@ -498,7 +498,9 @@
</td>
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
{dayjs(user.last_active_at * 1000)
.locale($i18n.language)
.fromNow()}
</td>
</tr>
{/each}

View file

@ -413,11 +413,15 @@
<td class=" px-3 py-1 max-w-48 truncate"> {user.email} </td>
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
{dayjs(user.last_active_at * 1000)
.locale($i18n.language)
.fromNow()}
</td>
<td class=" px-3 py-1">
{dayjs(user.created_at * 1000).format('LL')}
{dayjs(user.created_at * 1000)
.locale($i18n.language)
.format('LL')}
</td>
<td class="px-3 py-1 text-right">

View file

@ -16,7 +16,7 @@
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import UserProfileImage from '$lib/components/chat/Settings/Account/UserProfileImage.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
const dispatch = createEventDispatcher();
dayjs.extend(localizedFormat);
@ -122,7 +122,9 @@
<div class="text-xs text-gray-500">
{$i18n.t('Created at')}
{dayjs(selectedUser.created_at * 1000).format('LL')}
{dayjs(selectedUser.created_at * 1000)
.locale($i18n.language)
.format('LL')}
</div>
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { formatSchedule } from '$lib/utils/schedule';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
@ -79,7 +80,7 @@
const formatTime = (ts: number | null): string => {
if (!ts) return '-';
return new Date(ts / 1_000_000).toLocaleString(undefined, {
return new Date(ts / 1_000_000).toLocaleString($i18n.language, {
month: 'short',
day: 'numeric',
hour: '2-digit',
@ -89,63 +90,12 @@
const formatNextRun = (ts: number | null): string => {
if (!ts) return $i18n.t('Not scheduled');
const d = dayjs(ts / 1_000_000);
if (d.isSame(dayjs(), 'day')) return `${$i18n.t('Today at')} ${d.format('LT')}`;
const d = dayjs(ts / 1_000_000).locale($i18n.language);
if (d.isSame(dayjs().locale($i18n.language), 'day'))
return `${$i18n.t('Today at')} ${d.format('LT')}`;
return d.format('L LT');
};
const formatSchedule = (rrule: string): string => {
const match = rrule.match(/DTSTART[^:]*:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/i);
if (/COUNT=1(?!\d)/.test(rrule)) {
if (match) {
const d = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`);
return `${$i18n.t('Once')} · ${d.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric'
})} ${d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}`;
}
return $i18n.t('Once');
}
const parts: Record<string, string> = {};
rrule
.split(/\s+/)
.filter((line) => !line.toUpperCase().startsWith('DTSTART'))
.join('')
.replace('RRULE:', '')
.split(';')
.forEach((part) => {
const [key, value] = part.split('=');
if (key && value) parts[key] = value;
});
const freq = parts.FREQ || '';
const hour = parseInt(parts.BYHOUR || match?.[4] || '0');
const minute = (parts.BYMINUTE || match?.[5] || '0').padStart(2, '0');
const interval = parseInt(parts.INTERVAL || '1');
const ampm = hour >= 12 ? 'PM' : 'AM';
const hour12 = hour % 12 || 12;
const time = `${hour12}:${minute} ${ampm}`;
if (freq === 'MINUTELY')
return interval === 1
? $i18n.t('Every minute')
: $i18n.t('Every {{count}} minutes', { count: interval });
if (freq === 'HOURLY')
return interval === 1
? $i18n.t('Hourly')
: $i18n.t('Every {{count}} hours', { count: interval });
if (freq === 'DAILY') return `${$i18n.t('Daily at')} ${time}`;
if (freq === 'WEEKLY')
return parts.BYDAY
? `${parts.BYDAY} ${$i18n.t('at')} ${time}`
: `${$i18n.t('Weekly at')} ${time}`;
if (freq === 'MONTHLY')
return `${$i18n.t('Monthly')} ${parts.BYMONTHDAY ?? '1'} ${$i18n.t('at')} ${time}`;
return rrule;
};
const toggleHandler = async () => {
const previousState = automation.is_active;
const res = await toggleAutomationById(localStorage.token, automation.id).catch((err) => {
@ -302,7 +252,7 @@
{$i18n.t('Schedule')}
</span>
<span class="min-w-0 truncate text-xs text-gray-700 dark:text-gray-300">
{formatSchedule(automation.data.rrule)}
{formatSchedule(automation.data.rrule, $i18n)}
</span>
</div>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext<any>('i18n');
import { createEventDispatcher } from 'svelte';
import type { CalendarEventModel } from '$lib/apis/calendar';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@ -23,7 +25,7 @@
<span class="truncate">
{#if !event.all_day}<span class="text-gray-500 dark:text-gray-400"
>{new Date(event.start_at / 1_000_000)
.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
.toLocaleTimeString($i18n.language, { hour: 'numeric', minute: '2-digit' })
.replace(' ', '')}</span
>{/if}
{event.title}

View file

@ -3,7 +3,7 @@
import type { CalendarEventModel, CalendarModel } from '$lib/apis/calendar';
import CalendarEventChip from './CalendarEventChip.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
const dispatch = createEventDispatcher();
export let events: CalendarEventModel[] = [];
@ -113,10 +113,7 @@
}
function formatHour(h: number): string {
if (h === 0) return '12 AM';
if (h < 12) return `${h} AM`;
if (h === 12) return '12 PM';
return `${h - 12} PM`;
return new Date(2000, 0, 1, h).toLocaleTimeString($i18n.language, { hour: 'numeric' });
}
function handleDayClick(day: Date) {
@ -195,7 +192,7 @@
class="text-[0.625rem] text-gray-400 dark:text-gray-500 px-1 mt-auto hover:text-gray-700 dark:hover:text-gray-200 text-left w-full truncate z-10"
on:click|stopPropagation={() => goToDayView(day)}
>
+{dayEvents.length - 3} more
{$i18n.t('and {{COUNT}} more', { COUNT: dayEvents.length - 3 })}
</div>
{/if}
</div>
@ -272,7 +269,7 @@
class="text-[0.625rem] text-gray-400 dark:text-gray-500 px-1 mt-auto hover:text-gray-700 dark:hover:text-gray-200 text-left w-full truncate z-10"
on:click|stopPropagation={() => goToDayView(day)}
>
+{hourEvents.length - 3} more
{$i18n.t('and {{COUNT}} more', { COUNT: hourEvents.length - 3 })}
</div>
{/if}
</div>

View file

@ -45,7 +45,7 @@
import Skeleton from '../chat/Messages/Skeleton.svelte';
import XMark from '../icons/XMark.svelte';
export let placeholder = $i18n.t('Type here...');
export let placeholder: string | undefined = undefined;
export let chatInputElement: any;
export let id: string | null = null;
@ -1001,7 +1001,7 @@
json={true}
messageInput={true}
editable={!disabled}
{placeholder}
placeholder={placeholder ?? $i18n.t('Type here...')}
richText={$settings?.richTextInput ?? true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&

View file

@ -25,7 +25,7 @@
} from '$lib/apis/channels';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let id = null;
export let channel = null;
@ -106,7 +106,9 @@
{$i18n.t(
'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.',
{
createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'),
createdAt: dayjs(channel.created_at / 1000000)
.locale($i18n.language)
.format('MMMM D, YYYY'),
channelName: channel.name
}
)}

View file

@ -411,8 +411,14 @@
<div
class="-mx-1 mt-1 translate-y-px flex items-center justify-center text-[0.6875rem] leading-4 tabular-nums hover-reveal text-gray-500 dark:text-gray-400"
>
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
{dayjs(message.created_at / 1000000).format('HH:mm')}
<Tooltip
content={dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('LLLL')}
>
{dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('HH:mm')}
</Tooltip>
</div>
{/if}
@ -435,14 +441,26 @@
<div
class=" shrink-0 translate-y-px text-[0.6875rem] leading-4 text-gray-500 dark:text-gray-400 font-normal first-letter:capitalize"
>
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
<Tooltip
content={dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('LLLL')}
>
<span class="line-clamp-1">
{#if dayjs(message.created_at / 1000000).isToday()}
{dayjs(message.created_at / 1000000).format('LT')}
{#if dayjs(message.created_at / 1000000)
.locale($i18n.language)
.isToday()}
{dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('LT')}
{:else}
{$i18n.t(formatDate(message.created_at / 1000000), {
LOCALIZED_TIME: dayjs(message.created_at / 1000000).format('LT'),
LOCALIZED_DATE: dayjs(message.created_at / 1000000).format('L')
LOCALIZED_TIME: dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('LT'),
LOCALIZED_DATE: dayjs(message.created_at / 1000000)
.locale($i18n.language)
.format('L')
})}
{/if}
</span>
@ -656,7 +674,10 @@
{$i18n.t('{{COUNT}} Replies', { COUNT: message.reply_count })}</span
><span class="truncate">
{' - '}{$i18n.t('Last reply')}
{dayjs.unix(message.latest_reply_at / 1000000000).fromNow()}</span
{dayjs
.unix(message.latest_reply_at / 1000000000)
.locale($i18n.language)
.fromNow()}</span
>
<span class="ml-1">

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
@ -108,7 +108,9 @@
</div>
<div class="text-gray-500 text-xs">
{$i18n.t('Created on {{date}}', {
date: dayjs(webhook.created_at / 1000000).format('MMM D, YYYY')
date: dayjs(webhook.created_at / 1000000)
.locale($i18n.language)
.format('MMM D, YYYY')
})}
{#if webhook.user?.name}
{$i18n.t('by {{name}}', { name: webhook.user.name })}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
import LightBulb from '$lib/components/icons/LightBulb.svelte';
@ -16,11 +16,7 @@
let selectedText = '';
let floatingInputValue = '';
$: if (actions.length === 0) {
actions = DEFAULT_ACTIONS;
}
const DEFAULT_ACTIONS = [
$: DEFAULT_ACTIONS = [
{
id: 'ask',
label: $i18n.t('Ask'),
@ -36,13 +32,15 @@
}
];
$: displayActions = actions.length ? actions : DEFAULT_ACTIONS;
const actionHandler = (actionId) => {
let selectedContent = selectedText
.split('\n')
.map((line) => `> ${line}`)
.join('\n');
let selectedAction = actions.find((action) => action.id === actionId);
let selectedAction = displayActions.find((action) => action.id === actionId);
if (!selectedAction) {
return;
}
@ -92,7 +90,7 @@
<div
class="flex flex-row shrink-0 p-0.5 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl shadow-xl border border-gray-100 dark:border-gray-800"
>
{#each actions as action}
{#each displayActions as action}
<button
aria-label={action.label}
class="px-1.5 py-[0.0625rem] hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl flex items-center gap-1 min-w-fit transition"

View file

@ -1220,7 +1220,7 @@
}
const res = await createNoteHandler(
dayjs().format('YYYY-MM-DD'),
dayjs().locale($i18n.language).format('YYYY-MM-DD'),
inputContent?.md,
inputContent?.html
);
@ -2715,7 +2715,9 @@
{#if !embedded && prompt === '' && files.length === 0 && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.call ?? true))}
<div class=" flex items-center">
<!-- {$i18n.t('Call')} -->
<Tooltip content={callActive ? $i18n.t('Call in progress') : $i18n.t('Voice mode')}>
<Tooltip
content={callActive ? $i18n.t('Call in progress') : $i18n.t('Voice mode')}
>
<button
class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-[0.3125rem] self-center"
class:call-active={callActive}
@ -2781,7 +2783,9 @@
);
}
}}
aria-label={callActive ? $i18n.t('Return to call') : $i18n.t('Voice mode')}
aria-label={callActive
? $i18n.t('Return to call')
: $i18n.t('Voice mode')}
>
<Voice className="size-5" strokeWidth="2.5" />
</button>

View file

@ -67,7 +67,7 @@
...videoInputDevices,
{
deviceId: 'screen',
label: $i18n.t('Screen Share')
label: ''
}
];
}
@ -1048,7 +1048,9 @@
<div class="flex items-center justify-center gap-4 z-10">
{#if camera}
<VideoInputMenu
devices={videoInputDevices}
devices={videoInputDevices.map((device) =>
device.deviceId === 'screen' ? { ...device, label: $i18n.t('Screen Share') } : device
)}
on:change={async (e) => {
console.log(e.detail);
selectedVideoInputDeviceId = e.detail;

View file

@ -38,7 +38,6 @@
export let filteredItems: any[] = [];
let searchDebounceTimer: ReturnType<typeof setTimeout>;
let folderItems: any[] = [];
let filesystemItems: any[] = [];
let knowledgeItems: any[] = [];
let fileItems: any[] = [];
@ -98,7 +97,6 @@
const getItems = () => {
const terminal = getSelectedTerminal();
getFilesystemItems(terminal);
getFolderItems();
getKnowledgeItems();
getKnowledgeFileItems();
};
@ -158,16 +156,14 @@
return directTerminal?.url ? { url: directTerminal.url, key: directTerminal.key ?? '' } : null;
};
const getFolderItems = () => {
folderItems = (($folders ?? []) as any[])
.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}))
.filter((folder: any) => folder.name.toLowerCase().includes(query.toLowerCase()));
};
$: folderItems = (($folders ?? []) as any[])
.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}))
.filter((folder: any) => folder.name.toLowerCase().includes(query.toLowerCase()));
const getKnowledgeItems = async () => {
const res = await searchKnowledgeBases(localStorage.token, query).catch(() => null);

View file

@ -18,7 +18,7 @@
import Youtube from '$lib/components/icons/Youtube.svelte';
import Folder from '$lib/components/icons/Folder.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let query = '';
export let onSelect = (e) => {};
@ -64,7 +64,6 @@
}
};
let folderItems = [];
let knowledgeItems = [];
let fileItems = [];
@ -82,21 +81,18 @@
});
const getItems = () => {
getFolderItems();
getKnowledgeItems();
getKnowledgeFileItems();
};
const getFolderItems = async () => {
folderItems = $folders
.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}))
.filter((folder) => folder.name.toLowerCase().includes(query.toLowerCase()));
};
$: folderItems = $folders
.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}))
.filter((folder) => folder.name.toLowerCase().includes(query.toLowerCase()));
const getKnowledgeItems = async () => {
const res = await searchKnowledgeBases(localStorage.token, query).catch(() => {

View file

@ -12,7 +12,7 @@
resolveLocalizedModelName
} from '$lib/utils/localizedContent';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let query = '';
export let onSelect = (e) => {};
@ -20,7 +20,7 @@
let selectedIdx = 0;
export let filteredItems = [];
let fuse = new Fuse(
$: fuse = new Fuse(
$models
.filter((model) => !model?.info?.meta?.hidden)
.map((model) => {

View file

@ -51,7 +51,7 @@
let tab = '';
let showAttachWebpageModal = false;
const toolApprovalModes = [
$: toolApprovalModes = [
{
value: 'full',
label: $i18n.t('Full access'),

View file

@ -11,7 +11,7 @@
import { chatId } from '$lib/stores';
import SearchInput from './SearchInput.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let onSelect = (e) => {};
@ -83,8 +83,7 @@
return {
...item,
type: 'chat',
name: item.title,
description: item.snippet || dayjs(item.updated_at * 1000).fromNow()
name: item.title
};
})
];
@ -141,7 +140,10 @@
>
<div class="text-black dark:text-gray-100 flex items-center gap-1.5">
<Tooltip
content={item.description || decodeString(item?.name)}
content={item.snippet ||
dayjs(item.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
placement="top-start"
>
<div class="line-clamp-1 flex-1">

View file

@ -11,7 +11,7 @@
import Loader from '$lib/components/common/Loader.svelte';
import SearchInput from './SearchInput.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let onSelect = (e) => {};
@ -83,8 +83,7 @@
return {
...note,
type: 'note',
name: note.title,
description: dayjs(note.updated_at / 1000000).fromNow()
name: note.title
};
})
];
@ -146,7 +145,9 @@
</Tooltip>
<Tooltip
content={item.description || decodeString(item?.name)}
content={dayjs(item.updated_at / 1000000)
.locale($i18n.language)
.fromNow()}
placement="top-start"
>
<div class="line-clamp-1 flex-1">

View file

@ -3,10 +3,10 @@
import Search from '$lib/components/icons/Search.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let value = '';
export let placeholder = $i18n.t('Search');
export let placeholder: string | undefined = undefined;
</script>
<div class="my-0.5 flex ml-2 mr-0.5 h-[1.6875rem] shrink-0 items-center gap-2">
@ -15,9 +15,9 @@
<input
bind:value
class="w-full bg-transparent text-[0.8125rem] font-normal outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-500"
{placeholder}
placeholder={placeholder ?? $i18n.t('Search')}
autocomplete="off"
aria-label={placeholder}
aria-label={placeholder ?? $i18n.t('Search')}
on:click|stopPropagation
on:keydown|stopPropagation
/>

View file

@ -10,11 +10,11 @@
import Spinner from '$lib/components/common/Spinner.svelte';
import MapSelector from '$lib/components/common/Valves/MapSelector.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let show = false;
export let variables = {};
export let title = $i18n.t('Input Variables');
export let title: string | undefined = undefined;
export let onSave = (e) => {};
@ -81,7 +81,7 @@
<div>
<div class=" flex justify-between dark:text-gray-300 px-4 pt-3 pb-1">
<div class=" text-sm font-medium self-center">
{title}
{title ?? $i18n.t('Input Variables')}
</div>
<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"

View file

@ -11,7 +11,7 @@
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(LocalizedFormat);
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let recording = false;
export let transcribe = true;
@ -175,7 +175,10 @@
// Create a blob from the audio chunks
await tick();
const file = blobToFile(audioBlob, `Recording-${dayjs().format('L LT')}.${ext}`);
const file = blobToFile(
audioBlob,
`Recording-${dayjs().locale($i18n.language).format('L LT')}.${ext}`
);
if (transcribe) {
if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') {

View file

@ -62,7 +62,7 @@
let _token = null;
let renderHTML = null;
let renderError = null;
let renderError: { type: 'diagram' | 'visualization'; detail: string } | null = null;
let highlightedCode = null;
let executing = false;
@ -374,7 +374,7 @@
} catch (error) {
console.error('Failed to render mermaid diagram:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
renderError = $i18n.t('Failed to render diagram') + `: ${errorMsg}`;
renderError = { type: 'diagram', detail: errorMsg };
renderHTML = null;
}
} else if (
@ -386,7 +386,7 @@
} catch (error) {
console.error('Failed to render Vega visualization:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
renderError = $i18n.t('Failed to render visualization') + `: ${errorMsg}`;
renderError = { type: 'visualization', detail: errorMsg };
renderHTML = null;
}
}
@ -453,7 +453,9 @@
<div
class="flex gap-2.5 border px-4 py-3 border-red-600/10 bg-red-600/10 rounded-2xl mb-2"
>
{renderError}
{renderError.type === 'diagram'
? $i18n.t('Failed to render diagram')
: $i18n.t('Failed to render visualization')}: {renderError.detail}
</div>
{/if}
<pre>{code}</pre>

View file

@ -9,7 +9,7 @@
import UserStatus from '$lib/components/channel/Messages/Message/UserStatus.svelte';
import UserStatusLinkPreview from '$lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let token: Token;
@ -19,7 +19,7 @@
let idType = null;
let id = '';
$: if (token) {
$: if (token && $i18n && $models && $channels) {
init();
}

View file

@ -5,12 +5,13 @@
import { getUserInfoById } from '$lib/apis/users';
import { capitalizeFirstLetter } from '$lib/utils';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let noteId: string;
export let href: string;
let title = '';
let titleFallback = 'Untitled';
let author = '';
let loading = true;
@ -18,7 +19,7 @@
try {
const note = await getNoteById(localStorage.token, noteId);
if (note) {
title = note.title || $i18n.t('Untitled');
title = note.title || '';
if (note.user_id) {
try {
@ -32,7 +33,7 @@
}
}
} catch {
title = $i18n.t('Note');
titleFallback = 'Note';
} finally {
loading = false;
}
@ -59,7 +60,7 @@
{#if loading}
<span class="text-gray-400">...</span>
{:else}
{title}
{title || (titleFallback === 'Note' ? $i18n.t('Note') : $i18n.t('Untitled'))}
{/if}
</div>
<div class="text-gray-500 text-xs shrink-0">{$i18n.t('Note')}</div>

View file

@ -24,7 +24,7 @@
formatMessageTimestampFull,
getDeepestChildId
} from '$lib/utils';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let chatId;
export let history;
@ -421,14 +421,14 @@
>
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
content={formatMessageTimestampFull(message.timestamp * 1000, $i18n.language)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="hover-reveal ml-1 shrink-0 whitespace-nowrap text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
{formatMessageTimestamp(message.timestamp * 1000, $i18n.language)}
</time>
</Tooltip>
</div>

View file

@ -933,14 +933,14 @@
<div class="mt-0.5 flex justify-start whitespace-nowrap text-gray-600 dark:text-gray-500">
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
content={formatMessageTimestampFull(message.timestamp * 1000, $i18n.language)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="ml-1 shrink-0 whitespace-nowrap text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
{formatMessageTimestamp(message.timestamp * 1000, $i18n.language)}
</time>
</Tooltip>
</div>
@ -1606,14 +1606,14 @@
{#if message.timestamp}
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
content={formatMessageTimestampFull(message.timestamp * 1000, $i18n.language)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="hover-reveal ml-1 shrink-0 whitespace-nowrap text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
{formatMessageTimestamp(message.timestamp * 1000, $i18n.language)}
</time>
</Tooltip>
{/if}

View file

@ -22,7 +22,7 @@
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import SubagentResultRow from './SubagentResultRow.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let user;
export let chatId;
@ -448,7 +448,7 @@
className="flex self-center {($settings?.chatBubble ?? true) && isOwn
? 'mr-1'
: 'order-last'}"
content={formatMessageTimestampFull(message.timestamp * 1000)}
content={formatMessageTimestampFull(message.timestamp * 1000, $i18n.language)}
placement="bottom"
>
<time
@ -458,7 +458,7 @@
? 'mr-1'
: 'ml-1 shrink-0 whitespace-nowrap'} text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
{formatMessageTimestamp(message.timestamp * 1000, $i18n.language)}
</time>
</Tooltip>
{/if}

View file

@ -18,7 +18,7 @@
import Tag from '$lib/components/icons/Tag.svelte';
import Label from '$lib/components/icons/Label.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let selectedModelIdx: number = -1;
export let item: any = {};
@ -171,7 +171,9 @@
content={item.model.ollama?.expires_at &&
new Date(item.model.ollama?.expires_at * 1000) > new Date()
? `${$i18n.t('Unloads {{FROM_NOW}}', {
FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000)
.locale($i18n.language)
.fromNow()
})}`
: `${$i18n.t('Loaded')}`}
className="self-end"

View file

@ -62,9 +62,9 @@
export let compareEnabled = false;
export let multipleEnabled = false;
export let disabled = false;
export let placeholder = $i18n.t('Select a model');
export let placeholder: string | undefined = undefined;
export let searchEnabled = true;
export let searchPlaceholder = $i18n.t('Search a model');
export let searchPlaceholder: string | undefined = undefined;
export let selectionOnly = false;
export let includeHidden = false;
@ -195,7 +195,7 @@
? compareEnabled && selectedCount > 1
? `${selectedModel.label} +${selectedCount - 1}`
: selectedModel.label
: placeholder;
: (placeholder ?? $i18n.t('Select a model'));
let searchValue = '';
@ -226,7 +226,7 @@
const getProviderPoolKey = (connection, model: string) =>
`${connection.provider}:${connection.idx}:${model}`;
const fuse = new Fuse(
$: fuse = new Fuse(
items.map((item) => {
const _item = {
...item,
@ -242,26 +242,6 @@
}
);
const updateFuse = () => {
if (fuse) {
fuse.setCollection(
items.map((item) => {
const _item = {
...item,
modelName: resolveLocalizedModelName(item.model, $i18n.language),
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
desc: resolveLocalizedModelDescription(item.model, $i18n.language)
};
return _item;
})
);
}
};
$: if (items) {
updateFuse();
}
$: filteredItems = (
searchValue
? fuse
@ -913,7 +893,7 @@
: 'outline-hidden focus:outline-hidden'}"
aria-label={selectedModel
? $i18n.t('Selected model: {{modelName}}', { modelName: triggerLabel })
: placeholder}
: (placeholder ?? $i18n.t('Select a model'))}
aria-haspopup="listbox"
aria-expanded={show}
id="model-selector-{id}-button"
@ -960,7 +940,7 @@
id="model-search-input"
bind:value={searchValue}
class="w-full bg-transparent text-[0.8125rem] font-normal outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-500"
placeholder={searchPlaceholder}
placeholder={searchPlaceholder ?? $i18n.t('Search a model')}
autocomplete="off"
aria-label={$i18n.t('Search In Models')}
on:keydown={(e) => {

View file

@ -3,6 +3,7 @@
import type { Writable } from 'svelte/store';
type ChatListI18n = {
language: string;
t: (key: string, options?: Record<string, unknown>) => string;
};
@ -250,7 +251,9 @@
<div class="hidden sm:flex sm:basis-2/5 items-center justify-end gap-2">
<div class=" text-gray-500 dark:text-gray-400 text-xs">
{dayjs((chat.updated_at ?? chat.created_at ?? 0) * 1000).calendar()}
{dayjs((chat.updated_at ?? chat.created_at ?? 0) * 1000)
.locale($i18n.language)
.calendar()}
</div>
</div>
</a>

View file

@ -330,14 +330,16 @@
class="hidden w-24 shrink-0 self-center justify-end text-gray-400 dark:text-gray-600 sm:flex"
>
{$i18n.t(
dayjs(chat?.updated_at * 1000).calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L'
})
dayjs(chat?.updated_at * 1000)
.locale($i18n.language)
.calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L'
})
)}
</div>
<div class="flex shrink-0 items-center justify-end text-gray-500 dark:text-gray-500">

View file

@ -29,13 +29,13 @@
let events: { event: string; label: string; description?: string }[] = [
{
event: 'chat.finished',
label: $i18n.t('Chat finished'),
description: $i18n.t('A chat run finished successfully.')
label: 'Chat finished',
description: 'A chat run finished successfully.'
},
{
event: 'chat.failed',
label: $i18n.t('Chat failed'),
description: $i18n.t('A chat run failed.')
label: 'Chat failed',
description: 'A chat run failed.'
}
];
let loadingTargets = false;
@ -249,7 +249,7 @@
{#each targets as target, index}
{@const alertLabels = events
.filter((event) => target.events.includes(event.event))
.map((event) => event.label)
.map((event) => $i18n.t(event.label))
.join(', ')}
<div class="flex items-center gap-3 px-1 py-1.5">
<div class="min-w-0 flex-1">
@ -388,7 +388,7 @@
type="button"
on:click={() => toggleFormEvent(event.event)}
>
{event.label}
{$i18n.t(event.label)}
</button>
{/each}
</div>

View file

@ -123,9 +123,9 @@
];
const heatmapModes: Array<{ value: HeatmapMode; label: string }> = [
{ value: 'daily', label: $i18n.t('Daily') },
{ value: 'weekly', label: $i18n.t('Weekly') },
{ value: 'cumulative', label: $i18n.t('Cumulative') }
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'cumulative', label: 'Cumulative' }
];
const loadUsage = async () => {
@ -197,7 +197,9 @@
}
labels.push({
label: new Date(`${entry.date}T00:00:00`).toLocaleString(undefined, { month: 'short' }),
label: new Date(`${entry.date}T00:00:00`).toLocaleString($i18n.language, {
month: 'short'
}),
column,
span: Math.min(MIN_MONTH_LABEL_GAP, columns - column + 1)
});

View file

@ -10,7 +10,7 @@
dayjs.extend(calendar);
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
type SortKey = 'title' | 'updated_at' | 'user_name';
export let chatList: Array<{
@ -161,12 +161,14 @@
<div class="{showUserInfo ? 'w-28' : 'basis-2/5'} flex items-center justify-end">
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat.updated_at * 1000).calendar(null, {
sameDay: '[Today] h:mm A',
lastDay: '[Yesterday] h:mm A',
lastWeek: 'MMM D',
sameElse: 'MMM D, YYYY'
})}
{dayjs(chat.updated_at * 1000)
.locale($i18n.language)
.calendar(null, {
sameDay: '[Today] h:mm A',
lastDay: '[Yesterday] h:mm A',
lastWeek: 'MMM D',
sameElse: 'MMM D, YYYY'
})}
</div>
</div>
</div>

View file

@ -22,7 +22,7 @@
import { user } from '$lib/stores';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let boilerplate = '';
export let value = '';
@ -88,6 +88,12 @@
let isDarkMode = false;
let editorTheme = new Compartment();
let editorLanguage = new Compartment();
const editorPlaceholder = new Compartment();
$: if (codeEditor) {
codeEditor.dispatch({
effects: editorPlaceholder.reconfigure(placeholder($i18n.t('Enter your code here...')))
});
}
const getLang = async () => {
const language = languages.find((l) => l.alias.includes(lang));
@ -210,7 +216,7 @@ print("${endTag}")
basicSetup,
keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
indentUnit.of(' '),
placeholder($i18n.t('Enter your code here...')),
editorPlaceholder.of([]),
EditorView.updateListener.of((e) => {
if (e.docChanged) {
_value = e.state.doc.toString();

View file

@ -3,7 +3,7 @@
import { v4 as uuidv4 } from 'uuid';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import dayjs from '$lib/dayjs';
import duration from 'dayjs/plugin/duration';
@ -12,23 +12,6 @@
dayjs.extend(duration);
dayjs.extend(relativeTime);
async function loadLocale(locales) {
if (!locales || !Array.isArray(locales)) {
return;
}
for (const locale of locales) {
try {
dayjs.locale(locale);
break; // Stop after successfully loading the first available locale
} catch (error) {
console.error(`Could not load locale '${locale}':`, error);
}
}
}
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
@ -103,7 +86,10 @@
})}
{:else}
{$i18n.t('Thought for {{DURATION}}', {
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
DURATION: dayjs
.duration(attributes.duration, 'seconds')
.locale($i18n.language)
.humanize()
})}
{/if}
{:else if attributes?.done === 'true' || messageDone}

View file

@ -6,7 +6,7 @@
import { settings } from '$lib/stores';
import { matchKeybinding, Shortcut } from '$lib/shortcuts';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
const dispatch = createEventDispatcher();
import { fade } from 'svelte/transition';
@ -18,8 +18,8 @@
export let title = '';
export let message = '';
export let cancelLabel = $i18n.t('Cancel');
export let confirmLabel = $i18n.t('Confirm');
export let cancelLabel: string | undefined = undefined;
export let confirmLabel: string | undefined = undefined;
export let onConfirm = () => {};
@ -197,7 +197,7 @@
}}
type="button"
>
{cancelLabel}
{cancelLabel ?? $i18n.t('Cancel')}
</button>
<button
class="text-sm bg-gray-900 hover:bg-gray-900/90 text-gray-100 dark:bg-gray-100 dark:hover:bg-gray-100/90 dark:text-gray-800 font-normal w-full py-1.5 rounded-full transition"
@ -206,7 +206,7 @@
}}
type="button"
>
{confirmLabel}
{confirmLabel ?? $i18n.t('Confirm')}
</button>
</div>
</div>

View file

@ -20,7 +20,7 @@
let containerEl: HTMLDivElement;
let styleEl: HTMLDivElement;
let fallbackHtml = '';
let error = '';
let error = false;
let loading = false;
let renderId = 0;
let mounted = false;
@ -36,7 +36,7 @@
if (containerEl) containerEl.innerHTML = '';
if (styleEl) styleEl.innerHTML = '';
fallbackHtml = '';
error = '';
error = false;
};
const updateFitScale = () => {
@ -157,7 +157,7 @@
fallbackHtml = DOMPurify.sanitize(await docxToHtml(arrayBuffer.slice(0)));
} catch (fallbackError) {
console.error('Error rendering DOCX fallback:', fallbackError);
error = $i18n.t('Failed to load DOCX file. Please try downloading it instead.');
error = true;
}
} finally {
if (currentRender === renderId) loading = false;
@ -200,7 +200,9 @@
{/if}
{#if error}
<div class="text-red-500 text-sm p-4">{error}</div>
<div class="text-red-500 text-sm p-4">
{$i18n.t('Failed to load DOCX file. Please try downloading it instead.')}
</div>
{:else if fallbackHtml}
<div bind:this={outerContainer} class="h-full overflow-auto overscroll-contain">
<div

View file

@ -3,18 +3,14 @@
import Select from '$lib/components/common/Select.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let align = 'center';
export let className = '';
export let value = '';
export let placeholder = 'Select an option';
export let items = [
{ value: 'new', label: $i18n.t('New') },
{ value: 'top', label: $i18n.t('Top') }
];
export let placeholder: string | undefined = undefined;
export let items: { value: string; label: string }[] | undefined = undefined;
export let onChange: (value: string) => void = () => {};
let selectComponent;
@ -25,8 +21,11 @@
bind:this={selectComponent}
bind:value
bind:open
{items}
{placeholder}
items={items ?? [
{ value: 'new', label: $i18n.t('New') },
{ value: 'top', label: $i18n.t('Top') }
]}
placeholder={placeholder ?? $i18n.t('Select an option')}
{align}
triggerClass={className
? className

View file

@ -49,17 +49,17 @@
let excelSheetNames: string[] = [];
let selectedSheet = '';
let excelHtml = '';
let excelError = '';
let excelError = false;
let rowCount = 0;
// DOCX state
let docxData: ArrayBuffer | null = null;
let docxError = '';
let docxError = false;
// PPTX state
let pptxSlides: string[] = [];
let pptxCurrentSlide = 0;
let pptxError = '';
let pptxError = false;
let panzoomRef: PanzoomContainer;
const resetImageView = () => {
@ -140,7 +140,7 @@
const loadExcelContent = async () => {
try {
excelError = '';
excelError = false;
const [arrayBuffer, { read }] = await Promise.all([
getFileContentById(item.id),
import('xlsx')
@ -154,7 +154,7 @@
}
} catch (error) {
console.error('Error loading Excel/CSV file:', error);
excelError = $i18n.t('Failed to load Excel/CSV file. Please try downloading it instead.');
excelError = true;
}
};
@ -173,17 +173,17 @@
const loadDocxContent = async () => {
try {
docxError = '';
docxError = false;
docxData = await getFileContentById(item.id);
} catch (error) {
console.error('Error loading DOCX file:', error);
docxError = $i18n.t('Failed to load DOCX file. Please try downloading it instead.');
docxError = true;
}
};
const loadPptxContent = async () => {
try {
pptxError = '';
pptxError = false;
const [arrayBuffer, { pptxToImages }] = await Promise.all([
getFileContentById(item.id),
import('$lib/utils/pptxToHtml')
@ -193,7 +193,7 @@
pptxCurrentSlide = 0;
} catch (error) {
console.error('Error loading PPTX file:', error);
pptxError = $i18n.t('Failed to load PPTX file. Please try downloading it instead.');
pptxError = true;
}
};
@ -309,7 +309,9 @@
{#if item?.created_at}
<div class="capitalize shrink-0">
{dayjs(item.created_at * 1000).format('LL')}
{dayjs(item.created_at * 1000)
.locale($i18n.language)
.format('LL')}
</div>
{/if}
{/if}
@ -519,7 +521,7 @@
{:else if isExcel}
{#if excelError}
<div class="text-red-500 text-sm p-4">
{excelError}
{$i18n.t('Failed to load Excel/CSV file. Please try downloading it instead.')}
</div>
{:else}
{#if excelSheetNames.length > 1}
@ -567,7 +569,9 @@
</div>
{:else if isDocx}
{#if docxError}
<div class="text-red-500 text-sm p-4">{docxError}</div>
<div class="text-red-500 text-sm p-4">
{$i18n.t('Failed to load DOCX file. Please try downloading it instead.')}
</div>
{:else if docxData}
<DocxPreview data={docxData} className="h-[60vh]" />
{:else}
@ -575,7 +579,9 @@
{/if}
{:else if isPptx}
{#if pptxError}
<div class="text-red-500 text-sm p-4">{pptxError}</div>
<div class="text-red-500 text-sm p-4">
{$i18n.t('Failed to load PPTX file. Please try downloading it instead.')}
</div>
{:else if pptxSlides.length > 0}
<PptxPreview
slides={pptxSlides}

View file

@ -135,7 +135,7 @@
import { onMount, onDestroy, tick, getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
const eventDispatch = createEventDispatcher();
import { Fragment, DOMParser } from 'prosemirror-model';
@ -226,15 +226,15 @@
export let documentId = '';
export let className = 'input-prose min-h-fit h-full';
export let placeholder = $i18n.t('Type here...');
let _placeholder = placeholder;
export let placeholder: string | undefined = undefined;
let _placeholder = '';
$: if (placeholder !== _placeholder) {
setPlaceholder();
$: if ((placeholder ?? $i18n.t('Type here...')) !== _placeholder) {
setPlaceholder(placeholder ?? $i18n.t('Type here...'));
}
const setPlaceholder = () => {
_placeholder = placeholder;
const setPlaceholder = (value: string) => {
_placeholder = value;
if (editor) {
editor?.view.dispatch(editor.state.tr);
}

View file

@ -1,13 +1,15 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext<any>('i18n');
import ChevronDown from '../icons/ChevronDown.svelte';
import Check from '../icons/Check.svelte';
import Search from '../icons/Search.svelte';
import Select from './Select.svelte';
export let value = '';
export let placeholder = $i18n.t('Select a model');
export let placeholder: string | undefined = undefined;
export let searchEnabled = true;
export let searchPlaceholder = $i18n.t('Search a model');
export let searchPlaceholder: string | undefined = undefined;
export let items = [
{ value: 'mango', label: 'Mango' },
@ -29,7 +31,7 @@
bind:value
bind:this={selectRef}
{items}
{placeholder}
placeholder={placeholder ?? $i18n.t('Select a model')}
triggerClass="relative w-full"
contentClass="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-hidden"
onClose={() => {
@ -54,7 +56,7 @@
<input
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-hidden"
placeholder={searchPlaceholder}
placeholder={searchPlaceholder ?? $i18n.t('Search a model')}
on:click|stopPropagation
/>
</div>

View file

@ -26,7 +26,7 @@
import LinkSlash from '../icons/LinkSlash.svelte';
import Clipboard from '../icons/Clipboard.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let show = false;
@ -301,14 +301,16 @@
<div class="{showUserInfo ? 'w-28' : 'basis-2/5'} flex items-center justify-end">
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs">
{$i18n.t(
dayjs(chat?.updated_at * 1000).calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L' // use localized format, otherwise dayjs.calendar() defaults to DD/MM/YYYY
})
dayjs(chat?.updated_at * 1000)
.locale($i18n.language)
.calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L' // use localized format, otherwise dayjs.calendar() defaults to DD/MM/YYYY
})
)}
</div>

View file

@ -351,10 +351,14 @@
<div class="basis-2/5 flex items-center justify-end">
<Tooltip
content={dayjs(file.created_at * 1000).format('MMM D, YYYY h:mm A')}
content={dayjs(file.created_at * 1000)
.locale($i18n.language)
.format('MMM D, YYYY h:mm A')}
className="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs"
>
{dayjs(file.created_at * 1000).format('MMM D, YYYY')}
{dayjs(file.created_at * 1000)
.locale($i18n.language)
.format('MMM D, YYYY')}
</Tooltip>
<div class="flex justify-end pl-2.5 text-gray-600 dark:text-gray-300">

View file

@ -251,7 +251,7 @@
}
};
let actions = [
$: actions = [
{
label: $i18n.t('Start a new conversation'),
onClick: async () => {
@ -260,7 +260,21 @@
onClose();
},
icon: EditPencilIcon
}
},
...(($config?.features?.enable_notes ?? false) &&
($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))
? [
{
label: $i18n.t('Create a new note'),
onClick: async () => {
await goto(`/notes/new?content=${encodeURIComponent(query)}`);
show = false;
onClose();
},
icon: NotesIcon
}
]
: [])
];
let query = '';
@ -522,24 +536,6 @@
};
onMount(() => {
actions = [
...actions,
...(($config?.features?.enable_notes ?? false) &&
($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))
? [
{
label: $i18n.t('Create a new note'),
onClick: async () => {
await goto(`/notes/new?content=${encodeURIComponent(query)}`);
show = false;
onClose();
},
icon: NotesIcon
}
]
: [])
];
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keydown', onShiftKeyDown);
document.addEventListener('keyup', onShiftKeyUp);
@ -768,14 +764,16 @@
>
<div class="text-gray-500 dark:text-gray-400 text-xs">
{$i18n.t(
dayjs(chat?.updated_at * 1000).calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L'
})
dayjs(chat?.updated_at * 1000)
.locale($i18n.language)
.calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'L'
})
)}
</div>

View file

@ -7,7 +7,7 @@
import XMark from './icons/XMark.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let placeholder = '';
export let value = '';
@ -22,7 +22,7 @@
let lastWord = '';
$: lastWord = value ? value.split(' ').at(-1) : value;
let options = [
$: options = [
{
name: 'tag:',
description: $i18n.t('search for tags')
@ -56,7 +56,7 @@
let filteredItems = [];
$: if (lastWord && lastWord !== null) {
$: if (lastWord && $i18n) {
initItems();
}

View file

@ -4,7 +4,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import { marked } from 'marked';
import { toast } from 'svelte-sonner';
@ -51,20 +51,6 @@
import NotePanel from '$lib/components/notes/NotePanel.svelte';
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
async function loadLocale(locales) {
for (const locale of locales) {
try {
dayjs.locale(locale);
break; // Stop after successfully loading the first available locale
} catch (error) {
console.error(`Could not load locale '${locale}':`, error);
}
}
}
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
import {
deleteNoteById,
createNoteChatById,
@ -1277,24 +1263,36 @@ ${content}
<div class=" flex items-center gap-1 w-fit py-1 px-1.5 rounded-lg min-w-fit">
<!-- check for same date, yesterday, last week, and other -->
{#if dayjs(note.created_at / 1000000).isSame(dayjs(), 'day')}
{#if dayjs(note.created_at / 1000000)
.locale($i18n.language)
.isSame(dayjs().locale($i18n.language), 'day')}
<span
>{dayjs(note.created_at / 1000000).format($i18n.t('[Today at] h:mm A'))}</span
>{dayjs(note.created_at / 1000000)
.locale($i18n.language)
.format($i18n.t('[Today at] h:mm A'))}</span
>
{:else if dayjs(note.created_at / 1000000).isSame(dayjs().subtract(1, 'day'), 'day')}
{:else if dayjs(note.created_at / 1000000)
.locale($i18n.language)
.isSame(dayjs().locale($i18n.language).subtract(1, 'day'), 'day')}
<span
>{dayjs(note.created_at / 1000000).format(
$i18n.t('[Yesterday at] h:mm A')
)}</span
>{dayjs(note.created_at / 1000000)
.locale($i18n.language)
.format($i18n.t('[Yesterday at] h:mm A'))}</span
>
{:else if dayjs(note.created_at / 1000000).isSame(dayjs().subtract(1, 'week'), 'week')}
{:else if dayjs(note.created_at / 1000000)
.locale($i18n.language)
.isSame(dayjs().locale($i18n.language).subtract(1, 'week'), 'week')}
<span
>{dayjs(note.created_at / 1000000).format(
$i18n.t('[Last] dddd [at] h:mm A')
)}</span
>{dayjs(note.created_at / 1000000)
.locale($i18n.language)
.format($i18n.t('[Last] dddd [at] h:mm A'))}</span
>
{:else}
<span>{dayjs(note.created_at / 1000000).format($i18n.t('DD/MM/YYYY'))}</span>
<span
>{dayjs(note.created_at / 1000000)
.locale($i18n.language)
.format($i18n.t('DD/MM/YYYY'))}</span
>
{/if}
</div>

View file

@ -12,22 +12,9 @@
dayjs.extend(duration);
dayjs.extend(relativeTime);
async function loadLocale(locales) {
for (const locale of locales) {
try {
dayjs.locale(locale);
break; // Stop after successfully loading the first available locale
} catch (error) {
console.error(`Could not load locale '${locale}':`, error);
}
}
}
import { onMount, getContext, onDestroy } from 'svelte';
const i18n = getContext('i18n');
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import { goto } from '$app/navigation';
import { WEBUI_NAME, config, user, pinnedNotes, mobile, showSidebar } from '$lib/stores';
@ -448,7 +435,9 @@
id: 'notes-new',
label: $i18n.t('Create'),
onClick: async () => {
const res = await createNoteHandler(dayjs().format('YYYY-MM-DD'));
const res = await createNoteHandler(
dayjs().locale($i18n.language).format('YYYY-MM-DD')
);
if (res) {
goto(`/notes/${res.id}`);
@ -629,11 +618,17 @@
</div>
</Tooltip>
<Tooltip content={dayjs(note.updated_at / 1000000).format('LLLL')}>
<Tooltip
content={dayjs(note.updated_at / 1000000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs(note.updated_at / 1000000).fromNow()}
{dayjs(note.updated_at / 1000000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
</div>
@ -825,9 +820,15 @@
</div>
</Tooltip>
<Tooltip content={dayjs(note.updated_at / 1000000).format('LLLL')}>
<Tooltip
content={dayjs(note.updated_at / 1000000)
.locale($i18n.language)
.format('LLLL')}
>
<div class="shrink-0">
{dayjs(note.updated_at / 1000000).fromNow()}
{dayjs(note.updated_at / 1000000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
</div>

View file

@ -477,11 +477,17 @@
<Badge type="muted" content={$i18n.t('Read Only')} />
{/if}
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
<Tooltip
content={dayjs(item.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs(item.updated_at * 1000).fromNow()}
{dayjs(item.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
</div>

View file

@ -7,7 +7,7 @@
dayjs.extend(relativeTime);
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@ -150,9 +150,15 @@
<div class="flex items-center gap-2 shrink-0">
{#if directory.updated_at}
<Tooltip content={dayjs(directory.updated_at * 1000).format('LLLL')}>
<Tooltip
content={dayjs(directory.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div class="text-xs text-gray-400">
{dayjs(directory.updated_at * 1000).fromNow()}
{dayjs(directory.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
{/if}

View file

@ -7,7 +7,7 @@
dayjs.extend(relativeTime);
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import { capitalizeFirstLetter, formatFileSize } from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
@ -170,9 +170,15 @@
<div class="flex items-center gap-2 shrink-0">
{#if file?.updated_at}
<Tooltip content={dayjs(file.updated_at * 1000).format('LLLL')}>
<Tooltip
content={dayjs(file.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div class="text-xs text-gray-400">
{dayjs(file.updated_at * 1000).fromNow()}
{dayjs(file.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>
{/if}

View file

@ -832,11 +832,15 @@
</div>
<Tooltip
content={dayjs(model.updated_at * 1000).format('LLLL')}
content={dayjs(model.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
className="hidden shrink-0 sm:flex"
>
<span class="text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-500"
>{dayjs(model.updated_at * 1000).fromNow()}</span
>{dayjs(model.updated_at * 1000)
.locale($i18n.language)
.fromNow()}</span
>
</Tooltip>

View file

@ -16,7 +16,7 @@
import PageEdit from '$lib/components/icons/PageEdit.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
const dispatch = createEventDispatcher();
export let onClose: Function = () => {};
@ -61,8 +61,7 @@
return {
...note,
type: 'note',
name: note.title,
description: dayjs(note.updated_at / 1000000).fromNow()
name: note.title
};
});
}
@ -193,7 +192,11 @@
{/if}
<Tooltip
content={item.description || decodeString(item?.name)}
content={item.type === 'note'
? dayjs(item.updated_at / 1000000)
.locale($i18n.language)
.fromNow()
: item.description || decodeString(item?.name)}
placement="top-start"
tippyOptions={{ zIndex: 100000 }}
>

View file

@ -604,14 +604,16 @@
</div>
<Tooltip
content={dayjs((prompt.updated_at ?? prompt.created_at) * 1000).format(
'LLLL'
)}
content={dayjs((prompt.updated_at ?? prompt.created_at) * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs((prompt.updated_at ?? prompt.created_at) * 1000).fromNow()}
{dayjs((prompt.updated_at ?? prompt.created_at) * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>

View file

@ -217,8 +217,8 @@
const renderDate = (timestamp: number) => {
const dateVal = timestamp * 1000;
return $i18n.t(formatDate(dateVal), {
LOCALIZED_TIME: dayjs(dateVal).format('LT'),
LOCALIZED_DATE: dayjs(dateVal).format('L')
LOCALIZED_TIME: dayjs(dateVal).locale($i18n.language).format('LT'),
LOCALIZED_DATE: dayjs(dateVal).locale($i18n.language).format('L')
});
};

View file

@ -9,7 +9,7 @@
dayjs.extend(relativeTime);
import { onMount, getContext, tick, onDestroy } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import {
WEBUI_NAME,
@ -476,14 +476,16 @@
</div>
<Tooltip
content={dayjs((skill.updated_at ?? skill.created_at) * 1000).format(
'LLLL'
)}
content={dayjs((skill.updated_at ?? skill.created_at) * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs((skill.updated_at ?? skill.created_at) * 1000).fromNow()}
{dayjs((skill.updated_at ?? skill.created_at) * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>

View file

@ -9,7 +9,7 @@
dayjs.extend(relativeTime);
import { onMount, getContext, tick, onDestroy } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import {
WEBUI_NAME,
@ -464,11 +464,17 @@
</div>
{/if}
<Tooltip content={dayjs(tool.updated_at * 1000).format('LLLL')}>
<Tooltip
content={dayjs(tool.updated_at * 1000)
.locale($i18n.language)
.format('LLLL')}
>
<div
class="shrink-0 truncate text-[0.6875rem] leading-5 text-gray-400 dark:text-gray-600"
>
{dayjs(tool.updated_at * 1000).fromNow()}
{dayjs(tool.updated_at * 1000)
.locale($i18n.language)
.fromNow()}
</div>
</Tooltip>

View file

@ -6,10 +6,10 @@
import XMark from '$lib/components/icons/XMark.svelte';
import Select from '$lib/components/common/Select.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let value = '';
export let placeholder = $i18n.t('Tag');
export let placeholder: string | undefined = undefined;
export let align = 'start';
export let onChange: (value: string) => void = () => {};
export let triggerClass =
@ -24,7 +24,7 @@
<Select
bind:value
{items}
{placeholder}
placeholder={placeholder ?? $i18n.t('Tag')}
{align}
{triggerClass}
{itemClass}
@ -38,7 +38,7 @@
{#if value}
{selectedLabel}
{:else}
{placeholder}
{placeholder ?? $i18n.t('Tag')}
{/if}
</div>

View file

@ -5,14 +5,14 @@
import Check from '$lib/components/icons/Check.svelte';
import Select from '$lib/components/common/Select.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let value = '';
export let placeholder = $i18n.t('Select view');
export let placeholder: string | undefined = undefined;
export let align = 'start';
export let onChange: (value: string) => void = () => {};
const items = [
$: items = [
{ value: '', label: $i18n.t('All') },
{ value: 'created', label: $i18n.t('Created by you') },
{ value: 'shared', label: $i18n.t('Shared with you') }
@ -22,7 +22,7 @@
<Select
bind:value
{items}
{placeholder}
placeholder={placeholder ?? $i18n.t('Select view')}
{align}
triggerClass="relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[0.8125rem] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
labelClass="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"

View file

@ -102,5 +102,18 @@ import 'dayjs/locale/zh';
import 'dayjs/locale/zh-tw';
import 'dayjs/locale/et';
import 'dayjs/locale/en-gb';
import 'dayjs/locale/bn-bd';
import 'dayjs/locale/fr-ca';
import 'dayjs/locale/ms-my';
import 'dayjs/locale/pt-br';
import 'dayjs/locale/pa-in';
import 'dayjs/locale/ug-cn';
import 'dayjs/locale/zh-cn';
/** @param {string | undefined} language */
export const getDayjsLocale = (language) => {
const code = (language ?? 'en').toLowerCase();
return [code, code.split('-')[0], 'en'].find((candidate) => dayjs.Ls[candidate]);
};
export default dayjs;

View file

@ -3,6 +3,7 @@ import resourcesToBackend from 'i18next-resources-to-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { i18n as i18nType } from 'i18next';
import { writable } from 'svelte/store';
import dayjs, { getDayjsLocale } from '$lib/dayjs';
import type { I18nOverrides } from '$lib/utils/translationDictionary';
import { assembleSettingsTranslations } from './settings-translations';
@ -47,6 +48,7 @@ const createI18nStore = (i18n: i18nType) => {
});
i18n.on('added', () => i18nWritable.set(i18n));
i18n.on('languageChanged', (lang) => {
dayjs.locale(getDayjsLocale(lang));
i18nWritable.set(i18n);
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', lang);
@ -87,18 +89,16 @@ const toBundleCode = (code: string) => {
export const initI18n = (defaultLocale?: string, value: I18nOverrides = {}) => {
overrides = value;
const detectionOrder = defaultLocale
? ['querystring', 'localStorage']
: ['querystring', 'localStorage', 'navigator'];
const fallbackDefaultLocale = defaultLocale ? [defaultLocale, 'en-US'] : ['en-US'];
const detector = new LanguageDetector();
detector.addDetector({ name: 'defaultLocale', lookup: () => defaultLocale });
return i18next
.use(resourcesToBackend(loadResource))
.use(LanguageDetector)
.use(detector)
.init({
debug: false,
detection: {
order: detectionOrder,
order: ['querystring', 'localStorage', 'defaultLocale', 'navigator'],
caches: ['localStorage'],
lookupQuerystring: 'lang',
lookupLocalStorage: 'locale',
@ -106,7 +106,7 @@ export const initI18n = (defaultLocale?: string, value: I18nOverrides = {}) => {
},
fallbackLng: {
fr: ['fr-FR'],
default: fallbackDefaultLocale
default: ['en-US']
},
ns: 'translation',
keySeparator: false,
@ -122,9 +122,15 @@ const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next);
export const getLanguages = async () => languages;
// Serialize switches so a slower download cannot overwrite a newer selection.
let languageChange = Promise.resolve();
export const changeLanguage = (lang: string) => {
document.documentElement.setAttribute('lang', lang);
return i18next.changeLanguage(lang);
const change = languageChange.then(() => i18next.changeLanguage(toBundleCode(lang)));
languageChange = change.then(
() => {},
() => {}
);
return change;
};
export default i18n;

View file

@ -515,16 +515,16 @@ const messageTimestampDate = (inputDate) => {
return Number.isNaN(date.getTime()) ? null : date;
};
export const formatMessageTimestamp = (inputDate) =>
messageTimestampDate(inputDate)?.toLocaleString(undefined, {
export const formatMessageTimestamp = (inputDate, locale: string) =>
messageTimestampDate(inputDate)?.toLocaleString(locale, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
}) ?? '';
export const formatMessageTimestampFull = (inputDate) =>
messageTimestampDate(inputDate)?.toLocaleString(undefined, {
export const formatMessageTimestampFull = (inputDate, locale: string) =>
messageTimestampDate(inputDate)?.toLocaleString(locale, {
weekday: 'long',
year: 'numeric',
month: 'long',

54
src/lib/utils/schedule.ts Normal file
View file

@ -0,0 +1,54 @@
import type { i18n as I18n } from 'i18next';
export const formatSchedule = (rrule: string, i18n: I18n): string => {
const match = rrule.match(/DTSTART[^:]*:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/i);
if (/COUNT=1(?!\d)/.test(rrule)) {
if (match) {
const d = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`);
return `${i18n.t('Once')} · ${d.toLocaleDateString(i18n.language, {
month: 'short',
day: 'numeric'
})} ${d.toLocaleTimeString(i18n.language, { hour: 'numeric', minute: '2-digit' })}`;
}
return i18n.t('Once');
}
const parts: Record<string, string> = {};
rrule
.split(/\s+/)
.filter((line) => !line.toUpperCase().startsWith('DTSTART'))
.join('')
.replace('RRULE:', '')
.split(';')
.forEach((part) => {
const [key, value] = part.split('=');
if (key && value) parts[key] = value;
});
const freq = parts.FREQ || '';
const hour = parseInt(parts.BYHOUR || match?.[4] || '0');
const minute = (parts.BYMINUTE || match?.[5] || '0').padStart(2, '0');
const interval = parseInt(parts.INTERVAL || '1');
const time = new Date(2000, 0, 1, hour, Number(minute)).toLocaleTimeString(i18n.language, {
hour: 'numeric',
minute: '2-digit'
});
if (freq === 'MINUTELY')
return interval === 1
? i18n.t('Every minute')
: i18n.t('Every {{count}} minutes', { count: interval });
if (freq === 'HOURLY')
return interval === 1 ? i18n.t('Hourly') : i18n.t('Every {{count}} hours', { count: interval });
if (freq === 'DAILY') return `${i18n.t('Daily at')} ${time}`;
if (freq === 'WEEKLY')
return parts.BYDAY
? `${parts.BYDAY.split(',')
.map((day) => i18n.t(day[0] + day.slice(1).toLowerCase(), { context: 'day_of_week' }))
.join(', ')} ${i18n.t('at')} ${time}`
: `${i18n.t('Weekly at')} ${time}`;
if (freq === 'MONTHLY')
return `${i18n.t('Monthly')} ${parts.BYMONTHDAY ?? '1'} ${i18n.t('at')} ${time}`;
return rrule;
};

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { formatSchedule } from '$lib/utils/schedule';
import { onMount, onDestroy, getContext } from 'svelte';
import dayjs from '$lib/dayjs';
@ -101,6 +102,8 @@
});
};
$: if (loaded && $i18n) syncHeader();
const handleSearchInput = () => {
if (!loaded) return;
@ -225,7 +228,9 @@
const formatLastRun = (automation: AutomationResponse): string => {
return automation.last_run_at
? dayjs(automation.last_run_at / 1000000).fromNow()
? dayjs(automation.last_run_at / 1000000)
.locale($i18n.language)
.fromNow()
: $i18n.t('Never');
};
@ -307,55 +312,6 @@
await getAutomationList();
};
const formatRRule = (rrule: string): string => {
const match = rrule.match(/DTSTART[^:]*:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/i);
// Detect one-time schedule (ONCE)
if (/COUNT=1(?!\d)/.test(rrule)) {
if (match) {
const d = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`);
return `Once · ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}`;
}
return 'Once';
}
const parts: Record<string, string> = {};
rrule
.split(/\s+/)
.filter((line) => !line.toUpperCase().startsWith('DTSTART'))
.join('')
.replace('RRULE:', '')
.split(';')
.forEach((p) => {
const [k, v] = p.split('=');
if (k && v) parts[k] = v;
});
const freq = parts.FREQ || '';
const hour = parseInt(parts.BYHOUR || match?.[4] || '0');
const min = (parts.BYMINUTE || match?.[5] || '0').padStart(2, '0');
const iv = parseInt(parts.INTERVAL || '1');
const ampm = hour >= 12 ? 'PM' : 'AM';
const h12 = hour % 12 || 12;
const time = `${h12}:${min} ${ampm}`;
if (freq === 'MINUTELY') return iv === 1 ? 'Every minute' : `Every ${iv} minutes`;
if (freq === 'HOURLY') return iv === 1 ? 'Hourly' : `Every ${iv} hours`;
if (freq === 'DAILY') return `Daily at ${time}`;
if (freq === 'WEEKLY') {
const days = parts.BYDAY || '';
return days ? `${days} at ${time}` : `Weekly at ${time}`;
}
if (freq === 'MONTHLY')
return `Monthly on the ${parts.BYMONTHDAY || '1'}${ordinal(parts.BYMONTHDAY || '1')} at ${time}`;
return rrule;
};
const ordinal = (n: string): string => {
const num = parseInt(n);
if (num % 10 === 1 && num !== 11) return 'st';
if (num % 10 === 2 && num !== 12) return 'nd';
if (num % 10 === 3 && num !== 13) return 'rd';
return 'th';
};
onMount(() => {
if (
!($config?.features as any)?.enable_automations ||
@ -366,7 +322,6 @@
}
loaded = true;
syncHeader();
ensureChannels();
const onKeyDown = (event: KeyboardEvent) => {
@ -617,7 +572,9 @@
<Tooltip
content={automation.last_run_at
? dayjs(automation.last_run_at / 1000000).format('LLLL')
? dayjs(automation.last_run_at / 1000000)
.locale($i18n.language)
.format('LLLL')
: $i18n.t('Never')}
>
<div
@ -635,11 +592,11 @@
class="hidden max-w-56 shrink-0 self-center truncate text-right text-[0.6875rem] leading-5 text-gray-500 dark:text-gray-500 md:block"
>
<Tooltip
content={`${formatRRule(automation.data.rrule)} · ${formatDestination(automation)}`}
content={`${formatSchedule(automation.data.rrule, $i18n)} · ${formatDestination(automation)}`}
className="min-w-0"
>
<div class="truncate">
{formatRRule(automation.data.rrule)} · {formatDestination(automation)}
{formatSchedule(automation.data.rrule, $i18n)} · {formatDestination(automation)}
</div>
</Tooltip>
</div>

View file

@ -22,7 +22,7 @@
import Check from '$lib/components/icons/Check.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
let loaded = false;
let calendars: CalendarModel[] = [];
@ -261,7 +261,7 @@
<button
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => navigateCalendar(-1)}
aria-label="Previous"
aria-label={$i18n.t('Previous')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -280,7 +280,7 @@
<button
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => navigateCalendar(1)}
aria-label="Next"
aria-label={$i18n.t('Next')}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -82,7 +82,6 @@
import MfaManagement from '$lib/components/auth/MfaManagement.svelte';
import { getOutputText } from '$lib/components/chat/Messages/structuredOutput';
import { getUserSettings } from '$lib/apis/users';
import dayjs from 'dayjs';
import { getChannels } from '$lib/apis/channels';
import { resolveTerminalConnection, terminalRequest } from '$lib/apis/terminal';
@ -1308,10 +1307,7 @@
// Initialize i18n even if we didn't get a backend config,
// so `/error` can show something that's not `undefined`.
await initI18n(
localStorage?.locale ?? backendConfig?.default_locale,
backendConfig?.i18n ?? {}
);
await initI18n(backendConfig?.default_locale, backendConfig?.i18n ?? {});
if (!localStorage.locale) {
const languages = await getLanguages();
const browserLanguages = navigator.languages
@ -1321,7 +1317,6 @@
? backendConfig.default_locale
: bestMatchingLanguage(languages, browserLanguages, 'en-US');
await changeLanguage(lang);
dayjs.locale(lang);
}
if (backendConfig) {

View file

@ -17,7 +17,7 @@
import { getModels } from '$lib/apis';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
dayjs.extend(localizedFormat);
let loaded = false;
@ -205,9 +205,13 @@
</h1>
<time
class="ms-auto shrink-0 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500"
datetime={dayjs(chat.chat.timestamp || chat.created_at * 1000).toISOString()}
datetime={dayjs(chat.chat.timestamp || chat.created_at * 1000)
.locale($i18n.language)
.toISOString()}
>
{dayjs(chat.chat.timestamp || chat.created_at * 1000).format('LLL')}
{dayjs(chat.chat.timestamp || chat.created_at * 1000)
.locale($i18n.language)
.format('LLL')}
</time>
</div>
</header>