362 lines
13 KiB
Markdown
362 lines
13 KiB
Markdown
# 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:
|
|
```typescript
|
|
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):**
|
|
```typescript
|
|
// ❌ 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):**
|
|
```typescript
|
|
// ✅ Single source of truth - always in sync!
|
|
localStorage.setItem('currentUser', { language: 'fr' }); // ONLY source
|
|
// ^ Always matches backend! 🎯
|
|
```
|
|
|
|
---
|
|
|
|
## 💻 Code Implementation
|
|
|
|
### LanguageContext.tsx
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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
|
|
|
|
1. **User language is part of user data** - stored in `localStorage('currentUser').language`
|
|
2. **No separate language storage** - eliminates redundancy and sync issues
|
|
3. **Backend is the source of truth** - frontend always syncs with backend
|
|
4. **Settings update flow:**
|
|
- Update backend → Receive updated user → Cache in localStorage → Update UI
|
|
5. **Language changes persist** - because they're stored in the user profile in the database
|
|
|
|
---
|
|
|
|
## 🚫 Anti-Patterns to Avoid
|
|
|
|
### ❌ Don't do this:
|
|
```typescript
|
|
// 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:
|
|
```typescript
|
|
// 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 implementation
|
|
- `src/components/settings/settingsUser.tsx` - User settings with language update
|
|
- `src/hooks/useAuthentication.ts` - Login flow with user data fetch
|
|
- `src/hooks/useUsers.ts` - User data management
|
|
|
|
---
|
|
|
|
## 🔄 Migration Notes
|
|
|
|
If you had existing code that used `localStorage.language`:
|
|
|
|
### Before:
|
|
```typescript
|
|
const lang = localStorage.getItem('language') || 'de';
|
|
```
|
|
|
|
### After:
|
|
```typescript
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const lang = currentUser.language || navigator.language || 'de';
|
|
```
|
|
|
|
All existing references should now use `currentUser.language` exclusively.
|
|
|