From 6ca19fc48d0d0877d7bdd8ddee9b9b17c1691c55 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 24 Jan 2026 18:07:25 +0000 Subject: [PATCH] feat(translation): integrate useT hook for improved translation handling across components --- src/contexts/TranslationContext.jsx | 10 +- src/features/navbar/Navbar.jsx | 57 ++-- src/features/navbar/components/Notes.jsx | 16 +- src/features/navbar/components/Refresh.jsx | 4 +- src/features/navbar/components/Todo.jsx | 315 ++++++++---------- src/features/navbar/options/AppsOptions.jsx | 12 +- src/features/navbar/options/NavbarOptions.jsx | 28 +- src/features/quote/components/AuthorInfo.jsx | 11 +- .../quote/components/QuoteButtons.jsx | 28 +- src/lib/translations.js | 66 ++-- 10 files changed, 268 insertions(+), 279 deletions(-) diff --git a/src/contexts/TranslationContext.jsx b/src/contexts/TranslationContext.jsx index 39d2442a..12ec331a 100644 --- a/src/contexts/TranslationContext.jsx +++ b/src/contexts/TranslationContext.jsx @@ -7,15 +7,17 @@ const TranslationContext = createContext(); export function TranslationProvider({ children, initialLanguage }) { const [currentLanguage, setCurrentLanguage] = useState(initialLanguage); - const i18nInstance = useRef(null); + const i18nInstance = useRef(initTranslations(initialLanguage)); - // Initialize i18n instance once + // Update i18n instance when language changes useEffect(() => { - i18nInstance.current = initTranslations(currentLanguage); + if (currentLanguage !== initialLanguage) { + i18nInstance.current = initTranslations(currentLanguage); + } variables.language = i18nInstance.current; variables.languagecode = currentLanguage; document.documentElement.lang = currentLanguage.replace('_', '-'); - }, [currentLanguage]); + }, [currentLanguage, initialLanguage]); // Change language function const changeLanguage = useCallback((newLanguage) => { diff --git a/src/features/navbar/Navbar.jsx b/src/features/navbar/Navbar.jsx index 6d8d0056..29a70673 100644 --- a/src/features/navbar/Navbar.jsx +++ b/src/features/navbar/Navbar.jsx @@ -1,5 +1,6 @@ import variables from 'config/variables'; import { useState, useEffect, useRef } from 'react'; +import { useT } from 'contexts'; import { MdSettings } from 'react-icons/md'; @@ -10,34 +11,36 @@ import EventBus from 'utils/eventbus'; import './scss/index.scss'; -const getRefreshText = () => { - switch (localStorage.getItem('refreshOption')) { - case 'background': - return variables.getMessage('modals.main.settings.sections.background.title'); - case 'quote': - return variables.getMessage('modals.main.settings.sections.quote.title'); - case 'quotebackground': - return ( - variables.getMessage('modals.main.settings.sections.quote.title') + - ' ' + - variables.getMessage('modals.main.settings.sections.background.title') - ); - default: - return variables.getMessage( - 'modals.main.settings.sections.appearance.navbar.refresh_options.page', - ); - } -}; - -const getZoomFontSize = () => { - return Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'; -}; - const Navbar = ({ openModal }) => { + const t = useT(); const navbarContainer = useRef(); const [classList] = useState( localStorage.getItem('widgetStyle') === 'legacy' ? 'navbar old' : 'navbar new', ); + + const getRefreshText = () => { + switch (localStorage.getItem('refreshOption')) { + case 'background': + return t('modals.main.settings.sections.background.title'); + case 'quote': + return t('modals.main.settings.sections.quote.title'); + case 'quotebackground': + return ( + t('modals.main.settings.sections.quote.title') + + ' ' + + t('modals.main.settings.sections.background.title') + ); + default: + return t( + 'modals.main.settings.sections.appearance.navbar.refresh_options.page', + ); + } + }; + + const getZoomFontSize = () => { + return Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'; + }; + const [refreshText, setRefreshText] = useState(getRefreshText()); const [refreshEnabled, setRefreshEnabled] = useState(localStorage.getItem('refresh')); const [refreshOption, setRefreshOption] = useState(localStorage.getItem('refreshOption') || ''); @@ -99,16 +102,16 @@ const Navbar = ({ openModal }) => { {refreshEnabled !== 'false' && } @@ -91,27 +93,27 @@ const Notes = ({ notesRef, floatRef, position, xPosition, yPosition }) => {
- {variables.getMessage('widgets.navbar.notes.title')} + {t('widgets.navbar.notes.title')}
- + - + - +
+ diff --git a/src/features/navbar/components/Todo.jsx b/src/features/navbar/components/Todo.jsx index 2f43c92f..4614dbb0 100644 --- a/src/features/navbar/components/Todo.jsx +++ b/src/features/navbar/components/Todo.jsx @@ -1,5 +1,6 @@ import variables from 'config/variables'; -import { PureComponent, memo, useState } from 'react'; +import { memo, useState, useEffect } from 'react'; +import { useT } from 'contexts'; import { MdChecklist, @@ -65,206 +66,180 @@ const SortableList = ({ items, onDragEnd, children }) => { ); }; -class Todo extends PureComponent { - constructor() { - super(); - this.state = { - todo: JSON.parse(localStorage.getItem('todo')) || [], - visibility: localStorage.getItem('todoPinned') === 'true' ? 'visible' : 'hidden', - marginLeft: localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px', - showTodo: localStorage.getItem('todoPinned') === 'true', - }; - } +function Todo({ todoRef, floatRef, position, xPosition, yPosition }) { + const t = useT(); + const [todo, setTodo] = useState(JSON.parse(localStorage.getItem('todo')) || []); + const [showTodo, setShowTodo] = useState(localStorage.getItem('todoPinned') === 'true'); + const [zoomFontSize, setZoomFontSize] = useState('1.2rem'); - setZoom() { - this.setState({ - zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem', - }); - } - - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'navbar') { - this.forceUpdate(); - try { - this.setZoom(); - } catch { - // Ignore errors - } + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); } - }); + }; - this.setZoom(); - } + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh', handleRefresh); + }; + }, []); - /** - * It takes an array, removes an item from it, and then inserts it at a new index. - * @param {Array} array The array to move the item in. - * @param {Number} oldIndex The index of the item to move. - * @param {Number} newIndex The index to move the item to. - * @returns The result of the splice method. - */ - arrayMove(array, oldIndex, newIndex) { - const result = Array.from(array); - const [removed] = result.splice(oldIndex, 1); - result.splice(newIndex, 0, removed); + const handleShowTodo = () => { + setShowTodo(true); + }; - return result; - } + const handleHideTodo = () => { + setShowTodo(localStorage.getItem('todoPinned') === 'true'); + }; - handleDragEnd = (event) => { + const handleDragEnd = (event) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = Number(active.id); const newIndex = Number(over.id); - this.setState({ todo: arrayMove(this.state.todo, oldIndex, newIndex) }); + setTodo((currentTodo) => { + const newTodo = arrayMove(currentTodo, oldIndex, newIndex); + localStorage.setItem('todo', JSON.stringify(newTodo)); + return newTodo; + }); } }; - showTodo() { - this.setState({ showTodo: true }); - } - - hideTodo() { - this.setState({ showTodo: localStorage.getItem('todoPinned') === 'true' }); - } - /** * This function takes in an action, an index, and data, and then updates the todo list accordingly. * @param {String} action The action to perform. Can be 'add', 'remove', 'set', or 'done'. * @param {Number} index The index of the item to perform the action on. * @param {Object} data The data to use for the action. */ - updateTodo(action, index, data) { - const todo = this.state.todo; - switch (action) { - case 'add': - todo.push({ value: '', done: false }); - break; - case 'remove': - todo.splice(index, 1); - break; - case 'set': - todo[index] = { value: data.target.value, done: todo[index].done }; - break; - case 'done': - todo[index].done = !todo[index].done; - break; - default: - break; - } + const updateTodo = (action, index, data) => { + setTodo((currentTodo) => { + const newTodo = [...currentTodo]; - localStorage.setItem('todo', JSON.stringify(todo)); - this.setState({ todo }); - this.forceUpdate(); - } + switch (action) { + case 'add': + newTodo.push({ value: '', done: false }); + break; + case 'remove': + newTodo.splice(index, 1); + break; + case 'set': + newTodo[index] = { value: data.target.value, done: newTodo[index].done }; + break; + case 'done': + newTodo[index].done = !newTodo[index].done; + break; + default: + break; + } - pin() { + localStorage.setItem('todo', JSON.stringify(newTodo)); + return newTodo; + }); + }; + + const handlePin = () => { variables.stats.postEvent('feature', 'Todo pin'); const todoPinned = localStorage.getItem('todoPinned') === 'true'; localStorage.setItem('todoPinned', !todoPinned); - this.setState({ showTodo: !todoPinned }); - } + setShowTodo(!todoPinned); + }; - render() { - return ( -
this.hideTodo()} onFocus={() => this.showTodo()}> - + {showTodo && ( + - - - {this.state.showTodo && ( - -
-
- - {variables.getMessage('widgets.navbar.todo.title')} -
-
- - - - - - -
-
- {this.state.todo.length === 0 ? ( -
-
- - - {variables.getMessage('widgets.navbar.todo.no_todos')} - - - {variables.getMessage('modals.main.settings.sections.message.add_some')} - -
-
- ) : ( - index)} - onDragEnd={this.handleDragEnd} - > - {this.state.todo.map((todo, index) => ( - - {({ attributes, listeners }) => ( -
- this.updateTodo('done', index)} - /> - this.updateTodo('set', index, data)} - readOnly={todo.done} - /> - - this.updateTodo('remove', index)} /> - - -
- )} -
- ))} -
- )} -
+
+
+ + {t('widgets.navbar.todo.title')}
- - )} -
- ); - } +
+ + + + + + +
+
+ {todo.length === 0 ? ( +
+
+ + + {t('widgets.navbar.todo.no_todos')} + + + {t('modals.main.settings.sections.message.add_some')} + +
+
+ ) : ( + index)} + onDragEnd={handleDragEnd} + > + {todo.map((todoItem, index) => ( + + {({ attributes, listeners }) => ( +
+ updateTodo('done', index)} + /> + updateTodo('set', index, data)} + readOnly={todoItem.done} + /> + + updateTodo('remove', index)} /> + + +
+ )} +
+ ))} +
+ )} +
+
+
+ )} +
+ ); } function TodoWrapper() { diff --git a/src/features/navbar/options/AppsOptions.jsx b/src/features/navbar/options/AppsOptions.jsx index fe65e1d1..a53cc6e2 100644 --- a/src/features/navbar/options/AppsOptions.jsx +++ b/src/features/navbar/options/AppsOptions.jsx @@ -1,6 +1,7 @@ import variables from 'config/variables'; import { useState } from 'react'; +import { useT } from 'contexts'; import Modal from 'react-modal'; import EventBus from 'utils/eventbus'; @@ -15,6 +16,7 @@ import { getTitleFromUrl, isValidUrl } from 'utils/links'; import { QuickLink } from 'features/quicklinks/options/QuickLink'; function AppsOptions({ appsEnabled }) { + const t = useT(); const [appsModalInfo, setAppsModalInfo] = useState({ newLink: false, edit: false, @@ -34,14 +36,14 @@ function AppsOptions({ appsEnabled }) { if (url.length <= 0 || isValidUrl(url) === false) { return setAppsModalInfo((oldState) => ({ ...oldState, - urlError: variables.getMessage('widgets.quicklinks.url_error'), + urlError: t('widgets.quicklinks.url_error'), })); } if (icon.length > 0 && isValidUrl(icon) === false) { return setAppsModalInfo((oldState) => ({ ...oldState, - iconError: variables.getMessage('widgets.quicklinks.icon_error'), + iconError: t('widgets.quicklinks.icon_error'), })); } @@ -114,8 +116,8 @@ function AppsOptions({ appsEnabled }) { <> @@ -124,7 +126,7 @@ function AppsOptions({ appsEnabled }) { type="settings" onClick={() => setAppsModalInfo((oldState) => ({ ...oldState, newLink: true }))} icon={} - label={variables.getMessage('modals.main.settings.sections.quicklinks.add_link')} + label={t('modals.main.settings.sections.quicklinks.add_link')} /> diff --git a/src/features/navbar/options/NavbarOptions.jsx b/src/features/navbar/options/NavbarOptions.jsx index aed85388..9a33f94c 100644 --- a/src/features/navbar/options/NavbarOptions.jsx +++ b/src/features/navbar/options/NavbarOptions.jsx @@ -1,6 +1,7 @@ import variables from 'config/variables'; import { useState, memo } from 'react'; +import { useT } from 'contexts'; import { MdAssignment, MdCropFree, MdRefresh, MdChecklist, MdOutlineApps } from 'react-icons/md'; @@ -13,6 +14,7 @@ import { Header } from 'components/Layout/Settings'; import AppsOptions from './AppsOptions'; function NavbarOptions() { + const t = useT(); const [showRefreshOptions, setShowRefreshOptions] = useState( localStorage.getItem('refresh') === 'true', ); @@ -26,15 +28,15 @@ function NavbarOptions() { return ( @@ -70,7 +72,7 @@ function NavbarOptions() { className={`navbarButtonOption ${isDisabled === true ? 'disabled' : ''}`} > {icon} - {variables.getMessage(messageKey)} + {t(messageKey)} ); }; @@ -106,7 +108,7 @@ function NavbarOptions() { return (
@@ -123,8 +125,8 @@ function NavbarOptions() { return ( @@ -135,24 +137,24 @@ function NavbarOptions() { items={[ { value: 'page', - text: variables.getMessage( + text: t( 'modals.main.settings.sections.appearance.navbar.refresh_options.page', ), }, { value: 'background', - text: variables.getMessage('modals.main.settings.sections.background.title'), + text: t('modals.main.settings.sections.background.title'), }, { value: 'quote', - text: variables.getMessage('modals.main.settings.sections.quote.title'), + text: t('modals.main.settings.sections.quote.title'), }, { value: 'quotebackground', text: - 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'), }, ]} /> @@ -164,7 +166,7 @@ function NavbarOptions() { return ( <>
40 ? '…' : ''); return ( @@ -30,7 +31,7 @@ export default function AuthorInfo({ {!authorimg && authorimg !== undefined && }
)} - + {author ? (
{author} @@ -49,10 +50,10 @@ export default function AuthorInfo({ loading
)} - +
{hasLink && ( - + {showCopy && ( - + )} {showShare && ( - + )} {showFavourite && ( -