Browse Source

feat: WIP: Initial setup for i18next

Ased Mammad 1 year ago
parent
commit
fab89a76b1

+ 4 - 1
package.json

@@ -45,6 +45,9 @@
 		"dayjs": "^1.11.10",
 		"dayjs": "^1.11.10",
 		"file-saver": "^2.0.5",
 		"file-saver": "^2.0.5",
 		"highlight.js": "^11.9.0",
 		"highlight.js": "^11.9.0",
+		"i18next": "^23.10.0",
+		"i18next-browser-languagedetector": "^7.2.0",
+		"i18next-resources-to-backend": "^1.2.0",
 		"idb": "^7.1.1",
 		"idb": "^7.1.1",
 		"js-sha256": "^0.10.1",
 		"js-sha256": "^0.10.1",
 		"katex": "^0.16.9",
 		"katex": "^0.16.9",
@@ -53,4 +56,4 @@
 		"tippy.js": "^6.3.7",
 		"tippy.js": "^6.3.7",
 		"uuid": "^9.0.1"
 		"uuid": "^9.0.1"
 	}
 	}
-}
+}

+ 5 - 3
src/lib/components/chat/MessageInput.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
-	import { onMount, tick } from 'svelte';
+	import { onMount, tick, getContext } from 'svelte';
 	import { settings } from '$lib/stores';
 	import { settings } from '$lib/stores';
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 
 
@@ -14,6 +14,8 @@
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { transcribeAudio } from '$lib/apis/audio';
 	import Tooltip from '../common/Tooltip.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
 
 
+	const i18n = getContext('i18n');
+
 	export let submitPrompt: Function;
 	export let submitPrompt: Function;
 	export let stopResponse: Function;
 	export let stopResponse: Function;
 
 
