fix: prevent Chrome auto-translate from crashing onboarding (#65)

* fix: prevent Chrome auto-translate from crashing React during onboarding

Chrome auto-translate modifies DOM text nodes when it detects a Swedish
page (lang="sv") in a browser set to English. React does not expect
external DOM mutations and throws, crashing the entire component tree
into global-error.tsx on every step transition.

Add translate="no" and <meta name="google" content="notranslate"> to
suppress browser translation. Also fix timezone-unsafe date parsing in
fiscal period validation (new Date("YYYY-MM-DD") + getDate() returns
local-timezone values, shifting dates by -1 day in Western timezones).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add notranslate meta tag to global-error.tsx for consistency

Per review feedback — global-error.tsx renders its own <html> document,
so it needs the same <meta name="google" content="notranslate"> tag as
layout.tsx to fully suppress Chrome translation on error pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-20 15:11:44 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent db528c3652
commit 99bc462315
4 changed files with 38 additions and 26 deletions
+4 -1
View File
@@ -15,7 +15,10 @@ export default function GlobalError({
}, [error]);
return (
<html lang="sv">
<html lang="sv" translate="no">
<head>
<meta name="google" content="notranslate" />
</head>
<body>
<div className="flex min-h-screen items-center justify-center p-8">
<div className="text-center space-y-4">
+2 -1
View File
@@ -46,8 +46,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="sv" suppressHydrationWarning className={`${geistSans.variable} ${geistMono.variable} ${fraunces.variable}`}>
<html lang="sv" translate="no" suppressHydrationWarning className={`${geistSans.variable} ${geistMono.variable} ${fraunces.variable}`}>
<head>
<meta name="google" content="notranslate" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
<script
src="https://cdn.recapt.app/browser/glimt.js"
+10 -14
View File
@@ -13,7 +13,7 @@ import { InfoTooltip } from '@/components/ui/info-tooltip'
import { Loader2, ArrowRight, ArrowLeft, Check, CalendarDays } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/use-toast'
import { monthsBetween } from '@/lib/bookkeeping/validate-period-duration'
import { monthsBetween, parseDateParts } from '@/lib/bookkeeping/validate-period-duration'
import type { EntityType } from '@/types'
const schema = z.object({
@@ -68,11 +68,8 @@ const monthNames = [
]
function formatSwedishDate(dateStr: string): string {
const d = new Date(dateStr)
const day = d.getDate()
const month = monthNames[d.getMonth()].toLowerCase()
const year = d.getFullYear()
return `${day} ${month} ${year}`
const { year, month, day } = parseDateParts(dateStr)
return `${day} ${monthNames[month - 1].toLowerCase()} ${year}`
}
/**
@@ -180,28 +177,28 @@ export default function Step3TaxRegistration({
// State for first-year start date selectors (month/year)
const [startMonth, setStartMonth] = useState<number>(
initialData.first_year_start
? new Date(initialData.first_year_start).getMonth() + 1
? parseDateParts(initialData.first_year_start).month
: 0
)
const [startYear, setStartYear] = useState<number>(
initialData.first_year_start
? new Date(initialData.first_year_start).getFullYear()
? parseDateParts(initialData.first_year_start).year
: 0
)
// State for AB first-year end month selector
const [abEndMonth, setAbEndMonth] = useState<number>(
initialData.first_year_end
? new Date(initialData.first_year_end).getMonth() + 1
? parseDateParts(initialData.first_year_end).month
: 12
)
// Parse first year start for date computations
const parsedStart = useMemo(() => {
if (!firstYearStart) return null
const d = new Date(firstYearStart)
if (isNaN(d.getTime())) return null
return { year: d.getFullYear(), month: d.getMonth() + 1 }
const parts = parseDateParts(firstYearStart)
if (isNaN(parts.year) || isNaN(parts.month)) return null
return { year: parts.year, month: parts.month }
}, [firstYearStart])
// Compute end date options for first year
@@ -220,8 +217,7 @@ export default function Step3TaxRegistration({
if (data.is_first_fiscal_year && data.first_year_start && data.first_year_end) {
// Derive start month from end date
const endDate = new Date(data.first_year_end)
const endMonth = endDate.getMonth() + 1
const endMonth = parseDateParts(data.first_year_end).month
fiscalYearStartMonth = endMonth === 12 ? 1 : endMonth + 1
firstStart = data.first_year_start
firstEnd = data.first_year_end
+22 -10
View File
@@ -4,14 +4,25 @@
* Normal ongoing periods are 12 months.
*/
/**
* Parse a YYYY-MM-DD string into numeric parts without timezone issues.
* Using new Date(dateStr) is unsafe because it creates UTC midnight,
* but getDate()/getMonth()/getFullYear() return local-timezone values —
* shifting the date by -1 day in Western timezones.
*/
export function parseDateParts(dateStr: string): { year: number; month: number; day: number } {
const [year, month, day] = dateStr.split('-').map(Number)
return { year, month, day }
}
/**
* Calculate the number of months between two dates (inclusive of partial months).
* Assumes start is 1st of month and end is last of month.
*/
export function monthsBetween(start: string, end: string): number {
const s = new Date(start)
const e = new Date(end)
return (e.getFullYear() - s.getFullYear()) * 12 + (e.getMonth() - s.getMonth()) + 1
const s = parseDateParts(start)
const e = parseDateParts(end)
return (e.year - s.year) * 12 + (e.month - s.month) + 1
}
/**
@@ -19,22 +30,23 @@ export function monthsBetween(start: string, end: string): number {
* Returns null if valid, or an error message string if invalid.
*/
export function validatePeriodDuration(start: string, end: string): string | null {
const startDate = new Date(start)
const endDate = new Date(end)
const startParts = parseDateParts(start)
const endParts = parseDateParts(end)
// end must be after start
if (endDate <= startDate) {
// end must be after start (YYYY-MM-DD strings are lexicographically orderable)
if (end <= start) {
return 'Period end must be after period start'
}
// start must be 1st of month
if (startDate.getDate() !== 1) {
if (startParts.day !== 1) {
return 'Period start must be the 1st of a month'
}
// end must be last day of month
const lastDayOfEndMonth = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0).getDate()
if (endDate.getDate() !== lastDayOfEndMonth) {
// new Date(year, 1-indexed-month, 0) gives the last day of that month
const lastDayOfEndMonth = new Date(endParts.year, endParts.month, 0).getDate()
if (endParts.day !== lastDayOfEndMonth) {
return 'Period end must be the last day of a month'
}