mirror of
https://github.com/mue/mue.git
synced 2026-07-19 06:54:10 +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();
|
useAppSetup();
|
||||||
|
|
||||||
const languagecode = localStorage.getItem('language') || 'en_GB';
|
const languagecode = variables.languagecode || localStorage.getItem('language') || 'en_GB';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TranslationProvider initialLanguage={languagecode}>
|
<TranslationProvider
|
||||||
|
initialLanguage={languagecode}
|
||||||
|
initialTranslations={variables.language?.messages || {}}
|
||||||
|
>
|
||||||
{showBackground && <Background />}
|
{showBackground && <Background />}
|
||||||
<CustomWidgets />
|
<CustomWidgets />
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
|
|||||||
@@ -27,21 +27,35 @@ class ErrorBoundary extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error) {
|
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 (
|
return (
|
||||||
<div className="criticalError">
|
<div className="criticalError">
|
||||||
<div className="criticalError-message">
|
<div className="criticalError-message">
|
||||||
<h1>{variables.getMessage('error_boundary.title')}</h1>
|
<h1>{title}</h1>
|
||||||
<p>{variables.getMessage('error_boundary.message')}</p>
|
<p>{message}</p>
|
||||||
<div className="criticalError-actions">
|
<div className="criticalError-actions">
|
||||||
{this.state.showReport ? (
|
{this.state.showReport ? (
|
||||||
<button onClick={() => this.reportError()}>
|
<button onClick={() => this.reportError()}>{reportButton}</button>
|
||||||
{variables.getMessage('error_boundary.report_button')}
|
|
||||||
</button>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{variables.getMessage('error_boundary.sent_successfully')}</p>
|
<p>{sentSuccessfully}</p>
|
||||||
)}
|
)}
|
||||||
<a href="https://discord.gg/zv8C9F8" target="_blank" rel="noreferrer">
|
<a href="https://discord.gg/zv8C9F8" target="_blank" rel="noreferrer">
|
||||||
{variables.getMessage('error_boundary.support_discord')}
|
{supportDiscord}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Stats from 'features/stats/api/stats';
|
|||||||
const variables = {
|
const variables = {
|
||||||
language: {},
|
language: {},
|
||||||
languagecode: '',
|
languagecode: '',
|
||||||
|
getMessage: (key) => key,
|
||||||
stats: Stats,
|
stats: Stats,
|
||||||
constants,
|
constants,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { initTranslations, translations } from 'lib/translations';
|
import {
|
||||||
|
initTranslations,
|
||||||
|
loadTranslationWithFallback,
|
||||||
|
getLoadedTranslation,
|
||||||
|
} from 'lib/translations';
|
||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
import EventBus from 'utils/eventbus';
|
import EventBus from 'utils/eventbus';
|
||||||
|
|
||||||
@@ -16,45 +20,51 @@ const isRTLLanguage = (lang) => RTL_LANGUAGES.includes(lang.split('_')[0]);
|
|||||||
|
|
||||||
const TranslationContext = createContext();
|
const TranslationContext = createContext();
|
||||||
|
|
||||||
export function TranslationProvider({ children, initialLanguage }) {
|
export function TranslationProvider({ children, initialLanguage, initialTranslations }) {
|
||||||
const [currentLanguage, setCurrentLanguage] = useState(initialLanguage);
|
const [currentLanguage, setCurrentLanguage] = useState(initialLanguage);
|
||||||
const i18nInstance = useRef(initTranslations(initialLanguage));
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const i18nInstance = useRef(initTranslations(initialLanguage, initialTranslations));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentLanguage !== initialLanguage) {
|
|
||||||
i18nInstance.current = initTranslations(currentLanguage);
|
|
||||||
}
|
|
||||||
variables.language = i18nInstance.current;
|
variables.language = i18nInstance.current;
|
||||||
variables.languagecode = currentLanguage;
|
variables.languagecode = currentLanguage;
|
||||||
document.documentElement.lang = currentLanguage.replace('_', '-');
|
document.documentElement.lang = currentLanguage.replace('_', '-');
|
||||||
document.documentElement.dir = isRTLLanguage(currentLanguage) ? 'rtl' : 'ltr';
|
document.documentElement.dir = isRTLLanguage(currentLanguage) ? 'rtl' : 'ltr';
|
||||||
}, [currentLanguage, initialLanguage]);
|
}, [currentLanguage]);
|
||||||
|
|
||||||
const changeLanguage = useCallback(
|
const changeLanguage = useCallback(
|
||||||
(newLanguage) => {
|
async (newLanguage) => {
|
||||||
i18nInstance.current = initTranslations(newLanguage);
|
setIsLoading(true);
|
||||||
variables.language = i18nInstance.current;
|
try {
|
||||||
variables.languagecode = newLanguage;
|
const translations = await loadTranslationWithFallback(newLanguage);
|
||||||
document.documentElement.lang = newLanguage.replace('_', '-');
|
const newI18n = initTranslations(newLanguage, translations);
|
||||||
document.documentElement.dir = isRTLLanguage(newLanguage) ? 'rtl' : 'ltr';
|
|
||||||
|
|
||||||
const currentTabName = localStorage.getItem('tabName');
|
const currentTabName = localStorage.getItem('tabName');
|
||||||
const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname');
|
const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname');
|
||||||
|
|
||||||
if (currentTabName === oldDefaultTabName || !currentTabName) {
|
i18nInstance.current = newI18n;
|
||||||
const newTabName =
|
variables.language = newI18n;
|
||||||
translations[newLanguage.replace('-', '_')]?.tabname ||
|
variables.languagecode = newLanguage;
|
||||||
i18nInstance.current?.getMessage(newLanguage, 'tabname') ||
|
document.documentElement.lang = newLanguage.replace('_', '-');
|
||||||
'Mue';
|
document.documentElement.dir = isRTLLanguage(newLanguage) ? 'rtl' : 'ltr';
|
||||||
localStorage.setItem('tabName', newTabName);
|
|
||||||
document.title = newTabName;
|
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],
|
[currentLanguage],
|
||||||
);
|
);
|
||||||
@@ -92,9 +102,10 @@ export function TranslationProvider({ children, initialLanguage }) {
|
|||||||
language: currentLanguage,
|
language: currentLanguage,
|
||||||
languagecode: currentLanguage,
|
languagecode: currentLanguage,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
|
isLoading,
|
||||||
t,
|
t,
|
||||||
}),
|
}),
|
||||||
[currentLanguage, changeLanguage, t],
|
[currentLanguage, changeLanguage, isLoading, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
|
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
|
||||||
|
|||||||
@@ -10,27 +10,39 @@ import { migrateAPIUsersToPhotoPacks } from './utils/migrations';
|
|||||||
import './scss/index.scss';
|
import './scss/index.scss';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
import { initTranslations } from 'lib/translations';
|
import { initTranslations, loadTranslationWithFallback } 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
root.render(
|
|
||||||
<ErrorBoundary>
|
(async () => {
|
||||||
<App />
|
try {
|
||||||
</ErrorBoundary>,
|
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 I18n from '@eartharoid/i18n';
|
||||||
|
|
||||||
import ar from 'translations/ar.json';
|
const localeLoaders = import.meta.glob('../i18n/locales/*.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 loadedTranslations = {};
|
||||||
* 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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;
|
return i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const translations = {
|
export function getAvailableLocales() {
|
||||||
ar,
|
return Object.keys(localeLoaders).map((path) => {
|
||||||
arz,
|
const match = path.match(/locales\/(.+)\.json/);
|
||||||
az,
|
return match ? match[1] : null;
|
||||||
azb,
|
}).filter(Boolean);
|
||||||
bn,
|
}
|
||||||
de_DE,
|
|
||||||
el,
|
export function getLoadedTranslation(locale) {
|
||||||
en_GB,
|
return loadedTranslations[locale];
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user