From 18ecc07240d7a3ab9c438454ddc47269bb9e91eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Wed, 11 Mar 2026 17:09:51 +0100 Subject: [PATCH] feat(front-end): ssr i18n fix --- frontend/src/app/app.config.ts | 10 ++- .../interceptors/server-origin.interceptor.ts | 65 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/core/interceptors/server-origin.interceptor.ts diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index ae0b8c6..09271b3 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -17,6 +17,11 @@ import { TranslateHttpLoader, } from '@ngx-translate/http-loader'; import { adminAuthInterceptor } from './core/interceptors/admin-auth.interceptor'; +import { + provideClientHydration, + withEventReplay, +} from '@angular/platform-browser'; +import { serverOriginInterceptor } from './core/interceptors/server-origin.interceptor'; export const appConfig: ApplicationConfig = { providers: [ @@ -29,7 +34,9 @@ export const appConfig: ApplicationConfig = { scrollPositionRestoration: 'top', }), ), - provideHttpClient(withInterceptors([adminAuthInterceptor])), + provideHttpClient( + withInterceptors([serverOriginInterceptor, adminAuthInterceptor]), + ), provideTranslateHttpLoader({ prefix: './assets/i18n/', suffix: '.json', @@ -43,5 +50,6 @@ export const appConfig: ApplicationConfig = { }, }), ), + provideClientHydration(withEventReplay()), ], }; diff --git a/frontend/src/app/core/interceptors/server-origin.interceptor.ts b/frontend/src/app/core/interceptors/server-origin.interceptor.ts new file mode 100644 index 0000000..0660204 --- /dev/null +++ b/frontend/src/app/core/interceptors/server-origin.interceptor.ts @@ -0,0 +1,65 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject, REQUEST } from '@angular/core'; + +type RequestLike = { + protocol?: string; + get?: (name: string) => string | undefined; + headers?: Record; +}; + +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 })); +};