diff --git a/docker-compose.yml b/docker-compose.yml index 6eedd7e..9ecb25d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,8 @@ # ponytail: dev-server images (not multi-stage prod builds) — this is a demo. -# `docker compose up` → app at http://localhost:4200, Swagger at http://localhost:5000/swagger +# `docker compose up` → app at http://localhost:4200 (LOCALIZED: /nl/ + /en/, the header +# language switcher works), Swagger at http://localhost:5000/swagger. The web container does a +# one-time `ng build --localize` then serves both locale bundles statically (with /api proxied); +# for a fast HMR loop use `npm start` locally instead (nl-only at /). services: api: image: mcr.microsoft.com/dotnet/sdk:10.0 @@ -30,12 +33,14 @@ services: # msgpackr-extract) ship prebuilt glibc binaries, so no build toolchain is needed. image: node:24-slim working_dir: /app - # Uses the committed generated client (no codegen at startup); proxies /api → api container. - # ponytail: `--no-fund --loglevel=error` silences npm 11's startup noise (deprecation + - # allow-scripts + funding) so the demo boots clean; ng serve output is unaffected. The - # deprecation lines are Angular 22's webpack-builder (@angular-devkit/build-angular) — a - # known upstream deprecation; migrating to @angular/build is a separate, larger effort. - command: sh -c "npm ci --no-fund --loglevel=error && npx ng serve --host 0.0.0.0 --proxy-config proxy.conf.docker.json" + # Uses the committed generated client (no codegen at startup). Builds BOTH locale bundles + # once (`ng build --localize`) then serves them statically via scripts/serve-i18n.mjs + # (per-locale SPA fallback + /api reverse-proxy → the api container), so the language + # switcher actually switches. ponytail: `--no-fund --loglevel=error` silences npm 11 noise. + command: sh -c "npm ci --no-fund --loglevel=error && npx ng build --localize && node scripts/serve-i18n.mjs" + environment: + - PORT=4200 + - API_PROXY_TARGET=http://api:5000 volumes: - ./:/app:z - web-modules:/app/node_modules diff --git a/docs/reference/architecture/ARCHITECTURE.md b/docs/reference/architecture/ARCHITECTURE.md index 4962a3f..aaacbdc 100644 --- a/docs/reference/architecture/ARCHITECTURE.md +++ b/docs/reference/architecture/ARCHITECTURE.md @@ -534,8 +534,9 @@ no longer hand-written DTOs — it's an **NSwag-generated typed client** - **Proxy.** The app uses a relative base URL (`apiBaseUrl: ''`), so `/api` calls are same-origin and `ng serve` proxies them to the backend on `:5000`. ([`environment.ts`](../../../src/environments/environment.ts), - [`proxy.conf.json`](../../../proxy.conf.json); `proxy.conf.docker.json` targets the - compose service.) + [`proxy.conf.json`](../../../proxy.conf.json)). Under `docker compose up` the app is served + localized (both locales) by `scripts/serve-i18n.mjs`, which proxies `/api` to the `api` + container itself (`API_PROXY_TARGET`). - **Client → HttpClient seam.** The NSwag client's `fetch` is routed through Angular's `HttpClient` by `httpClientFetch` — the one place cross-cutting concerns live: `X-Correlation-Id` on every call, `Idempotency-Key` on non-GETs, a 10 s timeout, and diff --git a/proxy.conf.docker.json b/proxy.conf.docker.json deleted file mode 100644 index fc9056a..0000000 --- a/proxy.conf.docker.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/api": { - "target": "http://api:5000", - "secure": false, - "changeOrigin": true - } -} diff --git a/scripts/serve-i18n.mjs b/scripts/serve-i18n.mjs index ef64761..f851ca6 100644 --- a/scripts/serve-i18n.mjs +++ b/scripts/serve-i18n.mjs @@ -4,12 +4,15 @@ // Plain `ng serve` (npm start) serves only nl at /, so switching 404s there — this static // server serves both locale subdirs with per-locale SPA fallback (a miss under //… // serves that locale's index.html), so deep-link switches resolve. Demo only, not prod infra. -import { createServer } from 'node:http'; +import { createServer, request as httpRequest } from 'node:http'; import { readFile } from 'node:fs/promises'; import { join, extname, normalize } from 'node:path'; const ROOT = 'dist/atomic-design-poc/browser'; -const PORT = 4300; +const PORT = Number(process.env.PORT ?? 4300); +// Reverse-proxy /api → backend so the served app is the whole demo, not just static pages. +// Docker sets API_PROXY_TARGET=http://api:5000; locally it defaults to the dev backend. +const API_TARGET = new URL(process.env.API_PROXY_TARGET ?? 'http://localhost:5000'); const MIME = { '.html': 'text/html', '.js': 'text/javascript', @@ -28,7 +31,27 @@ const send = (res, status, body, type) => { }; createServer(async (req, res) => { - const url = decodeURIComponent((req.url ?? '/').split('?')[0]); + const rawUrl = req.url ?? '/'; + // Proxy API calls to the backend (keep the raw url incl. query). + if (rawUrl.startsWith('/api')) { + const proxied = httpRequest( + { + hostname: API_TARGET.hostname, + port: API_TARGET.port, + path: rawUrl, + method: req.method, + headers: req.headers, + }, + (pr) => { + res.writeHead(pr.statusCode ?? 502, pr.headers); + pr.pipe(res); + }, + ); + proxied.on('error', () => send(res, 502, 'Bad gateway (API)', 'text/plain')); + req.pipe(proxied); + return; + } + const url = decodeURIComponent(rawUrl.split('?')[0]); // Landing at / has no locale bundle — redirect to Dutch. if (url === '/') { res.writeHead(302, { location: '/nl/' }); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 6d0e2c2..6b55e65 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -9,6 +9,7 @@ import type { ActivatedRouteSnapshot } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { registerLocaleData } from '@angular/common'; import localeNl from '@angular/common/locales/nl'; +import localeEn from '@angular/common/locales/en'; import { routes } from './app.routes'; import { scenarioInterceptor } from '@shared/infrastructure/scenario.interceptor'; @@ -19,7 +20,9 @@ import { SessionStore } from '@auth/application/session.store'; import { provideRouteFocus } from '@shared/layout/route-focus'; import { provideUnloadFlush } from '@shared/application/pending-saves'; +// Both locales' data so DatePipe/number pipes work for whichever bundle is active. registerLocaleData(localeNl); +registerLocaleData(localeEn); export const appConfig: ApplicationConfig = { providers: [ @@ -50,7 +53,10 @@ export const appConfig: ApplicationConfig = { provideHttpClient(withInterceptors(isDevMode() ? [scenarioInterceptor, roleInterceptor] : [])), provideApiClient(), { provide: SESSION_PORT, useExisting: SessionStore }, - { provide: LOCALE_ID, useValue: 'nl' }, + // Per-bundle locale: the localize build sets `$localize.locale` ('nl'/'en'); the + // non-localized dev/source build leaves it undefined → fall back to 'nl'. (Was hardcoded + // 'nl', which mis-formatted dates/numbers in the en bundle.) + { provide: LOCALE_ID, useFactory: () => $localize.locale ?? 'nl' }, provideRouteFocus(), provideUnloadFlush(), ],