refactor/i18n/intlayer #73

Merged
martin merged 7 commits from refactor/i18n/intlayer into master 2026-09-18 17:48:21 +00:00
Owner
No description provided.
martin self-assigned this 2026-08-29 11:51:58 +00:00
refactor(i18n): Replace paraglide with Intlayer
All checks were successful
ci/woodpecker/push/bun-verify Pipeline was successful
d4879b7fa5
chore: Bump verwion to 1.2.0
All checks were successful
ci/woodpecker/push/bun-verify Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
fe75131856
Member

Review Summary

This PR migrates i18n from paraglide-js to intlayer. The overall structure is sound and follows intlayer's documented Astro patterns: colocated *.content.ts dictionaries, prefix-no-default routing via src/pages/[...locale]/..., and getLocalizedUrl/getLocaleFromPath helpers used consistently. Key matching between getIntlayer(key, locale) calls and dictionary files looks correct.

However, I found one likely runtime-breaking bug and a couple of behavioral regressions that should be addressed before merge:

  1. Breadcrumb uses a non-existent Astro global (Astro.originPathname) — this is the most serious issue. intlayer's own Astro docs use Astro.url.pathname; Astro.originPathname is not a standard Astro global. If it is undefined, getPathWithoutLocale(undefined) will throw at runtime and crash the breadcrumb (rendered on every page via Layout.astro).
  2. Catch-all [...locale] routing regresses 404s — because getCurrentLocale falls back to the default locale and the rest-param route matches unknown single-segment paths, any bogus URL like /foo (or /foo/projects/x) now renders localized content instead of returning a 404. The old folder-based + middleware setup 404'd these.
  3. vite.ssr.external applied to all builds, not just dev — the comment justifies the externals with a dev-mode module-runner issue, but ssr.external also affects the production build; combined with noExternal: true it means the intlayer CJS packages must be resolvable from node_modules at runtime, which can break a bundled/standalone deployment.
  4. Sitemap/SEO regression — removing the Astro i18n config and not exporting getStaticPaths from the [...locale] pages means @astrojs/sitemap can no longer enumerate the nb/en variants or emit hreflang alternates.

Net: the migration is well-executed but the breadcrumb crash and the silent 404 regression need fixes.


*🤖 Review by opencode (opencode/hy3-free) — verdict: changes

# Review Summary This PR migrates i18n from paraglide-js to intlayer. The overall structure is sound and follows intlayer's documented Astro patterns: colocated `*.content.ts` dictionaries, `prefix-no-default` routing via `src/pages/[...locale]/...`, and `getLocalizedUrl`/`getLocaleFromPath` helpers used consistently. Key matching between `getIntlayer(key, locale)` calls and dictionary files looks correct. However, I found one likely **runtime-breaking bug** and a couple of **behavioral regressions** that should be addressed before merge: 1. **Breadcrumb uses a non-existent Astro global (`Astro.originPathname`)** — this is the most serious issue. intlayer's own Astro docs use `Astro.url.pathname`; `Astro.originPathname` is not a standard Astro global. If it is `undefined`, `getPathWithoutLocale(undefined)` will throw at runtime and crash the breadcrumb (rendered on every page via `Layout.astro`). 2. **Catch-all `[...locale]` routing regresses 404s** — because `getCurrentLocale` falls back to the default locale and the rest-param route matches unknown single-segment paths, any bogus URL like `/foo` (or `/foo/projects/x`) now renders localized content instead of returning a 404. The old folder-based + middleware setup 404'd these. 3. **`vite.ssr.external` applied to all builds, not just dev** — the comment justifies the externals with a dev-mode module-runner issue, but `ssr.external` also affects the production build; combined with `noExternal: true` it means the intlayer CJS packages must be resolvable from `node_modules` at runtime, which can break a bundled/standalone deployment. 4. **Sitemap/SEO regression** — removing the Astro `i18n` config and not exporting `getStaticPaths` from the `[...locale]` pages means `@astrojs/sitemap` can no longer enumerate the `nb`/`en` variants or emit hreflang alternates. Net: the migration is well-executed but the breadcrumb crash and the silent 404 regression need fixes. --- *🤖 Review by opencode (opencode/hy3-free) — verdict: **changes** <!-- forgejo-opencode-review -->
fix reloading if language is already selected
All checks were successful
ci/woodpecker/push/bun-verify Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
7252f58203
clank-bot left a comment

Code review findings from opencode.

