feat(translation): refactor translation system to use useT hook for reactive updates

This commit is contained in:
alexsparkes
2026-01-24 17:18:29 +00:00
parent b8647538fd
commit c88b3cc03c
15 changed files with 218 additions and 127 deletions

View File

@@ -1,21 +1,20 @@
import variables from 'config/variables';
import { memo, useState, useEffect } from 'react'; import { memo, useState, useEffect } from 'react';
import { useTranslation } from 'contexts/TranslationContext'; import { useT } from 'contexts/TranslationContext';
import { getIconComponent, DIVIDER_LABELS } from '../constants/tabConfig'; import { getIconComponent, DIVIDER_LABELS } from '../constants/tabConfig';
function Tab({ label, currentTab, onClick, navbarTab }) { function Tab({ label, currentTab, onClick, navbarTab }) {
const { languagecode } = useTranslation(); const t = useT();
const [isExperimental, setIsExperimental] = useState(true); const [isExperimental, setIsExperimental] = useState(true);
useEffect(() => { useEffect(() => {
setIsExperimental(localStorage.getItem('experimental') !== 'false'); setIsExperimental(localStorage.getItem('experimental') !== 'false');
}, []); }, []);
// Get the icon component for this label // Get the icon component for this label (label is already translated)
const IconComponent = getIconComponent(label, variables); const IconComponent = getIconComponent(label, { getMessage: t });
// Determine if this label should have a divider after it // 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 // Build className
const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item'; const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item';
@@ -23,8 +22,7 @@ function Tab({ label, currentTab, onClick, navbarTab }) {
const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`; const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`;
// Hide experimental tab if experimental mode is disabled // Hide experimental tab if experimental mode is disabled
const isExperimentalTab = const isExperimentalTab = label === t('modals.main.settings.sections.experimental.title');
label === variables.getMessage('modals.main.settings.sections.experimental.title');
if (isExperimentalTab && !isExperimental) { if (isExperimentalTab && !isExperimental) {
return <hr />; return <hr />;
} }

View File

@@ -1,6 +1,6 @@
import variables from 'config/variables';
import { useState, useEffect } from 'react'; 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 Tab from './Tab';
import ReminderInfo from '../components/ReminderInfo'; import ReminderInfo from '../components/ReminderInfo';
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary'; import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
@@ -16,7 +16,7 @@ const Tabs = ({
navigationTrigger, navigationTrigger,
sections, sections,
}) => { }) => {
const { languagecode } = useTranslation(); const t = useT();
// Find initial section from deep link if available // Find initial section from deep link if available
const getInitialSection = () => { const getInitialSection = () => {
@@ -24,7 +24,7 @@ const Tabs = ({
const section = sections.find((s) => s.name === deepLinkData.section); const section = sections.find((s) => s.name === deepLinkData.section);
if (section) { if (section) {
return { return {
label: variables.getMessage(section.label), label: t(section.label),
name: section.name, name: section.name,
}; };
} }
@@ -66,23 +66,23 @@ const Tabs = ({
if (sections && currentName) { if (sections && currentName) {
const section = sections.find((s) => s.name === currentName); const section = sections.find((s) => s.name === currentName);
if (section) { if (section) {
const newLabel = variables.getMessage(section.label); const newLabel = t(section.label);
setCurrentTab(newLabel); setCurrentTab(newLabel);
} }
} }
}, [languagecode]); }, [t, sections, currentName]);
// Handle navigation trigger for settings sections (popstate) // Handle navigation trigger for settings sections (popstate)
useEffect(() => { useEffect(() => {
if (navigationTrigger?.type === 'settings-section' && sections) { if (navigationTrigger?.type === 'settings-section' && sections) {
const section = sections.find((s) => s.name === navigationTrigger.data); const section = sections.find((s) => s.name === navigationTrigger.data);
if (section) { if (section) {
const label = variables.getMessage(section.label); const label = t(section.label);
setCurrentTab(label); setCurrentTab(label);
setCurrentName(section.name); setCurrentName(section.name);
} }
} }
}, [navigationTrigger, sections]); }, [navigationTrigger, sections, t]);
// Reset to first tab when requested // Reset to first tab when requested
useEffect(() => { useEffect(() => {

View File

@@ -1,5 +1,4 @@
import variables from 'config/variables'; import { useT } from 'contexts/TranslationContext';
import { useTranslation } from 'contexts/TranslationContext';
import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md'; import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md';
import { Tooltip, Button } from 'components/Elements'; import { Tooltip, Button } from 'components/Elements';
import { NAVBAR_BUTTONS } from '../constants/tabConfig'; import { NAVBAR_BUTTONS } from '../constants/tabConfig';
@@ -32,13 +31,11 @@ function ModalTopBar({
canGoBack, canGoBack,
canGoForward, 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 currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab);
const currentTabLabel = languagecode && currentTabButton const currentTabLabel = currentTabButton ? t(currentTabButton.messageKey) : '';
? variables.getMessage(currentTabButton.messageKey)
: '';
// Utility function to get translated sub-section label // Utility function to get translated sub-section label
const getSubSectionLabel = (subSection, sectionName) => { const getSubSectionLabel = (subSection, sectionName) => {
@@ -46,7 +43,7 @@ function ModalTopBar({
// Use the same translation pattern as the section components // Use the same translation pattern as the section components
const translationKey = `modals.main.settings.sections.${sectionName}.${subSection}.title`; 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 // If translation key is returned as-is or empty, it means translation doesn't exist
// Fall back to capitalized sub-section name // Fall back to capitalized sub-section name
@@ -107,7 +104,7 @@ function ModalTopBar({
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type]; const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
if (categoryKey) { if (categoryKey) {
breadcrumbPath.push({ breadcrumbPath.push({
label: variables.getMessage(categoryKey), label: t(categoryKey),
onClick: productView.onBack || null, onClick: productView.onBack || null,
}); });
} }
@@ -214,11 +211,11 @@ function ModalTopBar({
onClick={() => onTabChange(tab)} onClick={() => onTabChange(tab)}
active={currentTab === tab} active={currentTab === tab}
icon={<Icon />} icon={<Icon />}
label={variables.getMessage(messageKey)} label={t(messageKey)}
/> />
))} ))}
</div> </div>
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')} key="closeTooltip"> <Tooltip title={t('modals.welcome.buttons.close')} key="closeTooltip">
<span className="closeModal" onClick={onClose}> <span className="closeModal" onClick={onClose}>
<MdClose /> <MdClose />
</span> </span>

View File

@@ -1,5 +1,6 @@
import variables from 'config/variables'; import variables from 'config/variables';
import { memo, useState, useCallback } from 'react'; import { memo, useState, useCallback } from 'react';
import { useTranslation } from 'contexts/TranslationContext';
import { import {
Radio as RadioUI, Radio as RadioUI,
RadioGroup, RadioGroup,
@@ -9,9 +10,9 @@ import {
} from '@mui/material'; } from '@mui/material';
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
import { translations } from 'lib/translations';
const Radio = memo((props) => { const Radio = memo((props) => {
const { changeLanguage } = useTranslation();
const [value, setValue] = useState(localStorage.getItem(props.name)); const [value, setValue] = useState(localStorage.getItem(props.name));
const handleChange = useCallback(async (e) => { const handleChange = useCallback(async (e) => {
@@ -22,14 +23,8 @@ const Radio = memo((props) => {
} }
if (props.name === 'language') { if (props.name === 'language') {
// old tab name // Use context to change language directly - no EventBus needed
if (localStorage.getItem('tabName') === variables.getMessage('tabname')) { changeLanguage(newValue);
localStorage.setItem('tabName', translations[newValue.replace('-', '_')].tabname);
}
// Emit language change event for instant update
localStorage.setItem(props.name, newValue);
EventBus.emit('languageChange', { language: newValue });
setValue(newValue); setValue(newValue);
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`); variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
@@ -59,7 +54,7 @@ const Radio = memo((props) => {
} }
EventBus.emit('refresh', props.category); EventBus.emit('refresh', props.category);
}, [value, props]); }, [value, props, changeLanguage]);
return ( return (
<FormControl component="fieldset"> <FormControl component="fieldset">

View File

@@ -1,43 +1,96 @@
# Translation System # 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 ```jsx
import { useMessage } from 'contexts/TranslationContext'; import { useT } from 'contexts/TranslationContext';
function MyComponent() { function MyComponent() {
const title = useMessage('modals.main.title'); const t = useT();
const description = useMessage('modals.main.description');
return ( return (
<div> <div>
<h1>{title}</h1> <h1>{t('modals.main.title')}</h1>
<p>{description}</p> <p>{t('modals.main.description')}</p>
</div> </div>
); );
} }
``` ```
### 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 return (
2. When language is changed via the Radio component, it emits a `languageChange` event <div>
3. The provider updates the i18n instance and triggers re-renders <h1>{t('modals.main.title')}</h1>
4. Components using `useMessage` automatically update to show new translations <button onClick={() => changeLanguage('es')}>Español</button>
</div>
);
}
```
### 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 ### Benefits
- No page refresh needed **Instant updates** - No page refresh needed
- Minimal performance impact (translations already loaded) **Single API** - One consistent way to translate
- Backward compatible with existing code **Automatic re-renders** - React handles updates efficiently
- Smooth user experience **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.

View File

@@ -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 { initTranslations, translations } from 'lib/translations';
import variables from 'config/variables'; import variables from 'config/variables';
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
@@ -6,25 +6,51 @@ import EventBus from 'utils/eventbus';
const TranslationContext = createContext(); const TranslationContext = createContext();
export function TranslationProvider({ children, initialLanguage }) { 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) => { const changeLanguage = useCallback((newLanguage) => {
// Update the i18n instance // Update the i18n instance
variables.language = initTranslations(newLanguage); i18nInstance.current = initTranslations(newLanguage);
variables.language = i18nInstance.current;
variables.languagecode = newLanguage; variables.languagecode = newLanguage;
document.documentElement.lang = newLanguage.replace('_', '-'); document.documentElement.lang = newLanguage.replace('_', '-');
// Update tab name if it's still the default // Update tab name if it's still the default
if (localStorage.getItem('tabName') === variables.getMessage('tabname')) { const currentTabName = localStorage.getItem('tabName');
const newTabName = translations[newLanguage.replace('-', '_')]?.tabname || variables.getMessage('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); localStorage.setItem('tabName', newTabName);
document.title = newTabName; document.title = newTabName;
} }
// Update state to trigger re-render // Update language in localStorage
setLanguagecode(newLanguage); 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(() => { useEffect(() => {
const handleLanguageChange = (data) => { const handleLanguageChange = (data) => {
if (data?.language) { if (data?.language) {
@@ -39,8 +65,20 @@ export function TranslationProvider({ children, initialLanguage }) {
}; };
}, [changeLanguage]); }, [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 ( return (
<TranslationContext.Provider value={{ languagecode, changeLanguage }}> <TranslationContext.Provider value={value}>
{children} {children}
</TranslationContext.Provider> </TranslationContext.Provider>
); );
@@ -54,10 +92,8 @@ export function useTranslation() {
return context; return context;
} }
// Hook for reactive translations - triggers re-render when language changes // Convenience hook - just returns the t function
export function useMessage(key, optional = {}) { export function useT() {
const { languagecode } = useTranslation(); const { t } = useTranslation();
// The languagecode dependency ensures this hook re-evaluates when language changes return t;
// This is intentional - we use it to trigger re-renders even though it's not directly used
return languagecode ? variables.getMessage(key, optional) : '';
} }

View File

@@ -1 +1 @@
export { TranslationProvider, useTranslation, useMessage } from './TranslationContext'; export { TranslationProvider, useTranslation, useT } from './TranslationContext';

View File

@@ -1,5 +1,6 @@
import variables from 'config/variables'; import variables from 'config/variables';
import { useState, memo } from 'react'; import { useState, memo } from 'react';
import { useT } from 'contexts';
import Favourite from './Favourite'; import Favourite from './Favourite';
import { import {
MdInfo, MdInfo,
@@ -54,6 +55,7 @@ const downloadImage = async (info) => {
}; };
function PhotoInformation({ info, url, api }) { function PhotoInformation({ info, url, api }) {
const t = useT();
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0); const [height, setHeight] = useState(0);
const [usePhotoMap, setPhotoMap] = useState(false); const [usePhotoMap, setPhotoMap] = useState(false);
@@ -64,7 +66,7 @@ function PhotoInformation({ info, url, api }) {
const [shareModal, openShareModal] = useState(false); const [shareModal, openShareModal] = useState(false);
const [excludeModal, openExcludeModal] = useState(false); const [excludeModal, openExcludeModal] = useState(false);
const [favouriteTooltipText, setFavouriteTooltipText] = useState( const [favouriteTooltipText, setFavouriteTooltipText] = useState(
variables.getMessage('widgets.quote.favourite'), t('widgets.quote.favourite'),
); );
if (info.hidden === true || !info.credit) { if (info.hidden === true || !info.credit) {
@@ -72,7 +74,7 @@ function PhotoInformation({ info, url, api }) {
} }
let credit = info.credit; let credit = info.credit;
let photo = variables.getMessage('widgets.background.credit'); let photo = t('widgets.background.credit');
// unsplash credit // unsplash credit
if (info.photographerURL && info.photographerURL !== '' && !info.offline && api) { if (info.photographerURL && info.photographerURL !== '' && !info.offline && api) {
@@ -142,31 +144,31 @@ function PhotoInformation({ info, url, api }) {
return ( return (
<div className="extra-content"> <div className="extra-content">
{info.location && info.location !== 'N/A' ? ( {info.location && info.location !== 'N/A' ? (
<div className="row" title={variables.getMessage('widgets.background.location')}> <div className="row" title={t('widgets.background.location')}>
<MdLocationOn /> <MdLocationOn />
<span id="infoLocation">{info.location}</span> <span id="infoLocation">{info.location}</span>
</div> </div>
) : null} ) : null}
{info.camera && info.camera !== 'N/A' ? ( {info.camera && info.camera !== 'N/A' ? (
<div className="row" title={variables.getMessage('widgets.background.camera')}> <div className="row" title={t('widgets.background.camera')}>
<MdPhotoCamera /> <MdPhotoCamera />
<span id="infoCamera">{info.camera}</span> <span id="infoCamera">{info.camera}</span>
</div> </div>
) : null} ) : null}
<div className="row" title={variables.getMessage('widgets.background.resolution')}> <div className="row" title={t('widgets.background.resolution')}>
<Resolution /> <Resolution />
<span id="infoResolution"> <span id="infoResolution">
{width}x{height} {width}x{height}
</span> </span>
</div> </div>
{info.category && ( {info.category && (
<div className="row" title={variables.getMessage('widgets.background.category')}> <div className="row" title={t('widgets.background.category')}>
<Category /> <Category />
<span id="infoCategory">{info.category[0].toUpperCase() + info.category.slice(1)}</span> <span id="infoCategory">{info.category[0].toUpperCase() + info.category.slice(1)}</span>
</div> </div>
)} )}
{api && ( {api && (
<div className="row" title={variables.getMessage('widgets.background.source')}> <div className="row" title={t('widgets.background.source')}>
<Source /> <Source />
<span id="infoSource"> <span id="infoSource">
{info.photoURL ? ( {info.photoURL ? (
@@ -189,7 +191,7 @@ function PhotoInformation({ info, url, api }) {
return ( return (
<div className="buttons"> <div className="buttons">
{!info.offline && ( {!info.offline && (
<Tooltip title={variables.getMessage('widgets.quote.share')} key="share" placement="top"> <Tooltip title={t('widgets.quote.share')} key="share" placement="top">
<Share onClick={() => openShareModal(true)} /> <Share onClick={() => openShareModal(true)} />
</Tooltip> </Tooltip>
)} )}
@@ -204,7 +206,7 @@ function PhotoInformation({ info, url, api }) {
</Tooltip> </Tooltip>
{!info.offline && ( {!info.offline && (
<Tooltip <Tooltip
title={variables.getMessage('widgets.background.download')} title={t('widgets.background.download')}
key="download" key="download"
placement="top" placement="top"
> >
@@ -213,7 +215,7 @@ function PhotoInformation({ info, url, api }) {
)} )}
{info.pun && info.category && ( {info.pun && info.category && (
<Tooltip <Tooltip
title={variables.getMessage('widgets.background.exclude')} title={t('widgets.background.exclude')}
key="exclude" key="exclude"
placement="top" placement="top"
> >
@@ -227,16 +229,16 @@ function PhotoInformation({ info, url, api }) {
const UnsplashStats = () => { const UnsplashStats = () => {
return ( return (
<div className="unsplashStats"> <div className="unsplashStats">
<div title={variables.getMessage('widgets.background.views')}> <div title={t('widgets.background.views')}>
<Views /> <Views />
<span>{info.views.toLocaleString()}</span> <span>{info.views.toLocaleString()}</span>
</div> </div>
<div title={variables.getMessage('widgets.background.downloads')}> <div title={t('widgets.background.downloads')}>
<Download /> <Download />
<span>{info.downloads.toLocaleString()}</span> <span>{info.downloads.toLocaleString()}</span>
</div> </div>
{info.likes ? ( {info.likes ? (
<div title={variables.getMessage('widgets.background.likes')}> <div title={t('widgets.background.likes')}>
<MdFavourite /> <MdFavourite />
<span>{info.likes.toLocaleString()}</span> <span>{info.likes.toLocaleString()}</span>
</div> </div>
@@ -366,7 +368,7 @@ function PhotoInformation({ info, url, api }) {
{(showExtraInfo || other) && excludeModal === false ? ( {(showExtraInfo || other) && excludeModal === false ? (
<> <>
<span className="subtitle"> <span className="subtitle">
{variables.getMessage('widgets.background.information')} {t('widgets.background.information')}
</span> </span>
<InformationItems /> <InformationItems />
<ActionButtons /> <ActionButtons />

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import variables from 'config/variables'; import variables from 'config/variables';
import { nth, convertTimezone } from 'utils/date'; import { nth, convertTimezone } from 'utils/date';
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
import { useT } from 'contexts';
import './greeting.scss'; import './greeting.scss';
/** /**
@@ -44,6 +45,7 @@ const calculateAge = (date) => {
}; };
const Greeting = () => { const Greeting = () => {
const t = useT();
const [greeting, setGreeting] = useState(''); const [greeting, setGreeting] = useState('');
const [display, setDisplay] = useState('block'); const [display, setDisplay] = useState('block');
const [fontSize, setFontSize] = useState('1.6em'); const [fontSize, setFontSize] = useState('1.6em');
@@ -63,13 +65,13 @@ const Greeting = () => {
let message; let message;
switch (true) { switch (true) {
case hour < 12: case hour < 12:
message = variables.getMessage('widgets.greeting.morning'); message = t('widgets.greeting.morning');
break; break;
case hour < 18: case hour < 18:
message = variables.getMessage('widgets.greeting.afternoon'); message = t('widgets.greeting.afternoon');
break; break;
default: default:
message = variables.getMessage('widgets.greeting.evening'); message = t('widgets.greeting.evening');
break; break;
} }
@@ -103,10 +105,10 @@ const Greeting = () => {
if (birth.getDate() === now.getDate() && birth.getMonth() === now.getMonth()) { if (birth.getDate() === now.getDate() && birth.getMonth() === now.getMonth()) {
if (localStorage.getItem('birthdayage') === 'true' && calculateAge(birth) !== 0) { if (localStorage.getItem('birthdayage') === 'true' && calculateAge(birth) !== 0) {
const text = variables.getMessage('widgets.greeting.birthday').split(' '); const text = t('widgets.greeting.birthday').split(' ');
message = `${text[0]} ${nth(calculateAge(birth))} ${text[1]}`; message = `${text[0]} ${nth(calculateAge(birth))} ${text[1]}`;
} else { } else {
message = variables.getMessage('widgets.greeting.birthday'); message = t('widgets.greeting.birthday');
} }
} }
} }
@@ -149,7 +151,7 @@ const Greeting = () => {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
} }
}; };
}, []); }, [t]);
return ( return (
<span className="greeting" ref={greetingRef} style={{ display, fontSize }}> <span className="greeting" ref={greetingRef} style={{ display, fontSize }}>

View File

@@ -1,6 +1,6 @@
import variables from 'config/variables';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useMessage } from 'contexts/TranslationContext'; import { useT } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md'; import { MdOutlineOpenInNew } from 'react-icons/md';
@@ -9,10 +9,11 @@ import { Radio } from 'components/Form/Settings';
import languages from '@/i18n/languages.json'; import languages from '@/i18n/languages.json';
const LanguageOptions = () => { const LanguageOptions = () => {
const loadingText = useMessage('modals.main.loading'); const t = useT();
const offlineText = useMessage('modals.main.marketplace.offline.description'); const loadingText = t('modals.main.loading');
const languageTitle = useMessage('modals.main.settings.sections.language.title'); const offlineText = t('modals.main.marketplace.offline.description');
const quoteTitle = useMessage('modals.main.settings.sections.language.quote'); const languageTitle = t('modals.main.settings.sections.language.title');
const quoteTitle = t('modals.main.settings.sections.language.quote');
const [quoteLanguages, setQuoteLanguages] = useState([ const [quoteLanguages, setQuoteLanguages] = useState([
{ {

View File

@@ -1,6 +1,5 @@
import variables from 'config/variables';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useTranslation } from 'contexts/TranslationContext'; import { useT } from 'contexts/TranslationContext';
import Tabs from 'components/Elements/MainModal/backend/Tabs'; import Tabs from 'components/Elements/MainModal/backend/Tabs';
@@ -90,15 +89,15 @@ const sections = [
]; ];
function Settings(props) { function Settings(props) {
const { languagecode } = useTranslation(); const t = useT();
// Recalculate section labels when language changes // Recalculate section labels when language changes
const translatedSections = useMemo(() => const translatedSections = useMemo(() =>
sections.map(section => ({ sections.map(section => ({
...section, ...section,
translatedLabel: variables.getMessage(section.label) translatedLabel: t(section.label)
})), })),
[languagecode] [t]
); );
return ( return (

View File

@@ -1,11 +1,13 @@
import variables from 'config/variables'; import variables from 'config/variables';
import { memo, useState, useCallback } from 'react'; import { memo, useState, useCallback } from 'react';
import { useT } from 'contexts';
import { MdCropFree } from 'react-icons/md'; import { MdCropFree } from 'react-icons/md';
import { Tooltip } from 'components/Elements'; import { Tooltip } from 'components/Elements';
const Maximise = memo(({ fontSize }) => { const Maximise = memo(({ fontSize }) => {
const t = useT();
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
const setAttribute = useCallback((blur, brightness, filter) => { const setAttribute = useCallback((blur, brightness, filter) => {
@@ -60,13 +62,13 @@ const Maximise = memo(({ fontSize }) => {
return ( return (
<Tooltip <Tooltip
title={variables.getMessage('modals.main.settings.sections.background.buttons.view')} title={t('modals.main.settings.sections.background.buttons.view')}
> >
<button <button
className="navbarButton" className="navbarButton"
style={{ fontSize }} style={{ fontSize }}
onClick={maximise} onClick={maximise}
aria-label={variables.getMessage('modals.main.settings.sections.background.buttons.view')} aria-label={t('modals.main.settings.sections.background.buttons.view')}
> >
<MdCropFree className="topicons" /> <MdCropFree className="topicons" />
</button> </button>

View File

@@ -1,34 +1,39 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import variables from 'config/variables'; import { useT } from 'contexts/TranslationContext';
import { MdRefresh } from 'react-icons/md'; import { MdRefresh } from 'react-icons/md';
import { Tooltip } from 'components/Elements'; import { Tooltip } from 'components/Elements';
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
import { useTranslation } from 'contexts/TranslationContext'; import variables from 'config/variables';
function Refresh() { function Refresh() {
const { languagecode } = useTranslation(); const t = useT();
const [refreshText, setRefreshText] = useState(''); const [refreshText, setRefreshText] = useState('');
const [refreshOption, setRefreshOption] = useState(localStorage.getItem('refreshOption') || ''); const [refreshOption, setRefreshOption] = useState(localStorage.getItem('refreshOption') || '');
useEffect(() => { useEffect(() => {
EventBus.on('refresh', (data) => { const handleRefresh = (data) => {
if (data === 'navbar' || data === 'background' || data === 'language') { if (data === 'navbar' || data === 'background' || data === 'language') {
setRefreshOption(localStorage.getItem('refreshOption')); setRefreshOption(localStorage.getItem('refreshOption'));
updateRefreshText(); updateRefreshText();
} }
}); };
EventBus.on('refresh', handleRefresh);
updateRefreshText(); updateRefreshText();
}, [languagecode]);
return () => {
EventBus.off('refresh', handleRefresh);
};
}, [t]);
function updateRefreshText() { function updateRefreshText() {
let text; let text;
switch (localStorage.getItem('refreshOption')) { switch (localStorage.getItem('refreshOption')) {
case 'background': case 'background':
text = variables.getMessage('modals.main.settings.sections.background.title'); text = t('modals.main.settings.sections.background.title');
break; break;
case 'quote': case 'quote':
text = variables.getMessage('modals.main.settings.sections.quote.title'); text = t('modals.main.settings.sections.quote.title');
break; break;
case 'quotebackground': case 'quotebackground':
text = new Intl.ListFormat( text = new Intl.ListFormat(
@@ -38,14 +43,12 @@ function Refresh() {
type: 'conjunction', type: 'conjunction',
}, },
).format([ ).format([
variables.getMessage('modals.main.settings.sections.quote.title'), t('modals.main.settings.sections.quote.title'),
variables.getMessage('modals.main.settings.sections.background.title'), t('modals.main.settings.sections.background.title'),
]); ]);
break; break;
default: default:
text = variables.getMessage( text = t('modals.main.settings.sections.appearance.navbar.refresh_options.page');
'modals.main.settings.sections.appearance.navbar.refresh_options.page',
);
break; break;
} }

View File

@@ -3,12 +3,14 @@ import variables from 'config/variables';
import { memo, useRef, useEffect, useState, useCallback } from 'react'; import { memo, useRef, useEffect, useState, useCallback } from 'react';
import { MdSearch, MdMic } from 'react-icons/md'; import { MdSearch, MdMic } from 'react-icons/md';
import { Tooltip } from 'components/Elements'; import { Tooltip } from 'components/Elements';
import { useT } from 'contexts';
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
import './search.scss'; import './search.scss';
function Search() { function Search() {
const t = useT();
const [microphone, setMicrophone] = useState(null); const [microphone, setMicrophone] = useState(null);
const [classList] = useState( const [classList] = useState(
localStorage.getItem('widgetStyle') === 'legacy' ? 'searchIcons old' : 'searchIcons', localStorage.getItem('widgetStyle') === 'legacy' ? 'searchIcons old' : 'searchIcons',
@@ -113,14 +115,14 @@ function Search() {
<div className="searchMain"> <div className="searchMain">
<div className={classList}> <div className={classList}>
<Tooltip <Tooltip
title={variables.getMessage('modals.main.settings.sections.search.voice_search')} title={t('modals.main.settings.sections.search.voice_search')}
> >
{microphone} {microphone}
</Tooltip> </Tooltip>
</div> </div>
<form onSubmit={searchButton} className="searchBar"> <form onSubmit={searchButton} className="searchBar">
<div className={classList}> <div className={classList}>
<Tooltip title={variables.getMessage('widgets.search')}> <Tooltip title={t('widgets.search')}>
<button className="navbarButton" onClick={searchButton} aria-label="Search"> <button className="navbarButton" onClick={searchButton} aria-label="Search">
<MdSearch /> <MdSearch />
</button> </button>
@@ -128,7 +130,7 @@ function Search() {
</div> </div>
<input <input
type="text" type="text"
placeholder={variables.getMessage('widgets.search')} placeholder={t('widgets.search')}
id="searchtext" id="searchtext"
className="searchInput" className="searchInput"
/> />

View File

@@ -1,14 +1,15 @@
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md'; import { MdOutlineOpenInNew } from 'react-icons/md';
import languages from '@/i18n/languages.json'; import languages from '@/i18n/languages.json';
import { useMessage } from 'contexts/TranslationContext'; import { useT } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { Radio } from 'components/Form/Settings'; import { Radio } from 'components/Form/Settings';
import { Header, Content } from '../Layout'; import { Header, Content } from '../Layout';
function ChooseLanguage() { function ChooseLanguage() {
const title = useMessage('modals.welcome.sections.language.title'); const t = useT();
const description = useMessage('modals.welcome.sections.language.description'); const title = t('modals.welcome.sections.language.title');
const description = t('modals.welcome.sections.language.description');
return ( return (
<Content> <Content>