Skip to content

Commit

Permalink
feat: Add browser language detection (#9062)
Browse files Browse the repository at this point in the history
- 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
devin-ai-integration[bot] and dillchen committed Dec 11, 2024
1 parent 373f274 commit d06ff55
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/commonwealth/client/scripts/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Configuration, fetchCustomDomainQuery } from 'state/api/configuration';
import { fetchNodesQuery } from 'state/api/nodes';
import { errorStore } from 'state/ui/error';
import { EXCEPTION_CASE_VANILLA_getCommunityById } from './api/communities/getCommuityById';
import { languageStore } from './ui/language/language';
import { userStore } from './ui/user';

export enum ApiStatus {
Expand Down Expand Up @@ -66,6 +67,9 @@ export async function initAppState(
updateSelectedCommunity = true,
): Promise<void> {
try {
// Initialize language settings first since it doesn't depend on API
languageStore.getState().initializeLanguage();

const [{ data: statusRes }] = await Promise.all([
axios.get(`${SERVER_URL}/status`),
]);
Expand Down
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,
});
});
});
});
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 packages/commonwealth/client/scripts/state/ui/language/language.ts
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 packages/commonwealth/test/unit/state/ui/language/language.spec.ts
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';
});
});
});

0 comments on commit d06ff55

Please sign in to comment.