Fix i18n language fallback for regional languages (#4185)

* Fix i18n language fallback for regional languages

* Add release notes

* Fix test
This commit is contained in:
Julian Dominguez-Schatz
2025-01-18 15:07:15 -05:00
committed by GitHub
parent 66619fa20d
commit 6070166f4e
3 changed files with 111 additions and 12 deletions

View File

@@ -0,0 +1,81 @@
import i18n from 'i18next';
import { setI18NextLanguage, availableLanguages } from './i18n';
vi.mock('i18next', () => {
const i18nMock = {
use: vi.fn().mockReturnThis(),
init: vi.fn().mockResolvedValue(undefined),
changeLanguage: vi.fn(),
};
return {
default: i18nMock,
};
});
vi.hoisted(vi.resetModules);
describe('setI18NextLanguage', () => {
beforeEach(async () => {
vi.clearAllMocks();
});
afterEach(vi.unstubAllGlobals);
test('should set system default language when no language is provided', () => {
vi.stubGlobal('navigator', { language: 'uk' });
setI18NextLanguage('');
expect(i18n.changeLanguage).toHaveBeenCalledWith('uk');
});
test('should set the provided language if it is available', () => {
const language = availableLanguages[0];
setI18NextLanguage(language);
expect(i18n.changeLanguage).toHaveBeenCalledWith(language);
});
test('should fallback to English if the provided language is unavailable', () => {
vi.spyOn(console, 'error');
setI18NextLanguage('unknown');
expect(console.error).toHaveBeenCalledWith(
'Unknown locale unknown, falling back to en',
);
expect(i18n.changeLanguage).toHaveBeenCalledWith('en');
});
test('should successfully use a language with a region code if it is known', () => {
const language = 'pt-BR';
setI18NextLanguage(language);
expect(i18n.changeLanguage).toHaveBeenCalledWith(language);
});
test('should fallback to base language if the provided language has an unknown region code', () => {
vi.spyOn(console, 'error');
setI18NextLanguage('uk-ZZ');
expect(console.error).toHaveBeenCalledWith(
'Unknown locale uk-ZZ, falling back to uk',
);
expect(i18n.changeLanguage).toHaveBeenCalledWith('uk');
});
test('should fallback to lowercase language if the provided language has uppercase letters', () => {
vi.spyOn(console, 'error');
setI18NextLanguage('EN');
expect(console.error).toHaveBeenCalledWith(
'Unknown locale EN, falling back to en',
);
expect(i18n.changeLanguage).toHaveBeenCalledWith('en');
});
});

View File

@@ -14,7 +14,6 @@ const isLanguageAvailable = (language: string) =>
const loadLanguage = (language: string) => {
if (!isLanguageAvailable(language)) {
console.error(`Unknown locale ${language}`);
throw new Error(`Unknown locale ${language}`);
}
return languages[`/locale/${language}.json`]();
@@ -41,26 +40,39 @@ i18n
});
export const setI18NextLanguage = (language: string) => {
if (language === 'en' && !isLanguageAvailable(language)) {
// English is always available since we use natural-language keys.
return;
}
if (!language) {
// System default
setI18NextLanguage(navigator.language || 'en');
return;
}
language = language.toLowerCase();
if (!availableLanguages.includes(language)) {
if (language.includes('-')) {
setI18NextLanguage(language.split('-')[0]);
if (!isLanguageAvailable(language)) {
if (language === 'en') {
// English is always available since we use natural-language keys.
return;
}
console.error(`Unknown locale ${language}`);
throw new Error(`Unknown locale ${language}`);
if (language.includes('-')) {
const fallback = language.split('-')[0];
console.error(`Unknown locale ${language}, falling back to ${fallback}`);
setI18NextLanguage(fallback);
return;
}
const lowercaseLanguage = language.toLowerCase();
if (lowercaseLanguage !== language) {
console.error(
`Unknown locale ${language}, falling back to ${lowercaseLanguage}`,
);
setI18NextLanguage(lowercaseLanguage);
return;
}
// Fall back to English
console.error(`Unknown locale ${language}, falling back to en`);
setI18NextLanguage('en');
return;
}
i18n.changeLanguage(language || 'en');
};

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [jfdoming]
---
Fix i18n language fallback for regional languages