diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 3b67f383ad..6a73624e72 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -258,6 +258,17 @@ const config = { filename: "static/chunks/[path][name].[hash][ext]" } }); + // en-US is the only eagerly bundled locale. Its FAQ articles (~17 KB gz, a + // quarter of the file) are only rendered by the FAQ surfaces, so the + // loader splits them out: the plain import gets the locale without them, + // `?faq` gets only them, loaded on demand by features/i18n/faq.ts (#1598). + // Applies to the server bundle too so SSR and client agree on what is + // eager. The JSON on disk stays whole for Crowdin. + config.module.rules.push({ + test: /[\\/]features[\\/]i18n[\\/]locales[\\/]en-US\.json$/, + type: "javascript/auto", + use: [{ loader: path.resolve(__dirname, "src/features/i18n/faq-split.js") }] + }); config.resolve.fallback = { ...config.resolve.fallback, fs: false diff --git a/apps/web/src/app/(staticPages)/about/page.tsx b/apps/web/src/app/(staticPages)/about/page.tsx index 6315d690ad..9d608eb84b 100644 --- a/apps/web/src/app/(staticPages)/about/page.tsx +++ b/apps/web/src/app/(staticPages)/about/page.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; import { blogSvg, discordSvg, githubSvg, mailSvg, newsSvg, telegramSvg, twitterSvg } from "@ui/svg"; import { Metadata, ResolvingMetadata } from "next"; import { PagesMetadataGenerator } from "@/features/metadata"; +import { ensureFaqLoaded } from "@/features/i18n"; export async function generateMetadata( props: unknown, @@ -16,7 +17,9 @@ export async function generateMetadata( return PagesMetadataGenerator.getForPage("about"); } -export default function About() { +export default async function About() { + // The FAQ headers below are not in the eager locale bundle (#1598). + await ensureFaqLoaded("en-US"); return ( <> diff --git a/apps/web/src/app/(staticPages)/faq/page.tsx b/apps/web/src/app/(staticPages)/faq/page.tsx index 26bb4ebaab..b7d82c5e80 100644 --- a/apps/web/src/app/(staticPages)/faq/page.tsx +++ b/apps/web/src/app/(staticPages)/faq/page.tsx @@ -12,7 +12,13 @@ import { } from "@/app/(staticPages)/faq/_components"; import { searchWithinFaq } from "@/app/(staticPages)/faq/utils"; import { Tsx } from "@/features/i18n/helper"; -import { NavigationLocaleWatcher } from "@/features/i18n"; +import { + NavigationLocaleWatcher, + ensureFaqLoaded, + getEnglishFaqResources, + langOptions +} from "@/features/i18n"; +import { FaqResources } from "@/features/i18n/faq-resources"; import { FaqSearchResult } from "@/app/(staticPages)/faq/_components/faq-search-result"; import { PagesMetadataGenerator } from "@/features/metadata"; @@ -32,6 +38,16 @@ interface Props { export default async function FAQ({ searchParams }: Props) { const params = await searchParams; + // The FAQ articles are not in the eager locale bundle (#1598). English is + // the fallback for every article and the language client components hydrate + // in, so it is always loaded and handed to them; a ?lang request (the same + // resolution NavigationLocaleWatcher uses) also loads that whole locale. + const requestedLang = langOptions.find( + (item) => item.code.split("-")[0] === params["lang"] + )?.code; + await Promise.all([ensureFaqLoaded("en-US"), requestedLang && ensureFaqLoaded(requestedLang)]); + const faqResources = getEnglishFaqResources(); + const searchResult = searchWithinFaq(params["q"] ?? ""); return ( @@ -40,6 +56,7 @@ export default async function FAQ({ searchParams }: Props) { + diff --git a/apps/web/src/app/decks/_components/columns/deck-faq-column.tsx b/apps/web/src/app/decks/_components/columns/deck-faq-column.tsx index 7cafb3b289..17e0b9f2f8 100644 --- a/apps/web/src/app/decks/_components/columns/deck-faq-column.tsx +++ b/apps/web/src/app/decks/_components/columns/deck-faq-column.tsx @@ -5,6 +5,7 @@ import { FormControl } from "@ui/input"; import { faqKeysGeneral } from "@/consts"; import i18next from "i18next"; import { articleSvg } from "@/assets/img/svg"; +import { useFaqTranslations } from "@/features/i18n/use-faq-translations"; interface Props { id: string; @@ -15,8 +16,11 @@ export const DeckFaqColumn = ({ id, draggable }: Props) => { const [expandedHelp, setExpandedHelp] = useState(true); const [searchText, setSearchText] = useState(""); const [dataToShow, setDataToShow] = useState([...faqKeysGeneral]); + // FAQ articles load on demand (#1598); the list renders once they are in. + const faqReady = useFaqTranslations(); useEffect(() => { + if (!faqReady) return; setDataToShow( faqKeysGeneral.filter((key) => i18next @@ -25,7 +29,7 @@ export const DeckFaqColumn = ({ id, draggable }: Props) => { .includes(searchText.toLocaleLowerCase()) ) ); - }, [searchText]); + }, [searchText, faqReady]); return ( { "" )}
- {dataToShow.map((x) => { + {faqReady && dataToShow.map((x) => { return (
{articleSvg}
diff --git a/apps/web/src/app/perks/points/_components/points-basic-info.tsx b/apps/web/src/app/perks/points/_components/points-basic-info.tsx index f359ff92f9..75925573f6 100644 --- a/apps/web/src/app/perks/points/_components/points-basic-info.tsx +++ b/apps/web/src/app/perks/points/_components/points-basic-info.tsx @@ -5,9 +5,12 @@ import { UilArrowLeft, UilSpinner } from "@tooni/iconscout-unicons-react"; import i18next from "i18next"; import Link from "next/link"; import { useActiveAccount } from "@/core/hooks/use-active-account"; +import { useFaqTranslations } from "@/features/i18n/use-faq-translations"; export function PointsBasicInfo() { const { activeUser } = useActiveAccount(); + // The explainer is a FAQ article, loaded on demand (#1598). + const faqReady = useFaqTranslations(); const { data: activeUserPoints, isPending } = useQuery( getPointsQueryOptions(activeUser?.username) ); @@ -27,7 +30,9 @@ export function PointsBasicInfo() {

{i18next.t("perks.points-title")}

{i18next.t("perks.points-description")}

-

+ {faqReady && ( +

+ )}

{i18next.t("redeem-common.balance")}:
diff --git a/apps/web/src/app/perks/promote-post/_components/promote-post-intro.tsx b/apps/web/src/app/perks/promote-post/_components/promote-post-intro.tsx index f96b5b2839..c7e7046268 100644 --- a/apps/web/src/app/perks/promote-post/_components/promote-post-intro.tsx +++ b/apps/web/src/app/perks/promote-post/_components/promote-post-intro.tsx @@ -3,19 +3,24 @@ import { Button } from "@/features/ui"; import { UilArrowRight } from "@tooni/iconscout-unicons-react"; import i18next from "i18next"; import Image from "next/image"; +import { useFaqTranslations } from "@/features/i18n/use-faq-translations"; interface Props { onContinue: () => void; } export function PromotePostIntro({ onContinue }: Props) { + // The explainer is a FAQ article, loaded on demand (#1598). + const faqReady = useFaqTranslations(); return (
-
+ {faqReady && ( +
+ )}