Aller au contenu principal

French Translation System Implementation

Overview

This document explains the implementation of a French translation system in an Angular application, the problems encountered, and how they were solved.

Initial State

The application already had a comprehensive translation infrastructure:

  • TranslationService with English and French translations
  • TranslatePipe for template translations
  • SettingsService for user preferences
  • Settings modal with language selector

Problems Encountered

1. Settings Dropdown Not Persisting Selection

Problem: When users selected "Français" in settings, closed the modal, and reopened it, the dropdown still showed "English" selected.

Root Cause: The dropdown was bound to a getter method (settingsService.language) instead of a reactive signal. Angular's change detection wasn't tracking changes to getter methods properly.

Solution:

// Before (problematic)
[value]="settingsService.language"

// After (fixed)
currentLanguage = computed(() => this.settingsService.settings().language);
[value]="currentLanguage()"

2. Translations Not Updating Immediately

Problem: When users changed language, the interface didn't update immediately - they had to refresh the page to see French translations.

Root Cause: The TranslatePipe wasn't reactive to language changes. It was marked as pure: false but had no mechanism to detect when the language signal changed.

Solution: Used Angular's effect() to watch for language signal changes:

// Before (not reactive)
constructor() {
this.subscription = new Subscription();
}

// After (reactive)
constructor() {
effect(() => {
this.translationService.currentLanguage(); // Watch signal
this.lastKey = ''; // Reset cache
this.cdr.markForCheck(); // Trigger change detection
});
}

3. Service Synchronization Issues

Problem: Both SettingsService and TranslationService were trying to manage language persistence independently, causing conflicts.

Root Cause:

  • TranslationService had its own localStorage key ('app-language')
  • SettingsService used a different key ('app-settings')
  • Browser language detection was overriding user preferences

Solution: Made SettingsService the single source of truth:

// TranslationService - removed independent storage
setLanguage(language: Language): void {
this.currentLanguageSignal.set(language);
// Don't save here - let SettingsService handle persistence
}

// SettingsService - handles both persistence and sync
setLanguage(language: Language): void {
this.settingsSignal.update(settings => ({ ...settings, language }));
this.translationService.setLanguage(language); // Sync translation service
this.saveSettings(); // Persist to localStorage
}

Technical Implementation Details

Angular Signals vs Observables

The solution leveraged Angular's modern signal-based reactivity:

  • Signals: Used for state management (currentLanguageSignal, settingsSignal)
  • Computed Signals: Used for derived state (currentLanguage = computed(...))
  • Effects: Used for side effects when signals change (effect(() => {...}))

Change Detection Strategy

The TranslatePipe uses Angular's change detection mechanism:

  1. effect() watches the language signal
  2. When language changes, it resets the translation cache
  3. markForCheck() triggers Angular's change detection
  4. All components using the pipe re-render with new translations

Data Flow

User selects language in settings

SettingsService.setLanguage()

Updates settings signal + saves to localStorage

Calls TranslationService.setLanguage()

Updates currentLanguage signal

effect() in TranslatePipe detects change

Triggers change detection across all components

All translations update immediately

Key Learnings

1. Single Source of Truth

Having multiple services manage the same data leads to synchronization issues. Designate one service as the authoritative source.

2. Angular Signals for Reactivity

Modern Angular signals provide better reactivity than traditional observables for simple state management.

3. Pipe Reactivity

Custom pipes need explicit reactivity mechanisms when depending on external state. Use effect() to watch signals.

4. Change Detection

When updating external state that affects templates, explicitly trigger change detection with markForCheck().

Final Architecture

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│ SettingsModal │───▶│ SettingsService │───▶│ TranslationService│
│ │ │ │ │ │
│ - Language │ │ - Persistence │ │ - Translations │
│ Dropdown │ │ - Single Source │ │ - Current Lang │
│ │ │ of Truth │ │ Signal │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Templates │◀───────────────────────────│ TranslatePipe │
│ │ │ │
│ - Reactive │ │ - effect() │
│ Bindings │ │ - Change │
│ │ │ Detection │
└─────────────────┘ └─────────────────┘

Testing the Solution

To verify the implementation works correctly:

  1. Language Selection Persistence:

    • Select French → Close settings → Reopen settings
    • ✅ Should show "Français" selected
  2. Immediate Translation Updates:

    • Select French → Interface immediately switches to French
    • ✅ No page refresh required
  3. Cross-Session Persistence:

    • Select French → Refresh page → App loads in French
    • ✅ Language preference persists

Conclusion

The key to solving this translation system was understanding Angular's reactivity model and ensuring proper data flow between services. By using signals, computed values, and effects appropriately, we achieved a seamless, reactive translation experience that updates immediately and persists user preferences correctly.