tududi/frontend/i18n.ts
Antonis Anastasiadis 220bc92b4a
Lint frontend (#131)
* Add lint-fix npm target

* Sync eslint+plugins with backend

* Add prettier

* Ignore no-explicit-any lint rule for now

* Silence eslint react warning

* Format frontend via prettier

* Lint frontend.

---------

Co-authored-by: antanst <>
2025-07-09 12:23:55 +03:00

251 lines
7.7 KiB
TypeScript

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
const isDevelopment = process.env.NODE_ENV === 'development';
const fallbackResources = {
en: {
translation: {
common: {
loading: 'Loading...',
appLoading: 'Loading application... Please wait.',
error: 'Error',
},
auth: {
login: 'Login',
register: 'Register',
},
errors: {
somethingWentWrong: 'Something went wrong, please try again',
},
},
},
};
const devResources = isDevelopment
? {
en: {
translation: fallbackResources.en.translation,
},
}
: undefined;
const i18nInstance = i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next);
i18nInstance
.init({
fallbackLng: 'en',
debug: isDevelopment,
load: 'languageOnly',
supportedLngs: ['en', 'es', 'el', 'jp', 'ua', 'de'],
nonExplicitSupportedLngs: true,
resources: devResources,
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage', 'cookie'],
},
interpolation: {
escapeValue: false,
},
defaultNS: 'translation',
ns: ['translation'],
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
queryStringParams: { v: '1' },
requestOptions: {
cache: 'default',
credentials: 'same-origin',
mode: 'cors',
},
},
})
.then(() => {
const loadPath = isDevelopment
? `./locales/${i18n.language}/translation.json`
: `/locales/${i18n.language}/translation.json`;
fetch(loadPath)
.then((response) => {
if (!response.ok) {
if (isDevelopment) {
return fetch(
`/locales/${i18n.language}/translation.json`
);
}
throw new Error(
`Failed to fetch translation: ${response.status}`
);
}
return response.json();
})
.then((data) => {
i18n.addResourceBundle(
i18n.language,
'translation',
data,
true,
true
);
})
.catch(() => {
if (isDevelopment) {
try {
setTimeout(() => {
fetch(
`/locales/${i18n.language}/translation.json`,
{
headers: { Accept: 'application/json' },
mode: 'cors',
}
)
.then((res) => res.json())
.then((data) => {
i18n.addResourceBundle(
i18n.language,
'translation',
data,
true,
true
);
})
.catch((error) => {
console.error(
'Error loading translation:',
error
);
});
}, 1000);
} catch (e) {
console.error('Error in retry mechanism:', e);
}
}
});
});
i18n.on('initialized', () => {});
i18n.on('loaded', () => {});
i18n.on('failedLoading', () => {});
i18n.on('missingKey', () => {});
const dispatchLanguageChangeEvent = (lng: string) => {
const event = new CustomEvent('app-language-changed', {
detail: { language: lng },
});
window.dispatchEvent(event);
};
i18n.on('languageChanged', (lng) => {
localStorage.setItem('i18nextLng', lng);
document.documentElement.lang = lng;
const handleTranslationsLoaded = () => {
dispatchLanguageChangeEvent(lng);
if (i18n.services && i18n.services.resourceStore) {
const currentNS = i18n.options.defaultNS || 'translation';
i18n.reloadResources(lng, currentNS);
}
};
if (!i18n.hasResourceBundle(lng, 'translation')) {
const loadPath = isDevelopment
? `./locales/${lng}/translation.json`
: `/locales/${lng}/translation.json`;
fetch(loadPath)
.then((response) => {
if (!response.ok) {
return fetch(`/locales/${lng}/translation.json`);
}
return response;
})
.then((response) => response.json())
.then((data) => {
if (data) {
i18n.addResourceBundle(
lng,
'translation',
data,
true,
true
);
handleTranslationsLoaded();
}
})
.catch((error) => {
console.error('Error loading translations:', error);
handleTranslationsLoaded();
});
} else {
handleTranslationsLoaded();
}
});
declare global {
interface WindowEventMap {
'app-language-changed': CustomEvent<{ language: string }>;
}
interface Window {
checkTranslation: (key: string) => void;
forceLanguageReload: (lng?: string) => void;
}
}
window.checkTranslation = (key: string) => {
try {
const translation = i18n.t(key);
return translation;
} catch (error) {
console.error('Error checking translation:', error);
return null;
}
};
window.forceLanguageReload = (lng?: string) => {
const targetLng = lng || i18n.language;
i18n.reloadResources(targetLng, 'translation')
.then(() => {
dispatchLanguageChangeEvent(targetLng);
if (i18n.services && i18n.services.resourceStore) {
Object.values(i18n.services.resourceStore.data).forEach(
(lang) => {
if (
lang.translation &&
typeof lang.translation === 'object' &&
lang.translation !== null
) {
const temp = {
...(lang.translation as Record<
string,
unknown
>),
};
lang.translation = temp;
}
}
);
}
if (lng) {
setTimeout(() => {
i18n.changeLanguage(targetLng);
}, 50);
}
})
.catch((error) => {
console.error('Error reloading language:', error);
});
};
export default i18n;