All files / localization si-locale.service.ts

89.23% Statements 58/65
82.85% Branches 29/35
92.3% Functions 12/13
89.23% Lines 58/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223                                1x 1x         12x 12x                                                                               1x                 17x     17x 17x   17x       17x                 17x 4x 2x   2x       17x 2x       17x   17x 17x 17x   17x         17x 17x 17x 2x 15x   15x   15x 14x   1x   17x 17x       17x 20x     17x 6x                     23x 18x   5x 1x   4x         4x 4x 4x                             3x       21x 21x   19x 19x       19x 2x         2x       2x       2x 1x                       57x 40x   17x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, InjectionToken, PLATFORM_ID } from '@angular/core';
import {
  getBrowserCultureLanguage,
  getBrowserLanguage,
  injectSiTranslateService
} from '@siemens/element-translate-ng/translate';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
 
import { SiDefaultLocaleStore, SiLocaleStore } from './si-locale-store';
 
export const SI_LOCALE_STORE = new InjectionToken<string>('SI_LOCALE_STORE');
export const SI_LOCALE_CONFIG = new InjectionToken<SiLocaleConfig>('SI_LOCALE_CONFIG');
 
// this is a function because Angular compiler exports arrows for no good reason
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function defaultLocaleInitializer(localeId: string): Promise<void> {
  if (localeId === 'en') {
    return Promise.resolve();
  } else E{
    return Promise.reject();
  }
}
 
export interface SiLocaleConfig {
  /**
   * The default locale to be used, when no user preference
   * available and the browser language is not part of the
   * available languages.
   */
  defaultLocale?: string;
  /**
   * The list of available locales (e.g. en, fr, de, en-GB, de-AT)
   */
  availableLocales?: string[];
  /**
   * The localeInitializer function is invoked on every locale change.
   * Make sure to invoke `registerLocaleData` with the locale to enable
   * the Angular localization.
   */
  localeInitializer?: (localeId: string) => Promise<any>;
  /**
   * Set to true to also enable the default language on ngx-translate. When true,
   * ngx-translate will use a translate value from the default language when a required
   * value is not available in the current language. But note, this will also enforce
   * to load the default language translation file into the application, even if a different
   * locale is active. In other words, the application start time increases.
   */
  fallbackEnabled?: boolean;
  /**
   * Default is false and defines that on setting a new locale, the locale is stored and the
   * browser is reloaded. When changing to true, window reload is not invoked, but angular
   * pure pipes like DatePipe will not work.
   */
  dynamicLanguageChange?: boolean;
}
 
@Injectable({ providedIn: 'root' })
export class SiLocaleService {
  /**
   * Holds the used locale definition like en, de, or en-US.
   */
  readonly locale$: BehaviorSubject<string>;
  /**
   * Emits to indicate that the localization package (e.g. \@angular/common/locales/$\{localeId\})
   * is loaded and registered. Emits after calling `localeInitializer` from the `config` object.
   */
  readonly localePackageLoaded$ = new ReplaySubject<void>(1);
 
  private _nextLocale!: string;
  private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  private translate = injectSiTranslateService();
  private localeStore =
    inject(SiLocaleStore, { optional: true }) ?? new SiDefaultLocaleStore(this.isBrowser);
  /**
   * The config for the local service.
   */
  config = inject(SI_LOCALE_CONFIG, { optional: true }) ?? {
    availableLocales: ['en'],
    defaultLocale: 'en',
    localeInitializer: defaultLocaleInitializer,
    fallbackEnabled: false,
    dynamicLanguageChange: false
  };
 
  constructor() {
    if (!this.config.defaultLocale) {
      if (this.config.availableLocales && this.config.availableLocales.length > 0) {
        this.config.defaultLocale = this.config.availableLocales[0];
      } else {
        this.config.defaultLocale = 'en';
      }
    }
 
    if (!this.config.availableLocales || this.config.availableLocales.length === 0) {
      this.config.availableLocales = [this.config.defaultLocale];
    }
    // Also adds all locales to the translate service to enable
    // components working with the translate service directly to still work.
    this.translate.availableLanguages = this.config.availableLocales;
 
    this.config.localeInitializer ??= defaultLocaleInitializer;
    this.config.fallbackEnabled ??= false;
    this.config.dynamicLanguageChange ??= false;
 
    const savedLocale = this.localeStore.locale;
    // The following check is important. We do not control the store and when it comes from
    // a remove backend, someone might give us a locale that we do not understand. In this
    // case we switch to the default.
    let initialLocale;
    const browserCultureLang = getBrowserCultureLanguage();
    const browserLang = getBrowserLanguage();
    if (this.hasLocale(savedLocale)) {
      initialLocale = savedLocale!;
    } else Iif (this.translate.currentLanguage) {
      initialLocale = this.translate.currentLanguage;
    } else Iif (this.hasLocale(browserCultureLang)) {
      initialLocale = browserCultureLang!;
    } else if (this.hasLocale(browserLang)) {
      initialLocale = browserLang!;
    } else {
      initialLocale = this.config.defaultLocale;
    }
    this.locale$ = new BehaviorSubject<string>(initialLocale);
    this.doSetLocale(initialLocale);
 
    // If a user changes the language on the translate service directly,
    // we synchronize the change again.
    this.translate.translationChange.subscribe(() => {
      this.locale = this.translate.currentLanguage;
    });
 
    if (this.config.fallbackEnabled) {
      this.translate.setDefaultLanguage(this.config.defaultLocale);
    }
  }
 
  /**
   * Sets a new locale to the locale service and also to the translate
   * service.
   * @throws An error if the new value is not configured in the available locales
   * or if the new locale cannot be saved, an error is thrown.
   */
  set locale(value: string) {
    if (value === this.locale$.value || value === this._nextLocale) {
      return;
    }
    if (!this.hasLocale(value)) {
      throw new Error(`The value ${value} does not exist in the available locales.`);
    }
    this.localeStore
      .saveLocale(value)
      .pipe(first())
      .subscribe({
        next: (saveSucceed: boolean) => {
          if (saveSucceed) {
            if (this.config.dynamicLanguageChange) {
              this.doSetLocale(value);
            } else IEif (this.isBrowser) {
              window.location.reload();
            }
          } else E{
            throw new Error(`Could not save new locale ${value}.`);
          }
        },
        error: () => {
          throw new Error(`Could not save new locale ${value}.`);
        }
      });
  }
 
  get locale(): string {
    return this.locale$.value;
  }
 
  private doSetLocale(value: string): void {
    this._nextLocale = value;
    this.config.localeInitializer!(value).then(
      () => {
        this.localePackageLoaded$.next();
        this.translate
          .setCurrentLanguage(value)
          .pipe(first())
          .subscribe(() => {
            if (this.locale$.value !== value) {
              this.locale$.next(value);
            }
          });
      },
      () => {
        console.error(
          `Could not initialize new locale ${value}. Setting default locale ${this.config.defaultLocale}`
        );
        // Initialization of locale rejected. Setting default locale.
        this.translate
          .setCurrentLanguage(this.config.defaultLocale!)
          .pipe(first())
          .subscribe(() => {
            if (this.locale$.value !== this.config.defaultLocale!) {
              this.locale$.next(this.config.defaultLocale!);
            }
          });
      }
    );
  }
 
  /**
   * Test if the given locale is part of the available locales.
   * @param locale - The locale to be tested.
   */
  hasLocale(locale?: string): boolean {
    if (locale) {
      return this.config.availableLocales!.includes(locale);
    }
    return false;
  }
}