Skip to main content

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.