diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index a0855f4689..a95d2e78d4 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1697,6 +1697,7 @@ if default_prompt_suggestions == []: ] DEFAULT_PROMPT_SUGGESTIONS = default_prompt_suggestions +DEFAULT_PROMPT_SUGGESTIONS_I18N = {} try: model_order_list = JSONCodec.loads(os.getenv('MODEL_ORDER_LIST', '[]')) @@ -3096,6 +3097,7 @@ DEFAULT_CONFIG = { 'ui.default_pinned_models': DEFAULT_PINNED_MODELS, 'ui.default_interface_settings': DEFAULT_INTERFACE_SETTINGS, 'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS, + 'ui.prompt_suggestions_i18n': DEFAULT_PROMPT_SUGGESTIONS_I18N, 'ui.model_order_list': MODEL_ORDER_LIST, 'models.default_metadata': DEFAULT_MODEL_METADATA, 'models.default_params': DEFAULT_MODEL_PARAMS, diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 0ecb9f2f2e..726882afb0 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2261,6 +2261,7 @@ async def get_app_config(request: Request): 'ui.default_pinned_models', 'ui.default_interface_settings', 'ui.prompt_suggestions', + 'ui.prompt_suggestions_i18n', 'code_execution.engine', 'code_interpreter.engine', 'audio.tts.engine', @@ -2361,6 +2362,7 @@ async def get_app_config(request: Request): 'default_models': config.get('ui.default_models'), 'default_pinned_models': config.get('ui.default_pinned_models'), 'default_prompt_suggestions': config.get('ui.prompt_suggestions'), + 'default_prompt_suggestions_i18n': config.get('ui.prompt_suggestions_i18n'), **({'user_count': user_count} if user_count is not None else {}), 'code': { 'engine': config.get('code_execution.engine'), diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 17083e4b42..5094276bf4 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -80,6 +80,7 @@ class ModelMeta(BaseModel): profile_image_url: str | None = None description: str | None = Field(default=None, description='User-facing description of the model.') + i18n: dict[str, Any] | None = None capabilities: dict | None = None knowledge: list[Any] | None = None diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 3e37f05eef..758d5ffbb4 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -1,8 +1,7 @@ from __future__ import annotations -import copy import logging -from typing import Optional +from typing import Any, Optional import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request @@ -808,17 +807,24 @@ class PromptSuggestion(BaseModel): class SetDefaultSuggestionsForm(BaseModel): suggestions: list[PromptSuggestion] + i18n: dict[str, Any] | None = None -@router.post('/suggestions', response_model=list[PromptSuggestion]) +@router.post('/suggestions', response_model=dict) async def set_default_suggestions( request: Request, form_data: SetDefaultSuggestionsForm, user=Depends(get_admin_user), ): data = form_data.model_dump() - await Config.upsert({'ui.prompt_suggestions': data['suggestions']}) + await Config.upsert( + { + 'ui.prompt_suggestions': data['suggestions'], + 'ui.prompt_suggestions_i18n': data.get('i18n') or {}, + } + ) suggestions = await Config.get('ui.prompt_suggestions') + suggestions_i18n = await Config.get('ui.prompt_suggestions_i18n') await publish_event( request, EVENTS.CONFIG_SUGGESTIONS_UPDATED, @@ -827,7 +833,7 @@ async def set_default_suggestions( subject_type='config', data={'count': len(suggestions or [])}, ) - return suggestions + return {'suggestions': suggestions, 'i18n': suggestions_i18n} ############################ diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index edb80028ed..ec5d4a12ab 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -742,7 +742,11 @@ export const setSubagentsConfig = async (token: string, config: object) => { return res.json(); }; -export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => { +export const setDefaultPromptSuggestions = async ( + token: string, + promptSuggestions: any[], + promptSuggestionsI18n: Record = {} +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/configs/suggestions`, { @@ -752,7 +756,8 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio Authorization: `Bearer ${token}` }, body: JSON.stringify({ - suggestions: promptSuggestions + suggestions: promptSuggestions, + i18n: promptSuggestionsI18n }) }) .then(async (res) => { diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 04d8323177..a6ece21d55 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1759,6 +1759,7 @@ export interface ModelConfig { export interface ModelMeta { toolIds: never[]; description?: string; + i18n?: Record>; hidden?: boolean; capabilities?: object; profile_image_url?: string; diff --git a/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte b/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte index 196fc212ab..df48e92b85 100644 --- a/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte +++ b/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte @@ -8,12 +8,14 @@ import { DEFAULT_CAPABILITIES } from '$lib/constants'; import { getModelsConfig, setModelsConfig, setDefaultPromptSuggestions } from '$lib/apis/configs'; import { getBackendConfig } from '$lib/apis'; + import { getLanguages } from '$lib/i18n'; import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte'; import DefaultFeatures from '$lib/components/workspace/Models/DefaultFeatures.svelte'; import BuiltinTools from '$lib/components/workspace/Models/BuiltinTools.svelte'; - import PromptSuggestions from '$lib/components/workspace/Models/PromptSuggestions.svelte'; + import LanguageModeSelect from '$lib/components/workspace/Models/LanguageModeSelect.svelte'; + import LocalizedPromptSuggestions from '$lib/components/workspace/Models/LocalizedPromptSuggestions.svelte'; export let initHandler = () => {}; export let dirty = false; @@ -32,6 +34,9 @@ let defaultParams = {}; let builtinTools = {}; let promptSuggestions = []; + let promptSuggestionsI18n = {}; + let languages = []; + let editingLocale = ''; $: configuredParams = Object.entries(defaultParams ?? {}).filter( ([_, value]) => value !== null && value !== '' && value !== undefined @@ -40,6 +45,10 @@ $: availableFeatures = enabledCapabilities .filter(([key]) => ['web_search', 'code_interpreter', 'image_generation'].includes(key)) .map(([key]) => key); + $: translatedPromptLocales = Object.entries(promptSuggestionsI18n ?? {}) + .filter(([_, value]) => Array.isArray(value?.suggestion_prompts)) + .map(([locale]) => locale); + $: editingLocaleLabel = languages.find((language) => language.code === editingLocale)?.title; const getSnapshot = () => JSON.stringify({ @@ -47,7 +56,8 @@ defaultFeatureIds, defaultParams: Object.fromEntries(configuredParams), builtinTools, - promptSuggestions: promptSuggestions.filter((p) => p.content !== '') + promptSuggestions: promptSuggestions.filter((p) => p.content !== ''), + promptSuggestionsI18n }); const updateDirty = async () => { @@ -74,6 +84,8 @@ defaultParams = config?.DEFAULT_MODEL_PARAMS ?? {}; promptSuggestions = $appConfig?.default_prompt_suggestions ?? []; + promptSuggestionsI18n = $appConfig?.default_prompt_suggestions_i18n ?? {}; + languages = await getLanguages(); savedSnapshot = getSnapshot(); dirty = false; loading = false; @@ -104,7 +116,13 @@ if (res) { config = res; promptSuggestions = promptSuggestions.filter((p) => p.content !== ''); - promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions); + const suggestionsRes = await setDefaultPromptSuggestions( + localStorage.token, + promptSuggestions, + promptSuggestionsI18n + ); + promptSuggestions = suggestionsRes?.suggestions ?? promptSuggestions; + promptSuggestionsI18n = suggestionsRes?.i18n ?? promptSuggestionsI18n; await appConfig.set(await getBackendConfig()); savedSnapshot = getSnapshot(); dirty = false; @@ -236,12 +254,26 @@ {#if showPromptSuggestions}
- + + +
{/if} diff --git a/src/lib/components/chat/ChatPlaceholder.svelte b/src/lib/components/chat/ChatPlaceholder.svelte index 1595701d13..75193c907b 100644 --- a/src/lib/components/chat/ChatPlaceholder.svelte +++ b/src/lib/components/chat/ChatPlaceholder.svelte @@ -10,6 +10,12 @@ import Suggestions from './Suggestions.svelte'; import { sanitizeResponseContent } from '$lib/utils'; + import { + resolveLocalizedModelDescription, + resolveLocalizedModelName, + resolveLocalizedModelPromptSuggestions, + resolveLocalizedPromptSuggestions + } from '$lib/utils/localizedContent'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import EyeSlash from '$lib/components/icons/EyeSlash.svelte'; @@ -23,12 +29,27 @@ let mounted = false; let selectedModelIdx = 0; + let selectedModel; + let selectedModelName = ''; + let selectedModelDescription = ''; + let selectedSuggestionPrompts = []; $: if (modelIds.length > 0) { selectedModelIdx = models.length - 1; } $: models = modelIds.map((id) => $_models.find((m) => m.id === id)); + $: selectedModel = atSelectedModel ?? models[selectedModelIdx]; + $: selectedModelName = resolveLocalizedModelName(selectedModel, $i18n.language); + $: selectedModelDescription = resolveLocalizedModelDescription(selectedModel, $i18n.language); + $: selectedSuggestionPrompts = + resolveLocalizedModelPromptSuggestions(atSelectedModel, $i18n.language) ?? + resolveLocalizedModelPromptSuggestions(models[selectedModelIdx], $i18n.language) ?? + resolveLocalizedPromptSuggestions( + $config?.default_prompt_suggestions ?? [], + $config?.default_prompt_suggestions_i18n ?? {}, + $i18n.language + ); onMount(() => { mounted = true; @@ -48,9 +69,7 @@ ') + sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '
') ) )} placement="right" @@ -90,23 +109,21 @@ >
- {#if models[selectedModelIdx]?.name} - {models[selectedModelIdx]?.name} + {#if selectedModelName} + {selectedModelName} {:else} {$i18n.t('Hello, {{name}}', { name: $user?.name })} {/if}
- {#if models[selectedModelIdx]?.info?.meta?.description ?? null} + {#if selectedModelDescription}
{@html DOMPurify.sanitize( marked.parse( - sanitizeResponseContent( - models[selectedModelIdx]?.info?.meta?.description - ).replaceAll('\n', '
') + sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '
') ) )}
@@ -138,10 +155,7 @@
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index ca982a1896..0461dd27c7 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -68,6 +68,7 @@ import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; import { matchKeybinding, Shortcut } from '$lib/shortcuts'; + import { resolveLocalizedModelName } from '$lib/utils/localizedContent'; import { createNoteHandler } from '../notes/utils'; import { getSuggestionRenderer } from '../common/RichTextInput/suggestions'; @@ -673,6 +674,7 @@ $: showCommands = ['/', '#', '@', '$', ':'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2); let suggestions = null; + $: atSelectedModelName = resolveLocalizedModelName(atSelectedModel, $i18n.language); let showTools = false; let showSkills = false; @@ -1874,7 +1876,7 @@ src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${$models.find((model) => model.id === atSelectedModel.id).id}&lang=${$i18n.language}`} />
- {atSelectedModel.name} + {atSelectedModelName}
diff --git a/src/lib/components/chat/MessageInput/Commands/AtCommands.svelte b/src/lib/components/chat/MessageInput/Commands/AtCommands.svelte index 2c8b005720..4b40400a8f 100644 --- a/src/lib/components/chat/MessageInput/Commands/AtCommands.svelte +++ b/src/lib/components/chat/MessageInput/Commands/AtCommands.svelte @@ -15,6 +15,10 @@ import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge'; import { searchFiles } from '$lib/apis/terminal'; import { decodeString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils'; + import { + resolveLocalizedModelDescription, + resolveLocalizedModelName + } from '$lib/utils/localizedContent'; import { isTemporaryChatId } from '$lib/utils/chatId'; import Tooltip from '$lib/components/common/Tooltip.svelte'; @@ -46,9 +50,9 @@ .filter((model) => !model?.info?.meta?.hidden) .map((model) => ({ ...model, - modelName: model?.name, + modelName: resolveLocalizedModelName(model, $i18n.language), tags: model?.info?.meta?.tags?.map((tag: any) => tag.name).join(' '), - desc: model?.info?.meta?.description + desc: resolveLocalizedModelDescription(model, $i18n.language) })); $: fuse = new Fuse(modelItems, { @@ -341,7 +345,7 @@
{model?.name { // LICENSE covers this Open WebUI fallback logo. @@ -351,7 +355,7 @@ }} />
- {model.name} + {resolveLocalizedModelName(model, $i18n.language)}
diff --git a/src/lib/components/chat/MessageInput/Commands/Models.svelte b/src/lib/components/chat/MessageInput/Commands/Models.svelte index 12fca4a24a..c159a4f8f6 100644 --- a/src/lib/components/chat/MessageInput/Commands/Models.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Models.svelte @@ -7,6 +7,10 @@ import { models } from '$lib/stores'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import Tooltip from '$lib/components/common/Tooltip.svelte'; + import { + resolveLocalizedModelDescription, + resolveLocalizedModelName + } from '$lib/utils/localizedContent'; const i18n = getContext('i18n'); @@ -22,9 +26,9 @@ .map((model) => { const _item = { ...model, - modelName: model?.name, + modelName: resolveLocalizedModelName(model, $i18n.language), tags: model?.info?.meta?.tags?.map((tag) => tag.name).join(' '), - desc: model?.info?.meta?.description + desc: resolveLocalizedModelDescription(model, $i18n.language) }; return _item; }), @@ -85,7 +89,7 @@
{model?.name { // LICENSE covers this Open WebUI fallback logo. @@ -95,7 +99,7 @@ }} />
- {model.name} + {resolveLocalizedModelName(model, $i18n.language)}
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index f6a6eb2f61..902933314b 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -37,6 +37,7 @@ removeAllDetails } from '$lib/utils'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + import { resolveLocalizedModelName } from '$lib/utils/localizedContent'; import equal from 'fast-deep-equal'; import Name from './Name.svelte'; @@ -181,6 +182,7 @@ let showDeleteConfirm = false; let model = null; + $: localizedModelName = model ? resolveLocalizedModelName(model, $i18n.language) : message.model; $: model = $models.find((m) => m.id === message.model); $: statusEntries = message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]; @@ -672,9 +674,9 @@
{#if !compactPreview} - + - {model?.name ?? message.model} + {localizedModelName} diff --git a/src/lib/components/chat/ModelSelector.svelte b/src/lib/components/chat/ModelSelector.svelte index 23c54ed627..f4591b0b73 100644 --- a/src/lib/components/chat/ModelSelector.svelte +++ b/src/lib/components/chat/ModelSelector.svelte @@ -5,6 +5,7 @@ import Selector from './ModelSelector/Selector.svelte'; import { updateUserSettings } from '$lib/apis/users'; + import { resolveLocalizedModelName } from '$lib/utils/localizedContent'; import equal from 'fast-deep-equal'; const i18n = getContext('i18n'); @@ -69,7 +70,7 @@ placeholder={$i18n.t('Select a model')} items={$models.map((model) => ({ value: model.id, - label: model.name, + label: resolveLocalizedModelName(model, $i18n.language), model: model }))} {pinModelHandler} diff --git a/src/lib/components/chat/ModelSelector/ModelItem.svelte b/src/lib/components/chat/ModelSelector/ModelItem.svelte index 6fd458259c..b15041b08c 100644 --- a/src/lib/components/chat/ModelSelector/ModelItem.svelte +++ b/src/lib/components/chat/ModelSelector/ModelItem.svelte @@ -9,6 +9,7 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import { copyToClipboard, sanitizeResponseContent } from '$lib/utils'; + import { resolveLocalizedModelDescription } from '$lib/utils/localizedContent'; import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte'; import Check from '$lib/components/icons/Check.svelte'; import ModelItemMenu from './ModelItemMenu.svelte'; @@ -33,6 +34,8 @@ export let onClick: () => void = () => {}; + $: localizedDescription = resolveLocalizedModelDescription(item.model, $i18n.language); + const copyLinkHandler = async (model) => { const baseUrl = window.location.origin; const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`); @@ -247,10 +250,10 @@ {/if} - {#if item.model?.info?.meta?.description} + {#if localizedDescription} ') + sanitizeResponseContent(localizedDescription).replaceAll('\n', '
') )}`} >
diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index 9a25162d64..2535320849 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -34,6 +34,10 @@ } from '$lib/stores'; import { toast } from 'svelte-sonner'; import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils'; + import { + resolveLocalizedModelDescription, + resolveLocalizedModelName + } from '$lib/utils/localizedContent'; import { getModels } from '$lib/apis'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; @@ -288,9 +292,9 @@ items.map((item) => { const _item = { ...item, - modelName: item.model?.name, + modelName: resolveLocalizedModelName(item.model, $i18n.language), tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '), - desc: item.model?.info?.meta?.description + desc: resolveLocalizedModelDescription(item.model, $i18n.language) }; return _item; }), @@ -306,9 +310,9 @@ items.map((item) => { const _item = { ...item, - modelName: item.model?.name, + modelName: resolveLocalizedModelName(item.model, $i18n.language), tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '), - desc: item.model?.info?.meta?.description + desc: resolveLocalizedModelDescription(item.model, $i18n.language) }; return _item; }) diff --git a/src/lib/components/chat/Overview/Node.svelte b/src/lib/components/chat/Overview/Node.svelte index 49725a84df..08b53edb66 100644 --- a/src/lib/components/chat/Overview/Node.svelte +++ b/src/lib/components/chat/Overview/Node.svelte @@ -7,6 +7,7 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import Heart from '$lib/components/icons/Heart.svelte'; import { getOutputText } from '../Messages/structuredOutput'; + import { resolveLocalizedModelName } from '$lib/utils/localizedContent'; const i18n = getContext('i18n'); @@ -17,6 +18,9 @@ getOutputText(nodeData?.message?.output) || nodeData?.message?.content || ''; $: messageContent = getMessageContent(data); + $: modelName = data?.model + ? resolveLocalizedModelName(data.model, $i18n.language) + : (data?.message?.model ?? 'Assistant');
- {data?.model?.name ?? data?.message?.model ?? 'Assistant'} + {modelName}
+ {/if} + + + + + +{/if} diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 8d15ff4726..715effe3c8 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -9,6 +9,7 @@ import { getSkills } from '$lib/apis/skills'; import { getFunctions } from '$lib/apis/functions'; import { getModelsDefaults } from '$lib/apis/configs'; + import { getLanguages } from '$lib/i18n'; import { getBaseModelTags, getModelTags } from '$lib/apis/models'; import { getVoices } from '$lib/apis/audio'; @@ -28,12 +29,15 @@ import DefaultFiltersSelector from './DefaultFiltersSelector.svelte'; import DefaultFeatures from './DefaultFeatures.svelte'; import BuiltinTools from './BuiltinTools.svelte'; + import LanguageModeSelect from './LanguageModeSelect.svelte'; + import LocalizedPromptSuggestions from './LocalizedPromptSuggestions.svelte'; import PromptSuggestions from './PromptSuggestions.svelte'; import TerminalSelector from './TerminalSelector.svelte'; import TTSVoiceInput from './TTSVoiceInput.svelte'; import AccessControlModal from '../common/AccessControlModal.svelte'; import AccessButton from '$lib/components/common/AccessButton.svelte'; import { extractInputVariables } from '$lib/utils'; + import { pruneEmptyLocaleEntries } from '$lib/utils/localizedContent'; const i18n: any = getContext('i18n'); @@ -63,6 +67,8 @@ let id = ''; let name = ''; + let languages: { code: string; title: string }[] = []; + let editingLocale = ''; let enableDescription = true; @@ -86,6 +92,7 @@ // https://docs.openwebui.com/license. profile_image_url: `${WEBUI_BASE_URL}/static/favicon.png`, description: '', + i18n: {}, suggestion_prompts: null, tags: [] }, @@ -117,6 +124,42 @@ export let suggestionTags: { name: string }[] = []; let voices: { id: string; name?: string }[] = []; + $: translatedLocales = Object.entries(info?.meta?.i18n ?? {}) + .filter(([_, value]: [string, any]) => Object.keys(value ?? {}).length > 0) + .map(([locale]) => locale); + $: editingLocaleLabel = languages.find((language) => language.code === editingLocale)?.title; + + const localizedField = (field: string) => info?.meta?.i18n?.[editingLocale]?.[field] ?? ''; + const setLocalizedField = (field: string, value: string) => { + if (!editingLocale) return; + + info.meta.i18n = { + ...(info.meta.i18n ?? {}), + [editingLocale]: { + ...(info.meta.i18n?.[editingLocale] ?? {}), + [field]: value + } + }; + info = info; + }; + + const clearLocalizedField = (field: string) => { + if (!editingLocale || !info.meta.i18n?.[editingLocale]) return; + + const nextLocale = { ...info.meta.i18n[editingLocale] }; + delete nextLocale[field]; + + const nextI18n = { ...(info.meta.i18n ?? {}) }; + if (Object.keys(nextLocale).length === 0) { + delete nextI18n[editingLocale]; + } else { + nextI18n[editingLocale] = nextLocale; + } + + info.meta.i18n = nextI18n; + info = info; + }; + const chatVariableKeyRegex = /^[a-z][a-z0-9_]*$/; const getChatVariablesPreview = (prompt: string) => { const variables = extractInputVariables(prompt); @@ -336,6 +379,11 @@ } } + info.meta.i18n = pruneEmptyLocaleEntries(info.meta.i18n); + if (Object.keys(info.meta.i18n).length === 0) { + delete info.meta.i18n; + } + if (terminalId) { info.meta.terminalId = terminalId; } else { @@ -375,6 +423,7 @@ }; onMount(async () => { + languages = await getLanguages(); await tools.set((await getTools(localStorage.token).catch(() => null)) ?? []); skillsList = (await getSkills(localStorage.token).catch(() => null)) ?? []; if (!$functions) { @@ -483,6 +532,7 @@ ) ) }; + info.meta.i18n = info.meta.i18n ?? {}; console.log(model); } @@ -676,11 +726,28 @@
- + setLocalizedField('name', (e.currentTarget as HTMLInputElement).value)} + /> + {:else} + + {/if} + +
+ {#if editingLocale} +
+ + {localizedField('name') + ? $i18n.t('Translated') + : $i18n.t('Using default')} + +
+ + {#if localizedField('name')} + + {/if} +
+
+ {/if} + + +
+ +
@@ -723,29 +820,63 @@
- {$i18n.t('Description')} + {editingLocale + ? $i18n.t('Description ({{language}})', { + language: editingLocaleLabel || editingLocale + }) + : $i18n.t('Description')}
- + {#if editingLocale} +
+ + {#if localizedField('description')} + + {/if} +
+ {:else} + + {/if}
- {#if enableDescription} + {#if editingLocale} +
@@ -187,8 +213,7 @@ type="button" aria-label={$i18n.t('Remove prompt suggestion')} on:click={() => { - promptSuggestions.splice(promptIdx, 1); - promptSuggestions = promptSuggestions; + updateSuggestions(promptSuggestions.filter((_, index) => index !== promptIdx)); }} > diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 597372090b..f688c2338d 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -332,6 +332,7 @@ type Config = { default_models: string; default_pinned_models?: string | null; default_prompt_suggestions: PromptSuggestion[]; + default_prompt_suggestions_i18n?: Record; features: { auth: boolean; auth_trusted_header: boolean; diff --git a/src/lib/utils/localizedContent.ts b/src/lib/utils/localizedContent.ts new file mode 100644 index 0000000000..3bcdb6b60e --- /dev/null +++ b/src/lib/utils/localizedContent.ts @@ -0,0 +1,87 @@ +type LocaleEntry = Record; +type LocalizedMap = Record; + +export type PromptSuggestion = { + title: [string, string] | string[]; + content: string; + [key: string]: any; +}; + +const isPresent = (value: unknown) => typeof value === 'string' && value.trim() !== ''; + +export const getLocaleCandidates = (locale?: string | null) => { + if (!locale) return []; + const normalized = locale.trim(); + if (!normalized) return []; + + const base = normalized.split('-')[0]; + return base && base !== normalized ? [normalized, base] : [normalized]; +}; + +export const resolveLocalizedString = ( + fallback: string | null | undefined, + i18n: LocalizedMap | null | undefined, + locale: string | null | undefined, + key: string +) => { + for (const candidate of getLocaleCandidates(locale)) { + const value = i18n?.[candidate]?.[key]; + if (isPresent(value)) return value; + } + + return fallback ?? ''; +}; + +export const resolveLocalizedModelName = (model: any, locale?: string | null) => { + const meta = model?.info?.meta ?? model?.meta; + const info = model?.info ?? model; + return resolveLocalizedString(model?.name ?? info?.name ?? model?.id, meta?.i18n, locale, 'name'); +}; + +export const resolveLocalizedModelDescription = (model: any, locale?: string | null) => { + const meta = model?.info?.meta ?? model?.meta; + return resolveLocalizedString(meta?.description, meta?.i18n, locale, 'description'); +}; + +export const resolveLocalizedPromptSuggestions = ( + fallback: PromptSuggestion[] | null | undefined, + i18n: LocalizedMap | null | undefined, + locale?: string | null +) => { + for (const candidate of getLocaleCandidates(locale)) { + const prompts = i18n?.[candidate]?.suggestion_prompts ?? i18n?.[candidate]; + if (Array.isArray(prompts)) return prompts; + } + + return fallback ?? []; +}; + +export const resolveLocalizedModelPromptSuggestions = (model: any, locale?: string | null) => { + const meta = model?.info?.meta ?? model?.meta; + + for (const candidate of getLocaleCandidates(locale)) { + const prompts = meta?.i18n?.[candidate]?.suggestion_prompts; + if (Array.isArray(prompts)) return prompts; + } + + return meta?.suggestion_prompts ?? null; +}; + +export const pruneEmptyLocaleEntries = (i18n: LocalizedMap | null | undefined) => { + if (!i18n) return {}; + + return Object.fromEntries( + Object.entries(i18n) + .map(([locale, entry]) => [ + locale, + Object.fromEntries( + Object.entries(entry ?? {}).filter(([_, value]) => { + if (typeof value === 'string') return value.trim() !== ''; + if (Array.isArray(value)) return true; + return value !== null && value !== undefined; + }) + ) + ]) + .filter(([_, entry]) => Object.keys(entry as LocaleEntry).length > 0) + ); +};