mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(web): streamline Web WASM locale startup
This commit is contained in:
1 parent
088d3ee54d
commit
14657847fe
12 files changed
+127
-35
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { resolveWebWasmInitialLocale } from './locale-bootstrap'
|
||||
|
||||
test('uses the saved Web WASM locale before browser language detection', () => {
|
||||
assert.equal(resolveWebWasmInitialLocale('ja-JP', ['zh-CN']), 'ja-JP')
|
||||
})
|
||||
|
||||
test('detects the browser language for a first visit or invalid saved locale', () => {
|
||||
assert.equal(resolveWebWasmInitialLocale('', ['zh-Hant-TW']), 'zh-TW')
|
||||
assert.equal(resolveWebWasmInitialLocale('unsupported', ['en-GB']), 'en-US')
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { detectSystemLocale, isValidLocale, setLocale, type LocaleType } from '@/i18n'
|
||||
import { WEB_WASM_LOCALE_STORAGE_KEY } from '@/services/preferences/browser'
|
||||
|
||||
export function resolveWebWasmInitialLocale(
|
||||
savedLocale: string | null | undefined,
|
||||
browserLanguages?: readonly string[]
|
||||
): LocaleType {
|
||||
return savedLocale && isValidLocale(savedLocale) ? savedLocale : detectSystemLocale(browserLanguages)
|
||||
}
|
||||
|
||||
export function initializeWebWasmLocale(): LocaleType {
|
||||
let savedLocale: string | null = null
|
||||
try {
|
||||
savedLocale = globalThis.localStorage?.getItem(WEB_WASM_LOCALE_STORAGE_KEY) ?? null
|
||||
} catch {
|
||||
// Storage may be unavailable in hardened browser contexts; browser language remains a safe default.
|
||||
}
|
||||
|
||||
const locale = resolveWebWasmInitialLocale(savedLocale)
|
||||
setLocale(locale)
|
||||
return locale
|
||||
}
|
||||
@@ -16,8 +16,11 @@ import { registerWebWasmAdapters } from '@/services/browser-runtime/register'
|
||||
import { handleWebWasmPageHide } from './runtime-lifecycle'
|
||||
import { WebWasmSessionSync } from './session-sync'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { initializeWebWasmLocale } from './locale-bootstrap'
|
||||
import '@/assets/styles/main.css'
|
||||
|
||||
initializeWebWasmLocale()
|
||||
|
||||
async function start(): Promise<void> {
|
||||
reportRuntimeLog({ level: 'info', scope: 'web-wasm-bootstrap', message: 'Initializing Web WASM services' })
|
||||
installGlobalErrorReporting()
|
||||
|
||||
@@ -5,10 +5,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import logoSvg from '@/assets/images/logo.svg'
|
||||
import AgreementModal from '@/components/home/AgreementModal.vue'
|
||||
import ChangelogModal from '@/components/home/ChangelogModal.vue'
|
||||
import HomeFooter from '@/components/home/HomeFooter.vue'
|
||||
import ImportArea from '@/components/import/ImportArea.vue'
|
||||
import LanguageSelectModal from '@/components/home/LanguageSelectModal.vue'
|
||||
import { availableLocales, type LocaleType } from '@/i18n'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
@@ -20,7 +18,6 @@ const sessionStore = useSessionStore()
|
||||
const { sessions } = storeToRefs(sessionStore)
|
||||
const isMounted = ref(false)
|
||||
const languageMenuOpen = ref(false)
|
||||
const changelogModalRef = ref<InstanceType<typeof ChangelogModal> | null>(null)
|
||||
const agreementModalRef = ref<InstanceType<typeof AgreementModal> | null>(null)
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
@@ -170,20 +167,10 @@ onMounted(() => {
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<HomeFooter
|
||||
:remote-config-enabled="false"
|
||||
@open-changelog="changelogModalRef?.open()"
|
||||
@open-terms="agreementModalRef?.open()"
|
||||
/>
|
||||
<HomeFooter :remote-config-enabled="false" :show-changelog="false" @open-terms="agreementModalRef?.open()" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Web WASM intentionally does not gate its fully local import flow on agreement acceptance.
|
||||
Keep the agreement available from HomeFooter only; do not open it from LanguageSelectModal's done event.
|
||||
-->
|
||||
<LanguageSelectModal />
|
||||
<AgreementModal ref="agreementModalRef" />
|
||||
<ChangelogModal ref="changelogModalRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,11 +4,17 @@ import { useI18n } from 'vue-i18n'
|
||||
import { CHATLAB_SITE_BASE, getChatlabSiteLocalePath, getChatlabSiteLangQuery } from '@/utils/chatlabSiteLocale'
|
||||
import { IS_ELECTRON } from '@/utils/platform'
|
||||
import { usePlatformService } from '@/services'
|
||||
import { resolveHomeFooterConfigSource } from './home-footer-config'
|
||||
import { filterHomeFooterLinks, resolveHomeFooterConfigSource } from './home-footer-config'
|
||||
|
||||
const props = defineProps<{
|
||||
remoteConfigEnabled: boolean
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
remoteConfigEnabled: boolean
|
||||
showChangelog?: boolean
|
||||
}>(),
|
||||
{
|
||||
showChangelog: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
openChangelog: []
|
||||
@@ -69,6 +75,7 @@ function getDefaultLinks(): FooterLink[] {
|
||||
}
|
||||
|
||||
const footerLinks = ref<FooterLink[]>(getDefaultLinks())
|
||||
const visibleFooterLinks = computed(() => filterHomeFooterLinks(footerLinks.value, props.showChangelog))
|
||||
|
||||
// 社交链接配置(由远程配置 socialData 直接提供)
|
||||
interface SocialData {
|
||||
@@ -210,7 +217,7 @@ watch(locale, () => {
|
||||
<template>
|
||||
<div class="absolute bottom-4 left-0 right-0">
|
||||
<div class="flex items-center justify-center">
|
||||
<template v-for="(link, index) in footerLinks" :key="link.id">
|
||||
<template v-for="(link, index) in visibleFooterLinks" :key="link.id">
|
||||
<!-- 分隔点 -->
|
||||
<span v-if="index > 0" class="mx-2 text-gray-300 dark:text-gray-600">·</span>
|
||||
<!-- 链接按钮 -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { resolveHomeFooterConfigSource } from './home-footer-config'
|
||||
import { filterHomeFooterLinks, resolveHomeFooterConfigSource } from './home-footer-config'
|
||||
|
||||
test('keeps Web WASM footer configuration cache-only', () => {
|
||||
assert.equal(resolveHomeFooterConfigSource({ remoteConfigEnabled: false, isElectron: false }), 'cache-only')
|
||||
@@ -13,3 +13,14 @@ test('uses the platform adapter for Electron footer configuration', () => {
|
||||
test('uses direct network fetch for CLI Web footer configuration', () => {
|
||||
assert.equal(resolveHomeFooterConfigSource({ remoteConfigEnabled: true, isElectron: false }), 'network')
|
||||
})
|
||||
|
||||
test('hides bundled and cached changelog links when the application disables changelogs', () => {
|
||||
const links = [
|
||||
{ id: 'website', url: 'https://chatlab.fun' },
|
||||
{ id: 'changelog', action: 'changelog' },
|
||||
{ id: 'legacy-release-notes', action: 'changelog' },
|
||||
]
|
||||
|
||||
assert.deepEqual(filterHomeFooterLinks(links, false), [links[0]])
|
||||
assert.deepEqual(filterHomeFooterLinks(links, true), links)
|
||||
})
|
||||
@@ -9,3 +9,11 @@ export function resolveHomeFooterConfigSource(options: HomeFooterConfigSourceOpt
|
||||
if (!options.remoteConfigEnabled) return 'cache-only'
|
||||
return options.isElectron ? 'platform' : 'network'
|
||||
}
|
||||
|
||||
export function filterHomeFooterLinks<T extends { id: string; action?: string }>(
|
||||
links: readonly T[],
|
||||
showChangelog: boolean
|
||||
): T[] {
|
||||
if (showChangelog) return [...links]
|
||||
return links.filter((link) => link.id !== 'changelog' && link.action !== 'changelog')
|
||||
}
|
||||
+9
-1
@@ -25,12 +25,19 @@ function getInitialLocale(): LocaleType {
|
||||
return detectSystemLocale()
|
||||
}
|
||||
|
||||
function setDocumentLocale(locale: LocaleType): void {
|
||||
if (typeof document !== 'undefined') document.documentElement.lang = locale
|
||||
}
|
||||
|
||||
const initialLocale = getInitialLocale()
|
||||
setDocumentLocale(initialLocale)
|
||||
|
||||
/**
|
||||
* 创建 i18n 实例
|
||||
*/
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: getInitialLocale(),
|
||||
locale: initialLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
@@ -45,6 +52,7 @@ export const i18n = createI18n({
|
||||
*/
|
||||
export function setLocale(locale: LocaleType) {
|
||||
i18n.global.locale.value = locale
|
||||
setDocumentLocale(locale)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { detectSystemLocale } from './types'
|
||||
|
||||
test('detects supported locales from the browser language preference order', () => {
|
||||
assert.equal(detectSystemLocale(['zh-Hant-HK', 'en-US']), 'zh-TW')
|
||||
assert.equal(detectSystemLocale(['zh-HK']), 'zh-TW')
|
||||
assert.equal(detectSystemLocale(['zh-CN']), 'zh-CN')
|
||||
assert.equal(detectSystemLocale(['ja-JP']), 'ja-JP')
|
||||
assert.equal(detectSystemLocale(['en-GB']), 'en-US')
|
||||
})
|
||||
|
||||
test('uses the first supported browser language and falls back to English', () => {
|
||||
assert.equal(detectSystemLocale(['fr-FR', 'ja-JP', 'en-US']), 'ja-JP')
|
||||
assert.equal(detectSystemLocale(['fr-FR']), 'en-US')
|
||||
assert.equal(detectSystemLocale([]), 'en-US')
|
||||
})
|
||||
+27
-10
@@ -42,20 +42,37 @@ export const defaultLocale: LocaleType = 'zh-CN'
|
||||
/**
|
||||
* 检测系统语言
|
||||
*/
|
||||
export function detectSystemLocale(): LocaleType {
|
||||
const systemLocale = navigator.language
|
||||
if (systemLocale === 'zh-TW' || systemLocale === 'zh-Hant') {
|
||||
return 'zh-TW'
|
||||
}
|
||||
if (systemLocale.startsWith('zh')) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
if (systemLocale.startsWith('ja')) {
|
||||
return 'ja-JP'
|
||||
export function detectSystemLocale(languages: readonly string[] = getBrowserLanguages()): LocaleType {
|
||||
for (const language of languages) {
|
||||
const locale = matchSupportedLocale(language)
|
||||
if (locale) return locale
|
||||
}
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
function getBrowserLanguages(): readonly string[] {
|
||||
if (typeof navigator === 'undefined') return []
|
||||
return navigator.languages.length > 0 ? navigator.languages : [navigator.language]
|
||||
}
|
||||
|
||||
function matchSupportedLocale(language: string): LocaleType | null {
|
||||
const normalized = language.trim().replaceAll('_', '-').toLowerCase()
|
||||
if (!normalized) return null
|
||||
if (
|
||||
normalized === 'zh-tw' ||
|
||||
normalized.startsWith('zh-hant') ||
|
||||
normalized.startsWith('zh-hk') ||
|
||||
normalized.startsWith('zh-mo') ||
|
||||
normalized.startsWith('zh-cht')
|
||||
) {
|
||||
return 'zh-TW'
|
||||
}
|
||||
if (normalized.startsWith('zh')) return 'zh-CN'
|
||||
if (normalized.startsWith('ja')) return 'ja-JP'
|
||||
if (normalized.startsWith('en')) return 'en-US'
|
||||
return null
|
||||
}
|
||||
|
||||
// ========== Locale 辅助函数 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Preferences, PreferencesAdapter, UiConfig } from './types'
|
||||
|
||||
const PREFERENCES_KEY = 'chatlab:web-wasm:preferences'
|
||||
const UI_CONFIG_KEY = 'chatlab:web-wasm:ui-config'
|
||||
const LOCALE_KEY = 'chatlab:web-wasm:locale'
|
||||
export const WEB_WASM_LOCALE_STORAGE_KEY = 'chatlab:web-wasm:locale'
|
||||
|
||||
type StoredUiConfig = Omit<UiConfig, 'default_session_tab'> & {
|
||||
default_session_tab: UiConfig['default_session_tab'] | 'overview'
|
||||
@@ -42,12 +42,12 @@ export class BrowserPreferencesAdapter implements PreferencesAdapter {
|
||||
}
|
||||
|
||||
async getLocale(): Promise<string> {
|
||||
return this.storage.getItem(LOCALE_KEY) ?? ''
|
||||
return this.storage.getItem(WEB_WASM_LOCALE_STORAGE_KEY) ?? ''
|
||||
}
|
||||
|
||||
async saveLocale(lang: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
this.storage.setItem(LOCALE_KEY, lang)
|
||||
this.storage.setItem(WEB_WASM_LOCALE_STORAGE_KEY, lang)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
|
||||
Reference in new issue
Block a user