diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue
index 3afd1325..5f171e53 100644
--- a/src/components/dialogs/UserDialog/UserDialog.vue
+++ b/src/components/dialogs/UserDialog/UserDialog.vue
@@ -249,6 +249,11 @@
style="margin-left: 5px"
@click="showBioDialog">
+
+
+
@@ -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;
diff --git a/src/localization/en/en.json b/src/localization/en/en.json
index 7bd15b36..92359062 100644
--- a/src/localization/en/en.json
+++ b/src/localization/en/en.json
@@ -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",
diff --git a/src/stores/settings/advanced.js b/src/stores/settings/advanced.js
index eef3f68f..2c61c2c5 100644
--- a/src/stores/settings/advanced.js
+++ b/src/stores/settings/advanced.js
@@ -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,
diff --git a/src/views/Settings/components/Tabs/AdvancedTab.vue b/src/views/Settings/components/Tabs/AdvancedTab.vue
index 78e0740f..9e2ef674 100644
--- a/src/views/Settings/components/Tabs/AdvancedTab.vue
+++ b/src/views/Settings/components/Tabs/AdvancedTab.vue
@@ -137,6 +137,20 @@
}}
+
+
+
+
+ {{
+ t('view.settings.advanced.advanced.translation_api.translation_api_key')
+ }}
+
+
@@ -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();
+ }
+ }
diff --git a/src/views/Settings/dialogs/TranslationApiDialog.vue b/src/views/Settings/dialogs/TranslationApiDialog.vue
new file mode 100644
index 00000000..d10527bf
--- /dev/null
+++ b/src/views/Settings/dialogs/TranslationApiDialog.vue
@@ -0,0 +1,115 @@
+
+
+
+
{{ t('view.settings.appearance.appearance.bio_language') }}
+
+
+
+ {{ messages[bioLanguage]?.language || bioLanguage }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('dialog.translation_api.description') }}
+
+
+
+
+
+
+
+ {{ t('dialog.translation_api.guide') }}
+
+
+ {{ t('dialog.translation_api.save') }}
+
+
+
+
+
+
+