Files
iptvnator/apps/web/src/app/app-date-locales.ts
T
4grayandClaude Fable 5.1 512e9787a8 perf(web): load Angular date locales lazily per language (#1695)
Plan thread C1, journey **J1 `launch`**, counter **`renderer.initialBytes`**. Stacked on #1694 → #1693 → #1692; merge in order.

`apps/web/src/app/app-date-locales.ts` imported the locale data of all 18 supported languages eagerly, so every user shipped and parsed all of it at startup. Each locale is now a dynamic import keyed by the Angular locale id that `normalizeDateLocale()` derives from the app language (`by` → `be`, `ary` → `ar-MA`, `zhtw` → `zh-Hant`); English needs no data.

Ordering is preserved so no template renders a locale whose data has not arrived (Angular throws in that case):
- `main.ts` awaits the initial language's data (from `getInitialLanguage()`) before `bootstrapApplication`.
- Both `TranslateService.use()` call sites, `AppComponent.initSettings()` and `SettingsFormFacade.applySavedSettings()`, register the data first through the new `AppDateLocaleService`.
- A failed load never leaves the locale without data: English formatting is registered under the requested id (eager 1.1 KB `@angular/common/locales/en`), so `DatePipe` renders instead of throwing; the locale is not marked registered, so the next call retries and a success replaces the fallback (review follow-up).
- `AppDateLocaleService.use()` orders switches by request, not by completion: a switch whose data arrives after a newer request is dropped, so the language chosen last wins (review follow-up).

No kill switch: behavior is identical once the locale resolves, and the only new failure mode (a same-origin chunk failing to load) is shared with every lazy route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:44:29 +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';
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);
}
}
}