diff --git a/src/components/Elements/MainModal/backend/Tab.jsx b/src/components/Elements/MainModal/backend/Tab.jsx
index 756f7c74..3373f097 100644
--- a/src/components/Elements/MainModal/backend/Tab.jsx
+++ b/src/components/Elements/MainModal/backend/Tab.jsx
@@ -1,21 +1,20 @@
-import variables from 'config/variables';
import { memo, useState, useEffect } from 'react';
-import { useTranslation } from 'contexts/TranslationContext';
+import { useT } from 'contexts/TranslationContext';
import { getIconComponent, DIVIDER_LABELS } from '../constants/tabConfig';
function Tab({ label, currentTab, onClick, navbarTab }) {
- const { languagecode } = useTranslation();
+ const t = useT();
const [isExperimental, setIsExperimental] = useState(true);
useEffect(() => {
setIsExperimental(localStorage.getItem('experimental') !== 'false');
}, []);
- // Get the icon component for this label
- const IconComponent = getIconComponent(label, variables);
+ // Get the icon component for this label (label is already translated)
+ const IconComponent = getIconComponent(label, { getMessage: t });
// Determine if this label should have a divider after it
- const hasDivider = DIVIDER_LABELS.some((key) => variables.getMessage(key) === label);
+ const hasDivider = DIVIDER_LABELS.some((key) => t(key) === label);
// Build className
const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item';
@@ -23,8 +22,7 @@ function Tab({ label, currentTab, onClick, navbarTab }) {
const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`;
// Hide experimental tab if experimental mode is disabled
- const isExperimentalTab =
- label === variables.getMessage('modals.main.settings.sections.experimental.title');
+ const isExperimentalTab = label === t('modals.main.settings.sections.experimental.title');
if (isExperimentalTab && !isExperimental) {
return
;
}
diff --git a/src/components/Elements/MainModal/backend/Tabs.jsx b/src/components/Elements/MainModal/backend/Tabs.jsx
index c543515c..fbbc1776 100644
--- a/src/components/Elements/MainModal/backend/Tabs.jsx
+++ b/src/components/Elements/MainModal/backend/Tabs.jsx
@@ -1,6 +1,6 @@
-import variables from 'config/variables';
import { useState, useEffect } from 'react';
-import { useTranslation } from 'contexts/TranslationContext';
+import { useT } from 'contexts/TranslationContext';
+import variables from 'config/variables';
import Tab from './Tab';
import ReminderInfo from '../components/ReminderInfo';
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
@@ -16,7 +16,7 @@ const Tabs = ({
navigationTrigger,
sections,
}) => {
- const { languagecode } = useTranslation();
+ const t = useT();
// Find initial section from deep link if available
const getInitialSection = () => {
@@ -24,7 +24,7 @@ const Tabs = ({
const section = sections.find((s) => s.name === deepLinkData.section);
if (section) {
return {
- label: variables.getMessage(section.label),
+ label: t(section.label),
name: section.name,
};
}
@@ -66,23 +66,23 @@ const Tabs = ({
if (sections && currentName) {
const section = sections.find((s) => s.name === currentName);
if (section) {
- const newLabel = variables.getMessage(section.label);
+ const newLabel = t(section.label);
setCurrentTab(newLabel);
}
}
- }, [languagecode]);
+ }, [t, sections, currentName]);
// Handle navigation trigger for settings sections (popstate)
useEffect(() => {
if (navigationTrigger?.type === 'settings-section' && sections) {
const section = sections.find((s) => s.name === navigationTrigger.data);
if (section) {
- const label = variables.getMessage(section.label);
+ const label = t(section.label);
setCurrentTab(label);
setCurrentName(section.name);
}
}
- }, [navigationTrigger, sections]);
+ }, [navigationTrigger, sections, t]);
// Reset to first tab when requested
useEffect(() => {
diff --git a/src/components/Elements/MainModal/components/ModalTopBar.jsx b/src/components/Elements/MainModal/components/ModalTopBar.jsx
index 6eeef96d..f98ef699 100644
--- a/src/components/Elements/MainModal/components/ModalTopBar.jsx
+++ b/src/components/Elements/MainModal/components/ModalTopBar.jsx
@@ -1,5 +1,4 @@
-import variables from 'config/variables';
-import { useTranslation } from 'contexts/TranslationContext';
+import { useT } from 'contexts/TranslationContext';
import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md';
import { Tooltip, Button } from 'components/Elements';
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
@@ -32,13 +31,11 @@ function ModalTopBar({
canGoBack,
canGoForward,
}) {
- const { languagecode } = useTranslation();
+ const t = useT();
- // Get the current tab label (uses languagecode to re-evaluate on language change)
+ // Get the current tab label
const currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab);
- const currentTabLabel = languagecode && currentTabButton
- ? variables.getMessage(currentTabButton.messageKey)
- : '';
+ const currentTabLabel = currentTabButton ? t(currentTabButton.messageKey) : '';
// Utility function to get translated sub-section label
const getSubSectionLabel = (subSection, sectionName) => {
@@ -46,7 +43,7 @@ function ModalTopBar({
// Use the same translation pattern as the section components
const translationKey = `modals.main.settings.sections.${sectionName}.${subSection}.title`;
- const translated = variables.getMessage(translationKey);
+ const translated = t(translationKey);
// If translation key is returned as-is or empty, it means translation doesn't exist
// Fall back to capitalized sub-section name
@@ -107,7 +104,7 @@ function ModalTopBar({
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
if (categoryKey) {
breadcrumbPath.push({
- label: variables.getMessage(categoryKey),
+ label: t(categoryKey),
onClick: productView.onBack || null,
});
}
@@ -214,11 +211,11 @@ function ModalTopBar({
onClick={() => onTabChange(tab)}
active={currentTab === tab}
icon={}
- label={variables.getMessage(messageKey)}
+ label={t(messageKey)}
/>
))}
-
+
diff --git a/src/components/Form/Settings/Radio/Radio.jsx b/src/components/Form/Settings/Radio/Radio.jsx
index 595371bc..d0a503b9 100644
--- a/src/components/Form/Settings/Radio/Radio.jsx
+++ b/src/components/Form/Settings/Radio/Radio.jsx
@@ -1,5 +1,6 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
+import { useTranslation } from 'contexts/TranslationContext';
import {
Radio as RadioUI,
RadioGroup,
@@ -9,9 +10,9 @@ import {
} from '@mui/material';
import EventBus from 'utils/eventbus';
-import { translations } from 'lib/translations';
const Radio = memo((props) => {
+ const { changeLanguage } = useTranslation();
const [value, setValue] = useState(localStorage.getItem(props.name));
const handleChange = useCallback(async (e) => {
@@ -22,14 +23,8 @@ const Radio = memo((props) => {
}
if (props.name === 'language') {
- // old tab name
- if (localStorage.getItem('tabName') === variables.getMessage('tabname')) {
- localStorage.setItem('tabName', translations[newValue.replace('-', '_')].tabname);
- }
-
- // Emit language change event for instant update
- localStorage.setItem(props.name, newValue);
- EventBus.emit('languageChange', { language: newValue });
+ // Use context to change language directly - no EventBus needed
+ changeLanguage(newValue);
setValue(newValue);
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
@@ -59,7 +54,7 @@ const Radio = memo((props) => {
}
EventBus.emit('refresh', props.category);
- }, [value, props]);
+ }, [value, props, changeLanguage]);
return (
diff --git a/src/contexts/README.md b/src/contexts/README.md
index 4b224966..a7587ab7 100644
--- a/src/contexts/README.md
+++ b/src/contexts/README.md
@@ -1,43 +1,96 @@
# Translation System
-## Using Reactive Translations
+## Refactored Translation System ✨
-The app now supports instant language switching without page refresh!
+The app now has a robust, centralized translation system that updates instantly without page refresh!
-### For new components
+### Using Translations (New API)
-Use the `useMessage` hook for reactive translations:
+**Recommended approach** - Use the `useT()` hook:
```jsx
-import { useMessage } from 'contexts/TranslationContext';
+import { useT } from 'contexts/TranslationContext';
function MyComponent() {
- const title = useMessage('modals.main.title');
- const description = useMessage('modals.main.description');
+ const t = useT();
return (
-
{title}
-
{description}
+
{t('modals.main.title')}
+
{t('modals.main.description')}
);
}
```
-### For existing components
+**Alternative** - Use the full hook:
-The old `variables.getMessage()` method still works but won't automatically update when language changes. Gradually migrate to `useMessage` for components that display translations prominently.
+```jsx
+import { useTranslation } from 'contexts/TranslationContext';
-### How it works
+function MyComponent() {
+ const { t, language, changeLanguage } = useTranslation();
-1. `TranslationProvider` wraps the app and listens for language change events
-2. When language is changed via the Radio component, it emits a `languageChange` event
-3. The provider updates the i18n instance and triggers re-renders
-4. Components using `useMessage` automatically update to show new translations
+ return (
+
+
{t('modals.main.title')}
+
+
+ );
+}
+```
+
+### Backward Compatibility
+
+The old `variables.getMessage()` method still works and is automatically reactive:
+
+```jsx
+// This still works and updates on language change
+const title = variables.getMessage('modals.main.title');
+```
+
+This allows gradual migration of components.
+
+### How It Works
+
+1. **TranslationProvider** wraps the app and manages the i18n instance
+2. **Single source of truth** - All translation logic is centralized in the context
+3. **Automatic reactivity** - Components using `t()` automatically re-render on language change
+4. **Backward compatible** - `variables.getMessage()` is updated to use the context internally
+5. **No EventBus needed** - Direct context updates for language changes
### Benefits
-- No page refresh needed
-- Minimal performance impact (translations already loaded)
-- Backward compatible with existing code
-- Smooth user experience
+✅ **Instant updates** - No page refresh needed
+✅ **Single API** - One consistent way to translate
+✅ **Automatic re-renders** - React handles updates efficiently
+✅ **Minimal performance impact** - Translations pre-loaded, only switching active language
+✅ **Type-safe ready** - Easy to add TypeScript support later
+✅ **Clean architecture** - Centralized translation logic
+
+### Migration Guide
+
+**Before:**
+```jsx
+const title = variables.getMessage('modals.main.title');
+```
+
+**After:**
+```jsx
+const t = useT();
+const title = t('modals.main.title');
+```
+
+### Components Updated
+
+Core navigation and UI:
+- ✅ TranslationContext (centralized API)
+- ✅ Radio component (language selector)
+- ✅ Tab, Tabs (sidebar navigation)
+- ✅ ModalTopBar (breadcrumbs)
+- ✅ Settings view
+- ✅ Language section
+- ✅ Refresh button
+- ✅ Welcome modal
+
+All other components continue to work via backward compatibility.
diff --git a/src/contexts/TranslationContext.jsx b/src/contexts/TranslationContext.jsx
index 3ef09a26..39d2442a 100644
--- a/src/contexts/TranslationContext.jsx
+++ b/src/contexts/TranslationContext.jsx
@@ -1,4 +1,4 @@
-import { createContext, useContext, useState, useEffect, useCallback } from 'react';
+import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { initTranslations, translations } from 'lib/translations';
import variables from 'config/variables';
import EventBus from 'utils/eventbus';
@@ -6,25 +6,51 @@ import EventBus from 'utils/eventbus';
const TranslationContext = createContext();
export function TranslationProvider({ children, initialLanguage }) {
- const [languagecode, setLanguagecode] = useState(initialLanguage);
+ const [currentLanguage, setCurrentLanguage] = useState(initialLanguage);
+ const i18nInstance = useRef(null);
+ // Initialize i18n instance once
+ useEffect(() => {
+ i18nInstance.current = initTranslations(currentLanguage);
+ variables.language = i18nInstance.current;
+ variables.languagecode = currentLanguage;
+ document.documentElement.lang = currentLanguage.replace('_', '-');
+ }, [currentLanguage]);
+
+ // Change language function
const changeLanguage = useCallback((newLanguage) => {
// Update the i18n instance
- variables.language = initTranslations(newLanguage);
+ i18nInstance.current = initTranslations(newLanguage);
+ variables.language = i18nInstance.current;
variables.languagecode = newLanguage;
document.documentElement.lang = newLanguage.replace('_', '-');
// Update tab name if it's still the default
- if (localStorage.getItem('tabName') === variables.getMessage('tabname')) {
- const newTabName = translations[newLanguage.replace('-', '_')]?.tabname || variables.getMessage('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;
}
- // Update state to trigger re-render
- setLanguagecode(newLanguage);
- }, []);
+ // Update language in localStorage
+ localStorage.setItem('language', newLanguage);
+ // Update state to trigger re-render
+ setCurrentLanguage(newLanguage);
+ }, [currentLanguage]);
+
+ // Single translation function - the main API
+ const t = useCallback((key, optional = {}) => {
+ if (!i18nInstance.current) return key;
+ return i18nInstance.current.getMessage(currentLanguage, key, optional);
+ }, [currentLanguage]);
+
+ // Listen for EventBus language change events (for backward compatibility)
useEffect(() => {
const handleLanguageChange = (data) => {
if (data?.language) {
@@ -39,8 +65,20 @@ export function TranslationProvider({ children, initialLanguage }) {
};
}, [changeLanguage]);
+ // Update variables.getMessage for backward compatibility
+ useEffect(() => {
+ variables.getMessage = (key, optional = {}) => t(key, optional);
+ }, [t]);
+
+ const value = useMemo(() => ({
+ language: currentLanguage,
+ languagecode: currentLanguage, // Alias for backward compatibility
+ changeLanguage,
+ t,
+ }), [currentLanguage, changeLanguage, t]);
+
return (
-
+
{children}
);
@@ -54,10 +92,8 @@ export function useTranslation() {
return context;
}
-// Hook for reactive translations - triggers re-render when language changes
-export function useMessage(key, optional = {}) {
- const { languagecode } = useTranslation();
- // The languagecode dependency ensures this hook re-evaluates when language changes
- // This is intentional - we use it to trigger re-renders even though it's not directly used
- return languagecode ? variables.getMessage(key, optional) : '';
+// Convenience hook - just returns the t function
+export function useT() {
+ const { t } = useTranslation();
+ return t;
}
diff --git a/src/contexts/index.js b/src/contexts/index.js
index 92d557f6..5e613ec0 100644
--- a/src/contexts/index.js
+++ b/src/contexts/index.js
@@ -1 +1 @@
-export { TranslationProvider, useTranslation, useMessage } from './TranslationContext';
+export { TranslationProvider, useTranslation, useT } from './TranslationContext';
diff --git a/src/features/background/components/PhotoInformation.jsx b/src/features/background/components/PhotoInformation.jsx
index c417153e..17e8ba4e 100644
--- a/src/features/background/components/PhotoInformation.jsx
+++ b/src/features/background/components/PhotoInformation.jsx
@@ -1,5 +1,6 @@
import variables from 'config/variables';
import { useState, memo } from 'react';
+import { useT } from 'contexts';
import Favourite from './Favourite';
import {
MdInfo,
@@ -54,6 +55,7 @@ const downloadImage = async (info) => {
};
function PhotoInformation({ info, url, api }) {
+ const t = useT();
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const [usePhotoMap, setPhotoMap] = useState(false);
@@ -64,7 +66,7 @@ function PhotoInformation({ info, url, api }) {
const [shareModal, openShareModal] = useState(false);
const [excludeModal, openExcludeModal] = useState(false);
const [favouriteTooltipText, setFavouriteTooltipText] = useState(
- variables.getMessage('widgets.quote.favourite'),
+ t('widgets.quote.favourite'),
);
if (info.hidden === true || !info.credit) {
@@ -72,7 +74,7 @@ function PhotoInformation({ info, url, api }) {
}
let credit = info.credit;
- let photo = variables.getMessage('widgets.background.credit');
+ let photo = t('widgets.background.credit');
// unsplash credit
if (info.photographerURL && info.photographerURL !== '' && !info.offline && api) {
@@ -142,31 +144,31 @@ function PhotoInformation({ info, url, api }) {
return (