diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx
new file mode 100644
index 000000000..3edc9f9a0
--- /dev/null
+++ b/frontend/src/app/[locale]/layout.tsx
@@ -0,0 +1,54 @@
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages, unstable_setRequestLocale } from "next-intl/server";
+
+import { GoogleAnalytics } from "@next/third-parties/google";
+import { PUBLIC_ENV } from "src/constants/environments";
+
+import Layout from "src/components/Layout";
+
+/**
+ * Root layout component, wraps all pages.
+ * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout
+ */
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`],
+};
+
+interface Props {
+ children: React.ReactNode;
+ params: {
+ locale: string;
+ };
+}
+
+const locales = ["en", "es"];
+
+export function generateStaticParams() {
+ return locales.map((locale) => ({ locale }));
+}
+
+export default async function LocaleLayout({ children, params }: Props) {
+ const { locale } = params;
+
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ // Providing all messages to the client
+ // side is the easiest way to get started
+ const messages = await getMessages();
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/frontend/src/app/[locale]/newsletter/confirmation/page.tsx b/frontend/src/app/[locale]/newsletter/confirmation/page.tsx
index e621b610a..5a9c2e2a0 100644
--- a/frontend/src/app/[locale]/newsletter/confirmation/page.tsx
+++ b/frontend/src/app/[locale]/newsletter/confirmation/page.tsx
@@ -8,7 +8,7 @@ import PageSEO from "src/components/PageSEO";
import BetaAlert from "src/components/BetaAlert";
import { useTranslations } from "next-intl";
import { Metadata } from "next";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -21,6 +21,7 @@ export async function generateMetadata() {
}
export default function NewsletterConfirmation() {
+ unstable_setRequestLocale("en");
const t = useTranslations("Newsletter_confirmation");
return (
diff --git a/frontend/src/app/[locale]/newsletter/page.tsx b/frontend/src/app/[locale]/newsletter/page.tsx
index 080aaed5a..f7b1be091 100644
--- a/frontend/src/app/[locale]/newsletter/page.tsx
+++ b/frontend/src/app/[locale]/newsletter/page.tsx
@@ -7,7 +7,7 @@ import PageSEO from "src/components/PageSEO";
import BetaAlert from "src/components/BetaAlert";
import NewsletterForm from "src/app/[locale]/newsletter/NewsletterForm";
import { Metadata } from "next";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
import {
useTranslations,
useMessages,
@@ -25,6 +25,7 @@ export async function generateMetadata() {
}
export default function Newsletter() {
+ unstable_setRequestLocale("en");
const t = useTranslations("Newsletter");
const messages = useMessages();
diff --git a/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx b/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx
index 9a095211a..925f7f67f 100644
--- a/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx
+++ b/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx
@@ -8,7 +8,7 @@ import PageSEO from "src/components/PageSEO";
import BetaAlert from "src/components/BetaAlert";
import { useTranslations } from "next-intl";
import { Metadata } from "next";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -21,6 +21,7 @@ export async function generateMetadata() {
}
export default function NewsletterUnsubscribe() {
+ unstable_setRequestLocale("en");
const t = useTranslations("Newsletter_unsubscribe");
return (
diff --git a/frontend/src/app/[locale]/page.tsx b/frontend/src/app/[locale]/page.tsx
index 795c8d84d..07e4a7806 100644
--- a/frontend/src/app/[locale]/page.tsx
+++ b/frontend/src/app/[locale]/page.tsx
@@ -5,7 +5,7 @@ import IndexGoalContent from "src/components/content/IndexGoalContent";
import ProcessAndResearchContent from "src/components/content/ProcessAndResearchContent";
import { Metadata } from "next";
import { useTranslations } from "next-intl";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -17,6 +17,8 @@ export async function generateMetadata() {
}
export default function Home() {
+ unstable_setRequestLocale("en");
+
const t = useTranslations("Index");
return (
diff --git a/frontend/src/app/[locale]/process/page.tsx b/frontend/src/app/[locale]/process/page.tsx
index 788627afa..eb5050e43 100644
--- a/frontend/src/app/[locale]/process/page.tsx
+++ b/frontend/src/app/[locale]/process/page.tsx
@@ -9,7 +9,7 @@ import ProcessIntro from "src/app/[locale]/process/ProcessIntro";
import ProcessInvolved from "src/app/[locale]/process/ProcessInvolved";
import ProcessMilestones from "src/app/[locale]/process/ProcessMilestones";
import { useTranslations } from "next-intl";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -21,6 +21,7 @@ export async function generateMetadata() {
}
export default function Process() {
+ unstable_setRequestLocale("en");
const t = useTranslations("Process");
return (
diff --git a/frontend/src/app/[locale]/research/page.tsx b/frontend/src/app/[locale]/research/page.tsx
index b6fc95076..2599e5402 100644
--- a/frontend/src/app/[locale]/research/page.tsx
+++ b/frontend/src/app/[locale]/research/page.tsx
@@ -10,7 +10,7 @@ import ResearchMethodology from "src/app/[locale]/research/ResearchMethodology";
import ResearchThemes from "src/app/[locale]/research/ResearchThemes";
import { Metadata } from "next";
import { useTranslations } from "next-intl";
-import { getTranslations } from "next-intl/server";
+import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -22,6 +22,8 @@ export async function generateMetadata() {
}
export default function Research() {
+ unstable_setRequestLocale("en");
+
const t = useTranslations("Research");
return (
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index e45e82c42..e914be1af 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -1,44 +1,10 @@
import "src/styles/styles.scss";
-import { GoogleAnalytics } from "@next/third-parties/google";
-import { PUBLIC_ENV } from "src/constants/environments";
+import { ReactNode } from "react";
-import Layout from "src/components/Layout";
-import { unstable_setRequestLocale } from "next-intl/server";
-/**
- * Root layout component, wraps all pages.
- * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout
- */
-import { Metadata } from "next";
-
-export const metadata: Metadata = {
- icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`],
+type Props = {
+ children: ReactNode;
};
-interface LayoutProps {
- children: React.ReactNode;
- params: {
- locale: string;
- };
-}
-
-export default function RootLayout({ children, params }: LayoutProps) {
- // Hardcoded until the [locale] routing is enabled.
- const locale = params.locale ? params.locale : "en";
- // TODO: Remove when https://github.com/amannn/next-intl/issues/663 lands.
- unstable_setRequestLocale(locale);
-
- return (
-
-
-
-
-
- {/* Separate layout component for the inner-body UI elements since Storybook
- and tests trip over the fact that this file renders an tag */}
-
- {/* TODO: Add locale="english" prop when ready for i18n */}
- {children}
-
-
- );
+export default function RootLayout({ children }: Props) {
+ return children;
}
diff --git a/frontend/src/app/template.tsx b/frontend/src/app/template.tsx
deleted file mode 100644
index b7250859f..000000000
--- a/frontend/src/app/template.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { sendGAEvent } from "@next/third-parties/google";
-import { PUBLIC_ENV } from "src/constants/environments";
-
-export default function Template({ children }: { children: React.ReactNode }) {
- const isProd = process.env.NODE_ENV === "production";
-
- useEffect(() => {
- isProd &&
- PUBLIC_ENV.GOOGLE_ANALYTICS_ID &&
- sendGAEvent("event", "page_view");
- }, [isProd]);
-
- return {children}
;
-}
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 9cce89c04..54c6290af 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -6,6 +6,7 @@ import {
useMessages,
NextIntlClientProvider,
} from "next-intl";
+import { unstable_setRequestLocale } from "next-intl/server";
import pick from "lodash/pick";
type Props = {
@@ -14,6 +15,8 @@ type Props = {
};
export default function Layout({ children, locale }: Props) {
+ unstable_setRequestLocale(locale);
+
const t = useTranslations();
const messages = useMessages();
diff --git a/frontend/tests/components/Layout.test.tsx b/frontend/tests/components/Layout.test.tsx
deleted file mode 100644
index e9d06997f..000000000
--- a/frontend/tests/components/Layout.test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { render, screen } from "tests/react-utils";
-import { axe } from "jest-axe";
-
-import Layout from "src/components/Layout";
-
-describe("AppLayout", () => {
- it("renders children in main section", () => {
- render(
-
- child
- ,
- );
-
- const header = screen.getByRole("heading", { name: /child/i, level: 1 });
-
- expect(header).toBeInTheDocument();
- });
-
- it("passes accessibility scan", async () => {
- const { container } = render(
-
- child
- ,
- );
- const results = await axe(container);
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/index.test.tsx b/frontend/tests/pages/index.test.tsx
deleted file mode 100644
index 6924cbbd0..000000000
--- a/frontend/tests/pages/index.test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { waitFor, screen, render } from "tests/react-utils";
-
-import { axe } from "jest-axe";
-import Index from "src/app/[locale]/page";
-
-describe("Index", () => {
- it("renders alert with grants.gov link", () => {
- render();
-
- const alert = screen.getByTestId("alert");
- const link = screen.getByRole("link", { name: /grants\.gov/i });
-
- expect(alert).toBeInTheDocument();
- expect(link).toHaveAttribute("href", "https://www.grants.gov");
- });
-
- it("renders the goals section", () => {
- render();
-
- const goalH2 = screen.getByRole("heading", {
- level: 2,
- name: /The goal?/i,
- });
-
- expect(goalH2).toBeInTheDocument();
- });
-
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/newsletter/confirmation.test.tsx b/frontend/tests/pages/newsletter/confirmation.test.tsx
deleted file mode 100644
index 7324e9c7c..000000000
--- a/frontend/tests/pages/newsletter/confirmation.test.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { render, waitFor } from "tests/react-utils";
-import { axe } from "jest-axe";
-import NewsletterConfirmation from "src/app/[locale]/newsletter/confirmation/page";
-
-describe("Newsletter", () => {
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/newsletter/index.test.tsx b/frontend/tests/pages/newsletter/index.test.tsx
deleted file mode 100644
index 9f430ef2b..000000000
--- a/frontend/tests/pages/newsletter/index.test.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { fireEvent, render, screen, waitFor } from "tests/react-utils";
-
-import userEvent from "@testing-library/user-event";
-import { axe } from "jest-axe";
-import Newsletter from "src/app/[locale]/newsletter/page";
-
-import { useRouter } from "next/navigation";
-
-jest.mock("next/navigation");
-
-describe("Newsletter", () => {
- it("renders signup form with a submit button", () => {
- render();
-
- const sendyform = screen.getByTestId("sendy-form");
-
- expect(sendyform).toBeInTheDocument();
- });
-
- it("submits the form successfully", async () => {
- const mockRouter = {
- push: jest.fn(), // the component uses `router.push` only
- };
- (useRouter as jest.Mock).mockReturnValue(mockRouter);
- render();
-
- // Mock the fetch function to return a successful response
- global.fetch = jest.fn().mockResolvedValue({
- ok: true,
- json: jest.fn().mockResolvedValue({ message: "Success" }),
- });
-
- // Fill out the form
- await userEvent.type(screen.getByLabelText(/First Name/i), "John");
- await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe");
- await userEvent.type(
- screen.getByLabelText(/Email/i),
- "john.doe@example.com",
- );
-
- // Submit the form
- fireEvent.click(screen.getByRole("button", { name: /subscribe/i }));
-
- // Wait for the form submission
- await waitFor(() => {
- expect(mockRouter.push).toHaveBeenCalledWith(
- "/newsletter/confirmation/?sendy=Success",
- );
- });
- });
-
- it("shows alert when recieving an error from Sendy", async () => {
- render();
-
- // Mock the fetch function to return a successful response
- global.fetch = jest.fn().mockResolvedValue({
- ok: false,
- json: jest.fn().mockResolvedValue({ error: "Already subscribed." }),
- });
- jest.spyOn(global.console, "error");
-
- // Fill out the form
- await userEvent.type(screen.getByLabelText(/First Name/i), "John");
- await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe");
- await userEvent.type(
- screen.getByLabelText(/Email/i),
- "john.doe@example.com",
- );
-
- // Submit the form
- fireEvent.click(screen.getByRole("button", { name: /subscribe/i }));
-
- // Wait for the form submission
- await waitFor(() => {
- const alert = screen.getByRole("heading", {
- level: 3,
- name: /You’re already signed up!?/i,
- });
- expect(alert).toBeInTheDocument();
- });
- await waitFor(() => {
- expect(console.error).toHaveBeenCalledWith(
- "client error",
- "Already subscribed.",
- );
- });
- });
-
- it("prevents the form from submitting when incomplete", async () => {
- render();
-
- // Fill out the form
- await userEvent.type(screen.getByLabelText(/First Name/i), "John");
- await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe");
-
- // Submit the form
- fireEvent.click(screen.getByRole("button", { name: /subscribe/i }));
-
- // Wait for the form submission
- await waitFor(() => {
- expect(screen.getByText("Enter your email address.")).toBeInTheDocument();
- });
- });
-
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/newsletter/unsubscribe.test.tsx b/frontend/tests/pages/newsletter/unsubscribe.test.tsx
deleted file mode 100644
index b544f941f..000000000
--- a/frontend/tests/pages/newsletter/unsubscribe.test.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { render, waitFor } from "tests/react-utils";
-import { axe } from "jest-axe";
-import NewsletterUnsubscribe from "src/app/[locale]/newsletter/unsubscribe/page";
-
-describe("Newsletter", () => {
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/process.test.tsx b/frontend/tests/pages/process.test.tsx
deleted file mode 100644
index bd9d43929..000000000
--- a/frontend/tests/pages/process.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { render, screen, waitFor } from "tests/react-utils";
-
-import { axe } from "jest-axe";
-import Process from "src/app/[locale]/process/page";
-
-describe("Process", () => {
- it("renders alert with grants.gov link", () => {
- render();
-
- const alert = screen.getByTestId("alert");
- // There are multiple links to grants.gov
- const link = screen.getAllByText("www.grants.gov")[0];
-
- expect(alert).toBeInTheDocument();
- expect(link).toHaveAttribute("href", "https://www.grants.gov");
- });
-
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/research.test.tsx b/frontend/tests/pages/research.test.tsx
deleted file mode 100644
index c4b55fed6..000000000
--- a/frontend/tests/pages/research.test.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { render, screen, waitFor } from "tests/react-utils";
-
-import { axe } from "jest-axe";
-import Research from "src/app/[locale]/research/page";
-
-describe("Research", () => {
- it("renders alert with grants.gov link", () => {
- render();
-
- const alert = screen.getByTestId("alert");
- const link = screen.getByRole("link", { name: /www\.grants\.gov/i });
-
- expect(alert).toBeInTheDocument();
- expect(link).toHaveAttribute("href", "https://www.grants.gov");
- });
-
- it("passes accessibility scan", async () => {
- const { container } = render();
- const results = await waitFor(() => axe(container));
-
- expect(results).toHaveNoViolations();
- });
-});
diff --git a/frontend/tests/pages/search.test.tsx b/frontend/tests/pages/search.test.tsx
deleted file mode 100644
index db4550732..000000000
--- a/frontend/tests/pages/search.test.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable jest/no-commented-out-tests */
-
-import { useFeatureFlags } from "src/hooks/useFeatureFlags";
-
-jest.mock("src/hooks/useFeatureFlags");
-
-const setFeatureFlag = (flag: string, value: boolean) => {
- (useFeatureFlags as jest.Mock).mockReturnValue({
- featureFlagsManager: {
- isFeatureEnabled: jest.fn((featureName: string) =>
- featureName === flag ? value : false,
- ),
- },
- mounted: true,
- });
-};
-
-const mockData = [
- {
- agency: "firstagency",
- category: "firstcategory",
- opportunity_title: "firsttitle",
- },
- {
- agency: "secondagency2",
- category: "secondcategory",
- opportunity_title: "secondtitle",
- },
-];
-
-// Mock both search fetchers in case we switch
-// Could also switch on a feature flag
-jest.mock("../../src/services/search/searchfetcher/APISearchFetcher", () => {
- return {
- APISearchFetcher: jest.fn().mockImplementation(() => {
- return {
- fetchOpportunities: jest.fn().mockImplementation(() => {
- return Promise.resolve(mockData);
- }),
- };
- }),
- };
-});
-
-jest.mock("../../src/services/search/searchfetcher/MockSearchFetcher", () => {
- return {
- MockSearchFetcher: jest.fn().mockImplementation(() => {
- return {
- fetchOpportunities: jest.fn().mockImplementation(() => {
- return Promise.resolve(mockData);
- }),
- };
- }),
- };
-});
-
-describe("Search", () => {
- it("should pass", () => {
- setFeatureFlag("showSearchV0", true);
- expect(1).toBe(1);
- });
- // TODO (Issue #1393): Redo tests after converting search to server component. Save below for reference
-
- // it("passes accessibility scan", async () => {
- // setFeatureFlag("showSearchV0", true);
- // const { container } = render();
- // const results = await waitFor(() => axe(container));
- // expect(results).toHaveNoViolations();
- // });
-
- // describe("Search feature flag", () => {
- // it("renders search results when feature flag is on", async () => {
- // setFeatureFlag("showSearchV0", true);
- // render();
- // fireEvent.click(screen.getByRole("button", { name: /update results/i }));
-
- // await waitFor(() => {
- // expect(screen.getByText(/firstcategory/i)).toBeInTheDocument();
- // });
- // expect(screen.getByText(/secondcategory/i)).toBeInTheDocument();
- // });
-
- // it("renders PageNotFound when feature flag is off", async () => {
- // setFeatureFlag("showSearchV0", false);
- // render();
-
- // await waitFor(() => {
- // expect(
- // screen.getByText(
- // /The page you have requested cannot be displayed because it does not exist, has been moved/i,
- // ),
- // ).toBeInTheDocument();
- // });
- // });
- // });
-});