feat(i18n): nl at root URLs + keep devtools in the docker demo

Serve the source locale (nl) at / instead of /nl/: angular.json sourceLocale is now
{ code: 'nl', subPath: '' } → nl output at browser/ root (base href /), en stays /en/.
Rework localeLinks (nl → bare path, en → /en/…) + spec, and serve-i18n.mjs (nl assets at
root, en under /en/, / serves the nl index). And build the docker demo (+ serve:i18n) with
`--configuration development --localize` so isDevMode() stays true and the dev `⚙ state`
panel + role/scenario switchers render in the localized compose demo (they were correctly
gated off in the previous production build). Verified: nl base href /, en /en/, ngDevMode present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 19:24:33 +02:00
co-authored by Claude Opus 4.8
parent deb5d77e04
commit c00e607b8f
6 changed files with 35 additions and 31 deletions
@@ -1,27 +1,30 @@
import { describe, it, expect } from 'vitest';
import { localeLinks } from './locale-links';
describe('localeLinks', () => {
it('preserves the route when already under a locale prefix, marks the active one', () => {
const links = localeLinks('/nl/dashboard', 'nl');
describe('localeLinks (nl at root, en under /en/)', () => {
it('an nl route (no prefix) links nl to the bare path, en under /en, marks active', () => {
const links = localeLinks('/dashboard', 'nl');
expect(links.map((l) => [l.locale, l.href, l.active])).toEqual([
['nl', '/nl/dashboard', true],
['nl', '/dashboard', true],
['en', '/en/dashboard', false],
]);
});
it('swaps the prefix and keeps a deep path (en active)', () => {
it('an en route strips the /en prefix for the nl target (deep path, en active)', () => {
const links = localeLinks('/en/beheer/audit', 'en');
expect(links.find((l) => l.locale === 'nl')!.href).toBe('/nl/beheer/audit');
expect(links.find((l) => l.locale === 'nl')!.href).toBe('/beheer/audit');
expect(links.find((l) => l.locale === 'en')!.active).toBe(true);
});
it('handles an unprefixed dev path (served at /), keeps query + hash', () => {
it('keeps query + hash on both targets', () => {
const links = localeLinks('/registreren', 'nl', '?scenario=slow', '#top');
expect(links.find((l) => l.locale === 'nl')!.href).toBe('/registreren?scenario=slow#top');
expect(links.find((l) => l.locale === 'en')!.href).toBe('/en/registreren?scenario=slow#top');
});
it('a bare locale root maps to the sibling root', () => {
expect(localeLinks('/nl', 'nl').find((l) => l.locale === 'en')!.href).toBe('/en/');
it('the root maps nl → / and en → /en/', () => {
const links = localeLinks('/', 'nl');
expect(links.find((l) => l.locale === 'nl')!.href).toBe('/');
expect(links.find((l) => l.locale === 'en')!.href).toBe('/en/');
});
});
@@ -16,10 +16,11 @@ const LOCALES: readonly { locale: Locale; label: string }[] = [
];
/**
* Build the two language links for the switcher. Compile-time i18n serves each locale as its
* own bundle under `/<locale>/`, so switching is a full navigation to the sibling bundle at the
* same route. Strips any leading `/nl` or `/en` from the current path and re-prefixes the target
* locale, keeping query + hash. Pure — no DOM (the component passes `location.*` in).
* Build the two language links for the switcher. Compile-time i18n serves the source locale
* (nl) at the ROOT (`subPath: ''`) and en under `/en/`, so switching is a full navigation to the
* sibling bundle at the same route. Strips a leading `/en` from the current path, then targets nl
* at the bare path and en under `/en`. Keeps query + hash. Pure — no DOM (the component passes
* `location.*` in).
*/
export function localeLinks(
pathname: string,
@@ -27,11 +28,12 @@ export function localeLinks(
search = '',
hash = '',
): LocaleLink[] {
const rest = pathname.replace(/^\/(nl|en)(?=\/|$)/, '') || '/';
const rest = pathname.replace(/^\/en(?=\/|$)/, '') || '/';
const href = (locale: Locale) => `${locale === 'en' ? `/en${rest}` : rest}${search}${hash}`;
return LOCALES.map(({ locale, label }) => ({
locale,
label,
href: `/${locale}${rest}${search}${hash}`,
href: href(locale),
active: locale === active,
}));
}