mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-29 11:43:48 +02:00
Translate bio button (#1432)
* Translation button finished * Fix bug in theme * revert cause some stuff broke * "Translated by Google" text * Translate that too * I am very stupid sorry * Add API Key * Make toggle work, oops * Fix merge issues * Fixed as requested * Change translation icon * Return settings back (oops) * Beep * Boop * Delete import again as the icon has been replaced with a better one * Change text
This commit is contained in:
@@ -249,6 +249,11 @@
|
||||
style="margin-left: 5px"
|
||||
@click="showBioDialog"></el-button>
|
||||
</div>
|
||||
<div v-if="userDialog.id !== currentUser.id" style="float: right">
|
||||
<el-button type="text" size="small" style="margin-left: 5px" @click="translateBio"
|
||||
><i class="ri-translate-2"></i
|
||||
></el-button>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
<el-tooltip v-for="(link, index) in userDialog.ref.bioLinks" :key="index">
|
||||
<template #content>
|
||||
@@ -1305,8 +1310,10 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const advancedSettingsStore = useAdvancedSettingsStore();
|
||||
|
||||
const { hideUserNotes, hideUserMemos } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { bioLanguage, avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
||||
const {
|
||||
cachedUsers,
|
||||
@@ -2270,6 +2277,52 @@
|
||||
D.visible = true;
|
||||
}
|
||||
|
||||
const bioCache = ref({
|
||||
userID: null,
|
||||
original: null,
|
||||
translated: null,
|
||||
showingTranslated: false
|
||||
});
|
||||
async function translateBio() {
|
||||
const bio = userDialog.value.ref.bio;
|
||||
if (!bio || bio === '-' || !advancedSettingsStore.translationApi) return;
|
||||
|
||||
const targetLang = bioLanguage.value;
|
||||
|
||||
if (bioCache.value.userID !== userDialog.value.id) {
|
||||
bioCache.value.userID = userDialog.value.id;
|
||||
bioCache.value.original = null;
|
||||
bioCache.value.translated = null;
|
||||
bioCache.value.showingTranslated = false;
|
||||
}
|
||||
|
||||
if (!bioCache.value.original) bioCache.value.original = bio;
|
||||
|
||||
if (bioCache.value.showingTranslated) {
|
||||
userDialog.value.ref.bio = bioCache.value.original;
|
||||
bioCache.value.showingTranslated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (bioCache.value.translated) {
|
||||
userDialog.value.ref.bio = bioCache.value.translated;
|
||||
bioCache.value.showingTranslated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const translated = await advancedSettingsStore.translateText(bio + '\n\nTranslated by Google', targetLang);
|
||||
|
||||
if (!translated) throw new Error('No translation returned');
|
||||
|
||||
bioCache.value.translated = translated;
|
||||
bioCache.value.showingTranslated = true;
|
||||
userDialog.value.ref.bio = translated;
|
||||
} catch (err) {
|
||||
console.error('Translation failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function showPreviousInstancesUserDialog(userRef) {
|
||||
const D = previousInstancesUserDialog.value;
|
||||
D.userRef = userRef;
|
||||
|
||||
@@ -409,6 +409,7 @@
|
||||
"appearance": {
|
||||
"header": "Appearance",
|
||||
"language": "Language",
|
||||
"bio_language": "Target Language",
|
||||
"theme_mode": "Theme",
|
||||
"theme_mode_system": "System",
|
||||
"theme_mode_light": "Light",
|
||||
@@ -638,6 +639,12 @@
|
||||
"youtube_api_key": "YouTube API Key",
|
||||
"enable_tooltip": "Fetches video titles for use with gameLog and duration for overlay progress bar"
|
||||
},
|
||||
"translation_api": {
|
||||
"header": "Translation API",
|
||||
"enable": "Enable",
|
||||
"translation_api_key": "Translation API Key",
|
||||
"enable_tooltip": "Translates user bios using a button on the bottom right"
|
||||
},
|
||||
"video_progress_pie": {
|
||||
"header": "Progress pie overlay for videos",
|
||||
"enable": "Enable",
|
||||
@@ -1297,6 +1304,13 @@
|
||||
"guide": "Guide",
|
||||
"save": "Save"
|
||||
},
|
||||
"translation_api": {
|
||||
"header": "Translation API",
|
||||
"description": "Enter your Translation API Key (optional)",
|
||||
"placeholder": "Translation API Key",
|
||||
"guide": "Guide (skip the restrict usage part)",
|
||||
"save": "Save"
|
||||
},
|
||||
"set_world_tags": {
|
||||
"header": "Set World Tags",
|
||||
"avatar_scaling_disabled": "Disable avatar scaling",
|
||||
|
||||
@@ -18,13 +18,14 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
const vrcxStore = useVrcxStore();
|
||||
const VRCXUpdaterStore = useVRCXUpdaterStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { availableLocales, t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
folderSelectorDialogVisible: false
|
||||
});
|
||||
|
||||
const enablePrimaryPassword = ref(false);
|
||||
const bioLanguage = ref('en');
|
||||
const relaunchVRChatAfterCrash = ref(false);
|
||||
const vrcQuitFix = ref(true);
|
||||
const autoSweepVRChatCache = ref(false);
|
||||
@@ -40,7 +41,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
const screenshotHelperModifyFilename = ref(false);
|
||||
const screenshotHelperCopyToClipboard = ref(false);
|
||||
const youTubeApi = ref(false);
|
||||
const translationApi = ref(false);
|
||||
const youTubeApiKey = ref('');
|
||||
const translationApiKey = ref('');
|
||||
const progressPie = ref(false);
|
||||
const progressPieFilter = ref(true);
|
||||
const showConfirmationOnSwitchAvatar = ref(false);
|
||||
@@ -68,6 +71,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
async function initAdvancedSettings() {
|
||||
const [
|
||||
enablePrimaryPasswordConfig,
|
||||
bioLanguageConfig,
|
||||
relaunchVRChatAfterCrashConfig,
|
||||
vrcQuitFixConfig,
|
||||
autoSweepVRChatCacheConfig,
|
||||
@@ -83,7 +87,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
screenshotHelperModifyFilenameConfig,
|
||||
screenshotHelperCopyToClipboardConfig,
|
||||
youTubeApiConfig,
|
||||
translationApiConfig,
|
||||
youTubeApiKeyConfig,
|
||||
translationApiKeyConfig,
|
||||
progressPieConfig,
|
||||
progressPieFilterConfig,
|
||||
showConfirmationOnSwitchAvatarConfig,
|
||||
@@ -97,6 +103,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
sentryErrorReportingConfig
|
||||
] = await Promise.all([
|
||||
configRepository.getBool('enablePrimaryPassword', false),
|
||||
configRepository.getString('VRCX_bioLanguage'),
|
||||
configRepository.getBool('VRCX_relaunchVRChatAfterCrash', false),
|
||||
configRepository.getBool('VRCX_vrcQuitFix', true),
|
||||
configRepository.getBool('VRCX_autoSweepVRChatCache', false),
|
||||
@@ -121,7 +128,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
false
|
||||
),
|
||||
configRepository.getBool('VRCX_youtubeAPI', false),
|
||||
configRepository.getBool('VRCX_translationAPI', false),
|
||||
configRepository.getString('VRCX_youtubeAPIKey', ''),
|
||||
configRepository.getString('VRCX_translationAPIKey', ''),
|
||||
configRepository.getBool('VRCX_progressPie', false),
|
||||
configRepository.getBool('VRCX_progressPieFilter', true),
|
||||
configRepository.getBool(
|
||||
@@ -138,6 +147,15 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
configRepository.getString('VRCX_SentryEnabled', '')
|
||||
]);
|
||||
|
||||
if (
|
||||
!bioLanguageConfig ||
|
||||
!availableLocales.includes(bioLanguageConfig)
|
||||
) {
|
||||
bioLanguage.value = 'en';
|
||||
} else {
|
||||
bioLanguage.value = bioLanguageConfig;
|
||||
}
|
||||
|
||||
enablePrimaryPassword.value = enablePrimaryPasswordConfig;
|
||||
relaunchVRChatAfterCrash.value = relaunchVRChatAfterCrashConfig;
|
||||
vrcQuitFix.value = vrcQuitFixConfig;
|
||||
@@ -157,7 +175,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
screenshotHelperCopyToClipboard.value =
|
||||
screenshotHelperCopyToClipboardConfig;
|
||||
youTubeApi.value = youTubeApiConfig;
|
||||
translationApi.value = translationApiConfig;
|
||||
youTubeApiKey.value = youTubeApiKeyConfig;
|
||||
translationApiKey.value = translationApiKeyConfig;
|
||||
progressPie.value = progressPieConfig;
|
||||
progressPieFilter.value = progressPieFilterConfig;
|
||||
showConfirmationOnSwitchAvatar.value =
|
||||
@@ -299,6 +319,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
youTubeApi.value = !youTubeApi.value;
|
||||
await configRepository.setBool('VRCX_youtubeAPI', youTubeApi.value);
|
||||
}
|
||||
async function setTranslationApi() {
|
||||
translationApi.value = !translationApi.value;
|
||||
await configRepository.setBool('VRCX_translationAPI', youTubeApi.value);
|
||||
}
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
@@ -309,6 +333,17 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
youTubeApiKey.value
|
||||
);
|
||||
}
|
||||
async function setTranslationApiKey(value) {
|
||||
translationApiKey.value = value;
|
||||
await configRepository.setString(
|
||||
'VRCX_translationAPIKey',
|
||||
translationApiKey.value
|
||||
);
|
||||
}
|
||||
function setBioLanguage(language) {
|
||||
bioLanguage.value = language;
|
||||
configRepository.setString('VRCX_bioLanguage', language);
|
||||
}
|
||||
async function setProgressPie() {
|
||||
progressPie.value = !progressPie.value;
|
||||
await configRepository.setBool('VRCX_progressPie', progressPie.value);
|
||||
@@ -548,6 +583,31 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function translateText(text, targetLang) {
|
||||
if (!translationApiKey.value) return null;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://translation.googleapis.com/language/translate/v2?key=${translationApiKey.value}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
q: text,
|
||||
target: targetLang,
|
||||
format: 'text'
|
||||
})
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) return null;
|
||||
return data.data.translations[0].translatedText;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cropPrintsChanged() {
|
||||
if (!cropInstancePrints.value) return;
|
||||
ElMessageBox.confirm(
|
||||
@@ -728,6 +788,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
return {
|
||||
state,
|
||||
|
||||
bioLanguage,
|
||||
enablePrimaryPassword,
|
||||
relaunchVRChatAfterCrash,
|
||||
vrcQuitFix,
|
||||
@@ -744,7 +805,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
screenshotHelperModifyFilename,
|
||||
screenshotHelperCopyToClipboard,
|
||||
youTubeApi,
|
||||
translationApi,
|
||||
youTubeApiKey,
|
||||
translationApiKey,
|
||||
progressPie,
|
||||
progressPieFilter,
|
||||
showConfirmationOnSwitchAvatar,
|
||||
@@ -761,6 +824,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
sentryErrorReporting,
|
||||
|
||||
setEnablePrimaryPasswordConfigRepository,
|
||||
setBioLanguage,
|
||||
setRelaunchVRChatAfterCrash,
|
||||
setVrcQuitFix,
|
||||
setAutoSweepVRChatCache,
|
||||
@@ -776,7 +840,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
setScreenshotHelperModifyFilename,
|
||||
setScreenshotHelperCopyToClipboard,
|
||||
setYouTubeApi,
|
||||
setTranslationApi,
|
||||
setYouTubeApiKey,
|
||||
setTranslationApiKey,
|
||||
setProgressPie,
|
||||
setProgressPieFilter,
|
||||
setShowConfirmationOnSwitchAvatar,
|
||||
@@ -788,6 +854,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
getSqliteTableSizes,
|
||||
handleSetAppLauncherSettings,
|
||||
lookupYouTubeVideo,
|
||||
translateText,
|
||||
resetUGCFolder,
|
||||
openUGCFolder,
|
||||
openUGCFolderSelector,
|
||||
|
||||
@@ -137,6 +137,20 @@
|
||||
}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.advanced.advanced.translation_api.header') }}</span>
|
||||
<simple-switch
|
||||
:label="t('view.settings.advanced.advanced.translation_api.enable')"
|
||||
:value="translationApi"
|
||||
:tooltip="t('view.settings.advanced.advanced.translation_api.enable_tooltip')"
|
||||
:long-label="true"
|
||||
@change="changeTranslationAPI('VRCX_translationAPI')" />
|
||||
<div class="options-container-item">
|
||||
<el-button size="small" :icon="CaretRight" @click="showTranslationApiDialog">{{
|
||||
t('view.settings.advanced.advanced.translation_api.translation_api_key')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.advanced.advanced.video_progress_pie.header') }}</span>
|
||||
<simple-switch
|
||||
@@ -323,6 +337,7 @@
|
||||
|
||||
<RegistryBackupDialog />
|
||||
<YouTubeApiDialog v-model:isYouTubeApiDialogVisible="isYouTubeApiDialogVisible" />
|
||||
<TranslationApiDialog v-model:isTranslationApiDialogVisible="isTranslationApiDialogVisible" />
|
||||
<AvatarProviderDialog v-model:isAvatarProviderDialogVisible="isAvatarProviderDialogVisible" />
|
||||
<PhotonSettings v-if="photonLoggingEnabled" />
|
||||
</div>
|
||||
@@ -368,6 +383,7 @@
|
||||
import PhotonSettings from '../PhotonSettings.vue';
|
||||
import RegistryBackupDialog from '../../dialogs/RegistryBackupDialog.vue';
|
||||
import SimpleSwitch from '../SimpleSwitch.vue';
|
||||
import TranslationApiDialog from '../../dialogs/TranslationApiDialog.vue';
|
||||
import YouTubeApiDialog from '../../dialogs/YouTubeApiDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -400,6 +416,7 @@
|
||||
enableAppLauncherAutoClose,
|
||||
enableAppLauncherRunProcessOnce,
|
||||
youTubeApi,
|
||||
translationApi,
|
||||
progressPie,
|
||||
progressPieFilter,
|
||||
showConfirmationOnSwitchAvatar,
|
||||
@@ -428,6 +445,7 @@
|
||||
const { showAvatarProviderDialog } = useAvatarProviderStore();
|
||||
|
||||
const isYouTubeApiDialogVisible = ref(false);
|
||||
const isTranslationApiDialogVisible = ref(false);
|
||||
|
||||
const cacheSize = reactive({
|
||||
cachedUsers: 0,
|
||||
@@ -496,6 +514,10 @@
|
||||
isYouTubeApiDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function showTranslationApiDialog() {
|
||||
isTranslationApiDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function refreshCacheSize() {
|
||||
cacheSize.cachedUsers = cachedUsers.size;
|
||||
cacheSize.cachedWorlds = cachedWorlds.size;
|
||||
@@ -516,4 +538,10 @@
|
||||
updateVRLastLocation();
|
||||
updateOpenVR();
|
||||
}
|
||||
|
||||
async function changeTranslationAPI(configKey = '') {
|
||||
if (configKey === 'VRCX_translationAPI') {
|
||||
advancedSettingsStore.setTranslationApi();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
115
src/views/Settings/dialogs/TranslationApiDialog.vue
Normal file
115
src/views/Settings/dialogs/TranslationApiDialog.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="x-dialog"
|
||||
:model-value="isTranslationApiDialogVisible"
|
||||
:title="t('dialog.translation_api.header')"
|
||||
width="400px"
|
||||
@close="closeDialog">
|
||||
<div class="options-container-item">
|
||||
<span class="name">{{ t('view.settings.appearance.appearance.bio_language') }}</span>
|
||||
<el-dropdown trigger="click" size="small" @click.stop>
|
||||
<el-button size="small">
|
||||
<span>
|
||||
{{ messages[bioLanguage]?.language || bioLanguage }}
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(obj, language) in messages"
|
||||
:key="language"
|
||||
@click="setBioLanguage(language)"
|
||||
v-text="obj.language" />
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div style="font-size: 12px">{{ t('dialog.translation_api.description') }} <br /></div>
|
||||
|
||||
<el-input
|
||||
v-model="translationApiKey"
|
||||
type="textarea"
|
||||
:placeholder="t('dialog.translation_api.placeholder')"
|
||||
maxlength="39"
|
||||
show-word-limit
|
||||
style="display: block; margin-top: 10px">
|
||||
</el-input>
|
||||
|
||||
<template #footer>
|
||||
<div style="display: flex">
|
||||
<el-button
|
||||
@click="
|
||||
openExternalLink(
|
||||
'https://translatepress.com/docs/automatic-translation/generate-google-api-key/'
|
||||
)
|
||||
">
|
||||
{{ t('dialog.translation_api.guide') }}
|
||||
</el-button>
|
||||
<el-button type="primary" style="margin-left: auto" @click="testTranslationApiKey">
|
||||
{{ t('dialog.translation_api.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { openExternalLink } from '../../../shared/utils';
|
||||
import { useAdvancedSettingsStore } from '../../../stores';
|
||||
|
||||
const advancedSettingsStore = useAdvancedSettingsStore();
|
||||
|
||||
const { bioLanguage, translationApiKey } = storeToRefs(advancedSettingsStore);
|
||||
|
||||
const { setBioLanguage, translateText, setTranslationApiKey } = advancedSettingsStore;
|
||||
|
||||
const { messages, t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
isTranslationApiDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isTranslationApiDialogVisible']);
|
||||
|
||||
async function testTranslationApiKey() {
|
||||
const previousKey = translationApiKey.value;
|
||||
if (!translationApiKey.value) {
|
||||
ElMessage({
|
||||
message: 'Translation API key removed',
|
||||
type: 'success'
|
||||
});
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const testText = 'Hello world';
|
||||
const data = await translateText(testText, 'fr');
|
||||
if (!data) {
|
||||
setTranslationApiKey(previousKey);
|
||||
ElMessage({
|
||||
message: 'Invalid Translation API key',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
setTranslationApiKey(translationApiKey.value);
|
||||
ElMessage({
|
||||
message: 'Translation API key valid!',
|
||||
type: 'success'
|
||||
});
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isTranslationApiDialogVisible', false);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user