import { registerLocaleData } from '@angular/common'; import localeEn from '@angular/common/locales/en'; import { inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { normalizeDateLocale } from '@iptvnator/pipes/date-format'; import { createDevLogger } from '@iptvnator/shared/interfaces'; type LocaleDataModule = { default: unknown }; export type AppDateLocaleLoader = () => Promise; const debug = createDevLogger('AppDateLocales'); /** * Angular ships only English locale data; every other UI language needs its * data registered before `DatePipe` can format with it. Each entry is a * separate lazy chunk, keyed by the Angular locale id that * `normalizeDateLocale()` produces for an app language (`by` -> `be`, * `ary` -> `ar-MA`, `zhtw` -> `zh-Hant`). Importing all of them eagerly put * the data for 18 languages into the initial bundle of every user. */ export const APP_DATE_LOCALE_LOADERS: Readonly< Record > = { ar: () => import('@angular/common/locales/ar'), 'ar-MA': () => import('@angular/common/locales/ar-MA'), be: () => import('@angular/common/locales/be'), de: () => import('@angular/common/locales/de'), el: () => import('@angular/common/locales/el'), es: () => import('@angular/common/locales/es'), fr: () => import('@angular/common/locales/fr'), hu: () => import('@angular/common/locales/hu'), it: () => import('@angular/common/locales/it'), ja: () => import('@angular/common/locales/ja'), ko: () => import('@angular/common/locales/ko'), nl: () => import('@angular/common/locales/nl'), pl: () => import('@angular/common/locales/pl'), pt: () => import('@angular/common/locales/pt'), ru: () => import('@angular/common/locales/ru'), tr: () => import('@angular/common/locales/tr'), zh: () => import('@angular/common/locales/zh'), 'zh-Hant': () => import('@angular/common/locales/zh-Hant'), }; const registeredLocales = new Set(['en']); const pendingLocales = new Map>(); /** * Registers the Angular locale data for an app language, loading it on first * use. Resolves once `DatePipe` can format with that locale, so call it before * `TranslateService.use()`: the language switch re-renders every date with the * new locale, and a locale whose data has not arrived throws. English resolves * immediately, as does a locale that is already registered or in flight. * * A failed load resolves rather than rejects, but never leaves the locale * without data: English formatting is registered under the requested id so * `DatePipe` keeps rendering instead of throwing in every template that passes * the locale. The locale is not marked registered, so the next call retries * the import and a success replaces the fallback. */ export function registerAppDateLocale( language: string | null | undefined, loaders: Readonly< Record > = APP_DATE_LOCALE_LOADERS ): Promise { const locale = normalizeDateLocale(language); if (registeredLocales.has(locale)) { return Promise.resolve(); } const inFlight = pendingLocales.get(locale); if (inFlight) { return inFlight; } const loader = loaders[locale]; if (!loader) { debug('No bundled Angular locale data for', locale); return Promise.resolve(); } const load = loader() .then((module) => { registerLocaleData(module.default, locale); registeredLocales.add(locale); }) .catch((error: unknown) => { debug( 'Loading Angular locale data failed; using English formatting for', locale, error ); registerLocaleData(localeEn, locale); }) .finally(() => { pendingLocales.delete(locale); }); pendingLocales.set(locale, load); return load; } /** Gates UI language switches on the locale data they render with. */ @Injectable({ providedIn: 'root' }) export class AppDateLocaleService { private readonly translate = inject(TranslateService); private latestRequest = 0; register(language: string | null | undefined): Promise { return registerAppDateLocale(language); } /** * Registers the locale data, then switches the UI language. Switches are * ordered by request, not by completion: a switch whose data arrives * after a newer request was made is dropped, so the language chosen last * is the one that ends up active. */ async use(language: string): Promise { const request = ++this.latestRequest; await registerAppDateLocale(language); if (request === this.latestRequest) { this.translate.use(language); } } }