various bundle optimizations (#1549)

* fix: missing "@element-plus/icons-vue" dependency

* fix: update vite (40% faster builds)

* fix: don't include sentry in non-nightly builds

* fix: swap to variable fonts & don't include font files in repo

* fix: lazy load languages to not keep them in memory

* nit: revert vite to stable

* nit: retain `.json` message files in bundle

* nit: remove bundle analyzer

* fix: availableLocales does not include unloaded locales
This commit is contained in:
Aries
2026-01-03 23:51:00 -07:00
committed by GitHub
parent 327e7d9b58
commit b02d287190
38 changed files with 574 additions and 619 deletions

View File

@@ -1,5 +1,6 @@
<template>
<el-config-provider :locale="currentLocale">
<el-config-provider
:locale="/** @type {import('element-plus/es/locale').Language} */ (messages[locale].elementPlus)">
<MacOSTitleBar></MacOSTitleBar>
<div
@@ -20,21 +21,6 @@
import { computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import cs from 'element-plus/es/locale/lang/cs';
import en from 'element-plus/es/locale/lang/en';
import es from 'element-plus/es/locale/lang/es';
import fr from 'element-plus/es/locale/lang/fr';
import hu from 'element-plus/es/locale/lang/hu';
import ja from 'element-plus/es/locale/lang/ja';
import ko from 'element-plus/es/locale/lang/ko';
import pl from 'element-plus/es/locale/lang/pl';
import pt from 'element-plus/es/locale/lang/pt';
import ru from 'element-plus/es/locale/lang/ru';
import th from 'element-plus/es/locale/lang/th';
import vi from 'element-plus/es/locale/lang/vi';
import zhCN from 'element-plus/es/locale/lang/zh-cn';
import zhTW from 'element-plus/es/locale/lang/zh-tw';
import { createGlobalStores } from './stores';
import { initNoty } from './plugin/noty';
@@ -45,35 +31,12 @@
console.log(`isLinux: ${LINUX}`);
const isMacOS = computed(() => {
return navigator.platform.indexOf('Mac') > -1;
});
const isMacOS = computed(() => navigator.platform.includes('Mac'));
const { locale } = useI18n();
const { locale, messages } = useI18n();
initNoty();
const langMap = {
en: en,
es: es,
fr: fr,
hu: hu,
ja: ja,
ko: ko,
pl: pl,
pt: pt,
cs: cs,
ru: ru,
vi: vi,
'zh-CN': zhCN,
'zh-TW': zhTW,
th: th
};
const currentLocale = computed(() => {
return langMap[locale.value] || en;
});
const store = createGlobalStores();
if (typeof window !== 'undefined') {

View File

@@ -7,7 +7,6 @@ For a copy, see <https://opensource.org/licenses/MIT>.
*/
@import 'tailwindcss';
@import '@fontsource-variable/inter';
@import 'element-plus/dist/index.css';
@import 'element-plus/theme-chalk/dark/css-vars.css';
@@ -26,23 +25,23 @@ For a copy, see <https://opensource.org/licenses/MIT>.
--font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-fallback-cjk: sans-serif;
--font-primary-cjk:
'Noto Sans JP', 'Noto Sans SC', 'Noto Sans KR', 'Noto Sans TC';
'Noto Sans JP Variable', 'Noto Sans SC Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='zh-CN'] {
--font-primary-cjk:
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC';
'Noto Sans SC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='ja'] {
--font-primary-cjk:
'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', 'Noto Sans SC';
'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='ko'] {
--font-primary-cjk:
'Noto Sans KR', 'Noto Sans JP', 'Noto Sans TC', 'Noto Sans SC';
'Noto Sans KR Variable', 'Noto Sans JP Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='zh-TW'] {
--font-primary-cjk:
'Noto Sans TC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans SC';
'Noto Sans TC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans SC Variable';
}
body {
font-family:

View File

@@ -1,142 +1,9 @@
/* noto-sans-tc-regular - chinese-traditional */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 400;
src: url('/fonts/noto-sans-tc-v38-chinese-traditional-regular.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-tc-500 - chinese-traditional */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 500;
src: url('/fonts/noto-sans-tc-v38-chinese-traditional-500.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-tc-600 - chinese-traditional */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 600;
src: url('/fonts/noto-sans-tc-v38-chinese-traditional-600.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-tc-700 - chinese-traditional */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 700;
src: url('/fonts/noto-sans-tc-v38-chinese-traditional-700.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
@import '@fontsource-variable/inter';
/* noto-sans-sc-regular - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 400;
src: url('/fonts/noto-sans-sc-v39-chinese-simplified-regular.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-500 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 500;
src: url('/fonts/noto-sans-sc-v39-chinese-simplified-500.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-600 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 600;
src: url('/fonts/noto-sans-sc-v39-chinese-simplified-600.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-700 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 700;
src: url('/fonts/noto-sans-sc-v39-chinese-simplified-700.woff2')
format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-jp-regular - japanese */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 400;
src: url('/fonts/noto-sans-jp-v55-japanese-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-jp-500 - japanese */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 500;
src: url('/fonts/noto-sans-jp-v55-japanese-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-jp-600 - japanese */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 600;
src: url('/fonts/noto-sans-jp-v55-japanese-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-jp-700 - japanese */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 700;
src: url('/fonts/noto-sans-jp-v55-japanese-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-kr-regular - korean */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 400;
src: url('/fonts/noto-sans-kr-v38-korean-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-kr-500 - korean */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 500;
src: url('/fonts/noto-sans-kr-v38-korean-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-kr-600 - korean */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 600;
src: url('/fonts/noto-sans-kr-v38-korean-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-kr-700 - korean */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 700;
src: url('/fonts/noto-sans-kr-v38-korean-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
@import '@fontsource-variable/noto-sans-jp';
@import '@fontsource-variable/noto-sans-kr';
@import '@fontsource-variable/noto-sans-sc';
@import '@fontsource-variable/noto-sans-tc';
@font-face {
font-family: 'ellipsis-font';

View File

@@ -56,57 +56,57 @@ body {
),
rgb(var(--md-sys-color-surface));
--md-sys-typescale-headline-medium-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-headline-medium-line-height: 36px;
--md-sys-typescale-headline-medium-size: 28px;
--md-sys-typescale-headline-medium-weight: 500;
--md-sys-typescale-headline-medium-tracking: 0;
--md-sys-typescale-headline-small-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-headline-small-line-height: 32px;
--md-sys-typescale-headline-small-size: 24px;
--md-sys-typescale-headline-small-weight: 500;
--md-sys-typescale-headline-small-tracking: 0;
--md-sys-typescale-title-medium-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-title-medium-line-height: 24px;
--md-sys-typescale-title-medium-size: 16px;
--md-sys-typescale-title-medium-weight: 600;
--md-sys-typescale-title-medium-tracking: 0.15px;
--md-sys-typescale-label-large-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-label-large-line-height: 20px;
--md-sys-typescale-label-large-size: 14px;
--md-sys-typescale-label-large-weight: 600;
--md-sys-typescale-label-large-tracking: 0.1px;
--md-sys-typescale-label-medium-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-label-medium-line-height: 16px;
--md-sys-typescale-label-medium-size: 12px;
--md-sys-typescale-label-medium-weight: 600;
--md-sys-typescale-label-medium-tracking: 0.5px;
--md-sys-typescale-body-large-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-body-large-line-height: 24px;
--md-sys-typescale-body-large-size: 16px;
--md-sys-typescale-body-large-weight: 400;
--md-sys-typescale-body-large-tracking: 0.5px;
--md-sys-typescale-body-medium-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-body-medium-line-height: 20px;
--md-sys-typescale-body-medium-size: 14px;
--md-sys-typescale-body-medium-weight: 400;
--md-sys-typescale-body-medium-tracking: 0.25px;
--md-sys-typescale-body-small-font:
'Google Sans', 'Noto Sans', 'Noto Sans TC', 'Noto Sans JP',
'Noto Sans SC', 'Roboto', sans-serif;
'Google Sans', 'Noto Sans', 'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans SC Variable', 'Roboto', sans-serif;
--md-sys-typescale-body-small-line-height: 16px;
--md-sys-typescale-body-small-size: 12px;
--md-sys-typescale-body-small-weight: 400;

View File

@@ -14,8 +14,8 @@
--lighter-lighter-lighter-lighter-bg: #857070;
--lighter-border: #aa6065;
--font:
'Poppins', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC',
'Noto Sans SC', sans-serif;
'Poppins', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable',
'Noto Sans SC Variable', sans-serif;
--group-calendar-event-bg: rgba(223, 162, 162, 0.1);
--group-calendar-badge-following: var(--theme);

View File

@@ -221,6 +221,7 @@
useVRCXUpdaterStore
} from '../stores';
import { THEME_CONFIG, links, navDefinitions } from '../shared/constants';
import { getSentry } from '../plugin';
import { openExternalLink } from '../shared/utils';
import configRepository from '../service/config';
@@ -737,12 +738,13 @@
onMounted(async () => {
await loadNavMenuConfig();
if (!sentryErrorReporting.value) return;
if (!NIGHTLY || !sentryErrorReporting.value) return;
try {
import('@sentry/vue').then((Sentry) => {
const feedback = Sentry.getFeedback();
feedback?.attachTo(document.getElementById('feedback'));
});
const Sentry = await getSentry();
const feedback = Sentry.getFeedback();
feedback?.attachTo(document.getElementById('feedback'));
} catch (error) {
console.error('Error setting up Sentry feedback:', error);
}

View File

@@ -1,37 +1,44 @@
const langCodes = [
'cs',
'en',
'es',
'fr',
'hu',
'ja',
'ko',
'pl',
'pt',
'ru',
'th',
'vi',
'zh-CN',
'zh-TW'
];
const elementPlusStrings = {
// Vite does not support dynamic imports to `node_modules`.
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
cs: () => import('element-plus/es/locale/lang/cs'),
en: () => import('element-plus/es/locale/lang/en'),
es: () => import('element-plus/es/locale/lang/es'),
fr: () => import('element-plus/es/locale/lang/fr'),
hu: () => import('element-plus/es/locale/lang/hu'),
ja: () => import('element-plus/es/locale/lang/ja'),
ko: () => import('element-plus/es/locale/lang/ko'),
pl: () => import('element-plus/es/locale/lang/pl'),
pt: () => import('element-plus/es/locale/lang/pt'),
ru: () => import('element-plus/es/locale/lang/ru'),
th: () => import('element-plus/es/locale/lang/th'),
vi: () => import('element-plus/es/locale/lang/vi'),
'zh-CN': () => import('element-plus/es/locale/lang/zh-cn'),
'zh-TW': () => import('element-plus/es/locale/lang/zh-tw')
};
async function getLocalizationStrings() {
const urlPromises = Promise.all(
langCodes.map((code) =>
import(`./${code}.json?url`).then((m) => m.default)
)
);
const urls = await urlPromises;
const fetchPromises = Promise.all(
urls.map((url) => fetch(url).then((res) => res.json()))
);
const results = await fetchPromises;
const entries = langCodes.map((code, index) => {
return [code, results[index]];
});
return Object.fromEntries(entries);
async function getElementPlusStrings(code) {
return (await elementPlusStrings[code]()).default;
}
export { getLocalizationStrings };
async function getLocalizedStrings(code) {
const localizedStringsUrl = new URL(`./${code}.json`, import.meta.url).href;
const localizedStrings = await fetch(localizedStringsUrl).then((response) => response.json())
return {
...localizedStrings,
elementPlus: await getElementPlusStrings(code)
};
}
const languageNames = import.meta.glob('./*.json', {
eager: true,
import: 'language'
});
function getLanguageName(code) {
return languageNames[`./${code}.json`];
}
export * from "./locales";
export { getLanguageName, getLocalizedStrings };

View File

@@ -0,0 +1,17 @@
// Separate file, to be importable in `vite.config.js`.
export const languageCodes = [
'cs',
'en',
'es',
'fr',
'hu',
'ja',
'ko',
'pl',
'pt',
'ru',
'th',
'vi',
'zh-CN',
'zh-TW'
];

View File

@@ -1,18 +1,9 @@
import { createI18n } from 'vue-i18n';
import { getLocalizationStrings } from '../localization/index.js';
const localizedStrings = await getLocalizationStrings();
import { getLocalizedStrings, languageCodes } from '../localization';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: Object.fromEntries(
Object.entries(localizedStrings).map(([key, value]) => [
key.replaceAll('_', '-'),
value
])
),
legacy: false,
globalInjection: false,
missingWarn: false,
@@ -20,11 +11,13 @@ const i18n = createI18n({
fallbackWarn: false
});
async function updateLocalizedStrings() {
const newStrings = await getLocalizationStrings();
Object.entries(newStrings).forEach(([key, value]) => {
i18n.global.setLocaleMessage(key.replaceAll('_', '-'), value);
});
async function loadLocalizedStrings(code) {
const messages = await getLocalizedStrings(code);
i18n.global.setLocaleMessage(code, messages);
}
export { i18n, updateLocalizedStrings };
async function updateLocalizedStrings() {
await loadLocalizedStrings(i18n.global.locale.value);
}
export { i18n, loadLocalizedStrings, updateLocalizedStrings };

View File

@@ -2,26 +2,23 @@ import { router } from './router';
import configRepository from '../service/config';
let version = '';
export async function isSentryOptedIn() {
return NIGHTLY && configRepository.getBool('VRCX_SentryEnabled', false);
}
export async function isSentryEnabled() {
const enabled = await configRepository.getString(
'VRCX_SentryEnabled',
'false'
);
version = await AppApi.GetVersion();
const isNightly = version.includes('Nightly');
if (enabled !== 'true' || !isNightly) {
return false;
}
return true;
/**
* Guarded import, prevents leaking Sentry into non-nightly bundles.
*/
export function getSentry() {
return NIGHTLY ? import('@sentry/vue') : null;
}
export async function initSentry(app) {
if (!NIGHTLY) return;
try {
if (!(await isSentryEnabled())) {
return;
}
if (!(await isSentryOptedIn())) return;
const vrcxId = await configRepository.getString('VRCX_id', '');
const response = await webApiService.execute({
url: 'https://api0.vrcx.app/errorreporting/getdsn',
@@ -40,12 +37,12 @@ export async function initSentry(app) {
return;
}
const dsn = atob(response.data);
const Sentry = await import('@sentry/vue');
const Sentry = await getSentry();
Sentry.init({
app,
dsn,
environment: 'nightly',
release: version,
release: VERSION,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0,
tracesSampleRate: 0.0001,

View File

@@ -1,6 +1,6 @@
import { createPinia } from 'pinia';
import { isSentryEnabled } from '../plugin';
import { getSentry, isSentryOptedIn } from '../plugin';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useAuthStore } from './auth';
@@ -39,11 +39,10 @@ import { useWristOverlaySettingsStore } from './settings/wristOverlay';
export const pinia = createPinia();
async function registerSentryPiniaPlugin() {
if (!(await isSentryEnabled())) {
return;
}
if (!NIGHTLY) return;
if (!(await isSentryOptedIn())) return;
const Sentry = await import('@sentry/vue');
const Sentry = await getSentry();
pinia.use(
Sentry.createSentryPiniaPlugin({
stateTransformer: (state) => ({

View File

@@ -12,13 +12,14 @@ import { watchState } from '../../service/watchState';
import configRepository from '../../service/config';
import webApiService from '../../service/webapi';
import { languageCodes } from '../../localization';
export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
const gameStore = useGameStore();
const vrcxStore = useVrcxStore();
const VRCXUpdaterStore = useVRCXUpdaterStore();
const { availableLocales, t } = useI18n();
const { t } = useI18n();
const state = reactive({
folderSelectorDialogVisible: false
@@ -163,7 +164,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
if (
!bioLanguageConfig ||
!availableLocales.includes(bioLanguageConfig)
!languageCodes.includes(bioLanguageConfig)
) {
bioLanguage.value = 'en';
} else {
@@ -467,7 +468,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
}
async function checkSentryConsent() {
ElMessageBox.confirm(
const { action: consentAction } = await ElMessageBox.confirm(
'Help improve VRCX by allowing anonymous error reporting?</br></br>' +
'• Only collects crash and error information.</br>' +
'• No personal data or VRChat information is collected.</br>' +
@@ -482,42 +483,35 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
closeOnPressEscape: false,
distinguishCancelAndClose: true
}
)
.then(() => {
sentryErrorReporting.value = true;
configRepository.setString('VRCX_SentryEnabled', 'true');
).catch(() => ({ action: 'cancel' }));
ElMessageBox.confirm(
'Error reporting setting has been enabled. Would you like to restart VRCX now for the change to take effect?',
'Restart Required',
{
confirmButtonText: 'Restart Now',
cancelButtonText: 'Later',
type: 'warning',
center: true,
closeOnClickModal: false,
closeOnPressEscape: false
}
).then(() => {
VRCXUpdaterStore.restartVRCX(false);
});
})
.catch((action) => {
const act =
typeof action === 'string' ? action : action?.action;
if (act === 'cancel') {
sentryErrorReporting.value = false;
configRepository.setString('VRCX_SentryEnabled', 'false');
}
});
if (consentAction === 'cancel') return;
const { action: restartAction } = await ElMessageBox.confirm(
'Error reporting setting has been enabled. Would you like to restart VRCX now for the change to take effect?',
'Restart Required',
{
confirmButtonText: 'Restart Now',
cancelButtonText: 'Later',
type: 'warning',
center: true,
closeOnClickModal: false,
closeOnPressEscape: false
}
).catch(() => ({ action: 'cancel' }));
if (restartAction === 'cancel') return;
sentryErrorReporting.value = true;
configRepository.setBool('VRCX_SentryEnabled', true);
VRCXUpdaterStore.restartVRCX(false);
}
async function setSentryErrorReporting() {
if (VRCXUpdaterStore.branch !== 'Nightly') {
return;
}
if (VRCXUpdaterStore.branch !== 'Nightly') return;
ElMessageBox.confirm(
const { action: restartAction } = await ElMessageBox.confirm(
'Error reporting setting has been disabled. Would you like to restart VRCX now for the change to take effect?',
'Restart Required',
{
@@ -526,16 +520,16 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
type: 'info',
center: true
}
)
.then(async () => {
sentryErrorReporting.value = !sentryErrorReporting.value;
await configRepository.setString(
'VRCX_SentryEnabled',
sentryErrorReporting.value ? 'true' : 'false'
);
VRCXUpdaterStore.restartVRCX(false);
})
.catch(() => {});
).catch(() => ({ action: 'cancel' }));
if (restartAction === 'cancel') return;
sentryErrorReporting.value = !sentryErrorReporting.value;
await configRepository.setBool(
'VRCX_SentryEnabled',
sentryErrorReporting.value
);
VRCXUpdaterStore.restartVRCX(false);
}
async function getSqliteTableSizes() {

View File

@@ -14,6 +14,7 @@ import {
} from '../../shared/utils/base/ui';
import { database } from '../../service/database';
import { getNameColour } from '../../shared/utils';
import { loadLocalizedStrings } from '../../plugin';
import { useFeedStore } from '../feed';
import { useGameLogStore } from '../gameLog';
import { useUiStore } from '../ui';
@@ -23,6 +24,7 @@ import { useVrcxStore } from '../vrcx';
import { watchState } from '../../service/watchState';
import configRepository from '../../service/config';
import { languageCodes } from '../../localization';
export const useAppearanceSettingsStore = defineStore(
'AppearanceSettings',
@@ -36,7 +38,7 @@ export const useAppearanceSettingsStore = defineStore(
const router = useRouter();
const uiStore = useUiStore();
const { t, availableLocales, locale } = useI18n();
const { t, locale } = useI18n();
const MAX_TABLE_PAGE_SIZE = 1000;
const DEFAULT_TABLE_PAGE_SIZES = [10, 15, 20, 25, 50, 100];
@@ -180,14 +182,15 @@ export const useAppearanceSettingsStore = defineStore(
const result = await AppApi.CurrentLanguage();
const lang = result.split('-')[0];
availableLocales.forEach((ref) => {
for (const ref of languageCodes) {
const refLang = ref.split('_')[0];
if (refLang === lang) {
changeAppLanguage(ref);
await changeAppLanguage(ref);
}
});
}
} else {
changeAppLanguage(appLanguageConfig);
await changeAppLanguage(appLanguageConfig);
}
themeMode.value = themeModeConfig;
@@ -257,19 +260,23 @@ export const useAppearanceSettingsStore = defineStore(
*
* @param {string} language
*/
function changeAppLanguage(language) {
setAppLanguage(language);
async function changeAppLanguage(language) {
await setAppLanguage(language);
vrStore.updateVRConfigVars();
}
/**
* @param {string} language
*/
function setAppLanguage(language) {
async function setAppLanguage(language) {
console.log('Language changed:', language);
await loadLocalizedStrings(language);
appLanguage.value = language;
configRepository.setString('VRCX_appLanguage', language);
locale.value = appLanguage.value;
changeHtmlLangAttribute(language);
}

View File

@@ -2,6 +2,9 @@
/// <reference types="jest" />
declare global {
const VERSION: string;
const NIGHTLY: boolean;
const WINDOWS: boolean;
const LINUX: boolean;
@@ -416,4 +419,4 @@ declare global {
};
}
export {};
export { };

View File

@@ -6,19 +6,18 @@
<span class="name">{{ t('view.settings.appearance.appearance.language') }}</span>
<el-dropdown trigger="click" size="small" @click.stop>
<el-button size="small">
<span
>{{ messages[appLanguage]?.language }}
<el-icon class="el-icon--right"><ArrowDown /></el-icon
<span>
{{ getLanguageName(appLanguage) }} <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"
v-for="language in languageCodes"
:key="language"
:class="{ 'is-active': appLanguage === language }"
@click="changeAppLanguage(language)"
v-text="obj.language" />
v-text="getLanguageName(language)" />
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -385,10 +384,11 @@
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores';
import { THEME_CONFIG } from '../../../../shared/constants';
import { getLanguageName, languageCodes } from '../../../../localization';
import SimpleSwitch from '../SimpleSwitch.vue';
const { messages, t } = useI18n();
const { t } = useI18n();
const appearanceSettingsStore = useAppearanceSettingsStore();
const { saveOpenVROption, updateVRConfigVars } = useVrStore();

View File

@@ -10,17 +10,17 @@
<el-dropdown trigger="click" size="small" style="float: right" @click.stop>
<el-button size="small">
<span>
{{ messages[bioLanguage]?.language || bioLanguage }}
{{ getLanguageName(bioLanguage) || 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"
v-for="language in languageCodes"
:key="language"
@click="setBioLanguage(language)"
v-text="obj.language" />
v-text="getLanguageName(language)" />
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -108,6 +108,7 @@
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { getLanguageName, languageCodes } from '../../../localization';
import { openExternalLink } from '../../../shared/utils';
import { useAdvancedSettingsStore } from '../../../stores';
@@ -132,7 +133,7 @@
setTranslationApiPrompt
} = advancedSettingsStore;
const { messages, t } = useI18n();
const { t } = useI18n();
const props = defineProps({
isTranslationApiDialogVisible: {

View File

@@ -1,77 +1,128 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import fs from 'node:fs';
import { defineConfig } from 'vite';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import { defineConfig, loadEnv } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { languageCodes } from './localization/locales';
const authToken = process.env.SENTRY_AUTH_TOKEN;
const buildAndUploadSourceMaps = authToken ? true : false;
const vrcxVersion = fs
.readFileSync(resolve(__dirname, '../Version'), 'utf-8')
.trim();
if (buildAndUploadSourceMaps) {
console.log('Source maps will be built and uploaded to Sentry');
function getAssetLanguage(assetId) {
if (!assetId) return null;
if (assetId.endsWith('.json')) {
const language = assetId.split('.json')[0];
if (languageCodes.includes(language)) return language;
}
const language =
assetId.split('element-plus/es/locale/lang/')[1]?.split('.')[0] ||
// Font assets, e.g., noto-sans-jp-regular.woff2 mapped to language code.
{
jp: 'ja',
sc: 'zh-CN',
tc: 'zh-TW',
kr: 'ko'
}[assetId.split('noto-sans-')[1]?.split('-')[0]];
return language || null;
}
// @ts-ignore
export default defineConfig(() => ({
base: '',
plugins: [
vue(),
tailwindcss(),
buildAndUploadSourceMaps &&
sentryVitePlugin({
authToken,
project: 'vrcx-web',
release: {
name: vrcxVersion
},
sourcemaps: {
assets: './build/html/**',
filesToDeleteAfterUpload: './build/html/**/*.js.map'
function getManualChunk(moduleId) {
const language = getAssetLanguage(moduleId);
if (!language) return;
return `languages/${language}`;
}
const defaultAssetName = '[hash][extname]';
function getAssetFilename({ name }) {
const language = getAssetLanguage(name);
if (!language) return `assets/${defaultAssetName}`;
return `assets/languages/${language}-${defaultAssetName}`;
}
export default defineConfig(({ mode }) => {
const { SENTRY_AUTH_TOKEN: sentryAuthToken } = loadEnv(
mode,
process.cwd(),
''
);
const buildAndUploadSourceMaps = !!sentryAuthToken;
const version = fs
.readFileSync(new URL('../Version', import.meta.url), 'utf-8')
.trim();
const nightly = version.split('-').at(-1).length === 7;
return {
base: '',
plugins: [
vue(),
tailwindcss(),
buildAndUploadSourceMaps &&
import('@sentry/vite-plugin').then(({ sentryVitePlugin }) =>
sentryVitePlugin({
authToken: sentryAuthToken,
project: 'vrcx-web',
release: {
name: version
},
sourcemaps: {
assets: './build/html/**',
filesToDeleteAfterUpload: './build/html/**/*.js.map'
}
})
)
],
css: {
transformer: 'lightningcss',
lightningcss: {
minify: true,
targets: {
chrome: 140
}
})
],
css: {
transformer: 'lightningcss',
lightningcss: {
minify: true,
targets: {
chrome: 140
}
}
},
define: {
LINUX: JSON.stringify(process.env.PLATFORM === 'linux'),
WINDOWS: JSON.stringify(process.env.PLATFORM === 'windows')
},
server: {
port: 9000,
strictPort: true
},
build: {
target: 'chrome140',
outDir: '../build/html',
cssMinify: 'lightningcss',
license: true,
emptyOutDir: true,
reportCompressedSize: false,
chunkSizeWarningLimit: 5000,
modulePreload: true,
assetsInlineLimit: 0,
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
vr: resolve(__dirname, 'vr.html')
}
},
sourcemap: buildAndUploadSourceMaps
}
}));
define: {
LINUX: JSON.stringify(process.env.PLATFORM === 'linux'),
WINDOWS: JSON.stringify(process.env.PLATFORM === 'windows'),
VERSION: JSON.stringify(version),
NIGHTLY: JSON.stringify(nightly)
},
server: {
port: 9000,
strictPort: true
},
build: {
target: 'chrome140',
outDir: '../build/html',
cssMinify: 'lightningcss',
license: true,
emptyOutDir: true,
copyPublicDir: false,
// reportCompressedSize: false,
// chunkSizeWarningLimit: 5000,
sourcemap: buildAndUploadSourceMaps,
assetsInlineLimit: 0,
rollupOptions: {
preserveEntrySignatures: false,
input: {
index: resolve(import.meta.dirname, './index.html'),
vr: resolve(import.meta.dirname, './vr.html')
},
output: {
assetFileNames: getAssetFilename,
manualChunks: getManualChunk
}
}
}
};
});

View File

@@ -1416,6 +1416,7 @@
import { escapeTag, escapeTagRecursive } from '../shared/utils/base/string';
import { changeHtmlLangAttribute } from '../shared/utils/base/ui';
import { displayLocation } from '../shared/utils/location';
import { loadLocalizedStrings } from '../plugin/i18n';
import { removeFromArray } from '../shared/utils/base/array';
import { timeToText } from '../shared/utils/base/format';
@@ -2094,12 +2095,14 @@
.replace(' pm', '');
};
function setAppLanguage(appLanguage) {
async function setAppLanguage(appLanguage) {
if (!appLanguage) {
return;
}
if (appLanguage !== vrState.appLanguage) {
vrState.appLanguage = appLanguage;
await loadLocalizedStrings(appLanguage);
changeHtmlLangAttribute(vrState.appLanguage);
//@ts-ignore
i18n.locale = vrState.appLanguage;

View File

@@ -7,7 +7,6 @@ For a copy, see <https://opensource.org/licenses/MIT>.
*/
@import 'tailwindcss';
@import '@fontsource-variable/inter';
@import 'animate.css/animate.min.css';
@import 'noty/lib/noty.css';
@@ -38,23 +37,23 @@ body {
--font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-fallback-cjk: sans-serif;
--font-primary-cjk:
'Noto Sans JP', 'Noto Sans SC', 'Noto Sans KR', 'Noto Sans TC';
'Noto Sans JP Variable', 'Noto Sans SC Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='zh-CN'] {
--font-primary-cjk:
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC';
'Noto Sans SC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='ja'] {
--font-primary-cjk:
'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', 'Noto Sans SC';
'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='ko'] {
--font-primary-cjk:
'Noto Sans KR', 'Noto Sans JP', 'Noto Sans TC', 'Noto Sans SC';
'Noto Sans KR Variable', 'Noto Sans JP Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='zh-TW'] {
--font-primary-cjk:
'Noto Sans TC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans SC';
'Noto Sans TC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans SC Variable';
}
body {
font-family:
@@ -225,8 +224,8 @@ textarea,
select,
button {
font-family:
'ellipsis-font', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC',
'Noto Sans SC', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
'ellipsis-font', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable',
'Noto Sans SC Variable', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
line-height: normal;
text-shadow:
#000 0px 0px 3px,