Files
iptvnator/apps/web/src/app/app-date-locales.ts
T
f0e51d2806 perf(web): keep lazy-only services and SafePipe out of main.js (#1729)
* perf(web): keep lazy-only services and SafePipe out of main.js

The eager shell imported barrels that re-export Angular injectables and a
pipe it never uses, and their static definitions keep those modules in
main.js: PlaylistFileImportService came with PlaylistContextFacade,
normalizeDateLocale with SafePipe, and the workspace-shell-util barrel with
SettingsContextService, which #1714 grew with match counts. That growth put
master 108 bytes over the renderer.initialBytes baseline #1712 had measured
on a branch without #1714.

Add file-level entries for the three modules and use them from the eager
and settings code: renderer.initialBytes 1,626,127 -> 1,619,993 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(performance): lower the initial-bytes baseline to 1,619,993 bytes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:21:40 +02:00

124 lines
4.8 KiB
TypeScript

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<LocaleDataModule>;
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<string, AppDateLocaleLoader>
> = {
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<string>(['en']);
const pendingLocales = new Map<string, Promise<void>>();
/**
* 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<string, AppDateLocaleLoader>
> = APP_DATE_LOCALE_LOADERS
): Promise<void> {
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<void> {
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<void> {
const request = ++this.latestRequest;
await registerAppDateLocale(language);
if (request === this.latestRequest) {
this.translate.use(language);
}
}
}