diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue index 49a81f06..cb09f422 100644 --- a/src/components/dialogs/UserDialog/UserDialog.vue +++ b/src/components/dialogs/UserDialog/UserDialog.vue @@ -238,6 +238,8 @@ v-if="translationApi && userDialog.ref.bio" text size="small" + :loading="translateLoading" + :disabled="translateLoading" style="margin-left: 5px; padding: 0" @click="translateBio" > { const youTubeApiKey = ref(''); const translationApi = ref(false); const translationApiKey = ref(''); + const translationApiType = ref('google'); // 'google' | 'openai' + const translationApiEndpoint = ref( + 'https://api.openai.com/v1/chat/completions' + ); + const translationApiModel = ref('gpt-4o-mini'); + const translationApiPrompt = ref(''); const progressPie = ref(false); const progressPieFilter = ref(true); const showConfirmationOnSwitchAvatar = ref(false); @@ -90,6 +96,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { youTubeApiKeyConfig, translationApiConfig, translationApiKeyConfig, + translationApiTypeConfig, + translationApiEndpointConfig, + translationApiModelConfig, + translationApiPromptConfig, progressPieConfig, progressPieFilterConfig, showConfirmationOnSwitchAvatarConfig, @@ -131,6 +141,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { configRepository.getString('VRCX_youtubeAPIKey', ''), configRepository.getBool('VRCX_translationAPI', false), configRepository.getString('VRCX_translationAPIKey', ''), + configRepository.getString('VRCX_translationAPIType', 'google'), + configRepository.getString('VRCX_translationAPIEndpoint', ''), + configRepository.getString('VRCX_translationAPIModel', ''), + configRepository.getString('VRCX_translationAPIPrompt', ''), configRepository.getBool('VRCX_progressPie', false), configRepository.getBool('VRCX_progressPieFilter', true), configRepository.getBool( @@ -178,6 +192,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { youTubeApiKey.value = youTubeApiKeyConfig; translationApi.value = translationApiConfig; translationApiKey.value = translationApiKeyConfig; + translationApiType.value = translationApiTypeConfig; + translationApiEndpoint.value = translationApiEndpointConfig; + translationApiModel.value = translationApiModelConfig; + translationApiPrompt.value = translationApiPromptConfig; progressPie.value = progressPieConfig; progressPieFilter.value = progressPieFilterConfig; showConfirmationOnSwitchAvatar.value = @@ -344,6 +362,34 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { translationApiKey.value ); } + async function setTranslationApiType(value) { + translationApiType.value = value || 'google'; + await configRepository.setString( + 'VRCX_translationAPIType', + translationApiType.value + ); + } + async function setTranslationApiEndpoint(value) { + translationApiEndpoint.value = value; + await configRepository.setString( + 'VRCX_translationAPIEndpoint', + translationApiEndpoint.value + ); + } + async function setTranslationApiModel(value) { + translationApiModel.value = value; + await configRepository.setString( + 'VRCX_translationAPIModel', + translationApiModel.value + ); + } + async function setTranslationApiPrompt(value) { + translationApiPrompt.value = value; + await configRepository.setString( + 'VRCX_translationAPIPrompt', + translationApiPrompt.value + ); + } function setBioLanguage(language) { bioLanguage.value = language; configRepository.setString('VRCX_bioLanguage', language); @@ -586,39 +632,121 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { return data; } - async function translateText(text, targetLang) { - if (!translationApiKey.value) { + async function translateText(text, targetLang, overrides) { + if (!translationApi.value) { ElMessage({ - message: 'No Translation API key configured', + message: 'Translation API disabled', type: 'warning' }); return null; } + const provider = + overrides?.type || translationApiType.value || 'google'; + + if (provider === 'google') { + const keyToUse = overrides?.key ?? translationApiKey.value; + if (!keyToUse) { + ElMessage({ + message: 'No Translation API key configured', + type: 'warning' + }); + return null; + } + try { + const response = await webApiService.execute({ + url: `https://translation.googleapis.com/language/translate/v2?key=${keyToUse}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Referer: 'https://vrcx.app' + }, + body: JSON.stringify({ + q: text, + target: targetLang, + format: 'text' + }) + }); + if (response.status !== 200) { + throw new Error( + `Translation API error: ${response.status} - ${response.data}` + ); + } + const data = JSON.parse(response.data); + if (AppDebug.debugWebRequests) { + console.log(data, response); + } + return data.data.translations[0].translatedText; + } catch (err) { + ElMessage({ + message: `Translation failed: ${err.message}`, + type: 'error' + }); + return null; + } + } + + const endpoint = + overrides?.endpoint || + translationApiEndpoint.value || + 'https://api.openai.com/v1/chat/completions'; + const model = + overrides?.model || translationApiModel.value || 'gpt-5.1'; + const prompt = + overrides?.prompt || + translationApiPrompt.value || + `You are a translation assistant. Translate the user message into ${targetLang}. Only return the translated text.`; + + if (!endpoint || !model) { + ElMessage({ + message: 'Translation endpoint/model missing', + type: 'warning' + }); + return null; + } + + const headers = { + 'Content-Type': 'application/json', + Referer: 'https://vrcx.app' + }; + const keyToUse = overrides?.key ?? translationApiKey.value; + if (keyToUse) { + headers.Authorization = `Bearer ${keyToUse}`; + } + try { const response = await webApiService.execute({ - url: `https://translation.googleapis.com/language/translate/v2?key=${translationApiKey.value}`, + url: endpoint, method: 'POST', - headers: { - 'Content-Type': 'application/json', - Referer: 'https://vrcx.app' - }, + headers, body: JSON.stringify({ - q: text, - target: targetLang, - format: 'text' + model, + messages: [ + { + role: 'system', + content: prompt + }, + { + role: 'user', + content: text + } + ] }) }); + if (response.status !== 200) { throw new Error( `Translation API error: ${response.status} - ${response.data}` ); } + const data = JSON.parse(response.data); if (AppDebug.debugWebRequests) { console.log(data, response); } - return data.data.translations[0].translatedText; + + const translated = data?.choices?.[0]?.message?.content; + return typeof translated === 'string' ? translated.trim() : null; } catch (err) { ElMessage({ message: `Translation failed: ${err.message}`, @@ -834,6 +962,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { translationApi, youTubeApiKey, translationApiKey, + translationApiType, + translationApiEndpoint, + translationApiModel, + translationApiPrompt, progressPie, progressPieFilter, showConfirmationOnSwitchAvatar, @@ -869,6 +1001,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { setTranslationApi, setYouTubeApiKey, setTranslationApiKey, + setTranslationApiType, + setTranslationApiEndpoint, + setTranslationApiModel, + setTranslationApiPrompt, setProgressPie, setProgressPieFilter, setShowConfirmationOnSwitchAvatar, diff --git a/src/views/Settings/dialogs/TranslationApiDialog.vue b/src/views/Settings/dialogs/TranslationApiDialog.vue index ca4e3eeb..0cdbcd4e 100644 --- a/src/views/Settings/dialogs/TranslationApiDialog.vue +++ b/src/views/Settings/dialogs/TranslationApiDialog.vue @@ -26,20 +26,65 @@
-
{{ t('dialog.translation_api.description') }}
+ + + + + + + + - - + + +