Adding: themes, predictions, match reporting, ui fixes, sse fixes, badge fixes, match flow, i18n, match edits, perf, auth fix #22
@@ -18,7 +18,13 @@ import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
|
|||||||
import { HeaderConfig } from "@/features/core/types/header-config";
|
import { HeaderConfig } from "@/features/core/types/header-config";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { DEFAULT_LOCALE, ensureMessages, resolveLocale } from "@/lib/i18n";
|
import {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
|
ensureMessages,
|
||||||
|
getCachedMessages,
|
||||||
|
i18nMessagesQueryKey,
|
||||||
|
resolveLocale,
|
||||||
|
} from "@/lib/i18n";
|
||||||
import { getRootMeta } from "@/lib/i18n/meta";
|
import { getRootMeta } from "@/lib/i18n/meta";
|
||||||
import FullScreenLoader from "@/components/full-screen-loader";
|
import FullScreenLoader from "@/components/full-screen-loader";
|
||||||
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
||||||
@@ -132,7 +138,16 @@ export const Route = createRootRouteWithContext<{
|
|||||||
context.queryClient,
|
context.queryClient,
|
||||||
playerQueries.auth()
|
playerQueries.auth()
|
||||||
);
|
);
|
||||||
await ensureMessages(resolveLocale(auth?.metadata?.locale));
|
const locale = resolveLocale(auth?.metadata?.locale);
|
||||||
|
await ensureMessages(locale);
|
||||||
|
// Dehydrate the catalog with the page so the client hydrates in the
|
||||||
|
// active locale instead of falling back to en until a chunk loads.
|
||||||
|
if (typeof window === "undefined" && locale !== DEFAULT_LOCALE) {
|
||||||
|
context.queryClient.setQueryData(
|
||||||
|
i18nMessagesQueryKey(locale),
|
||||||
|
getCachedMessages(locale)
|
||||||
|
);
|
||||||
|
}
|
||||||
return { auth };
|
return { auth };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (isRedirect(error) || error instanceof Response) throw error;
|
if (isRedirect(error) || error instanceof Response) throw error;
|
||||||
|
|||||||
+19
-1
@@ -32,6 +32,8 @@ export function resolveLocale(candidate: string | null | undefined): AppLocale {
|
|||||||
: DEFAULT_LOCALE;
|
: DEFAULT_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inflightLoads: Partial<Record<AppLocale, Promise<Messages>>> = {};
|
||||||
|
|
||||||
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
||||||
const cached = messageCache[locale];
|
const cached = messageCache[locale];
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
@@ -39,11 +41,27 @@ export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
|||||||
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
||||||
if (!loader) return messageCache[DEFAULT_LOCALE]!;
|
if (!loader) return messageCache[DEFAULT_LOCALE]!;
|
||||||
|
|
||||||
const { messages } = await loader();
|
// Dedupe concurrent loads; a rejected load is dropped so a retry can succeed.
|
||||||
|
const pending = (inflightLoads[locale] ??= loader()
|
||||||
|
.then(({ messages }) => {
|
||||||
messageCache[locale] = messages;
|
messageCache[locale] = messages;
|
||||||
return messages;
|
return messages;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
delete inflightLoads[locale];
|
||||||
|
}));
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function primeMessages(locale: AppLocale, messages: Messages) {
|
||||||
|
messageCache[locale] ??= messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seeded into the SSR-dehydrated query cache so hydration renders the active
|
||||||
|
// locale without depending on a client-side catalog chunk load.
|
||||||
|
export const i18nMessagesQueryKey = (locale: AppLocale) =>
|
||||||
|
["i18n-messages", locale] as const;
|
||||||
|
|
||||||
export function getCachedMessages(locale: AppLocale): Messages {
|
export function getCachedMessages(locale: AppLocale): Messages {
|
||||||
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
|
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { I18nProvider } from "@lingui/react";
|
import { I18nProvider } from "@lingui/react";
|
||||||
|
import type { Messages } from "@lingui/core";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import {
|
import {
|
||||||
createI18n,
|
createI18n,
|
||||||
ensureMessages,
|
ensureMessages,
|
||||||
hasCachedMessages,
|
hasCachedMessages,
|
||||||
|
i18nMessagesQueryKey,
|
||||||
|
primeMessages,
|
||||||
resolveLocale,
|
resolveLocale,
|
||||||
} from "@/lib/i18n";
|
} from "@/lib/i18n";
|
||||||
|
|
||||||
@@ -29,27 +32,49 @@ export const LinguiProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const locale = resolveLocale(data?.metadata?.locale);
|
const locale = resolveLocale(data?.metadata?.locale);
|
||||||
const [catalogVersion, setCatalogVersion] = useState(0);
|
const [catalogVersion, setCatalogVersion] = useState(0);
|
||||||
|
|
||||||
// Fresh instance per locale (and per SSR request tree). __root beforeLoad
|
// Fresh instance per locale (and per SSR request tree). __root beforeLoad
|
||||||
// preloads the catalog on authed routes; on public routes (login) it may
|
// dehydrates the active locale's catalog with the page, so hydration primes
|
||||||
// not be cached yet — createI18n falls back to en until the effect below
|
// it from the query cache and never regresses to en for the SSR'd locale.
|
||||||
// loads it and bumps catalogVersion.
|
// Public routes (login) may still lack it — createI18n falls back to en
|
||||||
|
// until the effect below loads it and bumps catalogVersion.
|
||||||
const i18n = useMemo(
|
const i18n = useMemo(
|
||||||
() => createI18n(locale),
|
() => {
|
||||||
|
if (!hasCachedMessages(locale)) {
|
||||||
|
const seeded = queryClient.getQueryData<Messages>(
|
||||||
|
i18nMessagesQueryKey(locale)
|
||||||
|
);
|
||||||
|
if (seeded) primeMessages(locale, seeded);
|
||||||
|
}
|
||||||
|
return createI18n(locale);
|
||||||
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[locale, catalogVersion]
|
[locale, catalogVersion, queryClient]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasCachedMessages(locale)) return;
|
if (hasCachedMessages(locale)) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
ensureMessages(locale).then(() => {
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
// A failed chunk load must not strand the UI in en — retry until the
|
||||||
|
// catalog arrives or the locale changes.
|
||||||
|
const load = () => {
|
||||||
|
ensureMessages(locale).then(
|
||||||
|
() => {
|
||||||
if (!cancelled) setCatalogVersion((v) => v + 1);
|
if (!cancelled) setCatalogVersion((v) => v + 1);
|
||||||
});
|
},
|
||||||
|
() => {
|
||||||
|
if (!cancelled) retryTimer = setTimeout(load, 3000);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
load();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
};
|
};
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user