Search code examples
angularlazy-loadingi18next

Is it possible have working Angular i18next multilingual translations after page reload and without component lazy loading?


I have installed angular-i18next library and I want to have working multilingual translations after I going to change language and reload the page. Now its working, but I need to have implemented lazy loading at Angular current component, now I want rid of lazy loading, because it's redudant for this time. Here is my code:

app.config.ts

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { I18NEXT_SERVICE, I18NextModule, defaultInterpolationFormat } from 'angular-i18next';
import i18next from 'i18next';
import HttpApi from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

const i18nextOptions = {
  debug: true,
  fallbackLng: 'en',
  supportedLngs: ['en', 'de', 'cz'],
  ns: ['translation', 'validation', 'error'],
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  },
  backend: {
    loadPath: '/assets/i18n/{{lng}}.json',
  },
  detection: {
    order: ['localStorage', 'navigator', 'htmlTag'],
    caches: ['localStorage'],
  },
};

// Initialize i18next before providing it
i18next
  .use(HttpApi)
  .use(LanguageDetector)
  .init(i18nextOptions);

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }), 
    provideRouter(routes), 
    provideAnimationsAsync(),
    {
      provide: I18NEXT_SERVICE,
      useValue: i18next
    },
    I18NextModule.forRoot({}).providers!
  ]
};

app.routes.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
    {
        path: '',
        loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
        title: 'App Home'
    },
    {
        path: 'languages',
        loadComponent: () => import('./pages/languages/languages.component').then(m => m.LanguagesComponent),
        title: 'App languages'
    }
];

app.component.ts

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
import { I18NextModule } from 'angular-i18next';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    RouterOutlet,
    CommonModule,
    I18NextModule
  ]
})
export class AppComponent implements OnInit{

  constructor(private router: Router) {}

  ngOnInit(): void {
    const setup = localStorage.getItem('setup');
    if (!setup) {
      this.router.navigate(['/languages']);
    }
  }
}

language-picker.component.ts

