diff --git a/next.config.ts b/next.config.ts index a4e23e3..05e1db0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import createNextIntlPlugin from "next-intl/plugin"; import type { NextConfig } from "next"; @@ -57,6 +58,20 @@ const SECURITY_HEADERS = [ ] as const; const nextConfig: NextConfig = { + // Pin file-tracing to this directory so Next.js doesn't ascend to the + // parent GHWebhosting folder (which has its own lockfile) and either + // bundle the wrong stuff or skip ours. Was visible as a workspace-root + // warning during builds. Also see `outputFileTracingIncludes` below — + // a wrong root makes the includes match incorrectly. + outputFileTracingRoot: path.join(__dirname), + // Force the production artifact to include the i18n message dictionaries. + // `src/i18n/request.ts` loads them via a static import map but the + // tracer can still miss JSON-only files that no code statically + // imports relative to this entry point. Belt-and-suspenders. + outputFileTracingIncludes: { + "/**/*": ["./messages/**/*.json"], + }, + /** * Cache-Control policy. * diff --git a/package-lock.json b/package-lock.json index 5f1c8c1..70a16ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "vitest": "^4.1.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.10.0" } }, "node_modules/@alloc/quick-lru": { diff --git a/package.json b/package.json index 51a0069..88b662c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "template" ], "engines": { - "node": ">=20.0.0" + "node": ">=20.10.0" }, "scripts": { "dev": "next dev", diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 1757c7e..278e5e5 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -1,12 +1,35 @@ import { hasLocale } from 'next-intl'; import { getRequestConfig } from 'next-intl/server'; -import { routing } from './routing'; +import { routing, type Locale } from './routing'; + +// Static import map. Template-literal dynamic imports (e.g. +// `import(\`../../messages/\${locale}.json\`)`) can confuse Next.js's +// file tracer in some build environments — the JSONs may be missing +// from the production artifact even though they resolve in `next dev` +// and `next start` locally. Naming each path explicitly forces every +// supported locale's message file into the bundle. +const LOADERS: Record Promise<{ default: Record }>> = { + en: () => import('../../messages/en.json'), + es: () => import('../../messages/es.json'), + lt: () => import('../../messages/lt.json'), +}; export default getRequestConfig(async ({ requestLocale }) => { const requested = await requestLocale; - const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale; + const locale: Locale = hasLocale(routing.locales, requested) + ? requested + : routing.defaultLocale; - const messages = (await import(`../../messages/${locale}.json`)).default; + // Fall back to English if the requested locale's bundle is missing + // at runtime. A missing locale should display English content rather + // than 503 the whole render path. + let messages: Record; + try { + messages = (await LOADERS[locale]()).default; + } catch (err) { + console.error(`[i18n] failed to load messages for locale "${locale}", falling back to en:`, err); + messages = (await LOADERS[routing.defaultLocale]()).default; + } return { locale, diff --git a/src/middleware.ts b/src/middleware.ts index 5c90122..e680037 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -33,11 +33,22 @@ export async function middleware(request: NextRequest) { // routes don't live under [locale] and would be wrongly prefixed). // If next-intl redirects (missing locale), return immediately; // the redirected URL re-enters middleware with the prefix in place. + // + // The intlMiddleware call is wrapped because an unhandled throw + // here would cascade to a 503 in front of every page — we'd + // rather degrade to "no locale routing applied for this request" + // than take the whole site down. Errors are logged so they show + // up in Hostinger's process logs without being swallowed silently. let intlResponse: NextResponse | null = null if (!isApiRoute) { - intlResponse = intlMiddleware(request) - if (intlResponse.headers.get('location')) { - return intlResponse + try { + intlResponse = intlMiddleware(request) + if (intlResponse.headers.get('location')) { + return intlResponse + } + } catch (err) { + console.error('[middleware] intl middleware threw, falling through to auth-only:', err) + intlResponse = null } }