Compare commits
9 Commits
feat/ssr
...
637541994a
| Author | SHA1 | Date | |
|---|---|---|---|
| 637541994a | |||
|
|
63cd4c4f5e | ||
| fd4104da39 | |||
| 5bb23fbcfa | |||
| 6a22c54e9f | |||
| 3ac3262e77 | |||
| 18ecc07240 | |||
| cb468492b3 | |||
| feee2b0bff |
@@ -1,26 +1,48 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
|
provideAppInitializer,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
importProvidersFrom,
|
importProvidersFrom,
|
||||||
|
inject,
|
||||||
|
REQUEST,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
provideRouter,
|
provideRouter,
|
||||||
withComponentInputBinding,
|
withComponentInputBinding,
|
||||||
withInMemoryScrolling,
|
withInMemoryScrolling,
|
||||||
withViewTransitions,
|
withViewTransitions,
|
||||||
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import {
|
import {
|
||||||
provideTranslateHttpLoader,
|
TranslateLoader,
|
||||||
TranslateHttpLoader,
|
TranslateModule,
|
||||||
} from '@ngx-translate/http-loader';
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
|
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
|
||||||
import {
|
import {
|
||||||
provideClientHydration,
|
provideClientHydration,
|
||||||
withEventReplay,
|
withEventReplay,
|
||||||
} from '@angular/platform-browser';
|
} from '@angular/platform-browser';
|
||||||
|
import { serverOriginInterceptor } from './core/interceptors/server-origin.interceptor';
|
||||||
|
import { catchError, firstValueFrom, of } from 'rxjs';
|
||||||
|
import { StaticTranslateLoader } from './core/i18n/static-translate.loader';
|
||||||
|
|
||||||
|
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
|
||||||
|
const SUPPORTED_LANGS: readonly SupportedLang[] = ['it', 'en', 'de', 'fr'];
|
||||||
|
|
||||||
|
function resolveLangFromUrl(url: string): SupportedLang {
|
||||||
|
const firstSegment = (url || '/')
|
||||||
|
.split('?')[0]
|
||||||
|
.split('#')[0]
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean)[0]
|
||||||
|
?.toLowerCase();
|
||||||
|
return SUPPORTED_LANGS.includes(firstSegment as SupportedLang)
|
||||||
|
? (firstSegment as SupportedLang)
|
||||||
|
: 'it';
|
||||||
|
}
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -33,20 +55,44 @@ export const appConfig: ApplicationConfig = {
|
|||||||
scrollPositionRestoration: 'top',
|
scrollPositionRestoration: 'top',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
provideHttpClient(withInterceptors([adminAuthInterceptor])),
|
provideHttpClient(
|
||||||
provideTranslateHttpLoader({
|
withInterceptors([serverOriginInterceptor, adminAuthInterceptor]),
|
||||||
prefix: './assets/i18n/',
|
),
|
||||||
suffix: '.json',
|
|
||||||
}),
|
|
||||||
importProvidersFrom(
|
importProvidersFrom(
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
defaultLanguage: 'it',
|
defaultLanguage: 'it',
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
useClass: TranslateHttpLoader,
|
useClass: StaticTranslateLoader,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
provideAppInitializer(() => {
|
||||||
|
const translate = inject(TranslateService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const request = inject(REQUEST, { optional: true }) as {
|
||||||
|
url?: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
translate.addLangs([...SUPPORTED_LANGS]);
|
||||||
|
translate.setDefaultLang('it');
|
||||||
|
const requestedUrl =
|
||||||
|
(typeof request?.url === 'string' && request.url) || router.url || '/';
|
||||||
|
const lang = resolveLangFromUrl(requestedUrl);
|
||||||
|
|
||||||
|
return firstValueFrom(
|
||||||
|
translate.use(lang).pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('[i18n] Failed to preload language for SSR', {
|
||||||
|
lang,
|
||||||
|
requestedUrl,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
return of({});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).then(() => undefined);
|
||||||
|
}),
|
||||||
provideClientHydration(withEventReplay()),
|
provideClientHydration(withEventReplay()),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
22
frontend/src/app/core/i18n/static-translate.loader.ts
Normal file
22
frontend/src/app/core/i18n/static-translate.loader.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { TranslateLoader, TranslationObject } from '@ngx-translate/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import de from '../../../assets/i18n/de.json';
|
||||||
|
import en from '../../../assets/i18n/en.json';
|
||||||
|
import fr from '../../../assets/i18n/fr.json';
|
||||||
|
import it from '../../../assets/i18n/it.json';
|
||||||
|
|
||||||
|
const TRANSLATIONS: Record<string, TranslationObject> = {
|
||||||
|
it: it as TranslationObject,
|
||||||
|
en: en as TranslationObject,
|
||||||
|
de: de as TranslationObject,
|
||||||
|
fr: fr as TranslationObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StaticTranslateLoader implements TranslateLoader {
|
||||||
|
getTranslation(lang: string): Observable<TranslationObject> {
|
||||||
|
const normalized = String(lang || 'it').toLowerCase();
|
||||||
|
return of(TRANSLATIONS[normalized] ?? TRANSLATIONS['it']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
import { inject, REQUEST } from '@angular/core';
|
||||||
|
|
||||||
|
type RequestLike = {
|
||||||
|
protocol?: string;
|
||||||
|
get?: (name: string) => string | undefined;
|
||||||
|
headers?: Record<string, string | string[] | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAbsoluteUrl(url: string): boolean {
|
||||||
|
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstHeaderValue(value: string | string[] | undefined): string | null {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value[0] ?? null;
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOrigin(request: RequestLike | null): string | null {
|
||||||
|
if (!request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host =
|
||||||
|
request.get?.('host') ??
|
||||||
|
firstHeaderValue(request.headers?.['host']) ??
|
||||||
|
firstHeaderValue(request.headers?.['x-forwarded-host']);
|
||||||
|
if (!host) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardedProtoRaw = firstHeaderValue(
|
||||||
|
request.headers?.['x-forwarded-proto'],
|
||||||
|
);
|
||||||
|
const forwardedProto = forwardedProtoRaw
|
||||||
|
?.split(',')
|
||||||
|
.map((part) => part.trim().toLowerCase())
|
||||||
|
.find(Boolean);
|
||||||
|
const protocol = forwardedProto || request.protocol || 'http';
|
||||||
|
return `${protocol}://${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelativePath(url: string): string {
|
||||||
|
const withoutDot = url.replace(/^\.\//, '');
|
||||||
|
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serverOriginInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
if (isAbsoluteUrl(req.url)) {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = inject(REQUEST, { optional: true }) as RequestLike | null;
|
||||||
|
const origin = resolveOrigin(request);
|
||||||
|
if (!origin) {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteUrl = `${origin}${normalizeRelativePath(req.url)}`;
|
||||||
|
return next(req.clone({ url: absoluteUrl }));
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022"
|
"module": "ES2022"
|
||||||
|
|||||||
Reference in New Issue
Block a user