mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(settings): the offline language mirror does not outlive the account (#2146)
The mirror of the account language stayed in localStorage past logout, so the next account on the device booted in the previous user's language forever. It is cleared on logout and rewritten on login, and the rate-limit message follows the same order the boot language does.
This commit is contained in:
@@ -126,7 +126,7 @@ const RATE_LIMIT_MESSAGES: Record<string, string> = {
|
||||
function translateRateLimit(): string {
|
||||
const fallback = RATE_LIMIT_MESSAGES['en']!
|
||||
try {
|
||||
const lang = localStorage.getItem('app_language') || 'en'
|
||||
const lang = localStorage.getItem('app_language') || localStorage.getItem('app_language_server') || 'en'
|
||||
return RATE_LIMIT_MESSAGES[lang] ?? fallback
|
||||
} catch {
|
||||
return fallback
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useSystemNoticeStore } from './systemNoticeStore.js'
|
||||
import { clearAppearanceSnapshot } from '../theme/applyAppearance'
|
||||
import { clearAllPluginSessions } from './pluginStore'
|
||||
import { forgetStartDestination } from '../utils/startDestination'
|
||||
import { forgetServerLanguage } from './settingsStore'
|
||||
import { markSignedOut, clearSignedOut } from '../utils/signedOut'
|
||||
|
||||
interface AuthResponse {
|
||||
@@ -236,6 +237,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
// And the startup-destination mirror, or the next account on this browser
|
||||
// gets bounced into a trip it may not even be able to see.
|
||||
forgetStartDestination()
|
||||
// Likewise the language mirror: the login page only overrides it when the
|
||||
// browser language is one TREK ships, so otherwise the next user here stays
|
||||
// in the previous account's language, launch after launch.
|
||||
forgetServerLanguage()
|
||||
// 4. Tell server to clear the httpOnly cookie (best-effort).
|
||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||||
// 5. Clear service worker caches containing sensitive data.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { DEFAULT_SETTINGS, useSettingsStore } from './settingsStore'
|
||||
import { DEFAULT_SETTINGS, forgetServerLanguage, useSettingsStore } from './settingsStore'
|
||||
import { settingsApi } from '../api/client'
|
||||
import { clearTileCache } from '../sync/tilePrefetcher'
|
||||
|
||||
@@ -128,4 +128,35 @@ describe('offline language fallback', () => {
|
||||
const fresh = await import('./settingsStore')
|
||||
expect(fresh.DEFAULT_SETTINGS.language).toBe('de')
|
||||
})
|
||||
|
||||
// The account of the user who just signed in decides, and an account without a
|
||||
// language of its own sends no language key at all. Leaving the previous
|
||||
// account's mirror standing is what stranded the next user on a shared browser.
|
||||
it('SETTINGS-LANG-005: an account with no language of its own clears the mirror', async () => {
|
||||
localStorage.setItem('app_language_server', 'ja')
|
||||
vi.mocked(settingsApi.get).mockResolvedValue({ settings: { default_currency: 'EUR' } } as never)
|
||||
|
||||
await useSettingsStore.getState().loadSettings()
|
||||
|
||||
expect(localStorage.getItem('app_language_server')).toBeNull()
|
||||
})
|
||||
|
||||
it('SETTINGS-LANG-006: a language TREK does not ship never reaches the mirror', async () => {
|
||||
localStorage.setItem('app_language_server', 'ja')
|
||||
vi.mocked(settingsApi.get).mockResolvedValue({ settings: { language: 'kl' } } as never)
|
||||
|
||||
await useSettingsStore.getState().loadSettings()
|
||||
|
||||
expect(localStorage.getItem('app_language_server')).toBeNull()
|
||||
})
|
||||
|
||||
it('SETTINGS-LANG-007: forgetServerLanguage drops the mirror and leaves the explicit choice alone', () => {
|
||||
localStorage.setItem('app_language', 'de')
|
||||
localStorage.setItem('app_language_server', 'ja')
|
||||
|
||||
forgetServerLanguage()
|
||||
|
||||
expect(localStorage.getItem('app_language_server')).toBeNull()
|
||||
expect(localStorage.getItem('app_language')).toBe('de')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,6 +24,33 @@ interface SettingsState {
|
||||
export const hasStoredLanguage = (): boolean =>
|
||||
typeof localStorage !== 'undefined' && !!localStorage.getItem('app_language')
|
||||
|
||||
const SERVER_LANGUAGE_KEY = 'app_language_server'
|
||||
|
||||
// An account that never picked a language sends no language key at all
|
||||
// (getUserSettings returns only what was set), so a mirror left over from
|
||||
// whoever used this browser before has to go rather than survive the load.
|
||||
function rememberServerLanguage(settings: Partial<Settings>): void {
|
||||
try {
|
||||
const language = settings.language
|
||||
if (language && SUPPORTED_LANGUAGE_CODES.includes(language)) {
|
||||
localStorage.setItem(SERVER_LANGUAGE_KEY, language)
|
||||
} else {
|
||||
localStorage.removeItem(SERVER_LANGUAGE_KEY)
|
||||
}
|
||||
} catch {
|
||||
// Private mode: the session still has the value in the store.
|
||||
}
|
||||
}
|
||||
|
||||
/** On logout, so the next account on this browser starts in its own language. */
|
||||
export function forgetServerLanguage(): void {
|
||||
try {
|
||||
localStorage.removeItem(SERVER_LANGUAGE_KEY)
|
||||
} catch {
|
||||
// Nothing to clean up if storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
// The effective client-side defaults for a fresh instance. The server sends no value for
|
||||
// a setting an admin hasn't defaulted (see settingsService.getAdminUserDefaults), so these
|
||||
// are what a brand-new user actually sees. Keep them internally consistent — one
|
||||
@@ -39,7 +66,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
// language (written by loadSettings below), then English. Without the mirror
|
||||
// an offline cold start boots in English: the account's language lives on the
|
||||
// server, and the fetch that would apply it cannot happen.
|
||||
language: localStorage.getItem('app_language') || localStorage.getItem('app_language_server') || 'en',
|
||||
language: localStorage.getItem('app_language') || localStorage.getItem(SERVER_LANGUAGE_KEY) || 'en',
|
||||
temperature_unit: 'celsius',
|
||||
distance_unit: 'metric',
|
||||
time_format: '24h',
|
||||
@@ -110,9 +137,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
// key rather than 'app_language': that one means an explicit in-app
|
||||
// choice, and the login page's detection chain must keep running for
|
||||
// users who never made one.
|
||||
try {
|
||||
if (incoming.language) localStorage.setItem('app_language_server', incoming.language)
|
||||
} catch { /* private mode — the session still has the value in the store */ }
|
||||
rememberServerLanguage(incoming)
|
||||
} catch (err: unknown) {
|
||||
// Leave isLoaded false so a transient failure — offline at launch, or a
|
||||
// (docker) server cold-start racing the first request — is retried on
|
||||
|
||||
Reference in New Issue
Block a user