Nazad na blog
Miloš Krstić5 min čitanja

Internacionalizovani Angular Ruter

Internacionalizovani Angular Ruter
Kliknite na sliku za uvećanje

Koji problem rešavamo?

Angular je neverovatno napredovao u poslednjih nekoliko verzija.

S obzirom da je Server-Side Rendering (SSR) sada zreo i ugrađen, Angular je postao odličan izbor za pravljenje SEO prijateljskih aplikacija.

Za lokalizaciju, integrisali smo ngx-translate za rukovanje prevodima na čist i poznat način. Taj deo je radio odlično.

Međutim, SEO uvodi dodatni zahtev.

Bili su nam potrebni lokalizovani URL-ovi koje pretraživači mogu da indeksiraju — što znači rute kao što su:

  • /en
  • /sr

Na prvi pogled, ovo zvuči jednostavno.

Ali kada počnete da povezujete rutiranje, lokalizaciju, čuvare (guards), početnu navigaciju i SSR zajedno, stvari postaju interesantne.

Tu na scenu stupa Angular Ruter — i još važnije, Dependency Injection (DI).

O čemu će biti reči u ovom članku

U ovom članku istražićemo:

  • zašto je rutiranje svesno lokalizacije važno za SEO
  • gde podrazumevani Angular Ruter ima nedostatke
  • kako proširiti ponašanje rutiranja pomoću DI-ja
  • kako održati rešenje čistim, pogodnim za testiranje i SSR

Krenimo redom.

Zašto je rutiranje svesno lokalizacije važno za SEO

Pretraživači indeksiraju URL-ove, a ne stanje korisničkog interfejsa (UI).

Ako jezik nije prikazan u URL-u, pretraživači ne mogu pravilno da razumeju, rangiraju ili ciljaju vaš sadržaj po lokaciji.

Pretraživači pretpostavljaju da su:

  • /en/about
  • /sr/about

dva različita dokumenta namenjena za dve različite publike.

Ako umesto toga uradite ovo:

/about   // jezik odlučuju JS, kolačići ili zaglavlja

onda iz perspektive pretraživača:

  • postoji samo jedna stranica
  • promena jezika je nejasna
  • SEO signali se mešaju
  • indeksiranje postaje nepouzdano

👉 Rezultat: slabije rangiranje i pogrešno ciljanje jezika.

Korak 1: Inicijalizacija jezika na početku

Prva stvar koja nam je potrebna je da inicijalizujemo jezik koji koristi ngx-translate.

Ovo radimo koristeći App Initializer, obezbeđujući da se tačan jezik postavi pre rutiranja i renderovanja, što je posebno važno za SSR.

App Config:

import { ApplicationConfig, provideAppInitializer } from '@angular/core';
import { languageInitializer } from './core/language';

export const appConfig: ApplicationConfig = {
  providers: [
    provideTranslateService({
      lang: 'en',
      fallbackLang: 'en',
      loader: provideTranslateHttpLoader({
        prefix: '/i18n/',
        suffix: '.json'
      })
    }),
    provideAppInitializer(() => languageInitializer())
  ],
};

languageInitializer:

export type Language = 'sr' | 'en';

export const SUPPORTED_LANGUAGES = new InjectionToken<Language[]>(
  'SUPPORTED_LANGUAGES',
  {
    providedIn: 'root',
    factory: () => ['en', 'sr'] as const,
  }
);

export function languageInitializer() {
  const translate = inject(TranslateService);
  const platformLocation = inject(PlatformLocation);
  const supportedLanguages = inject(SUPPORTED_LANGUAGES);

  const path = platformLocation.pathname;
  const segments = path.split('/').filter(Boolean);
  const language = segments[0] || 'en';

  if (supportedLanguages.includes(language as Language)) {
    translate.use(language);
  }
}

Ovo osigurava da je tačan jezik aktivan na osnovu URL-a, pre nego što se bilo šta drugo dogodi.

Korak 2: Rute svesne jezika

Sada definišemo naše rute sa dinamičnim :language segmentom.

import { Routes } from '@angular/router';
import { canMatchLanguageRoute } from './core/language';

export const routes: Routes = [
  {
    path: ':language',
    canMatch: [canMatchLanguageRoute],
    children: [
      {
        path: '',
        pathMatch: 'full',
        loadComponent: () =>
          import('./features/home/home').then(m => m.Home),
      },
      {
        path: 'about',
        loadComponent: () =>
          import('./features/about/about').then(m => m.About),
      },
    ],
  },
];

Čuvanje nepodržanih jezika:

export function canMatchLanguageRoute(
  _route: Route,
  segments: UrlSegment[]
): boolean {
  const supportedLanguages = inject(SUPPORTED_LANGUAGES);
  const router = inject(Router);

  const language = segments[0]?.path as Language || 'en';

  if (!supportedLanguages.includes(language)) {
    router.navigateByUrl('/en');
    return false;
  }

  return true;
}

Ovo sprečava korisnike i pretraživače da pristupe rutama nepodržanog jezika, dok održava definiciju rute čistom.