Code review findings from opencode. <!-- forgejo-opencode-review -->
@ -29,22 +25,28 @@ export default defineConfig({
sitemap(),
Member
[style] Removing the Astro `i18n` config (which previously fed `@astrojs/sitemap` with the `nb`/`en` variants) and not exporting `getStaticPaths` from the `[...locale]` pages means the sitemap can no longer enumerate localized URLs or emit hreflang alternates — a silent SEO regression. If localized sitemaps matter, provide a `getStaticPaths`/locale list or another mechanism to generate the `nb`/`en` URLs.
<!-- forgejo-opencode-review --> [style] Removing the Astro `i18n` config (which previously fed `@astrojs/sitemap` with the `nb`/`en` variants) and not exporting `getStaticPaths` from the `[...locale]` pages means the sitemap can no longer enumerate localized URLs or emit hreflang alternates — a silent SEO regression. If localized sitemaps matter, provide a `getStaticPaths`/locale list or another mechanism to generate the `nb`/`en` URLs.
astro.config.ts Outdated
@ -47,1 +40,4 @@
noExternal: true,
// Intlayer's file watcher (fast-glob/picomatch) is CJS-only and breaks
// when routed through Vite's dev-mode SSR module runner; keep it external.
external: [
Member
[bug] `vite.ssr.external` is applied to the production build as well as dev/serve, even though the comment only motivates it for a dev-mode SSR module-runner issue. Combined with `noExternal: true` (whose purpose is to inline everything into the standalone `/dist`), externalizing `@intlayer/engine`, `@intlayer/config`, `@parcel/watcher`, `picomatch`, and `fast-glob` means they must be resolvable from `node_modules` at runtime. If the deployment ever ships only the bundled output without `node_modules`, this will throw `Cannot find module`. Scope the externals to dev/serve only (e.g. conditional config or `apply: 'serve'`) so production keeps inlining.
<!-- forgejo-opencode-review --> [bug] `vite.ssr.external` is applied to the production build as well as dev/serve, even though the comment only motivates it for a dev-mode SSR module-runner issue. Combined with `noExternal: true` (whose purpose is to inline everything into the standalone `/dist`), externalizing `@intlayer/engine`, `@intlayer/config`, `@parcel/watcher`, `picomatch`, and `fast-glob` means they must be resolvable from `node_modules` at runtime. If the deployment ever ships only the bundled output without `node_modules`, this will throw `Cannot find module`. Scope the externals to dev/serve only (e.g. conditional config or `apply: 'serve'`) so production keeps inlining.
@ -4,3 +4,3 @@
import type { NavLink } from "@/utils/linking"
const pathname = deLocalizeHref(Astro.originPathname)
const pathname = getPathWithoutLocale(Astro.originPathname)
Member
[bug] `getPathWithoutLocale(Astro.originPathname)` — `Astro.originPathname` does not appear to be a standard Astro global (intlayer's own Astro docs use `Astro.url.pathname`). If it's `undefined`, `getPathWithoutLocale(undefined)` will throw at runtime and crash the breadcrumb, which is rendered on every page through `Layout.astro`. Replace with `Astro.url.pathname` (and verify `originPathname` actually exists in your Astro version).
<!-- forgejo-opencode-review --> [bug] `getPathWithoutLocale(Astro.originPathname)` — `Astro.originPathname` does not appear to be a standard Astro global (intlayer's own Astro docs use `Astro.url.pathname`). If it's `undefined`, `getPathWithoutLocale(undefined)` will throw at runtime and crash the breadcrumb, which is rendered on every page through `Layout.astro`. Replace with `Astro.url.pathname` (and verify `originPathname` actually exists in your Astro version).
@ -0,0 +5,4 @@
import { getCurrentLocale } from "@/utils/locale"
import "@/styles/global.css"
const locale = getCurrentLocale(Astro.url.pathname)
Member
[bug] The `[...locale]` rest-param catch-all combined with `getCurrentLocale`'s `?? defaultLocale` fallback means unknown single-segment paths (e.g. `/foo`, `/foo/projects/x`) silently render localized content instead of 404-ing. Previously (folder-based routing + middleware) these returned 404. Validate the locale segment against the configured locales before rendering — for `prefix-no-default` only `""` (default) and `"en"` are valid path prefixes — and return/redirect otherwise.
<!-- forgejo-opencode-review --> [bug] The `[...locale]` rest-param catch-all combined with `getCurrentLocale`'s `?? defaultLocale` fallback means unknown single-segment paths (e.g. `/foo`, `/foo/projects/x`) silently render localized content instead of 404-ing. Previously (folder-based routing + middleware) these returned 404. Validate the locale segment against the configured locales before rendering — for `prefix-no-default` only `""` (default) and `"en"` are valid path prefixes — and return/redirect otherwise.
@ -0,0 +5,4 @@
* default locale when the path doesn't resolve to a known locale.
*/
export function getCurrentLocale(pathname: string): LocalesValues {
return getLocaleFromPath(pathname) ?? defaultLocale
Member
[bug] `getLocaleFromPath(pathname) ?? defaultLocale` collapses two distinct cases — "no locale segment" (legitimate for the unprefixed default locale) and "an unrecognized segment" (should be a 404). Because this helper is used across many components (Layout, Footer, ProjectPage, Greeting, etc.), the silent default fallback masks the routing bug above. Consider validating the resolved segment before falling back, or returning `null` so callers can decide between "default locale" and "invalid path".
<!-- forgejo-opencode-review --> [bug] `getLocaleFromPath(pathname) ?? defaultLocale` collapses two distinct cases — "no locale segment" (legitimate for the unprefixed default locale) and "an unrecognized segment" (should be a 404). Because this helper is used across many components (Layout, Footer, ProjectPage, Greeting, etc.), the silent default fallback masks the routing bug above. Consider validating the resolved segment before falling back, or returning `null` so callers can decide between "default locale" and "invalid path".
fix: Language switching and e2e tests
Some checks failed
ci/woodpecker/pr/pr-review Pipeline failed
ci/woodpecker/push/bun-verify Pipeline was successful
4ded5a8cd2
refactor: Move page components into pages directory
Some checks failed
ci/woodpecker/pr/pr-review Pipeline failed
ci/woodpecker/push/bun-verify Pipeline was successful
8fb5117fb6
martin force-pushed refactor/i18n/intlayer from 8fb5117fb6
Some checks failed
ci/woodpecker/pr/pr-review Pipeline failed
ci/woodpecker/push/bun-verify Pipeline was successful
to c358d665ad
Some checks failed
ci/woodpecker/pr/pr-review Pipeline failed
ci/woodpecker/push/bun-verify Pipeline was successful
2026-09-18 17:47:15 +00:00
Compare
martin merged commit 0e7ecbf2b0 into master 2026-09-18 17:48:21 +00:00
martin deleted branch refactor/i18n/intlayer 2026-09-18 17:48:21 +00:00
martin referenced this pull request from a commit 2026-09-18 17:48:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Personal/martials.no!73
No description provided.