diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 84c99841d4..d303ce80b5 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1886,6 +1886,12 @@ ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig( os.environ.get("ENABLE_AUTOCOMPLETE_GENERATION", "False").lower() == "true", ) +TRANSLATION_LANGUAGES = PersistentConfig( + "TRANSLATION_LANGUAGES", + "translation_languages", + os.getenv("TRANSLATION_LANGUAGES", "en").split(","), +) + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig( "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", "task.autocomplete.input_max_length", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 5609289166..7163177bd2 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -432,6 +432,7 @@ from open_webui.config import ( QUERY_GENERATION_PROMPT_TEMPLATE, AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + TRANSLATION_LANGUAGES, AppConfig, reset_config, ) @@ -1189,6 +1190,7 @@ app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION +app.state.config.TRANSLATION_LANGUAGES = TRANSLATION_LANGUAGES app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE @@ -1864,6 +1866,7 @@ async def get_app_config(request: Request): "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + "translation_languages": app.state.config.TRANSLATION_LANGUAGES, **( { "enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL, diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 040c2382d7..ad230edc08 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Response, status, Request from fastapi.responses import JSONResponse, RedirectResponse from pydantic import BaseModel -from typing import Optional +from typing import Optional, List import logging import re @@ -69,6 +69,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)): "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + "TRANSLATION_LANGUAGES": request.app.state.config.TRANSLATION_LANGUAGES, "VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, } @@ -89,6 +90,7 @@ class TaskConfigForm(BaseModel): ENABLE_RETRIEVAL_QUERY_GENERATION: bool QUERY_GENERATION_PROMPT_TEMPLATE: str TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str + TRANSLATION_LANGUAGES: Optional[List[str]] = [] VOICE_MODE_PROMPT_TEMPLATE: Optional[str] @@ -138,6 +140,10 @@ async def update_task_config( request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE ) + + request.app.state.config.TRANSLATION_LANGUAGES = ( + form_data.TRANSLATION_LANGUAGES + ) request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = ( form_data.VOICE_MODE_PROMPT_TEMPLATE @@ -159,6 +165,7 @@ async def update_task_config( "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + "TRANSLATION_LANGUAGES": request.app.state.config.TRANSLATION_LANGUAGES, "VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, } diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index fbd1089382..bb983ccce1 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -2,7 +2,7 @@ import time import logging import asyncio import sys - +import json from aiocache import cached from fastapi import Request @@ -37,6 +37,33 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) + + +def translate_model_title(model_name, accept_language: str) -> str: + """ + model_name: either a dict like {"de":"test DE","en":"test EN","fr":"test","it":"test IT"} + or a JSON string representation of such a dict + accept_language: string from header, e.g., "en-US", "de-CH", "fr" + """ + # if model_name is a string, try to parse as JSON + if isinstance(model_name, str): + try: + model_name = json.loads(model_name) + except (json.JSONDecodeError, TypeError): + # if it's not valid JSON (plain string), return the string as is + return model_name + + + # Handle None or empty accept_language + if not accept_language: + accept_language = 'en' # default to English if not provided + + # normalize language code to primary subtag + lang = accept_language.split('-')[0] # "en-US" -> "en" + + # return the translation if available, else fallback to 'de' or any available + return model_name.get(lang) or model_name.get('de') or next(iter(model_name.values())) + async def fetch_ollama_models(request: Request, user: UserModel = None): raw_ollama_models = await ollama.get_all_models(request, user=user) return [ @@ -160,6 +187,8 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) 0 ] # Ollama may return model ids in different formats (e.g., 'llama3' vs. 'llama3:7b') ): + # This is what is answered in the info part + custom_model.name = translate_model_title(custom_model.name, request.headers.get("X-Language")), if custom_model.is_active: model["name"] = custom_model.name model["info"] = custom_model.model_dump() @@ -237,6 +266,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) if "filterIds" in meta: filter_ids.extend(meta["filterIds"]) + # Apply translation to the model name + model["name"] = translate_model_title(custom_model.name, request.headers.get("X-Language")) + model["current_language"] = model["name"] + model["action_ids"] = action_ids model["filter_ids"] = filter_ids diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000000..ac865d60be --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,33 @@ +import { get } from 'svelte/store'; +import { temporaryChatEnabled } from '$lib/stores'; +import { getI18nStore } from './lib/i18n'; + +export function customHeadersFetch() { + const originalFetch = window.fetch; + const i18nStore = getI18nStore(); + + window.fetch = async (input: RequestInfo | URL, init: RequestInit = {}) => { + // get current values + const i18n = get(i18nStore); + + // normalize headers to a plain object + const existingHeaders = + init.headers instanceof Headers + ? Object.fromEntries(init.headers.entries()) + : Array.isArray(init.headers) + ? Object.fromEntries(init.headers) + : { ...(init.headers || {}) }; + + const newHeaders: Record = { ...existingHeaders }; + + + if (i18n?.language) { + newHeaders['X-Language'] = i18n.language; + } + + return originalFetch(input, { + ...init, + headers: newHeaders, + }); + }; +} diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts index d03a83e9ca..2feeb8a190 100644 --- a/src/lib/apis/models/index.ts +++ b/src/lib/apis/models/index.ts @@ -1,4 +1,30 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { models, config, type Model } from '$lib/stores'; +import { get } from 'svelte/store'; +export const getModels = async (token: string = ''): Promise => { + const lang = get(config)?.default_locale || 'en'; + try { + const res = await fetch(`${WEBUI_API_BASE_URL}/models/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Language': lang, + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok) throw await res.json(); + + const data = (await res.json()) as Model[]; + models.set(data); // update global store so components react + return data; + } catch (err) { + console.error('Failed to fetch models:', err); + models.set([]); // clear store on error + return null; + } +}; export const getModelItems = async ( token: string = '', @@ -9,6 +35,7 @@ export const getModelItems = async ( direction, page ) => { + const lang = get(config)?.default_locale || 'en'; let error = null; const searchParams = new URLSearchParams(); @@ -61,12 +88,13 @@ export const getModelItems = async ( export const getModelTags = async (token: string = '') => { let error = null; - + const lang = get(config)?.default_locale || 'en'; const res = await fetch(`${WEBUI_API_BASE_URL}/models/tags`, { method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', + 'X-Language': lang, authorization: `Bearer ${token}` } }) diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index acd5fbf67c..80d77cff6e 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -23,6 +23,8 @@ import Banners from './Interface/Banners.svelte'; import PromptSuggestions from '$lib/components/workspace/Models/PromptSuggestions.svelte'; + import LangPicker from './LangPicker.svelte'; + const dispatch = createEventDispatcher(); const i18n = getContext('i18n'); @@ -43,6 +45,7 @@ ENABLE_RETRIEVAL_QUERY_GENERATION: true, QUERY_GENERATION_PROMPT_TEMPLATE: '', TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: '', + TRANSLATION_LANGUAGES: [], VOICE_MODE_PROMPT_TEMPLATE: '' }; @@ -50,6 +53,12 @@ let banners: Banner[] = []; const updateInterfaceHandler = async () => { + // Trim any spaces from translation languages before saving + if (taskConfig.TRANSLATION_LANGUAGES && Array.isArray(taskConfig.TRANSLATION_LANGUAGES)) { + taskConfig.TRANSLATION_LANGUAGES = taskConfig.TRANSLATION_LANGUAGES + .map((lang: string) => lang.trim()) + .filter((lang: string) => lang !== ''); + } taskConfig = await updateTaskConfig(localStorage.token, taskConfig); promptSuggestions = promptSuggestions.filter((p) => p.content !== ''); @@ -442,7 +451,7 @@ id: uuidv4(), type: '', title: '', - content: '', + content: JSON.stringify({ de: '', en: '', fr: '', it: '' }), dismissible: true, timestamp: Math.floor(Date.now() / 1000) } @@ -467,15 +476,23 @@ {#if $user?.role === 'admin'} - - - {#if promptSuggestions.length > 0} -
- {$i18n.t('Adjusting these settings will apply changes universally to all users.')} +
+
+ {$i18n.t('Select languages for translations')}
- {/if} + +
+
+ + + + {#if promptSuggestions.length > 0} +
+ {$i18n.t('Adjusting these settings will apply changes universally to all users.')} +
+ {/if} +
{/if} -
diff --git a/src/lib/components/admin/Settings/Interface/Banners.svelte b/src/lib/components/admin/Settings/Interface/Banners.svelte index 6b96374d5e..d749f3d1b2 100644 --- a/src/lib/components/admin/Settings/Interface/Banners.svelte +++ b/src/lib/components/admin/Settings/Interface/Banners.svelte @@ -1,102 +1,290 @@ -
- {#each banners as banner, bannerIdx (banner.id)} - + + +{#if showBannerModal} +
+
+
+

{$i18n.t('Edit Translations')}

+ +
+ + {#each LANGS as lang} +
+ +