Re-applies PR #182's next-intl scaffold with defensive measures aimed
at the most-likely-but-unconfirmed causes of the 503 that hit prod
after that PR merged. PR #183 reverted #182 to restore service; this
branch carries the same i18n payload plus the safeguards below.
Defensive changes vs PR #182:
1. **`outputFileTracingRoot: __dirname`** in next.config.ts.
Builds were warning that Next.js inferred the workspace root as
the parent GHWebhosting directory (because of a stray lockfile
there). With the wrong root, file tracing for the production
artifact can include the wrong files or skip ours. Pinning the
root to the wacrm directory silences the warning and makes tracing
deterministic across local / CI / Hostinger.
2. **`outputFileTracingIncludes: { "/**/*": ["./messages/**/*.json"] }`**.
Belt-and-suspenders: explicitly force the i18n message dictionaries
into the production artifact even if static-import tracing somehow
misses them in Hostinger's build environment.
3. **Static locale import map in `src/i18n/request.ts`**.
Replaces `import(\`../../messages/\${locale}.json\`)` with an
explicit `{ en: () => import(...), es: ..., lt: ... }` map.
Template-literal dynamic imports are well-supported by Turbopack's
tracer but the static form leaves zero room for the tracer to
skip a locale.
4. **Try/catch around the intl middleware call**.
If `intlMiddleware(request)` ever throws at runtime, fall through
to auth-only handling and log the error rather than 503-ing every
page request behind a single bad call.
5. **Try/catch around `LOADERS[locale]()`** with English fallback.
Same idea: a missing locale bundle at runtime returns English
content instead of crashing the render path.
6. **`engines.node: ">=20.10.0"`** (was `>=20.0.0`). Next 16.2's
actual minimum. Tells Hostinger's Node.js app picker not to land
us on a too-old subversion.
Verified locally:
- npm run lint → 0 errors (same 18 pre-existing warnings)
- npm run typecheck → clean
- npm run test → 366 passing
- npm run build → 62 prerendered pages across en/es/lt; workspace
root warning gone; messages bundled into `.next/server/chunks/ssr/
messages_{en,es,lt}_json_[json]_cjs_*.js`
- npm start probes:
/ → 307 → /en
/en/login → 200
/es/login → 200
/lt/login → 200
/en/dashboard → 307 → /en/login (signed out)
/api/whatsapp/templates → 401
If 503 reappears in prod after this lands, the local repro is clean,
so the next step is hPanel build/runtime logs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces an internationalization layer so wacrm can ship in multiple
languages — initially en (canonical), es, lt (untranslated mirrors of
en, ready for translators).
Foundation
- next-intl ^4.13 (Next 16 + React 19 compatible)
- src/i18n/{routing,navigation,request}.ts — locale config, locale-aware
Link/router/redirect, server-side message loader
- next.config.ts wired with `createNextIntlPlugin`
- src/app/* moved under src/app/[locale]/ (excluding /api and globals.css)
- [locale]/layout.tsx is now the root layout: <html lang={locale}>,
hasLocale guard, setRequestLocale, NextIntlClientProvider,
generateStaticParams for build-time prerendering of all locales
- middleware.ts composes next-intl's createMiddleware with the existing
Supabase auth check: intl middleware runs first to handle locale
prefix; auth redirects retain the active locale (e.g. signed-out user
on /es/dashboard → /es/login); intl cookies (NEXT_LOCALE) are carried
onto auth-issued responses so the sticky preference survives
Strings extracted (~80 keys across en/es/lt.json)
- common, nav (sidebar + header), roles (Owner/Admin/Agent/Viewer chips)
- auth.login, auth.signup, auth.forgotPassword (all forms + success states)
- join (all four UI states: loading, fail, accept, sign-up CTA + conflict modal)
- localeSwitcher
UI
- New `<LocaleSwitcher />` in the header — globe icon + locale dropdown
using next-intl's useLocale + locale-aware useRouter
Out of scope for this PR (intentional — keeps diff reviewable)
- Dashboard feature pages (dashboard, inbox, contacts, pipelines,
broadcasts, automations, flows, settings) and their components/
namespaces still have hardcoded English; they continue to render
fine because next/link redirects through the locale middleware
- Shared UI a11y strings (dialog close, etc.)
- Translating es.json / lt.json — they currently mirror en.json so the
app runs in any locale without crashes (English fallback)
- Renaming middleware.ts → proxy.ts per Next 16 deprecation
Verification
- npm run build → 62 static pages prerendered across en/es/lt
- npm run typecheck → clean
- npm run test → 366 passing
- npm run lint → 0 errors (18 pre-existing warnings untouched)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a catch-all rule in next.config.ts that merges a set of baseline
security headers into every response — served alongside the existing
Cache-Control rules, not instead of them.
Enforced today:
- Strict-Transport-Security: 2yr, includeSubDomains, preload-eligible.
No-op on http://localhost.
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY (same defense as frame-ancestors below, but
older browsers only read this one)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: deny camera/mic/geolocation/payment/usb. We
don't use any of those APIs; closing them off prevents a supply-
chain compromise or a forgotten plugin from silently enabling them.
Report-only:
- Content-Security-Policy-Report-Only with a pragmatic allowlist:
default-src 'self'
script-src 'self' 'unsafe-inline' 'unsafe-eval'
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob: https:
font-src 'self' data:
connect-src 'self' https://*.supabase.co wss://*.supabase.co
frame-ancestors 'none'
base-uri 'self'
form-action 'self'
'unsafe-inline' / 'unsafe-eval' are there because Next's inline
hydration script + dev-mode require them; nonce-based CSP is a
later project. Violations appear in the browser console but
nothing is blocked until we flip the header name to
`Content-Security-Policy`.
Verified against the running dev server:
- /, /docs, /login, /api/whatsapp/webhook, /_next/static/* all
return the six headers.
- No CSP violations on / or /docs in the browser console.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: Hostinger's CDN was applying s-maxage=31536000 (1 year)
to prerendered HTML pages by default. Each deploy produced fresh
Turbopack chunk hashes, but the edge kept serving year-old HTML that
referenced chunk filenames the new build no longer had on disk —
resulting in HTML 200 + every /_next/static/*.{js,css} 404, page
rendered unstyled. Private/incognito didn't help because the cache
is at the edge, not the browser.
Fix: explicit Cache-Control via next.config.ts headers():
- /_next/static/* — public, max-age=31536000, immutable
Content-hashed filenames; safe forever.
- /api/* — no-store
Per-user, must never be shared at the edge.
- everything else — public, max-age=0, s-maxage=300,
stale-while-revalidate=86400
Edge serves instantly from cache for 5 min, then keeps serving
cached content while refreshing in the background for up to 24h.
Chunk-hash drift after a deploy self-heals within ~5 min.
Verification after deploy:
curl -I https://<host>/login | grep -i cache-control
Expect: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
(If Hostinger overwrites this, fall back to configuring
purge-on-deploy at the platform layer.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>