From d7e06ce361942ff999e7e6f2fe8082a12d0de7a7 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 13 Mar 2026 19:20:37 +0530 Subject: [PATCH] feat(i18n): enhance language detection from browser preferences and add tests --- src/js/i18n/i18n.ts | 16 ++++- src/tests/i18n.test.ts | 129 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/tests/i18n.test.ts diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index f325e72..b8eead4 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -73,7 +73,21 @@ export const getLanguageFromUrl = (): SupportedLanguage => { return storedLang as SupportedLanguage; } - const envLang = import.meta.env.VITE_DEFAULT_LANGUAGE; + // Check browser language preferences + if (typeof navigator !== 'undefined' && navigator.languages) { + for (const lang of navigator.languages) { + if (supportedLanguages.includes(lang as SupportedLanguage)) { + return lang as SupportedLanguage; + } + + const primaryLang = lang.split('-')[0]; + if (supportedLanguages.includes(primaryLang as SupportedLanguage)) { + return primaryLang as SupportedLanguage; + } + } + } + + const envLang = import.meta.env?.VITE_DEFAULT_LANGUAGE; if (envLang && supportedLanguages.includes(envLang as SupportedLanguage)) { return envLang as SupportedLanguage; } diff --git a/src/tests/i18n.test.ts b/src/tests/i18n.test.ts new file mode 100644 index 0000000..2935bb3 --- /dev/null +++ b/src/tests/i18n.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { getLanguageFromUrl } from '@/js/i18n/i18n'; + +describe('getLanguageFromUrl', () => { + const originalLocation = window.location; + const originalNavigator = window.navigator; + + beforeEach(() => { + Object.defineProperty(window, 'location', { + value: { ...originalLocation, pathname: '/' }, + writable: true, + configurable: true, + }); + + localStorage.clear(); + + // Reset navigator + Object.defineProperty(window, 'navigator', { + value: { ...originalNavigator }, + writable: true, + configurable: true, + }); + Object.defineProperty(window.navigator, 'languages', { + value: [], + configurable: true, + }); + + // Reset import.meta.env + vi.stubEnv('BASE_URL', '/'); + vi.stubEnv('VITE_DEFAULT_LANGUAGE', 'en'); + }); + + afterEach(() => { + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + configurable: true, + }); + Object.defineProperty(window, 'navigator', { + value: originalNavigator, + writable: true, + configurable: true, + }); + vi.unstubAllEnvs(); + }); + + it('should return language from URL path', () => { + window.location.pathname = '/de/about'; + expect(getLanguageFromUrl()).toBe('de'); + }); + + it('should prioritize URL path over localStorage', () => { + window.location.pathname = '/fr/'; + localStorage.setItem('i18nextLng', 'es'); + expect(getLanguageFromUrl()).toBe('fr'); + }); + + it('should return language from localStorage if URL has no language', () => { + window.location.pathname = '/about'; + localStorage.setItem('i18nextLng', 'it'); + expect(getLanguageFromUrl()).toBe('it'); + }); + + it('should return exact match from navigator.languages', () => { + window.location.pathname = '/'; + Object.defineProperty(window.navigator, 'languages', { + value: ['zh-TW', 'en-US', 'en'], + configurable: true, + }); + expect(getLanguageFromUrl()).toBe('zh-TW'); + }); + + it('should return primary language match from navigator.languages', () => { + window.location.pathname = '/'; + // 'de-AT' is not in supportedLanguages, but we should match its primary 'de' + Object.defineProperty(window.navigator, 'languages', { + value: ['de-AT', 'en-US', 'en'], + configurable: true, + }); + expect(getLanguageFromUrl()).toBe('de'); + }); + + it('should return first matched language from navigator.languages', () => { + window.location.pathname = '/'; + Object.defineProperty(window.navigator, 'languages', { + value: ['fr-CA', 'de-DE', 'en'], + configurable: true, + }); + expect(getLanguageFromUrl()).toBe('fr'); + }); + + it('should ignore unsupported languages in navigator.languages', () => { + window.location.pathname = '/'; + Object.defineProperty(window.navigator, 'languages', { + value: ['xx-XX', 'es-ES'], + configurable: true, + }); + expect(getLanguageFromUrl()).toBe('es'); + }); + + it('should fallback to env variable if no earlier match', () => { + window.location.pathname = '/'; + Object.defineProperty(window.navigator, 'languages', { + value: ['xx'], + configurable: true, + }); // unsupported + vi.stubEnv('VITE_DEFAULT_LANGUAGE', 'vi'); + expect(getLanguageFromUrl()).toBe('vi'); + }); + + it('should fallback to en if everything else fails', () => { + window.location.pathname = '/'; + Object.defineProperty(window.navigator, 'languages', { + value: [], + configurable: true, + }); + vi.stubEnv('VITE_DEFAULT_LANGUAGE', ''); + expect(getLanguageFromUrl()).toBe('en'); + }); + + it('should handle missing navigator object gracefully', () => { + window.location.pathname = '/'; + Object.defineProperty(window, 'navigator', { + value: undefined, + writable: true, + }); + expect(getLanguageFromUrl()).toBe('en'); + }); +});