@@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/svelte';
22import userEvent from '@testing-library/user-event' ;
33import { expect , test , describe , vi , beforeEach , afterEach } from 'vitest' ;
44import LanguageSwitcher from '../LanguageSwitcher.svelte' ;
5+ import { __setTranslatorMode } from 'svelte-i18n' ;
56
67// Mock languages array - must be defined inline in mock factory
78vi . mock ( '$lib/i18n' , ( ) => ( {
@@ -20,7 +21,30 @@ let currentLocale = 'en';
2021const localeSubscribers = [ ] ;
2122
2223vi . mock ( 'svelte-i18n' , ( ) => {
23- // Create the mock locale object inside the factory
24+ let translatorMode = 'normal' ;
25+ const tSubscribers = [ ] ;
26+
27+ const normalT = ( key , options ) => {
28+ if ( key === 'language_switcher.select_language' ) {
29+ return `Select language: ${ options ?. values ?. language || '' } ` ;
30+ }
31+ if ( key === 'language_switcher.available_languages' ) {
32+ return 'Available languages' ;
33+ }
34+ return key ;
35+ } ;
36+
37+ const getTranslator = ( ) => {
38+ if ( translatorMode === 'non-function' ) return null ;
39+ if ( translatorMode === 'throws' ) {
40+ return ( ) => {
41+ throw new Error ( 'Translator error' ) ;
42+ } ;
43+ }
44+ if ( translatorMode === 'returns-key' ) return ( key ) => key ;
45+ return normalT ;
46+ } ;
47+
2448 const mockLocale = {
2549 subscribe : vi . fn ( ( fn ) => {
2650 fn ( currentLocale ) ;
@@ -32,26 +56,26 @@ vi.mock('svelte-i18n', () => {
3256 localeSubscribers . forEach ( ( fn ) => fn ( newLocale ) ) ;
3357 } )
3458 } ;
35- // Mock t function that returns the key with interpolated values
36- const mockT = vi . fn ( ( key , options ) => {
37- if ( key === 'language_switcher.select_language' ) {
38- return `Select language: ${ options ?. values ?. language || '' } ` ;
39- }
40- if ( key === 'language_switcher.available_languages' ) {
41- return 'Available languages' ;
42- }
43- return key ;
44- } ) ;
45- // Create a mock svelte store for t
59+
4660 const mockTStore = {
4761 subscribe : vi . fn ( ( fn ) => {
48- fn ( mockT ) ;
49- return { unsubscribe : ( ) => { } } ;
62+ tSubscribers . push ( fn ) ;
63+ fn ( getTranslator ( ) ) ;
64+ return ( ) => {
65+ const index = tSubscribers . indexOf ( fn ) ;
66+ if ( index !== - 1 ) tSubscribers . splice ( index , 1 ) ;
67+ } ;
5068 } )
5169 } ;
70+
5271 return {
5372 locale : mockLocale ,
54- t : mockTStore
73+ t : mockTStore ,
74+ __setTranslatorMode : ( mode ) => {
75+ translatorMode = mode ;
76+ const translator = getTranslator ( ) ;
77+ tSubscribers . forEach ( ( fn ) => fn ( translator ) ) ;
78+ }
5579 } ;
5680} ) ;
5781
@@ -80,6 +104,7 @@ describe('LanguageSwitcher', () => {
80104
81105 currentLocale = 'en' ;
82106 localeSubscribers . length = 0 ;
107+ __setTranslatorMode ( 'normal' ) ;
83108 mockEnvValue = {
84109 PUBLIC_LANGUAGE_SWITCHER_ENABLED : 'true' ,
85110 PUBLIC_LANGUAGE_SWITCHER_BUTTON_FORMAT : undefined ,
@@ -572,6 +597,49 @@ describe('LanguageSwitcher', () => {
572597 } ) ;
573598 } ) ;
574599
600+ describe ( 'safeTranslate fallbacks' , ( ) => {
601+ test ( 'returns the raw key when $t resolves to a non-function' , ( ) => {
602+ __setTranslatorMode ( 'non-function' ) ;
603+ render ( LanguageSwitcher ) ;
604+
605+ const button = screen . getByRole ( 'button' ) ;
606+ // safeTranslate returns the key itself when the translator is not callable
607+ expect ( button ) . toHaveAttribute ( 'aria-label' , 'language_switcher.select_language' ) ;
608+ } ) ;
609+
610+ test ( 'returns the raw key and logs a warning when $t throws' , async ( ) => {
611+ const consoleWarnSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
612+ __setTranslatorMode ( 'throws' ) ;
613+
614+ const user = userEvent . setup ( ) ;
615+ render ( LanguageSwitcher ) ;
616+
617+ const button = screen . getByRole ( 'button' ) ;
618+ expect ( button ) . toHaveAttribute ( 'aria-label' , 'language_switcher.select_language' ) ;
619+
620+ await user . click ( button ) ;
621+ expect ( screen . getByRole ( 'listbox' ) ) . toHaveAttribute (
622+ 'aria-label' ,
623+ 'language_switcher.available_languages'
624+ ) ;
625+
626+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
627+ expect . stringContaining ( '[i18n fallback]' ) ,
628+ 'Translator error'
629+ ) ;
630+ consoleWarnSpy . mockRestore ( ) ;
631+ } ) ;
632+
633+ test ( 'returns the raw key when the translator returns the key unchanged' , ( ) => {
634+ __setTranslatorMode ( 'returns-key' ) ;
635+ render ( LanguageSwitcher ) ;
636+
637+ const button = screen . getByRole ( 'button' ) ;
638+ // Translator passes the key through, so the aria-label is the raw key
639+ expect ( button ) . toHaveAttribute ( 'aria-label' , 'language_switcher.select_language' ) ;
640+ } ) ;
641+ } ) ;
642+
575643 describe ( 'Locale Variant Handling' , ( ) => {
576644 test ( 'handles locale variants like en-US by extracting base code' , ( ) => {
577645 currentLocale = 'en-US' ;
0 commit comments