mirror of
https://github.com/mue/mue.git
synced 2026-07-17 14:04:09 +02:00
feat: enhance translation handling by adding fallback mechanism and loading translations dynamically
This commit is contained in:
@@ -56,10 +56,13 @@ const App = () => {
|
||||
|
||||
useAppSetup();
|
||||
|
||||
const languagecode = localStorage.getItem('language') || 'en_GB';
|
||||
const languagecode = variables.languagecode || localStorage.getItem('language') || 'en_GB';
|
||||
|
||||
return (
|
||||
<TranslationProvider initialLanguage={languagecode}>
|
||||
<TranslationProvider
|
||||
initialLanguage={languagecode}
|
||||
initialTranslations={variables.language?.messages || {}}
|
||||
>
|
||||
{showBackground && <Background />}
|
||||
<CustomWidgets />
|
||||
<ToastContainer
|
||||
|
||||
@@ -27,21 +27,35 @@ class ErrorBoundary extends PureComponent {
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
const title = variables.getMessage
|
||||
? variables.getMessage('error_boundary.title')
|
||||
: 'An error occurred';
|
||||
const message = variables.getMessage
|
||||
? variables.getMessage('error_boundary.message')
|
||||
: 'Something went wrong. Please try refreshing the page.';
|
||||
const reportButton = variables.getMessage
|
||||
? variables.getMessage('error_boundary.report_button')
|
||||
: 'Report Error';
|
||||
const sentSuccessfully = variables.getMessage
|
||||
? variables.getMessage('error_boundary.sent_successfully')
|
||||
: 'Report sent successfully';
|
||||
const supportDiscord = variables.getMessage
|
||||
? variables.getMessage('error_boundary.support_discord')
|
||||
: 'Get Support on Discord';
|
||||
|
||||
return (
|
||||
<div className="criticalError">
|
||||
<div className="criticalError-message">
|
||||
<h1>{variables.getMessage('error_boundary.title')}</h1>
|
||||
<p>{variables.getMessage('error_boundary.message')}</p>
|
||||
<h1>{title}</h1>
|
||||
<p>{message}</p>
|
||||
<div className="criticalError-actions">
|
||||
{this.state.showReport ? (
|
||||
<button onClick={() => this.reportError()}>
|
||||
{variables.getMessage('error_boundary.report_button')}
|
||||
</button>
|
||||
<button onClick={() => this.reportError()}>{reportButton}</button>
|
||||
) : (
|
||||
<p>{variables.getMessage('error_boundary.sent_successfully')}</p>
|
||||
<p>{sentSuccessfully}</p>
|
||||
)}
|
||||
<a href="https://discord.gg/zv8C9F8" target="_blank" rel="noreferrer">
|
||||
{variables.getMessage('error_boundary.support_discord')}
|
||||
{supportDiscord}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Stats from 'features/stats/api/stats';
|
||||
const variables = {
|
||||
language: {},
|
||||
languagecode: '',
|
||||
getMessage: (key) => key,
|
||||
stats: Stats,
|
||||
constants,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { initTranslations, translations } from 'lib/translations';
|
||||
import {
|
||||
initTranslations,
|
||||
loadTranslationWithFallback,
|
||||
getLoadedTranslation,
|
||||
} from 'lib/translations';
|
||||
import variables from 'config/variables';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
@@ -16,45 +20,51 @@ const isRTLLanguage = (lang) => RTL_LANGUAGES.includes(lang.split('_')[0]);
|
||||
|
||||
const TranslationContext = createContext();
|
||||
|
||||
export function TranslationProvider({ children, initialLanguage }) {
|
||||
export function TranslationProvider({ children, initialLanguage, initialTranslations }) {
|
||||
const [currentLanguage, setCurrentLanguage] = useState(initialLanguage);
|
||||
const i18nInstance = useRef(initTranslations(initialLanguage));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const i18nInstance = useRef(initTranslations(initialLanguage, initialTranslations));
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLanguage !== initialLanguage) {
|
||||
i18nInstance.current = initTranslations(currentLanguage);
|
||||
}
|
||||
variables.language = i18nInstance.current;
|
||||
variables.languagecode = currentLanguage;
|
||||
document.documentElement.lang = currentLanguage.replace('_', '-');
|
||||
document.documentElement.dir = isRTLLanguage(currentLanguage) ? 'rtl' : 'ltr';
|
||||
}, [currentLanguage, initialLanguage]);
|
||||
}, [currentLanguage]);
|
||||
|
||||
const changeLanguage = useCallback(
|
||||
(newLanguage) => {
|
||||
i18nInstance.current = initTranslations(newLanguage);
|
||||
variables.language = i18nInstance.current;
|
||||
variables.languagecode = newLanguage;
|
||||
document.documentElement.lang = newLanguage.replace('_', '-');
|
||||
document.documentElement.dir = isRTLLanguage(newLanguage) ? 'rtl' : 'ltr';
|
||||
async (newLanguage) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const translations = await loadTranslationWithFallback(newLanguage);
|
||||
const newI18n = initTranslations(newLanguage, translations);
|
||||
|
||||
const currentTabName = localStorage.getItem('tabName');
|
||||
const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname');
|
||||
const currentTabName = localStorage.getItem('tabName');
|
||||
const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname');
|
||||
|
||||
if (currentTabName === oldDefaultTabName || !currentTabName) {
|
||||
const newTabName =
|
||||
translations[newLanguage.replace('-', '_')]?.tabname ||
|
||||
i18nInstance.current?.getMessage(newLanguage, 'tabname') ||
|
||||
'Mue';
|
||||
localStorage.setItem('tabName', newTabName);
|
||||
document.title = newTabName;
|
||||
i18nInstance.current = newI18n;
|
||||
variables.language = newI18n;
|
||||
variables.languagecode = newLanguage;
|
||||
document.documentElement.lang = newLanguage.replace('_', '-');
|
||||
document.documentElement.dir = isRTLLanguage(newLanguage) ? 'rtl' : 'ltr';
|
||||
|
||||
if (currentTabName === oldDefaultTabName || !currentTabName) {
|
||||
const loadedTranslation = getLoadedTranslation(newLanguage);
|
||||
const newTabName =
|
||||
loadedTranslation?.tabname || newI18n.getMessage(newLanguage, 'tabname') || 'Mue';
|
||||
localStorage.setItem('tabName', newTabName);
|
||||
document.title = newTabName;
|
||||
}
|
||||
|
||||
localStorage.setItem('language', newLanguage);
|
||||
localStorage.removeItem('currentWeather');
|
||||
|
||||
setCurrentLanguage(newLanguage);
|
||||
} catch (error) {
|
||||
console.error('Failed to load language:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
localStorage.setItem('language', newLanguage);
|
||||
|
||||
localStorage.removeItem('currentWeather');
|
||||
|
||||
setCurrentLanguage(newLanguage);
|
||||
},
|
||||
[currentLanguage],
|
||||
);
|
||||
@@ -92,9 +102,10 @@ export function TranslationProvider({ children, initialLanguage }) {
|
||||
language: currentLanguage,
|
||||
languagecode: currentLanguage,
|
||||
changeLanguage,
|
||||
isLoading,
|
||||
t,
|
||||
}),
|
||||
[currentLanguage, changeLanguage, t],
|
||||
[currentLanguage, changeLanguage, isLoading, t],
|
||||
);
|
||||
|
||||
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
|
||||
|
||||
@@ -10,27 +10,39 @@ import { migrateAPIUsersToPhotoPacks } from './utils/migrations';
|
||||
import './scss/index.scss';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import { initTranslations } from 'lib/translations';
|
||||
|
||||
const languagecode = localStorage.getItem('language') || 'en_GB';
|
||||
variables.language = initTranslations(languagecode);
|
||||
variables.languagecode = languagecode;
|
||||
document.documentElement.lang = languagecode.replace('_', '-');
|
||||
|
||||
window.t = (text, optional) => variables.language.getMessage(variables.languagecode, text, optional || {});
|
||||
|
||||
migrateAPIUsersToPhotoPacks();
|
||||
|
||||
Sentry.init({
|
||||
dsn: variables.constants.SENTRY_DSN,
|
||||
defaultIntegrations: false,
|
||||
autoSessionTracking: false,
|
||||
});
|
||||
import { initTranslations, loadTranslationWithFallback } from 'lib/translations';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const languagecode = localStorage.getItem('language') || 'en_GB';
|
||||
|
||||
const translations = await loadTranslationWithFallback(languagecode);
|
||||
variables.language = initTranslations(languagecode, translations);
|
||||
variables.languagecode = languagecode;
|
||||
|
||||
document.documentElement.lang = languagecode.replace('_', '-');
|
||||
|
||||
window.t = (text, optional) =>
|
||||
variables.language.getMessage(variables.languagecode, text, optional || {});
|
||||
|
||||
migrateAPIUsersToPhotoPacks();
|
||||
|
||||
Sentry.init({
|
||||
dsn: variables.constants.SENTRY_DSN,
|
||||
defaultIntegrations: false,
|
||||
autoSessionTracking: false,
|
||||
});
|
||||
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize translations:', error);
|
||||
root.render(<div>Failed to load application. Please refresh.</div>);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,117 +1,55 @@
|
||||
import I18n from '@eartharoid/i18n';
|
||||
|
||||
import ar from 'translations/ar.json';
|
||||
import arz from 'translations/arz.json';
|
||||
import az from 'translations/az.json';
|
||||
import azb from 'translations/azb.json';
|
||||
import bn from 'translations/bn.json';
|
||||
import de_DE from 'translations/de_DE.json';
|
||||
import el from 'translations/el.json';
|
||||
import en_GB from 'translations/en_GB.json';
|
||||
import en_US from 'translations/en_US.json';
|
||||
import es from 'translations/es.json';
|
||||
import es_419 from 'translations/es_419.json';
|
||||
import et from 'translations/et.json';
|
||||
import fa from 'translations/fa.json';
|
||||
import fr from 'translations/fr.json';
|
||||
import hu from 'translations/hu.json';
|
||||
import id_ID from 'translations/id_ID.json';
|
||||
import ja from 'translations/ja.json';
|
||||
import lt from 'translations/lt.json';
|
||||
import lv from 'translations/lv.json';
|
||||
import nl from 'translations/nl.json';
|
||||
import no from 'translations/no.json';
|
||||
import peo from 'translations/peo.json';
|
||||
import pt from 'translations/pt.json';
|
||||
import pt_BR from 'translations/pt_BR.json';
|
||||
import ru from 'translations/ru.json';
|
||||
import sl from 'translations/sl.json';
|
||||
import sv from 'translations/sv.json';
|
||||
import ta from 'translations/ta.json';
|
||||
import tr_TR from 'translations/tr_TR.json';
|
||||
import uk from 'translations/uk.json';
|
||||
import vi from 'translations/vi.json';
|
||||
import zh_CN from 'translations/zh_CN.json';
|
||||
import zh_Hant from 'translations/zh_Hant.json';
|
||||
const localeLoaders = import.meta.glob('../i18n/locales/*.json');
|
||||
|
||||
/**
|
||||
* Initialise the i18n object.
|
||||
* The i18n object is then returned.
|
||||
* @param locale _ The locale to use.
|
||||
* @returns The i18n object.
|
||||
*/
|
||||
export function initTranslations(locale) {
|
||||
const i18n = new I18n('en_GB', {
|
||||
ar,
|
||||
arz,
|
||||
az,
|
||||
azb,
|
||||
bn,
|
||||
de_DE,
|
||||
el,
|
||||
en_GB,
|
||||
en_US,
|
||||
es,
|
||||
es_419,
|
||||
et,
|
||||
fa,
|
||||
fr,
|
||||
hu,
|
||||
id_ID,
|
||||
ja,
|
||||
lt,
|
||||
lv,
|
||||
nl,
|
||||
no,
|
||||
peo,
|
||||
pt,
|
||||
pt_BR,
|
||||
ru,
|
||||
sl,
|
||||
sv,
|
||||
ta,
|
||||
tr_TR,
|
||||
uk,
|
||||
vi,
|
||||
zh_CN,
|
||||
zh_Hant,
|
||||
});
|
||||
const loadedTranslations = {};
|
||||
|
||||
export async function loadTranslation(locale) {
|
||||
if (loadedTranslations[locale]) {
|
||||
return loadedTranslations[locale];
|
||||
}
|
||||
|
||||
const path = `../i18n/locales/${locale}.json`;
|
||||
const loader = localeLoaders[path];
|
||||
|
||||
if (!loader) {
|
||||
throw new Error(`Translation file not found for locale: ${locale}`);
|
||||
}
|
||||
|
||||
const module = await loader();
|
||||
const data = module.default;
|
||||
loadedTranslations[locale] = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function loadTranslationWithFallback(locale) {
|
||||
const enGB = await loadTranslation('en_GB');
|
||||
|
||||
if (locale === 'en_GB') {
|
||||
return { en_GB: enGB };
|
||||
}
|
||||
|
||||
const targetLocale = await loadTranslation(locale);
|
||||
|
||||
return {
|
||||
en_GB: enGB,
|
||||
[locale]: targetLocale,
|
||||
};
|
||||
}
|
||||
|
||||
export function initTranslations(locale, translations) {
|
||||
const i18n = new I18n('en_GB', translations);
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export const translations = {
|
||||
ar,
|
||||
arz,
|
||||
az,
|
||||
azb,
|
||||
bn,
|
||||
de_DE,
|
||||
el,
|
||||
en_GB,
|
||||
en_US,
|
||||
es,
|
||||
es_419,
|
||||
et,
|
||||
fa,
|
||||
fr,
|
||||
hu,
|
||||
id_ID,
|
||||
ja,
|
||||
lt,
|
||||
lv,
|
||||
nl,
|
||||
no,
|
||||
peo,
|
||||
pt,
|
||||
pt_BR,
|
||||
ru,
|
||||
sl,
|
||||
sv,
|
||||
ta,
|
||||
tr_TR,
|
||||
uk,
|
||||
vi,
|
||||
zh_CN,
|
||||
zh_Hant,
|
||||
};
|
||||
export function getAvailableLocales() {
|
||||
return Object.keys(localeLoaders).map((path) => {
|
||||
const match = path.match(/locales\/(.+)\.json/);
|
||||
return match ? match[1] : null;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
export function getLoadedTranslation(locale) {
|
||||
return loadedTranslations[locale];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user