import { ChangeDetectionStrategy, signal, Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { I18NextModule, ITranslationService, I18NEXT_SERVICE } from 'angular-i18next';
import { Language } from '@app/enums/language.enum';

@Component({
  selector: 'app-language-picker',
  standalone: true,
  imports: [
    CommonModule,
    MatButtonModule,
    MatIconModule,
    I18NextModule
  ],
  templateUrl: './language-picker.component.html',
  styleUrls: ['./language-picker.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LanguagePickerComponent implements OnInit {
  @Output() nextStep = new EventEmitter<void>();
  languageEnum: typeof Language = Language;
  language = signal<string>(Language.NONE);

  constructor(@Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) {
    const savedLanguage = window.sessionStorage.getItem('selectedLanguage') as Language;
    if (savedLanguage) {
      this.language.set(savedLanguage);
    }
  }

  async ngOnInit(): Promise<void> {
    // Subscribe to language changes
    this.i18NextService.events.initialized.subscribe((initialized) => {
      if (initialized) {
        this.language.set(this.i18NextService.language);
      }
    });
  }

  selectLanguage(lang: Language) {
    window.sessionStorage.setItem('selectedLanguage', lang);

    if (lang !== this.i18NextService.language) {
      this.i18NextService.changeLanguage(lang).then(() => {
        this.language.set(lang);
      });
    }
    this.nextStep.emit();
  }
}

language-picker.component.scss

.language-select {
    .row {
        .col-lg-4 {
            img {
                cursor: pointer;
                height: 125px;

                &:hover {
                    transform: scale(1.1);
                    transition: transform 0.3s ease;
                }

                &:not(:hover):not(.selected) {
                    transform: scale(1);
                    transition: transform 0.3s ease;
                }

                &.selected {
                    border-radius: 5px;
                    transform: scale(1.3) !important;
                    transition: transform 0.3s ease;
                }
            }
        }
    }
}

language-picker.component.html

<div class="px-xs-2 px-sm-2 px-md-2 px-lg-5 py-3 py-sm-3 py-md-3 py-lg-3 d-flex flex-column row-gap-2 align-items-center column-gap-3">
    <div class="d-flex flex-column text-center">
        <h1 class="text-center">{{ 'Select language' | i18next }}</h1>
        <p>{{ 'Selected language will be used for the setup' | i18next }}</p>
    </div>
    <section class="language-select">
        <div class="row g-5 flex-lg-row flex-column align-items-center py-2">
            <div class="col-lg-4 col-12 text-center">
                <img
                    width="200"
                    height="125"
                    src="assets/images/de.webp"
                    class="img-fluid"
                    [class.selected]="language() === languageEnum.DE" 
                    (click)="selectLanguage(languageEnum.DE)"
                >
            </div>
            <div class="col-lg-4 col-12 text-center">
                <img
                    width="200"
                    height="125"
                    src="assets/images/cz.webp"
                    class="img-fluid"
                    [class.selected]="language() === languageEnum.CZ"
                    (click)="selectLanguage(languageEnum.CZ)"
                >
            </div>
            <div class="col-lg-4 col-12 text-center">
                <img
                    width="200"
                    height="125"
                    src="assets/images/gb.webp"
                    class="img-fluid"
                    [class.selected]="language() === languageEnum.EN"
                    (click)="selectLanguage(languageEnum.EN)"
                >
            </div>
        </div>
    </section>
</div>

Now I need this app.routes.ts looks like this (without lazy loading):

import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { LanguagesComponent } from './pages/languages/languages.component';

export const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
        title: 'App home'
    },
    {
        path: 'languages',
        component: LanguagesComponent,
        title: 'App languages'
    }
];

And not getting this warnings and have working current translations where is current language stored in session storage after page reload. How can I achieve this?

Console warnings and logs (when I not using lazy loading):

language-picker.component.html:3 i18next: hasLoadedNamespace: i18next was not initialized undefined

language-picker.component.html:3 i18next::translator: key "Select language" for languages "en" won't get resolved as namespace "translation" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!

language-picker.component.html:3 i18next::translator: missingKey undefined translation Select language Select language

language-picker.component.html:4 i18next::translator: missingKey undefined translation Selected language will be used for the setup Selected language will be used for the setup

language-picker.component.html:3 i18next: hasLoadedNamespace: i18next was not initialized (2) ['de', 'en']

language-picker.component.html:3 i18next::translator: key "Select language" for languages "de, en" won't get resolved as namespace "translation" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!

language-picker.component.html:3 i18next::translator: missingKey de translation Select language Select language

language-picker.component.html:4 i18next::translator: missingKey de translation Selected language will be used for the setup Selected language will be used for the setup

Solution

  • Try adjusting the initializer code, so that you are initializing using provideAppInitializer and using importProvidersFrom to add services for the module.

    export function appInit() {
      const i18next: ITranslationService = inject(I18NEXT_SERVICE);
      return i18next.use(HttpApi).use(LanguageDetector).init({
        debug: true,
        fallbackLng: 'en',
        supportedLngs: ['en', 'de', 'cz'],
        ns: ['translation', 'validation', 'error'],
        interpolation: {
          format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
        },
        backend: {
          loadPath: '/assets/i18n/{{lng}}.json',
        },
        detection: {
          order: ['localStorage', 'navigator', 'htmlTag'],
          caches: ['localStorage'],
        },
      });
    }
    
    export function localeIdFactory(i18next: ITranslationService)  {
        return i18next.language;
    }
    
    export const appConfig: ApplicationConfig = {
      providers: [
        provideZoneChangeDetection({ eventCoalescing: true }), 
        provideRouter(routes), 
        provideAnimationsAsync(),
        {
          provide: I18NEXT_SERVICE,
          useValue: i18next
        },
        provideAppInitializer(appInit),
        {
          provide: LOCALE_ID,
          deps: [I18NEXT_SERVICE],
          useFactory: localeIdFactory,
        },
        importProvidersFrom([
          I18NextModule.forRoot(),
        ]),
      ]
    };