frontend_nyla/docs/LANGUAGE_ARCHITECTURE.md

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

  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:

// 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);

  • 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:

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.