mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
feat: add system language detection and prompt on first launch
This commit is contained in:
116
src/localization/__tests__/resolveSystemLanguage.test.js
Normal file
116
src/localization/__tests__/resolveSystemLanguage.test.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { resolveSystemLanguage } from '../index';
|
||||||
|
import { languageCodes } from '../locales';
|
||||||
|
|
||||||
|
describe('resolveSystemLanguage', () => {
|
||||||
|
describe('returns null for invalid input', () => {
|
||||||
|
test('empty string', () => {
|
||||||
|
expect(resolveSystemLanguage('', languageCodes)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null', () => {
|
||||||
|
expect(resolveSystemLanguage(null, languageCodes)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undefined', () => {
|
||||||
|
expect(resolveSystemLanguage(undefined, languageCodes)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsupported language', () => {
|
||||||
|
expect(resolveSystemLanguage('de-DE', languageCodes)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exact match', () => {
|
||||||
|
test('zh-CN matches zh-CN', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-CN', languageCodes)).toBe('zh-CN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-TW matches zh-TW', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-TW', languageCodes)).toBe('zh-TW');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('en matches en', () => {
|
||||||
|
expect(resolveSystemLanguage('en', languageCodes)).toBe('en');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prefix match', () => {
|
||||||
|
test('ja-JP matches ja', () => {
|
||||||
|
expect(resolveSystemLanguage('ja-JP', languageCodes)).toBe('ja');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ko-KR matches ko', () => {
|
||||||
|
expect(resolveSystemLanguage('ko-KR', languageCodes)).toBe('ko');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fr-FR matches fr', () => {
|
||||||
|
expect(resolveSystemLanguage('fr-FR', languageCodes)).toBe('fr');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('en-US matches en', () => {
|
||||||
|
expect(resolveSystemLanguage('en-US', languageCodes)).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('es-MX matches es', () => {
|
||||||
|
expect(resolveSystemLanguage('es-MX', languageCodes)).toBe('es');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pt-BR matches pt', () => {
|
||||||
|
expect(resolveSystemLanguage('pt-BR', languageCodes)).toBe('pt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ru-RU matches ru', () => {
|
||||||
|
expect(resolveSystemLanguage('ru-RU', languageCodes)).toBe('ru');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Chinese region-aware mapping', () => {
|
||||||
|
test('zh-HK maps to zh-TW (traditional)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-HK', languageCodes)).toBe('zh-TW');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-MO maps to zh-TW (traditional)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-MO', languageCodes)).toBe('zh-TW');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-SG maps to zh-CN (simplified)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-SG', languageCodes)).toBe('zh-CN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bare zh maps to zh-CN', () => {
|
||||||
|
expect(resolveSystemLanguage('zh', languageCodes)).toBe('zh-CN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-Hant maps to zh-TW (traditional script tag)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-Hant', languageCodes)).toBe(
|
||||||
|
'zh-TW'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-Hans maps to zh-CN (simplified script tag)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-Hans', languageCodes)).toBe(
|
||||||
|
'zh-CN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-Hant-HK maps to zh-TW (script + region)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-Hant-HK', languageCodes)).toBe(
|
||||||
|
'zh-TW'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-Hans-CN maps to zh-CN (script + region)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-Hans-CN', languageCodes)).toBe(
|
||||||
|
'zh-CN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zh-Hant-MO maps to zh-TW (script + traditional region)', () => {
|
||||||
|
expect(resolveSystemLanguage('zh-Hant-MO', languageCodes)).toBe(
|
||||||
|
'zh-TW'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Detekován jazyk",
|
||||||
|
"description": "Váš systémový jazyk je {language}. Chcete na něj přepnout?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -215,6 +215,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Language Detected",
|
||||||
|
"description": "Your system language appears to be {language}. Would you like to switch to it?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
@@ -2680,7 +2684,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"header_menu": {
|
"header_menu": {
|
||||||
"reset_all": "Reset All",
|
"reset_all": "Reset All",
|
||||||
"lock_column_order": "Lock Column Order"
|
"lock_column_order": "Lock Column"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"rows_per_page": "Rows per page",
|
"rows_per_page": "Rows per page",
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
"devEndpoint": "Punto Final de Desarrollo",
|
"devEndpoint": "Punto Final de Desarrollo",
|
||||||
"endpoint": "Punto Final",
|
"endpoint": "Punto Final",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Idioma detectado",
|
||||||
|
"description": "El idioma de su sistema parece ser {language}. ¿Le gustaría cambiar a él?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Langue détectée",
|
||||||
|
"description": "La langue de votre système semble être {language}. Souhaitez-vous y passer ?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "Fejlesztői végpont",
|
"devEndpoint": "Fejlesztői végpont",
|
||||||
"endpoint": "Végpont",
|
"endpoint": "Végpont",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Nyelv észlelve",
|
||||||
|
"description": "A rendszernyelv úgy tűnik, hogy {language}. Szeretne átváltani rá?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -4,29 +4,25 @@ const localizedStringsUrls = import.meta.glob('./*.json', {
|
|||||||
import: 'default'
|
import: 'default'
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchJson(url) {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn(`Failed to fetch localization: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLocalizedStrings(code) {
|
async function getLocalizedStrings(code) {
|
||||||
const fallbackUrl = localizedStringsUrls['./en.json'];
|
const fallbackUrl = localizedStringsUrls['./en.json'];
|
||||||
const localizedStringsUrl =
|
const url = localizedStringsUrls[`./${code}.json`] || fallbackUrl;
|
||||||
localizedStringsUrls[`./${code}.json`] || fallbackUrl;
|
|
||||||
|
|
||||||
let localizedStrings = {};
|
|
||||||
try {
|
try {
|
||||||
localizedStrings = await fetchJson(localizedStringsUrl);
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(res.status);
|
||||||
|
return await res.json();
|
||||||
} catch {
|
} catch {
|
||||||
if (localizedStringsUrl !== fallbackUrl) {
|
if (url !== fallbackUrl) {
|
||||||
localizedStrings = await fetchJson(fallbackUrl).catch(() => ({}));
|
try {
|
||||||
|
const res = await fetch(fallbackUrl);
|
||||||
|
return await res.json();
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return localizedStrings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageNames = import.meta.glob('./*.json', {
|
const languageNames = import.meta.glob('./*.json', {
|
||||||
@@ -38,5 +34,45 @@ function getLanguageName(code) {
|
|||||||
return String(languageNames[`./${code}.json`] ?? code);
|
return String(languageNames[`./${code}.json`] ?? code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} systemLanguage - BCP-47 code from AppApi.CurrentLanguage()
|
||||||
|
* @param {string[]} codes - supported language codes
|
||||||
|
* @returns {string | null} matched language code, or null
|
||||||
|
*/
|
||||||
|
function resolveSystemLanguage(systemLanguage, codes) {
|
||||||
|
if (!systemLanguage) return null;
|
||||||
|
|
||||||
|
// Exact match (e.g. zh-CN → zh-CN)
|
||||||
|
if (codes.includes(systemLanguage)) {
|
||||||
|
return systemLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = systemLanguage.split('-')[0];
|
||||||
|
|
||||||
|
// Chinese: script-tag and region-aware mapping
|
||||||
|
// BCP-47 forms: zh-CN, zh-TW, zh-Hant, zh-Hans, zh-Hant-HK, zh-Hans-CN, etc.
|
||||||
|
if (lang === 'zh') {
|
||||||
|
const parts = systemLanguage.split('-').slice(1); // everything after 'zh'
|
||||||
|
const hasHant = parts.includes('Hant');
|
||||||
|
const hasHans = parts.includes('Hans');
|
||||||
|
const traditionalRegions = ['TW', 'HK', 'MO'];
|
||||||
|
const hasTraditionalRegion = parts.some((p) =>
|
||||||
|
traditionalRegions.includes(p)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasHant || hasTraditionalRegion) {
|
||||||
|
return codes.includes('zh-TW') ? 'zh-TW' : null;
|
||||||
|
}
|
||||||
|
if (hasHans) {
|
||||||
|
return codes.includes('zh-CN') ? 'zh-CN' : null;
|
||||||
|
}
|
||||||
|
// Bare 'zh' or unknown region (e.g. zh-SG) → simplified
|
||||||
|
return codes.includes('zh-CN') ? 'zh-CN' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic prefix match (e.g. ja-JP → ja)
|
||||||
|
return codes.find((code) => code.split('-')[0] === lang) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export * from './locales';
|
export * from './locales';
|
||||||
export { getLanguageName, getLocalizedStrings };
|
export { getLanguageName, getLocalizedStrings, resolveSystemLanguage };
|
||||||
|
|||||||
@@ -99,6 +99,10 @@
|
|||||||
"devEndpoint": "開発者用エンドポイント",
|
"devEndpoint": "開発者用エンドポイント",
|
||||||
"endpoint": "エンドポイント",
|
"endpoint": "エンドポイント",
|
||||||
"websocket": "ウェブソケット"
|
"websocket": "ウェブソケット"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "言語を検出しました",
|
||||||
|
"description": "システム言語は {language} のようです。切り替えますか?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "개발자용 API",
|
"devEndpoint": "개발자용 API",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "언어 감지됨",
|
||||||
|
"description": "시스템 언어가 {language}인 것 같습니다. 전환하시겠습니까?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
"devEndpoint": "Tryb deweloperski",
|
"devEndpoint": "Tryb deweloperski",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Wykryto język",
|
||||||
|
"description": "Język systemu to {language}. Czy chcesz na niego przełączyć?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Ponto final",
|
"endpoint": "Ponto final",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Idioma detectado",
|
||||||
|
"description": "O idioma do seu sistema parece ser {language}. Gostaria de mudar para ele?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
"devEndpoint": "Разработчик",
|
"devEndpoint": "Разработчик",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Обнаружен язык",
|
||||||
|
"description": "Язык вашей системы — {language}. Хотите переключиться на него?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "ตรวจพบภาษา",
|
||||||
|
"description": "ภาษาของระบบของคุณดูเหมือนจะเป็น {language} คุณต้องการเปลี่ยนไปใช้หรือไม่?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"devEndpoint": "Dev Endpoint",
|
"devEndpoint": "Dev Endpoint",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "Đã phát hiện ngôn ngữ",
|
||||||
|
"description": "Ngôn ngữ hệ thống của bạn có vẻ là {language}. Bạn có muốn chuyển sang không?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -99,6 +99,10 @@
|
|||||||
"devEndpoint": "自定义 API 接口",
|
"devEndpoint": "自定义 API 接口",
|
||||||
"endpoint": "接口地址",
|
"endpoint": "接口地址",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "检测到语言",
|
||||||
|
"description": "您的系统语言似乎是 {language}。是否切换到该语言?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -99,6 +99,10 @@
|
|||||||
"devEndpoint": "開發介面",
|
"devEndpoint": "開發介面",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
|
},
|
||||||
|
"language_detect": {
|
||||||
|
"title": "偵測到語言",
|
||||||
|
"description": "您的系統語言似乎是 {language}。是否切換到該語言?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|||||||
@@ -28,4 +28,17 @@ async function updateLocalizedStrings() {
|
|||||||
await loadLocalizedStrings(i18n.global.locale.value);
|
await loadLocalizedStrings(i18n.global.locale.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { i18n, loadLocalizedStrings, updateLocalizedStrings };
|
/**
|
||||||
|
* Translate a single key using a specific locale without switching global UI language.
|
||||||
|
*
|
||||||
|
* @param {string} locale
|
||||||
|
* @param {string} key
|
||||||
|
* @param {import('vue-i18n').NamedValue=} params
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function tForLocale(locale, key, params = {}) {
|
||||||
|
await loadLocalizedStrings(locale);
|
||||||
|
return i18n.global.t(key, params, { locale });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { i18n, loadLocalizedStrings, tForLocale, updateLocalizedStrings };
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '../../shared/utils/base/ui';
|
} from '../../shared/utils/base/ui';
|
||||||
import { computeTrustLevel, getNameColour } from '../../shared/utils';
|
import { computeTrustLevel, getNameColour } from '../../shared/utils';
|
||||||
import { database } from '../../services/database';
|
import { database } from '../../services/database';
|
||||||
import { languageCodes } from '../../localization';
|
|
||||||
import { loadLocalizedStrings } from '../../plugins';
|
import { loadLocalizedStrings } from '../../plugins';
|
||||||
import { useFeedStore } from '../feed';
|
import { useFeedStore } from '../feed';
|
||||||
import { useGameLogStore } from '../gameLog';
|
import { useGameLogStore } from '../gameLog';
|
||||||
@@ -262,19 +262,15 @@ export const useAppearanceSettingsStore = defineStore(
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!appLanguageConfig) {
|
if (appLanguageConfig) {
|
||||||
const result = await AppApi.CurrentLanguage();
|
|
||||||
|
|
||||||
const lang = result.split('-')[0];
|
|
||||||
|
|
||||||
for (const ref of languageCodes) {
|
|
||||||
const refLang = ref.split('_')[0];
|
|
||||||
if (refLang === lang) {
|
|
||||||
await changeAppLanguage(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await changeAppLanguage(appLanguageConfig);
|
await changeAppLanguage(appLanguageConfig);
|
||||||
|
} else {
|
||||||
|
// First launch: load en in-memory only, do NOT persist.
|
||||||
|
// Login.vue detectAndPromptLanguage() will handle first-time language selection.
|
||||||
|
await loadLocalizedStrings('en');
|
||||||
|
appLanguage.value = 'en';
|
||||||
|
locale.value = 'en';
|
||||||
|
changeHtmlLangAttribute('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
themeMode.value = initThemeMode;
|
themeMode.value = initThemeMode;
|
||||||
|
|||||||
@@ -202,9 +202,12 @@
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAppearanceSettingsStore, useAuthStore, useVrcStatusStore, useVRCXUpdaterStore } from '../../stores';
|
import { useAppearanceSettingsStore, useAuthStore, useModalStore, useVrcStatusStore, useVRCXUpdaterStore } from '../../stores';
|
||||||
import { getLanguageName, languageCodes } from '../../localization';
|
import { getLanguageName, languageCodes, resolveSystemLanguage } from '../../localization';
|
||||||
|
import { tForLocale } from '../../plugins';
|
||||||
import { openExternalLink } from '../../shared/utils';
|
import { openExternalLink } from '../../shared/utils';
|
||||||
|
|
||||||
|
import configRepository from '../../services/config';
|
||||||
import { useUserDisplay } from '../../composables/useUserDisplay';
|
import { useUserDisplay } from '../../composables/useUserDisplay';
|
||||||
import { watchState } from '../../services/watchState';
|
import { watchState } from '../../services/watchState';
|
||||||
|
|
||||||
@@ -221,6 +224,7 @@
|
|||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
const { appLanguage } = storeToRefs(appearanceSettingsStore);
|
const { appLanguage } = storeToRefs(appearanceSettingsStore);
|
||||||
const { changeAppLanguage } = appearanceSettingsStore;
|
const { changeAppLanguage } = appearanceSettingsStore;
|
||||||
|
const modalStore = useModalStore();
|
||||||
|
|
||||||
const vrcStatusStore = useVrcStatusStore();
|
const vrcStatusStore = useVrcStatusStore();
|
||||||
|
|
||||||
@@ -311,12 +315,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
let isActive = true;
|
||||||
|
let isLanguagePromptOpen = false;
|
||||||
|
|
||||||
|
async function detectAndPromptLanguage() {
|
||||||
|
try {
|
||||||
|
const savedLanguage = await configRepository.getString('VRCX_appLanguage');
|
||||||
|
if (savedLanguage || !isActive) return;
|
||||||
|
|
||||||
|
const systemLanguage = await AppApi.CurrentLanguage();
|
||||||
|
if (!systemLanguage || !isActive) return;
|
||||||
|
|
||||||
|
const matchedCode = resolveSystemLanguage(systemLanguage, languageCodes);
|
||||||
|
|
||||||
|
if (!matchedCode || matchedCode === 'en') {
|
||||||
|
if (isActive) await changeAppLanguage('en');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageName = getLanguageName(matchedCode);
|
||||||
|
const [
|
||||||
|
promptTitle,
|
||||||
|
promptDescription,
|
||||||
|
promptConfirmText,
|
||||||
|
promptCancelText
|
||||||
|
] = await Promise.all([
|
||||||
|
tForLocale(matchedCode, 'view.login.language_detect.title'),
|
||||||
|
tForLocale(
|
||||||
|
matchedCode,
|
||||||
|
'view.login.language_detect.description',
|
||||||
|
{
|
||||||
|
language: languageName
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tForLocale(matchedCode, 'dialog.alertdialog.confirm'),
|
||||||
|
tForLocale(matchedCode, 'dialog.alertdialog.cancel')
|
||||||
|
]);
|
||||||
|
|
||||||
|
isLanguagePromptOpen = true;
|
||||||
|
const { ok } = await modalStore.confirm({
|
||||||
|
title: promptTitle,
|
||||||
|
description: promptDescription,
|
||||||
|
confirmText: promptConfirmText,
|
||||||
|
cancelText: promptCancelText
|
||||||
|
});
|
||||||
|
isLanguagePromptOpen = false;
|
||||||
|
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
// Re-check: user may have manually switched language while the dialog was open
|
||||||
|
const currentLanguage = await configRepository.getString('VRCX_appLanguage');
|
||||||
|
if (currentLanguage || !isActive) return;
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
await changeAppLanguage(matchedCode);
|
||||||
|
} else {
|
||||||
|
await changeAppLanguage('en');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
isLanguagePromptOpen = false;
|
||||||
|
console.error('Language detection failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
updateSavedCredentials();
|
updateSavedCredentials();
|
||||||
|
detectAndPromptLanguage();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
isActive = false;
|
||||||
|
if (isLanguagePromptOpen) {
|
||||||
|
modalStore.handleCancel();
|
||||||
|
isLanguagePromptOpen = false;
|
||||||
|
}
|
||||||
resetForm({
|
resetForm({
|
||||||
values: {
|
values: {
|
||||||
username: '',
|
username: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user