Skip to content

Commit

Permalink
frontend: lazy-load languages
Browse files Browse the repository at this point in the history
  • Loading branch information
cstenglein committed Nov 11, 2024
1 parent 074e758 commit 91d7407
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 64 deletions.
9 changes: 8 additions & 1 deletion frontends/web/src/components/language/language.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import { useTranslation } from 'react-i18next';
import { TLanguagesList } from './types';

vi.mock('react-i18next', () => ({
useTranslation: vi.fn()
useTranslation: vi.fn(),
hasResourceBundle: vi.fn(),
addResourceBundle: vi.fn(),
changeLanguage: vi.fn()
}));

vi.mock('@/i18n/i18n', () => ({
changei18nLanguage: vi.fn()
}));

describe('components/language/language', () => {
Expand Down
7 changes: 4 additions & 3 deletions frontends/web/src/components/language/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Dialog } from '@/components/dialog/dialog';
import { defaultLanguages, TActiveLanguageCodes, TLanguagesList } from './types';
import style from './language.module.css';
import { getSelectedIndex } from '@/utils/language';
import { changei18nLanguage } from '@/i18n/i18n';

type TLanguageSwitchProps = {
languages?: TLanguagesList;
Expand All @@ -34,10 +35,10 @@ const LanguageSwitch = ({ languages }: TLanguageSwitchProps) => {
const [selectedIndex, setSelectedIndex] = useState<number>(getSelectedIndex(allLanguages, i18n));
const [activeDialog, setActiveDialog] = useState<boolean>(false);

const changeLanguage = (langCode: TActiveLanguageCodes, index: number) => {
const changeLanguage = async (langCode: TActiveLanguageCodes, index: number) => {
setSelectedIndex(index);
setActiveDialog(false);
i18n.changeLanguage(langCode);
await changei18nLanguage(langCode);
};

if (allLanguages.length === 1) {
Expand Down Expand Up @@ -106,4 +107,4 @@ const LanguageSwitch = ({ languages }: TLanguageSwitchProps) => {
);
};

export { LanguageSwitch };
export { LanguageSwitch };
134 changes: 75 additions & 59 deletions frontends/web/src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,59 @@

import i18n from 'i18next';
import { getNativeLocale } from '@/api/nativelocale';
import appTranslationsAR from '@/locales/ar/app.json';
import appTranslationsCS from '@/locales/cs/app.json';
import appTranslationsDE from '@/locales/de/app.json';
import appTranslationsEN from '@/locales/en/app.json';
import appTranslationsFR from '@/locales/fr/app.json';
import appTranslationsJA from '@/locales/ja/app.json';
import appTranslationsRU from '@/locales/ru/app.json';
import appTranslationsMS from '@/locales/ms/app.json';
import appTranslationsNL from '@/locales/nl/app.json';
import appTranslationsPT from '@/locales/pt/app.json';
import appTranslationsHI from '@/locales/hi/app.json';
import appTranslationsBG from '@/locales/bg/app.json';
import appTranslationsTR from '@/locales/tr/app.json';
import appTranslationsZH from '@/locales/zh/app.json';
import appTranslationsFA from '@/locales/fa/app.json';
import appTranslationsES from '@/locales/es/app.json';
import appTranslationsSL from '@/locales/sl/app.json';
import appTranslationsHE from '@/locales/he/app.json';
import appTranslationsIT from '@/locales/it/app.json';
import { languageFromConfig } from './config';
import { localeMainLanguage } from './utils';
import { setConfig } from '@/utils/config';

const locizeProjectID = 'fe4e5a24-e4a2-4903-96fc-3d62c11fc502';

let i18Init = i18n
.use(languageFromConfig);
let isChangingLanguage = false;
const defaultLang = 'en';

const languageResources = {
ar: () => import('@/locales/ar/app.json'),
cs: () => import('@/locales/cs/app.json'),
de: () => import('@/locales/de/app.json'),
en: () => import('@/locales/en/app.json'),
fr: () => import('@/locales/fr/app.json'),
ja: () => import('@/locales/ja/app.json'),
ru: () => import('@/locales/ru/app.json'),
ms: () => import('@/locales/ms/app.json'),
nl: () => import('@/locales/nl/app.json'),
pt: () => import('@/locales/pt/app.json'),
hi: () => import('@/locales/hi/app.json'),
bg: () => import('@/locales/bg/app.json'),
tr: () => import('@/locales/tr/app.json'),
zh: () => import('@/locales/zh/app.json'),
fa: () => import('@/locales/fa/app.json'),
es: () => import('@/locales/es/app.json'),
sl: () => import('@/locales/sl/app.json'),
he: () => import('@/locales/he/app.json'),
it: () => import('@/locales/it/app.json')
};

type LanguageKey = keyof typeof languageResources;

export const loadLanguage = async (language: string) => {
try {
const resources = await languageResources[language as LanguageKey]();
if (!i18n.hasResourceBundle(language, 'app')) {
i18n.addResourceBundle(language, 'app', resources.default || resources);
}
} catch (error) {
console.error(`Failed to load language resources for ${language}:`, error);
}
};

export const changei18nLanguage = async (language: string) => {
await loadLanguage(language);
await i18n.changeLanguage(language);
};

let i18Init = i18n.use(languageFromConfig);

i18Init.init({
fallbackLng: 'en',
fallbackLng: defaultLang,

// have a common namespace used around the full app
ns: ['app', 'wallet'],
Expand All @@ -59,56 +82,49 @@ i18Init.init({
},

react: {
useSuspense : true, // Not using Suspense you will need to handle the not ready state yourself
useSuspense: true // Not using Suspense you will need to handle the not ready state yourself
},

backend: {
projectId: locizeProjectID,
referenceLng: 'en'
},
referenceLng: defaultLang
}
});

i18n.addResourceBundle('ar', 'app', appTranslationsAR);
i18n.addResourceBundle('cs', 'app', appTranslationsCS);
i18n.addResourceBundle('de', 'app', appTranslationsDE);
i18n.addResourceBundle('en', 'app', appTranslationsEN);
i18n.addResourceBundle('fr', 'app', appTranslationsFR);
i18n.addResourceBundle('ja', 'app', appTranslationsJA);
i18n.addResourceBundle('ms', 'app', appTranslationsMS);
i18n.addResourceBundle('nl', 'app', appTranslationsNL);
i18n.addResourceBundle('ru', 'app', appTranslationsRU);
i18n.addResourceBundle('pt', 'app', appTranslationsPT);
i18n.addResourceBundle('hi', 'app', appTranslationsHI);
i18n.addResourceBundle('bg', 'app', appTranslationsBG);
i18n.addResourceBundle('tr', 'app', appTranslationsTR);
i18n.addResourceBundle('zh', 'app', appTranslationsZH);
i18n.addResourceBundle('fa', 'app', appTranslationsFA);
i18n.addResourceBundle('es', 'app', appTranslationsES);
i18n.addResourceBundle('sl', 'app', appTranslationsSL);
i18n.addResourceBundle('he', 'app', appTranslationsHE);
i18n.addResourceBundle('it', 'app', appTranslationsIT);

i18n.on('languageChanged', (lng) => {
// Set userLanguage in config back to empty if system locale matches
// the newly selected language lng to make the app use native-locale again.
// This also covers partial matches. For example, if native locale is pt_BR
// and the app has only pt translation, assume they match.
//
// Since userLanguage is stored in the backend config as a string,
// setting it to null here in JS turns it into an empty string "" in Go backend.
// This is ok since we're just checking for a truthy value in the language detector.
return getNativeLocale().then((nativeLocale) => {
// load the default language first
loadLanguage(defaultLang);

i18n.on('languageChanged', async (lng) => {
// changei18nLanguage triggers languageChanged, thus this check to prevent loop
if (isChangingLanguage) {
return;
}

try {
isChangingLanguage = true;
// Set userLanguage in config back to empty if system locale matches
// the newly selected language lng to make the app use native-locale again.
// This also covers partial matches. For example, if native locale is pt_BR
// and the app has only pt translation, assume they match.
//
// Since userLanguage is stored in the backend config as a string,
// setting it to null here in JS turns it into an empty string "" in Go backend.
// This is ok since we're just checking for a truthy value in the language detector.
const nativeLocale = await getNativeLocale();
let match = lng === nativeLocale;

if (!match) {
// There are too many combinations. So, we compare only the main
// language tag.
const lngLang = localeMainLanguage(lng);
const localeLang = localeMainLanguage(nativeLocale);
const lngLang = localeMainLanguage(lng);
await changei18nLanguage(localeMainLanguage(lng));
match = lngLang === localeLang;
}

const uiLang = match ? null : lng;
return setConfig({ backend: { userLanguage: uiLang } });
});
} finally {
isChangingLanguage = false;
}
});

export { i18n };
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SingleDropdown } from '@/routes/settings/components/dropdowns/singledro
import { GlobeDark, GlobeLight } from '@/components/icon/icon';
import { useDarkmode } from '@/hooks/darkmode';
import styles from './languageDropDownSetting.module.css';
import { changei18nLanguage } from '@/i18n/i18n';

export const LanguageDropdownSetting = () => {
const { i18n, t } = useTranslation();
Expand All @@ -41,7 +42,7 @@ export const LanguageDropdownSetting = () => {
extraComponent={
<SingleDropdown
options={formattedLanguages}
handleChange={i18n.changeLanguage}
handleChange={changei18nLanguage}
value={{ label: selectedLanguage.display, value: selectedLanguage.code }}
/>
}
Expand Down

0 comments on commit 91d7407

Please sign in to comment.