@@ -669,8 +671,8 @@
 							placeholder={chatInputPlaceholder !== ''
 							placeholder={chatInputPlaceholder !== ''
 								? chatInputPlaceholder
 								? chatInputPlaceholder
 								: isRecording
 								: isRecording
-								? 'Listening...'
-								: 'Send a message'}
+								? $i18n.t('ChatInputPlaceholderListening')
+								: $i18n.t('ChatInputPlaceholder')}
 							bind:value={prompt}
 							bind:value={prompt}
 							on:keypress={(e) => {
 							on:keypress={(e) => {
 								if (e.keyCode == 13 && !e.shiftKey) {
 								if (e.keyCode == 13 && !e.shiftKey) {

+ 3 - 1
src/lib/components/chat/MessageInput/Models.svelte

@@ -2,9 +2,11 @@
 	import { generatePrompt } from '$lib/apis/ollama';
 	import { generatePrompt } from '$lib/apis/ollama';
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 	import { splitStream } from '$lib/utils';
-	import { tick } from 'svelte';
+	import { tick, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
+	const i18n = getContext('i18n');
+
 	export let prompt = '';
 	export let prompt = '';
 	export let user = null;
 	export let user = null;
 
 

+ 5 - 3
src/lib/components/chat/Messages/Placeholder.svelte

@@ -1,7 +1,9 @@
 <script lang="ts">
 <script lang="ts">
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { user } from '$lib/stores';
 	import { user } from '$lib/stores';
-	import { onMount } from 'svelte';
+	import { onMount, getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
 
 
 	export let models = [];
 	export let models = [];
 	export let modelfiles = [];
 	export let modelfiles = [];
@@ -64,9 +66,9 @@
 					</div>
 					</div>
 				{/if}
 				{/if}
 			{:else}
 			{:else}
-				<div class=" line-clamp-1">Hello, {$user.name}</div>
+				<div class=" line-clamp-1">{$i18n.t('Hello', { name: $user.name })}</div>
 
 
-				<div>How can I help you today?</div>
+				<div>{$i18n.t('GreetingPlaceholder')}</div>
 			{/if}
 			{/if}
 		</div>
 		</div>
 	</div>
 	</div>

+ 7 - 3
src/lib/components/chat/ModelSelector.svelte

@@ -1,9 +1,11 @@
 <script lang="ts">
 <script lang="ts">
 	import { setDefaultModels } from '$lib/apis/configs';
 	import { setDefaultModels } from '$lib/apis/configs';
 	import { models, showSettings, settings, user } from '$lib/stores';
 	import { models, showSettings, settings, user } from '$lib/stores';
-	import { onMount, tick } from 'svelte';
+	import { onMount, tick, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
+	const i18n = getContext('i18n');
+
 	export let selectedModels = [''];
 	export let selectedModels = [''];
 	export let disabled = false;
 	export let disabled = false;
 
 
@@ -39,7 +41,9 @@
 				bind:value={selectedModel}
 				bind:value={selectedModel}
 				{disabled}
 				{disabled}
 			>
 			>
-				<option class=" text-gray-700" value="" selected disabled>Select a model</option>
+				<option class=" text-gray-700" value="" selected disabled
+					>{$i18n.t('ModelSelectorPlaceholder')}</option
+				>
 
 
 				{#each $models as model}
 				{#each $models as model}
 					{#if model.name === 'hr'}
 					{#if model.name === 'hr'}
@@ -133,5 +137,5 @@
 </div>
 </div>
 
 
 <div class="text-left mt-1.5 text-xs text-gray-500">
 <div class="text-left mt-1.5 text-xs text-gray-500">
-	<button on:click={saveDefaultModel}> Set as default</button>
+	<button on:click={saveDefaultModel}> {$i18n.t('SetAsDefault')}</button>
 </div>
 </div>

+ 25 - 1
src/lib/components/chat/Settings/General.svelte

@@ -1,10 +1,12 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
-	import { createEventDispatcher, onMount } from 'svelte';
+	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	import { models, user } from '$lib/stores';
 	import { models, user } from '$lib/stores';
 
 
+	const i18n = getContext('i18n');
+
 	import AdvancedParams from './Advanced/AdvancedParams.svelte';
 	import AdvancedParams from './Advanced/AdvancedParams.svelte';
 
 
 	export let saveSettings: Function;
 	export let saveSettings: Function;
@@ -13,6 +15,9 @@
 	// General
 	// General
 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
 	let theme = 'dark';
 	let theme = 'dark';
+	// TODO: Get these dynamically from the i18n module
+	let languages = ['en', 'fa', 'fr', 'de'];
+	let lang = $i18n.language;
 	let notificationEnabled = false;
 	let notificationEnabled = false;
 	let system = '';
 	let system = '';
 
 
@@ -149,6 +154,25 @@
 				</div>
 				</div>
 			</div>
 			</div>
 
 
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">Language</div>
+				<div class="flex items-center relative">
+					<select
+						class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
+						bind:value={lang}
+						placeholder="Select a language"
+						on:change={(e) => {
+							console.log($i18n);
+							$i18n.changeLanguage(lang);
+						}}
+					>
+						{#each languages as value}
+							<option {value}>{value}</option>
+						{/each}
+					</select>
+				</div>
+			</div>
+
 			<div>
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
 				<div class=" py-0.5 flex w-full justify-between">
 					<div class=" self-center text-xs font-medium">Notification</div>
 					<div class=" self-center text-xs font-medium">Notification</div>

+ 5 - 3
src/lib/components/chat/Settings/Images.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
-	import { createEventDispatcher, onMount } from 'svelte';
+	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { config, user } from '$lib/stores';
 	import { config, user } from '$lib/stores';
 	import {
 	import {
 		getAUTOMATIC1111Url,
 		getAUTOMATIC1111Url,
@@ -19,6 +19,8 @@
 	import { getBackendConfig } from '$lib/apis';
 	import { getBackendConfig } from '$lib/apis';
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
+	const i18n = getContext('i18n');
+
 	export let saveSettings: Function;
 	export let saveSettings: Function;
 
 
 	let loading = false;
 	let loading = false;
@@ -193,10 +195,10 @@
 						<select
 						<select
 							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 							bind:value={selectedModel}
 							bind:value={selectedModel}
-							placeholder="Select a model"
+							placeholder={$i18n.t('ModelSelectorPlaceholder')}
 						>
 						>
 							{#if !selectedModel}
 							{#if !selectedModel}
-								<option value="" disabled selected>Select a model</option>
+								<option value="" disabled selected>{$i18n.t('ModelSelectorPlaceholder')}</option>
 							{/if}
 							{/if}
 							{#each models ?? [] as model}
 							{#each models ?? [] as model}
 								<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
 								<option value={model.title} class="bg-gray-100 dark:bg-gray-700"

+ 4 - 2
src/lib/components/chat/Settings/Interface.svelte

@@ -2,10 +2,12 @@
 	import { getBackendConfig } from '$lib/apis';
 	import { getBackendConfig } from '$lib/apis';
 	import { setDefaultPromptSuggestions } from '$lib/apis/configs';
 	import { setDefaultPromptSuggestions } from '$lib/apis/configs';
 	import { config, models, user } from '$lib/stores';
 	import { config, models, user } from '$lib/stores';
-	import { createEventDispatcher, onMount } from 'svelte';
+	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
+	const i18n = getContext('i18n');
+
 	export let saveSettings: Function;
 	export let saveSettings: Function;
 
 
 	// Addons
 	// Addons
@@ -188,7 +190,7 @@
 					<select
 					<select
 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 						bind:value={titleAutoGenerateModel}
 						bind:value={titleAutoGenerateModel}
-						placeholder="Select a model"
+						placeholder={$i18n.t('ModelSelectorPlaceholder')}
 					>
 					>
 						<option value="" selected>Current Model</option>
 						<option value="" selected>Current Model</option>
 						{#each $models as model}
 						{#each $models as model}

+ 8 - 5
src/lib/components/chat/Settings/Models.svelte

@@ -6,9 +6,11 @@
 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_NAME, models, user } from '$lib/stores';
 	import { WEBUI_NAME, models, user } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 	import { splitStream } from '$lib/utils';
-	import { onMount } from 'svelte';
+	import { onMount, getContext } from 'svelte';
 	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
 	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
 
 
+	const i18n = getContext('i18n');
+
 	export let getModels: Function;
 	export let getModels: Function;
 
 
 	let showLiteLLM = false;
 	let showLiteLLM = false;
@@ -465,10 +467,10 @@
 							<select
 							<select
 								class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 								class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 								bind:value={deleteModelTag}
 								bind:value={deleteModelTag}
-								placeholder="Select a model"
+								placeholder={$i18n.t('ModelSelectorPlaceholder')}
 							>
 							>
 								{#if !deleteModelTag}
 								{#if !deleteModelTag}
-									<option value="" disabled selected>Select a model</option>
+									<option value="" disabled selected>{$i18n.t('ModelSelectorPlaceholder')}</option>
 								{/if}
 								{/if}
 								{#each $models.filter((m) => m.size != null) as model}
 								{#each $models.filter((m) => m.size != null) as model}
 									<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 									<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
@@ -805,10 +807,11 @@
 								<select
 								<select
 									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 									bind:value={deleteLiteLLMModelId}
 									bind:value={deleteLiteLLMModelId}
-									placeholder="Select a model"
+									placeholder={$i18n.t('ModelSelectorPlaceholder')}
 								>
 								>
 									{#if !deleteLiteLLMModelId}
 									{#if !deleteLiteLLMModelId}
-										<option value="" disabled selected>Select a model</option>
+										<option value="" disabled selected>{$i18n.t('ModelSelectorPlaceholder')}</option
+										>
 									{/if}
 									{/if}
 									{#each liteLLMModelInfo as model}
 									{#each liteLLMModelInfo as model}
 										<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
 										<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"

+ 6 - 2
src/lib/components/layout/Sidebar.svelte

@@ -8,6 +8,10 @@
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 	import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
 	import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
+	import { getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
+
 	import {
 	import {
 		deleteChatById,
 		deleteChatById,
 		getChatList,
 		getChatList,
@@ -124,7 +128,7 @@
 						/>
 						/>
 					</div>
 					</div>
 
 
-					<div class=" self-center font-medium text-sm">New Chat</div>
+					<div class=" self-center font-medium text-sm">{$i18n.t('NewChat')}</div>
 				</div>
 				</div>
 
 
 				<div class="self-center">
 				<div class="self-center">
@@ -169,7 +173,7 @@
 					</div>
 					</div>
 
 
 					<div class="flex self-center">
 					<div class="flex self-center">
-						<div class=" self-center font-medium text-sm">Modelfiles</div>
+						<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
 					</div>
 					</div>
 				</a>
 				</a>
 			</div>
 			</div>

+ 30 - 0
src/lib/i18n/index.ts

@@ -0,0 +1,30 @@
+import i18next from 'i18next';
+import resourcesToBackend from 'i18next-resources-to-backend';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import { createI18nStore, isLoading as isLoadingStore } from './store';
+
+i18next
+	.use(
+		resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`))
+	)
+	.use(LanguageDetector)
+	.init({
+		debug: true,
+		detection: {
+			order: ['querystring', 'localStorage', 'navigator'],
+			caches: ['localStorage'],
+			lookupQuerystring: 'lang',
+			lookupLocalStorage: 'locale'
+		},
+		fallbackLng: 'en',
+		ns: 'common',
+		// backend: {
+		// 	loadPath: '/locales/{{lng}}/{{ns}}.json'
+		// }
+		interpolation: {
+			escapeValue: false // not needed for svelte as it escapes by default
+		}
+	});
+const i18n = createI18nStore(i18next);
+export default i18n;
+export const isLoading = isLoadingStore;

+ 10 - 0
src/lib/i18n/locales/de/common.json

@@ -0,0 +1,10 @@
+{
+	"NewChat": "Neuer Chat",
+	"Modelfiles": "Modelfiles",
+	"GreetingPlaceholder": "Wie kann ich dir heute behilflich sein?",
+	"Hello": "Hallo, {{name}}",
+	"ChatInputPlaceholderListening": "nimmt auf...",
+	"ChatInputPlaceholder": "Sende eine Nachricht",
+	"ModelSelectorPlaceholder": "Wähle ein Modell",
+	"SetAsDefault": "Als Standard festlegen"
+}

+ 10 - 0
src/lib/i18n/locales/en/common.json

@@ -0,0 +1,10 @@
+{
+	"NewChat": "New Chat",
+	"Modelfiles": "Modelfiles",
+	"GreetingPlaceholder": "How can I help you today?",
+	"Hello": "Hello, {{name}}",
+	"ChatInputPlaceholderListening": "Listening...",
+	"ChatInputPlaceholder": "Send a Message",
+	"ModelSelectorPlaceholder": "Select a model",
+	"SetAsDefault": "Set as default"
+}

+ 10 - 0
src/lib/i18n/locales/fa/common.json

@@ -0,0 +1,10 @@
+{
+	"NewChat": "چت جدید",
+	"Modelfiles": "فایل‌های مدل",
+	"GreetingPlaceholder": "امروز چطور می توانم کمک تان کنم؟",
+	"Hello": "سلام، {{name}}",
+	"ChatInputPlaceholderListening": "در حال گوش دادن...",
+	"ChatInputPlaceholder": "یک پیام ارسال کنید",
+	"ModelSelectorPlaceholder": "یک مدل انتخاب کنید",
+	"SetAsDefault": "تنظیم به عنوان پیشفرض"
+}

+ 10 - 0
src/lib/i18n/locales/fr/common.json

@@ -0,0 +1,10 @@
+{
+	"NewChat": "New Chat",
+	"Modelfiles": "Modelfiles",
+	"GreetingPlaceholder": "How can I help you today?",
+	"Hello": "Hello, {{name}}",
+	"ChatInputPlaceholderListening": "Listening...",
+	"ChatInputPlaceholder": "Send a Message",
+	"ModelSelectorPlaceholder": "Select a model",
+	"SetAsDefault": "Set as default"
+}

+ 34 - 0
src/lib/i18n/store.ts

@@ -0,0 +1,34 @@
+import type { i18n } from 'i18next';
+import { writable } from 'svelte/store';
+
+export const createI18nStore = (i18n: i18n) => {
+	const i18nWritable = writable(i18n);
+
+	i18n.on('initialized', () => {
+		i18nWritable.set(i18n);
+	});
+	i18n.on('loaded', () => {
+		i18nWritable.set(i18n);
+	});
+	i18n.on('added', () => i18nWritable.set(i18n));
+	i18n.on('languageChanged', () => {
+		i18nWritable.set(i18n);
+	});
+	return i18nWritable;
+};
+
+export const isLoading = (i18n: i18n) => {
+	const isLoading = writable(false);
+
+	// if loaded resources are empty || {}, set loading to true
+	i18n.on('loaded', (resources) => {
+		Object.keys(resources).length !== 0 && isLoading.set(false);
+	});
+
+	// if resources failed loading, set loading to true
+	i18n.on('failedLoading', () => {
+		isLoading.set(true);
+	});
+
+	return isLoading;
+};

+ 4 - 1
src/routes/+layout.svelte

@@ -1,5 +1,5 @@
 <script>
 <script>
-	import { onMount, tick } from 'svelte';
+	import { onMount, tick, setContext } from 'svelte';
 	import { config, user, theme, WEBUI_NAME } from '$lib/stores';
 	import { config, user, theme, WEBUI_NAME } from '$lib/stores';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { Toaster, toast } from 'svelte-sonner';
 	import { Toaster, toast } from 'svelte-sonner';
@@ -11,6 +11,9 @@
 	import '../tailwind.css';
 	import '../tailwind.css';
 	import 'tippy.js/dist/tippy.css';
 	import 'tippy.js/dist/tippy.css';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
+	import i18n from '$lib/i18n';
+
+	setContext('i18n', i18n);
 
 
 	let loaded = false;
 	let loaded = false;