diff --git a/src/App.jsx b/src/App.jsx index 023f5579..b9fcd4f2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -56,10 +56,13 @@ const App = () => { useAppSetup(); - const languagecode = localStorage.getItem('language') || 'en_GB'; + const languagecode = variables.languagecode || localStorage.getItem('language') || 'en_GB'; return ( - + {showBackground && }
-

{variables.getMessage('error_boundary.title')}

-

{variables.getMessage('error_boundary.message')}

+

{title}

+

{message}

{this.state.showReport ? ( - + ) : ( -

{variables.getMessage('error_boundary.sent_successfully')}

+

{sentSuccessfully}

)} - {variables.getMessage('error_boundary.support_discord')} + {supportDiscord}
diff --git a/src/config/variables.js b/src/config/variables.js index b5c2d119..23cdc385 100644 --- a/src/config/variables.js +++ b/src/config/variables.js @@ -4,6 +4,7 @@ import Stats from 'features/stats/api/stats'; const variables = { language: {}, languagecode: '', + getMessage: (key) => key, stats: Stats, constants, }; diff --git a/src/contexts/TranslationContext.jsx b/src/contexts/TranslationContext.jsx index 3591d9d9..48f9a44a 100644 --- a/src/contexts/TranslationContext.jsx +++ b/src/contexts/TranslationContext.jsx @@ -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 {children}; diff --git a/src/index.jsx b/src/index.jsx index 8d7a4c7f..b6c7e3de 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -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( - - - , -); + +(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( + + + , + ); + } catch (error) { + console.error('Failed to initialize translations:', error); + root.render(
Failed to load application. Please refresh.
); + } +})(); diff --git a/src/lib/translations.js b/src/lib/translations.js index 3394ae0d..8c29083f 100644 --- a/src/lib/translations.js +++ b/src/lib/translations.js @@ -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]; +}