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 { playerQueries } from "@/features/players/queries";
|
||||
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 FullScreenLoader from "@/components/full-screen-loader";
|
||||
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
||||
@@ -132,7 +138,16 @@ export const Route = createRootRouteWithContext<{
|
||||
context.queryClient,
|
||||
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 };
|
||||
} catch (error: any) {
|
||||
if (isRedirect(error) || error instanceof Response) throw error;
|
||||
|
||||
+19
-1
@@ -32,6 +32,8 @@ export function resolveLocale(candidate: string | null | undefined): AppLocale {
|
||||
: DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
const inflightLoads: Partial<Record<AppLocale, Promise<Messages>>> = {};
|
||||
|
||||
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
||||
const cached = messageCache[locale];
|
||||
if (cached) return cached;
|
||||
@@ -39,11 +41,27 @@ export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
||||
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
||||
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;
|
||||
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 {
|
||||
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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 type { Messages } from "@lingui/core";
|
||||
import { playerQueries } from "@/features/players/queries";
|
||||
import {
|
||||
createI18n,
|
||||
ensureMessages,
|
||||
hasCachedMessages,
|
||||
i18nMessagesQueryKey,
|
||||
primeMessages,
|
||||
resolveLocale,
|
||||
} from "@/lib/i18n";
|
||||
|
||||
@@ -29,27 +32,49 @@ export const LinguiProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const locale = resolveLocale(data?.metadata?.locale);
|
||||
const [catalogVersion, setCatalogVersion] = useState(0);
|
||||
|
||||
// Fresh instance per locale (and per SSR request tree). __root beforeLoad
|
||||
// preloads the catalog on authed routes; on public routes (login) it may
|
||||
// not be cached yet — createI18n falls back to en until the effect below
|
||||
// loads it and bumps catalogVersion.
|
||||
// dehydrates the active locale's catalog with the page, so hydration primes
|
||||
// it from the query cache and never regresses to en for the SSR'd locale.
|
||||
// Public routes (login) may still lack it — createI18n falls back to en
|
||||
// until the effect below loads it and bumps catalogVersion.
|
||||
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
|
||||
[locale, catalogVersion]
|
||||
[locale, catalogVersion, queryClient]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCachedMessages(locale)) return;
|
||||
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) retryTimer = setTimeout(load, 3000);
|
||||
}
|
||||
);
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user