-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add browser language detection (#9062)
- Implement getBrowserLanguage to detect and validate browser language - Add initializeLanguage to set initial language preference - Add unit tests for language detection - Initialize language on app start Closes #9062 Co-Authored-By: Dillon Chen <[email protected]>
- Loading branch information
1 parent
373f274
commit d06ff55
Showing
5 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
packages/commonwealth/client/scripts/state/ui/language/__tests__/language.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { beforeEach, describe, expect, it } from 'vitest'; | ||
import { languageStore } from '../language'; | ||
|
||
describe('Language Store', () => { | ||
beforeEach(() => { | ||
// Clear localStorage before each test | ||
localStorage.clear(); | ||
// Reset language store to default state | ||
languageStore.setState({ currentLanguage: 'en' }); | ||
}); | ||
|
||
describe('getBrowserLanguage', () => { | ||
it('should return browser language if supported', () => { | ||
// Mock navigator.language | ||
const originalNavigator = window.navigator; | ||
Object.defineProperty(window, 'navigator', { | ||
value: { ...originalNavigator, language: 'de' }, | ||
configurable: true, | ||
}); | ||
|
||
const browserLang = languageStore.getState().getBrowserLanguage(); | ||
expect(browserLang).toBe('de'); | ||
|
||
// Restore original navigator | ||
Object.defineProperty(window, 'navigator', { | ||
value: originalNavigator, | ||
configurable: true, | ||
}); | ||
}); | ||
|
||
it('should return "en" if browser language is not supported', () => { | ||
// Mock unsupported language | ||
const originalNavigator = window.navigator; | ||
Object.defineProperty(window, 'navigator', { | ||
value: { ...originalNavigator, language: 'fr' }, | ||
configurable: true, | ||
}); | ||
|
||
const browserLang = languageStore.getState().getBrowserLanguage(); | ||
expect(browserLang).toBe('en'); | ||
|
||
// Restore original navigator | ||
Object.defineProperty(window, 'navigator', { | ||
value: originalNavigator, | ||
configurable: true, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('initializeLanguage', () => { | ||
it('should set language from browser if no stored preference', () => { | ||
// Mock German browser language | ||
const originalNavigator = window.navigator; | ||
Object.defineProperty(window, 'navigator', { | ||
value: { ...originalNavigator, language: 'de' }, | ||
configurable: true, | ||
}); | ||
|
||
languageStore.getState().initializeLanguage(); | ||
expect(languageStore.getState().currentLanguage).toBe('de'); | ||
|
||
// Restore original navigator | ||
Object.defineProperty(window, 'navigator', { | ||
value: originalNavigator, | ||
configurable: true, | ||
}); | ||
}); | ||
|
||
it('should keep stored language preference if available', () => { | ||
// Set stored preference to Turkish | ||
languageStore.setState({ currentLanguage: 'tr' }); | ||
|
||
// Mock different browser language | ||
const originalNavigator = window.navigator; | ||
Object.defineProperty(window, 'navigator', { | ||
value: { ...originalNavigator, language: 'de' }, | ||
configurable: true, | ||
}); | ||
|
||
languageStore.getState().initializeLanguage(); | ||
expect(languageStore.getState().currentLanguage).toBe('tr'); | ||
|
||
// Restore original navigator | ||
Object.defineProperty(window, 'navigator', { | ||
value: originalNavigator, | ||
configurable: true, | ||
}); | ||
}); | ||
}); | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/commonwealth/client/scripts/state/ui/language/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const SUPPORTED_LANGUAGES = { | ||
en: { name: 'English', flag: '🇺🇸' }, | ||
ru: { name: 'Russian', flag: '🇷🇺' }, | ||
uk: { name: 'Ukrainian', flag: '🇺🇦' }, | ||
zh: { name: 'Chinese (Traditional)', flag: '🇨🇳' }, | ||
hi: { name: 'Hindi', flag: '🇮🇳' }, | ||
de: { name: 'German', flag: '🇩🇪' }, | ||
tr: { name: 'Turkish', flag: '🇹🇷' }, | ||
} as const; |
44 changes: 44 additions & 0 deletions
44
packages/commonwealth/client/scripts/state/ui/language/language.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { devtools, persist } from 'zustand/middleware'; | ||
import { createStore } from 'zustand/vanilla'; | ||
import { createBoundedUseStore } from '../utils'; | ||
import { SUPPORTED_LANGUAGES } from './constants'; | ||
|
||
interface LanguageStore { | ||
currentLanguage: keyof typeof SUPPORTED_LANGUAGES; | ||
setLanguage: (lang: keyof typeof SUPPORTED_LANGUAGES) => void; | ||
getBrowserLanguage: () => keyof typeof SUPPORTED_LANGUAGES; | ||
initializeLanguage: () => void; | ||
} | ||
|
||
export const languageStore = createStore<LanguageStore>()( | ||
devtools( | ||
persist( | ||
(set) => ({ | ||
currentLanguage: 'en', | ||
setLanguage: (lang) => set({ currentLanguage: lang }), | ||
getBrowserLanguage: () => { | ||
// Get browser language and remove region code if present (e.g., 'en-US' -> 'en') | ||
const browserLang = navigator.language.split('-')[0].toLowerCase(); | ||
|
||
// Check if browser language is supported, return 'en' if not | ||
return browserLang in SUPPORTED_LANGUAGES | ||
? (browserLang as keyof typeof SUPPORTED_LANGUAGES) | ||
: 'en'; | ||
}, | ||
initializeLanguage: () => { | ||
const state = languageStore.getState(); | ||
// Only set language from browser if no stored preference exists | ||
if (!localStorage.getItem('language-store')) { | ||
state.setLanguage(state.getBrowserLanguage()); | ||
} | ||
}, | ||
}), | ||
{ | ||
name: 'language-store', | ||
}, | ||
), | ||
), | ||
); | ||
|
||
const useLanguageStore = createBoundedUseStore(languageStore); | ||
export default useLanguageStore; |
97 changes: 97 additions & 0 deletions
97
packages/commonwealth/test/unit/state/ui/language/language.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; | ||
import { languageStore } from '../../../../../client/scripts/state/ui/language/language'; | ||
|
||
// Mock localStorage | ||
const localStorageMock = (() => { | ||
let store: { [key: string]: string } = {}; | ||
return { | ||
getItem: (key: string) => store[key] || null, | ||
setItem: (key: string, value: string) => { | ||
store[key] = value.toString(); | ||
}, | ||
clear: () => { | ||
store = {}; | ||
}, | ||
}; | ||
})(); | ||
|
||
// Mock window.navigator | ||
const navigatorMock = { | ||
language: 'en', | ||
}; | ||
|
||
describe('Language Store', () => { | ||
beforeAll(() => { | ||
// Setup global mocks | ||
Object.defineProperty(global, 'localStorage', { | ||
value: localStorageMock, | ||
writable: true, | ||
}); | ||
Object.defineProperty(global, 'navigator', { | ||
value: navigatorMock, | ||
writable: true, | ||
}); | ||
}); | ||
|
||
beforeEach(() => { | ||
// Clear localStorage before each test | ||
localStorage.clear(); | ||
// Reset language store to default state | ||
languageStore.setState({ currentLanguage: 'en' }); | ||
}); | ||
|
||
describe('getBrowserLanguage', () => { | ||
it('should return browser language if supported', () => { | ||
// Set navigator language to German | ||
(global.navigator as any).language = 'de'; | ||
|
||
const browserLang = languageStore.getState().getBrowserLanguage(); | ||
expect(browserLang).toBe('de'); | ||
|
||
// Reset navigator language | ||
(global.navigator as any).language = 'en'; | ||
}); | ||
|
||
it('should return "en" if browser language is not supported', () => { | ||
// Set navigator language to unsupported language | ||
(global.navigator as any).language = 'fr'; | ||
|
||
const browserLang = languageStore.getState().getBrowserLanguage(); | ||
expect(browserLang).toBe('en'); | ||
|
||
// Reset navigator language | ||
(global.navigator as any).language = 'en'; | ||
}); | ||
}); | ||
|
||
describe('initializeLanguage', () => { | ||
it('should set language from browser if no stored preference', () => { | ||
// Set navigator language to German | ||
(global.navigator as any).language = 'de'; | ||
|
||
languageStore.getState().initializeLanguage(); | ||
expect(languageStore.getState().currentLanguage).toBe('de'); | ||
|
||
// Reset navigator language | ||
(global.navigator as any).language = 'en'; | ||
}); | ||
|
||
it('should keep stored language preference if available', () => { | ||
// Set stored preference to Turkish | ||
languageStore.setState({ currentLanguage: 'tr' }); | ||
localStorage.setItem( | ||
'language-store', | ||
JSON.stringify({ currentLanguage: 'tr' }), | ||
); | ||
|
||
// Set navigator language to German | ||
(global.navigator as any).language = 'de'; | ||
|
||
languageStore.getState().initializeLanguage(); | ||
expect(languageStore.getState().currentLanguage).toBe('tr'); | ||
|
||
// Reset navigator language | ||
(global.navigator as any).language = 'en'; | ||
}); | ||
}); | ||
}); |