Korak 3: Proširenje Rutera pomoću DI-ja

Sada dolazi onaj zanimljiv deo.

Želeli smo da svaka navigacija — uključujući router.navigateByUrl() i routerLink — automatski uključuje trenutni jezik bez ručnog prosleđivanja.

Umesto da posipamo logiku jezika svuda po aplikaciji, proširili smo sam Angular Router.

LanguageRouter

@Injectable()
class LanguageRouter extends Router {
  private readonly translate = inject(TranslateService);
  private readonly supportedLanguages = inject<Language[]>(SUPPORTED_LANGUAGES);

  override navigateByUrl(
    url: string | UrlTree,
    extras?: NavigationBehaviorOptions,
  ): Promise<boolean> {
    const currentLang = this.translate.getCurrentLang() || 'en';
    const urlString = typeof url === 'string' ? url : this.serializeUrl(url);

    const { path, query, fragment } = this.parseUrlParts(urlString);
    const finalPath = this.addLanguagePrefix(path, currentLang);
    const finalUrlString = this.reconstructUrl(finalPath, query, fragment);

    const finalUrl: string | UrlTree =
      typeof url === 'string' ? finalUrlString : this.parseUrl(finalUrlString);

    const currentUrl = this.url;
    const finalUrlSerialized =
      typeof finalUrl === 'string' ? finalUrl : this.serializeUrl(finalUrl);

    if (currentUrl === finalUrlSerialized) {
      return Promise.resolve(false);
    }

    return super.navigateByUrl(finalUrl, extras);
  }

  private isLanguagePath(segment: string): boolean {
    return this.supportedLanguages.includes(segment as Language);
  }

  private parseUrlParts(urlString: string): { path: string; query?: string; fragment?: string } {
    const questionMarkIndex = urlString.indexOf('?');
    const hashIndex = urlString.indexOf('#');

    let path: string;
    let query: string | undefined;
    let fragment: string | undefined;

    if (questionMarkIndex !== -1) {
      path = urlString.substring(0, questionMarkIndex);
      const afterQuestion = urlString.substring(questionMarkIndex + 1);
      if (hashIndex !== -1 && hashIndex > questionMarkIndex) {
        query = urlString.substring(questionMarkIndex + 1, hashIndex);
        fragment = urlString.substring(hashIndex + 1);
      } else {
        query = afterQuestion;
      }
    } else if (hashIndex !== -1) {
      path = urlString.substring(0, hashIndex);
      fragment = urlString.substring(hashIndex + 1);
    } else {
      path = urlString;
    }

    return { path, query, fragment };
  }

  private addLanguagePrefix(path: string, language: string): string {
    const pathSegments = path.split('/').filter((s) => s.length > 0);
    const firstSegment = pathSegments[0];
    const hasLanguagePrefix = firstSegment && this.isLanguagePath(firstSegment);

    if (hasLanguagePrefix) {
      if (firstSegment === language) {
        return path;
      }
      pathSegments[0] = language;
      return '/' + pathSegments.join('/');
    }

    const trimmedPath = path.startsWith('/') ? path.substring(1) : path;
    return `/${language}${trimmedPath ? '/' + trimmedPath : ''}`;
  }

  private reconstructUrl(path: string, query?: string, fragment?: string): string {
    let url = path;
    if (query) {
      url += '?' + query;
    }
    if (fragment) {
      url += '#' + fragment;
    }
    return url;
  }
}

Korak 4: Zamena Rutera preko DI-ja

Sada govorimo Angularu da koristi naš ruter umesto podrazumevanog.

export function provideLanguageRouter(): Provider[] {
  return [{ provide: Router, useClass: LanguageRouter }];
}

I registrujemo ga nakon provideRouter:

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideAppInitializer(() => languageInitializer()),
    provideRouter(routes),
    provideClientHydration(withEventReplay()),
    provideLanguageRouter(), // mora doći nakon provideRouter
  ],
};

Rezultat

Sada:

  • svaki routerLink
  • svaki navigateByUrl
  • svaka programska navigacija

automatski poštuje aktivni jezik.

Nema koda koji se ponavlja.

Nema ručnog dodavanja prefiksa.

Nema "curenja" logike lokalizacije u komponente.

Završne reči

Ovaj pristup održava:

  • rutiranje deklarativnim
  • lokalizaciju eksplicitnom
  • SEO čvrstim
  • SSR srećnim

I što je najvažnije, koristi Angular-ov sistem Dependency Injection onako kako je i zamišljeno — proširivanjem ponašanja bez borbe sa framework-om.

Ako pravite lokalizovane Angular aplikacije prilagođene SEO-u, ovaj obrazac se iznenađujuće dobro skalira.

Srećno rutiranje! 🚀

Imate sličan frontend problem?

Zatražite Frontend Dijagnozu
  • Identifikujte uska grla u performansama
  • Uočite probleme u arhitekturi
  • Dobijte konkretne preporuke
Analiziraj frontend arhitekturu