9 Commits

Author SHA1 Message Date
637541994a Merge branch 'main' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
PR Checks / prettier-autofix (pull_request) Successful in 8s
Build and Deploy / test-frontend (push) Successful in 1m1s
PR Checks / security-sast (pull_request) Successful in 31s
PR Checks / test-backend (pull_request) Successful in 26s
Build and Deploy / build-and-push (push) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / deploy (push) Successful in 22s
2026-03-11 17:32:53 +01:00
printcalc-ci
63cd4c4f5e style: apply prettier formatting 2026-03-11 16:31:15 +00:00
fd4104da39 fix(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 1m1s
Build and Deploy / build-and-push (push) Successful in 29s
Build and Deploy / deploy (push) Successful in 21s
PR Checks / prettier-autofix (pull_request) Successful in 11s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 59s
2026-03-11 17:27:28 +01:00
5bb23fbcfa fix(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
Build and Deploy / test-frontend (push) Successful in 1m0s
Build and Deploy / build-and-push (push) Successful in 26s
Build and Deploy / deploy (push) Successful in 19s
2026-03-11 17:23:32 +01:00
6a22c54e9f feat(front-end): ssr i18n fix
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 30s
Build and Deploy / deploy (push) Successful in 19s
2026-03-11 17:19:26 +01:00
3ac3262e77 Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 27s
Build and Deploy / deploy (push) Successful in 20s
# Conflicts:
#	frontend/src/app/app.config.ts
2026-03-11 17:10:06 +01:00
18ecc07240 feat(front-end): ssr i18n fix 2026-03-11 17:09:51 +01:00
cb468492b3 Merge pull request 'feat(front-end): ssr implementation' (#41) from feat/ssr into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / build-and-push (push) Successful in 52s
Build and Deploy / deploy (push) Successful in 10s
Build and Deploy / test-frontend (push) Successful in 1m0s
Reviewed-on: #41
2026-03-11 16:59:21 +01:00
feee2b0bff Merge pull request 'dev' (#40) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 15s
Build and Deploy / deploy (push) Successful in 7s
Reviewed-on: #40
2026-03-11 15:32:14 +01:00
5 changed files with 143 additions and 10 deletions

View File

@@ -1,26 +1,48 @@
import {
ApplicationConfig,
provideAppInitializer,
provideZoneChangeDetection,
importProvidersFrom,
inject,
REQUEST,
} from '@angular/core';
import {
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
withViewTransitions,
Router,
} from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import {
provideTranslateHttpLoader,
TranslateHttpLoader,
} from '@ngx-translate/http-loader';
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor';
import {
provideClientHydration,
withEventReplay,
} 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 = {
providers: [
@@ -33,20 +55,44 @@ export const appConfig: ApplicationConfig = {
scrollPositionRestoration: 'top',
}),
),
provideHttpClient(withInterceptors([adminAuthInterceptor])),
provideTranslateHttpLoader({
prefix: './assets/i18n/',
suffix: '.json',
}),
provideHttpClient(
withInterceptors([serverOriginInterceptor, adminAuthInterceptor]),
),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'it',
loader: {
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()),
],
};

View 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']);
}
}

View File

@@ -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 }));
};

View File

@@ -19,6 +19,7 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
</head>
<body>
<app-root></app-root>

View File

@@ -14,6 +14,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"