13 KiB
13 KiB
Language Architecture - Single Source of Truth
✅ Correct Architecture (Current)
Single Source of Truth
User Profile in Database → localStorage('currentUser').language → UI
There is NO separate localStorage.language storage!
📊 Data Flow
1. On Login
User logs in
↓
Backend authenticates
↓
GET /api/*/me returns User object
{
username: "user@example.com",
privilege: "admin",
language: "de", ← Language is part of user data
...
}
↓
Store ONCE in localStorage:
localStorage.setItem('currentUser', JSON.stringify(userData))
↓
LanguageContext reads: currentUser.language
↓
UI displays in correct language ✅
2. When User Changes Language
User selects new language in settings
↓
Settings component updates backend:
PUT /api/users/{id} with { language: "fr" }
↓
Backend returns updated user object
↓
Update localStorage('currentUser') with new data ✅
localStorage.setItem('currentUser', JSON.stringify(updatedUser))
↓
Call setLanguage(newLanguage)
↓
LanguageContext loads new translations
↓
Trigger 'userInfoUpdated' event
↓
All components sync with new language ✅
3. On Page Load/Refresh
App initializes
↓
LanguageContext checks:
1. localStorage('currentUser').language ← Primary source
2. Browser language (navigator.language) ← Fallback if no user data
↓
Load translations for selected language
↓
UI displays in correct language ✅
🎯 Priority System
Language Resolution Order:
Priority 1: currentUser.language ← From database (logged-in users)
Priority 2: Browser language ← Fallback (before login or no user data)
Priority 3: Default 'de' ← Ultimate fallback
Why No localStorage.language?
Before (Wrong):
// ❌ Multiple sources of truth - can get out of sync!
localStorage.setItem('language', 'fr'); // UI preference
localStorage.setItem('currentUser', { language: 'de' }); // Backend data
// ^ Which one is correct? 🤔
After (Correct):
// ✅ Single source of truth - always in sync!
localStorage.setItem('currentUser', { language: 'fr' }); // ONLY source
// ^ Always matches backend! 🎯
💻 Code Implementation
LanguageContext.tsx
// On mount: Read from currentUser.language
useEffect(() => {
const currentUserData = localStorage.getItem('currentUser');
if (currentUserData) {
const userData = JSON.parse(currentUserData);
if (userData.language) {
initialLanguage = userData.language; // ✅ From user profile
}
} else {
// Fallback to browser language if no user data
initialLanguage = navigator.language;
}
loadAndSetLanguage(initialLanguage);
}, []);
// When user updates language
const setLanguage = async (language: Language) => {
await loadAndSetLanguage(language);
// Note: This should ONLY be called AFTER:
// 1. Backend is updated
// 2. localStorage('currentUser') is updated
// The settings component handles this flow
};
settingsUser.tsx
const handleSaveUserInfo = async () => {
// 1. Update backend
const updatedUser = await updateUser(user.id, {
...userData,
language: newLanguage
});
// 2. Update localStorage (single source of truth!)
localStorage.setItem('currentUser', JSON.stringify(updatedUser));
// 3. Update UI language
if (newLanguage !== currentLanguage) {
await setLanguage(newLanguage);
}
// 4. Notify other components
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
};
useAuthentication.ts
// On login: Fetch and cache user data
const userResponse = await api.get('/api/local/me');
if (userResponse.data) {
// Store user data ONCE (includes language)
localStorage.setItem('currentUser', JSON.stringify(userResponse.data));
// ✅ No separate language storage!
}
🔄 Complete Flow Diagram
┌──────────────────────────────────────────────────────────┐
│ USER LOGS IN │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ GET /api/*/me returns: │
│ { username, privilege, language: "de", ... } │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ localStorage('currentUser') = userData │
│ ✅ Language is part of user data │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ LanguageContext reads: currentUser.language │
│ Loads translations for 'de' │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ UI displays in German ✅ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ USER CHANGES LANGUAGE TO FRENCH │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ PUT /api/users/{id} │
│ { language: "fr" } │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ Backend returns: │
│ { username, privilege, language: "fr", ... } │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ localStorage('currentUser') = updatedUserData │
│ ✅ Language updated in user data │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ setLanguage('fr') called │
│ Loads French translations │
└────────────────────┬─────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ UI displays in French ✅ │
└──────────────────────────────────────────────────────────┘
🧪 Testing
Test 1: Login with Different Languages
# User with language='de'
1. Log in
2. Check console: "🌍 Using language from user profile: de"
3. Check localStorage: currentUser.language === 'de'
4. Verify UI is in German ✅
# User with language='fr'
1. Log in
2. Check console: "🌍 Using language from user profile: fr"
3. Check localStorage: currentUser.language === 'fr'
4. Verify UI is in French ✅
Test 2: Change Language in Settings
1. Log in with language='de'
2. Go to settings
3. Change language to 'fr'
4. Click Save
5. Check console:
- "✅ User update successful"
- "💾 Updated user data cached in localStorage"
- "🌍 Frontend language updated to: fr"
6. Check localStorage: currentUser.language === 'fr'
7. Verify UI immediately changes to French ✅
8. Refresh page
9. Verify UI is still in French ✅
Test 3: Multiple Browser Tabs
1. Open app in two tabs
2. In Tab 1: Change language to 'fr'
3. In Tab 2: Reload page
4. Both tabs should display in French ✅
(Because both read from currentUser.language)
Test 4: Before Login (No User Data)
1. Clear localStorage
2. Open app
3. Should use browser language as fallback
4. After login, should switch to user's profile language ✅
🎯 Benefits of Single Source of Truth
| Aspect | Before (Multiple Sources) | After (Single Source) |
|---|---|---|
| Consistency | ❌ Can get out of sync | ✅ Always in sync |
| Simplicity | ❌ Check multiple places | ✅ One place to check |
| Reliability | ❌ Which source is correct? | ✅ Always correct |
| Maintenance | ❌ Update multiple places | ✅ Update one place |
| Debugging | ❌ Hard to trace issues | ✅ Easy to trace |
📋 Key Points
- User language is part of user data - stored in
localStorage('currentUser').language - No separate language storage - eliminates redundancy and sync issues
- Backend is the source of truth - frontend always syncs with backend
- Settings update flow:
- Update backend → Receive updated user → Cache in localStorage → Update UI
- Language changes persist - because they're stored in the user profile in the database
🚫 Anti-Patterns to Avoid
❌ Don't do this:
// Don't store language separately
localStorage.setItem('language', 'fr');
// Don't read from separate storage
const lang = localStorage.getItem('language');
// Don't update UI before backend
setLanguage('fr'); // Then update backend
✅ Do this instead:
// Update backend first
const updatedUser = await updateUser(id, { language: 'fr' });
// Cache the complete user data
localStorage.setItem('currentUser', JSON.stringify(updatedUser));
// Then update UI
setLanguage(updatedUser.language);
📁 Related Files
src/contexts/LanguageContext.tsx- Language context implementationsrc/components/settings/settingsUser.tsx- User settings with language updatesrc/hooks/useAuthentication.ts- Login flow with user data fetchsrc/hooks/useUsers.ts- User data management
🔄 Migration Notes
If you had existing code that used localStorage.language:
Before:
const lang = localStorage.getItem('language') || 'de';
After:
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
const lang = currentUser.language || navigator.language || 'de';
All existing references should now use currentUser.language exclusively.