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:
co-authored by
Claude Opus 4.6
parent
db528c3652
commit
99bc462315
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user