feat(translation): implement reactive translation system with context and hooks

This commit is contained in:
alexsparkes
2026-01-24 17:02:41 +00:00
parent d0dbd7e75c
commit b8647538fd
13 changed files with 210 additions and 35 deletions

43
src/contexts/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Translation System
## Using Reactive Translations
The app now supports instant language switching without page refresh!
### For new components
Use the `useMessage` hook for reactive translations:
```jsx
import { useMessage } from 'contexts/TranslationContext';
function MyComponent() {
const title = useMessage('modals.main.title');
const description = useMessage('modals.main.description');
return (
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
);
}
```
### For existing components
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.
### How it works
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
### Benefits
- No page refresh needed
- Minimal performance impact (translations already loaded)
- Backward compatible with existing code
- Smooth user experience

View File

@@ -0,0 +1,63 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { initTranslations, translations } from 'lib/translations';
import variables from 'config/variables';
import EventBus from 'utils/eventbus';
const TranslationContext = createContext();
export function TranslationProvider({ children, initialLanguage }) {
const [languagecode, setLanguagecode] = useState(initialLanguage);
const changeLanguage = useCallback((newLanguage) => {
// Update the i18n instance
variables.language = initTranslations(newLanguage);
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');
localStorage.setItem('tabName', newTabName);
document.title = newTabName;
}
// Update state to trigger re-render
setLanguagecode(newLanguage);
}, []);
useEffect(() => {
const handleLanguageChange = (data) => {
if (data?.language) {
changeLanguage(data.language);
}
};
EventBus.on('languageChange', handleLanguageChange);
return () => {
EventBus.off('languageChange', handleLanguageChange);
};
}, [changeLanguage]);
return (
<TranslationContext.Provider value={{ languagecode, changeLanguage }}>
{children}
</TranslationContext.Provider>
);
}
export function useTranslation() {
const context = useContext(TranslationContext);
if (!context) {
throw new Error('useTranslation must be used within a TranslationProvider');
}
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) : '';
}

1
src/contexts/index.js Normal file
View File

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