Pass full locale from Accept-Language to ClientDate
Add detectLocale() that extracts the full locale tag (e.g. "de-DE") from the request, not just the language. The journal root loader passes it via LocaleContext so ClientDate renders with the correct locale on both server and client — no more hardcoded "en-US" fallback. Also fixes settings page hydration mismatch by using ClientDate for passkey creation dates instead of inline toLocaleDateString(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79f000fcc5
commit
38ada3bd4f
5 changed files with 54 additions and 9 deletions
|
|
@ -1,11 +1,16 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useLocale } from "./LocaleContext";
|
||||
|
||||
/**
|
||||
* Renders a date only on the client to avoid SSR hydration mismatches.
|
||||
* Server renders empty, client fills in with user's locale.
|
||||
* Renders a date using the server-detected locale for the initial render
|
||||
* (matching SSR output), then updates to the browser's native locale
|
||||
* after hydration.
|
||||
*/
|
||||
export function ClientDate({ iso }: { iso: string }) {
|
||||
const [formatted, setFormatted] = useState(new Date(iso).toLocaleDateString("en-US"));
|
||||
const locale = useLocale();
|
||||
const [formatted, setFormatted] = useState(
|
||||
new Date(iso).toLocaleDateString(locale),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFormatted(new Date(iso).toLocaleDateString());
|
||||
|
|
|
|||
17
apps/journal/app/components/LocaleContext.tsx
Normal file
17
apps/journal/app/components/LocaleContext.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
const LocaleContext = createContext<string>("en");
|
||||
|
||||
export function LocaleProvider({
|
||||
locale,
|
||||
children,
|
||||
}: {
|
||||
locale: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <LocaleContext value={locale}>{children}</LocaleContext>;
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return useContext(LocaleContext);
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ import type { LinksFunction } from "react-router";
|
|||
import type { Route } from "./+types/root";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { detectLocale } from "@trails-cool/i18n";
|
||||
import { getSessionUser } from "~/lib/auth.server";
|
||||
import { LocaleProvider } from "~/components/LocaleContext";
|
||||
import stylesheet from "@trails-cool/ui/styles.css?url";
|
||||
|
||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
|
||||
|
|
@ -33,7 +35,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const user = await getSessionUser(request);
|
||||
return { user: user ? { id: user.id, username: user.username } : null };
|
||||
const locale = detectLocale(request);
|
||||
return { user: user ? { id: user.id, username: user.username } : null, locale };
|
||||
}
|
||||
|
||||
function NavBar({ user }: { user: { id: string; username: string } | null }) {
|
||||
|
|
@ -110,6 +113,7 @@ function NavBar({ user }: { user: { id: string; username: string } | null }) {
|
|||
|
||||
export default function App({ loaderData }: Route.ComponentProps) {
|
||||
const user = loaderData?.user;
|
||||
const locale = loaderData?.locale ?? "en";
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
Sentry.setUser({ id: user.id, username: user.username });
|
||||
|
|
@ -119,10 +123,10 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
|||
}, [user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocaleProvider locale={locale}>
|
||||
<NavBar user={user ?? null} />
|
||||
<Outlet />
|
||||
</>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import { data, redirect, useFetcher } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClientDate } from "~/components/ClientDate";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Route } from "./+types/settings";
|
||||
import { getSessionUser } from "~/lib/auth.server";
|
||||
|
|
@ -217,9 +218,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
|||
{transportLabel(passkey.transports, t)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{t("settings.security.addedOn", {
|
||||
date: new Date(passkey.createdAt).toLocaleDateString(),
|
||||
})}
|
||||
{t("settings.security.addedOn", { date: "" })}
|
||||
<ClientDate iso={passkey.createdAt} />
|
||||
</p>
|
||||
</div>
|
||||
<deleteFetcher.Form
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue