feat: enhance translation handling by adding fallback mechanism and loading translations dynamically

This commit is contained in:
alexsparkes
2026-02-06 16:41:02 +00:00
parent 5532d02603
commit dbfe7e52b5
6 changed files with 148 additions and 169 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -4,6 +4,7 @@ import Stats from 'features/stats/api/stats';
const variables = {
language: {},
languagecode: '',
getMessage: (key) => key,
stats: Stats,
constants,
};

View File

@@ -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>;

View File

@@ -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>);
}
})();

View File

@@ -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];
}