Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions src/app/internal-marketing/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import type { Metadata } from "next";
import { MarketingHomeView } from "#/features/homePage/ui/MarketingHomeView";
import type { Locales } from "#/i18n/i18n-types";
import { locales } from "#/i18n/i18n-util";
import { DEFAULT_SITE_DESCRIPTION } from "#/shared/lib/seo";

import { internalMarketingPilotMetadata } from "#/app/internal-marketing/internalMarketingPilotMetadata";

const homeTitle = "dStruct — visualize LeetCode solutions";
import { pilotPageMetadataFromTranslation } from "#/app/internal-marketing/pilotPageMetadata";

export async function generateMetadata({
params,
Expand All @@ -16,16 +13,17 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { locale: localeParam } = await params;
if (!locales.includes(localeParam as Locales)) {
return { title: homeTitle, robots: { index: false, follow: false } };
return pilotPageMetadataFromTranslation("en", "/", (translation) => ({
title: translation.SITE_SEO_TITLE,
description: translation.SITE_SEO_DESCRIPTION,
}));
}
const locale = localeParam as Locales;

return internalMarketingPilotMetadata({
locale,
pagePath: "/",
title: homeTitle,
description: DEFAULT_SITE_DESCRIPTION,
});
return pilotPageMetadataFromTranslation(locale, "/", (translation) => ({
title: translation.SITE_SEO_TITLE,
description: translation.SITE_SEO_DESCRIPTION,
}));
}

/**
Expand Down
81 changes: 81 additions & 0 deletions src/i18n/__tests__/getI18nProps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from "vitest";

import {
loadI18nServerProps,
localeFromContext,
withI18nServerSideProps,
} from "#/i18n/getI18nProps";

vi.mock("#/i18n/i18n-util.async", () => ({
importLocaleAsync: vi.fn(async (locale: string) => ({
SITE_SEO_TITLE: `title-${locale}`,
SITE_SEO_DESCRIPTION: `description-${locale}`,
})),
}));

describe("localeFromContext", () => {
it("prefers locale over defaultLocale", () => {
expect(localeFromContext({ locale: "de", defaultLocale: "en" })).toBe("de");
});

it("falls back to defaultLocale then en", () => {
expect(localeFromContext({ locale: undefined, defaultLocale: "fr" })).toBe(
"fr",
);
expect(localeFromContext({ locale: undefined, defaultLocale: undefined })).toBe(
"en",
);
});
});

describe("loadI18nServerProps", () => {
it("loads translations for the active locale", async () => {
const { i18n } = await loadI18nServerProps({
locale: "de",
defaultLocale: "en",
});

expect(i18n.translations.de).toEqual({
SITE_SEO_TITLE: "title-de",
SITE_SEO_DESCRIPTION: "description-de",
});
});
});

describe("withI18nServerSideProps", () => {
it("merges i18n into successful props", async () => {
const wrapped = withI18nServerSideProps(async () => ({
props: { value: 1 },
}));

const result = await wrapped({
locale: "es",
defaultLocale: "en",
} as Parameters<typeof wrapped>[0]);

expect(result).toEqual({
props: {
value: 1,
i18n: {
translations: {
es: {
SITE_SEO_TITLE: "title-es",
SITE_SEO_DESCRIPTION: "description-es",
},
},
},
},
});
});

it("passes through notFound without loading i18n", async () => {
const wrapped = withI18nServerSideProps(async () => ({ notFound: true }));

const result = await wrapped({
locale: "en",
defaultLocale: "en",
} as Parameters<typeof wrapped>[0]);

expect(result).toEqual({ notFound: true });
});
});
22 changes: 22 additions & 0 deletions src/i18n/createTranslationFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { i18nObject as initI18nObject } from "typesafe-i18n";

import { initFormatters } from "#/i18n/formatters";
import type {
Formatters,
Locales,
TranslationFunctions,
Translations,
} from "#/i18n/i18n-types";

/** Server-side `LL` helpers from a loaded locale dictionary (no React provider). */
export function createTranslationFunctions(
locale: Locales,
translation: Translations,
): TranslationFunctions {
return initI18nObject<
Locales,
Translations,
TranslationFunctions,
Formatters
>(locale, translation, initFormatters(locale));
}
6 changes: 6 additions & 0 deletions src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const en: BaseTranslation = {
PENDING_CHANGES: "Pending changes",
PLAYBACK_INTERVAL: "Playback interval",
PLAYGROUND: "Playground",
PLAYGROUND_PROJECT_SEO_DESCRIPTION:
"Practice {title:string} in dStruct — visualize solutions and run code in the browser.",
PLAYGROUND_SEO_TITLE: "Playground | dStruct",
PLEASE_ENTER_YOUR_LEETCODE_ACCOUNT_NAME: "Please enter your LeetCode account name:",
PRIVACY_CCPA_BODY:
"If you are a California resident, you may have additional rights under the CCPA/CPRA, including the right to know what personal information is collected and to opt out of certain sharing. dStruct does not sell personal information. Analytics run only with your consent. Contact us using the email below to exercise your rights.",
Expand Down Expand Up @@ -197,6 +200,9 @@ const en: BaseTranslation = {
SORT_CATEGORY: "Category",
SORT_CATEGORY_ASC: "Category (A-Z)",
SORT_CATEGORY_DESC: "Category (Z-A)",
SITE_SEO_DESCRIPTION:
"dStruct is a web app that helps you understand LeetCode problems. It allows you to visualize your solutions that you write in a built-in code editor.",
SITE_SEO_TITLE: "dStruct — visualize LeetCode solutions",
REPLAY: "Replay",
REPLAY_PREVIOUS_CODE_RESULT_VISUALIZATION: "Replay previous code result visualization",
RESET: "Reset",
Expand Down
58 changes: 54 additions & 4 deletions src/i18n/getI18nProps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { type GetStaticProps, type GetStaticPropsContext } from "next";
import type {
GetServerSideProps,
GetServerSidePropsContext,
GetStaticProps,
GetStaticPropsContext,
} from "next";

import type { Locales } from "#/i18n/i18n-types";
import { importLocaleAsync } from "#/i18n/i18n-util.async";
import { localePathForPage } from "#/i18n/localePathForPage";
import { SITE_ORIGIN } from "#/shared/lib/seo";
import { type TranslationDictionary } from "#/shared/ui/providers/I18nProvider";

type LocaleContext = Pick<
GetStaticPropsContext | GetServerSidePropsContext,
"locale" | "defaultLocale"
>;

/**
* Serializable i18n payload for static pages: locale dictionary loaded at build time
* for the active locale.
Expand Down Expand Up @@ -47,12 +57,19 @@ export function absoluteCanonicalFromStaticContext(
}

/**
* Loads translation bundle for the request locale (used by static page props).
* Active locale from Next.js static or server props context.
*/
export function localeFromContext(context: LocaleContext): Locales {
return (context.locale ?? context.defaultLocale ?? "en") as Locales;
}

/**
* Loads translation bundle for the request locale (static and server pages).
*/
async function loadI18nPageProps(
context: Pick<GetStaticPropsContext, "locale">,
context: LocaleContext,
): Promise<{ i18n: I18nProps }> {
const locale = (context.locale as Locales) || "en";
const locale = localeFromContext(context);
const translations = { [locale]: await importLocaleAsync(locale) };
return {
i18n: {
Expand All @@ -61,6 +78,39 @@ async function loadI18nPageProps(
};
}

/**
* `getServerSideProps` helper: provides `i18n.translations` for the active locale.
*/
export async function loadI18nServerProps(
context: LocaleContext,
): Promise<{ i18n: I18nProps }> {
return loadI18nPageProps(context);
}

/**
* Wraps `getServerSideProps` to merge `i18n.translations` into successful page props.
*/
export function withI18nServerSideProps<P extends Record<string, unknown>>(
handler: GetServerSideProps<P>,
): GetServerSideProps<P & { i18n: I18nProps }> {
return async (context) => {
const result = await handler(context);
if ("notFound" in result && result.notFound) {
return result;
}
if ("redirect" in result && result.redirect) {
return result;
}
const i18n = await loadI18nPageProps(context);
return {
props: {
...(result as { props: P }).props,
...i18n,
},
};
};
}

/**
* `getStaticProps` helper: provides `i18n.translations` for the active locale only.
*/
Expand Down
35 changes: 34 additions & 1 deletion src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'

export type BaseTranslation = BaseTranslationType
Expand Down Expand Up @@ -310,6 +310,15 @@ type RootTranslation = {
* P​l​a​y​g​r​o​u​n​d
*/
PLAYGROUND: string
/**
* P​r​a​c​t​i​c​e​ ​{​t​i​t​l​e​}​ ​i​n​ ​d​S​t​r​u​c​t​ ​—​ ​v​i​s​u​a​l​i​z​e​ ​s​o​l​u​t​i​o​n​s​ ​a​n​d​ ​r​u​n​ ​c​o​d​e​ ​i​n​ ​t​h​e​ ​b​r​o​w​s​e​r​.
* @param {string} title
*/
PLAYGROUND_PROJECT_SEO_DESCRIPTION: RequiredParams<'title'>
/**
* P​l​a​y​g​r​o​u​n​d​ ​|​ ​d​S​t​r​u​c​t
*/
PLAYGROUND_SEO_TITLE: string
/**
* P​l​e​a​s​e​ ​e​n​t​e​r​ ​y​o​u​r​ ​L​e​e​t​C​o​d​e​ ​a​c​c​o​u​n​t​ ​n​a​m​e​:
*/
Expand Down Expand Up @@ -734,6 +743,14 @@ type RootTranslation = {
* C​a​t​e​g​o​r​y​ ​(​Z​-​A​)
*/
SORT_CATEGORY_DESC: string
/**
* d​S​t​r​u​c​t​ ​i​s​ ​a​ ​w​e​b​ ​a​p​p​ ​t​h​a​t​ ​h​e​l​p​s​ ​y​o​u​ ​u​n​d​e​r​s​t​a​n​d​ ​L​e​e​t​C​o​d​e​ ​p​r​o​b​l​e​m​s​.​ ​I​t​ ​a​l​l​o​w​s​ ​y​o​u​ ​t​o​ ​v​i​s​u​a​l​i​z​e​ ​y​o​u​r​ ​s​o​l​u​t​i​o​n​s​ ​t​h​a​t​ ​y​o​u​ ​w​r​i​t​e​ ​i​n​ ​a​ ​b​u​i​l​t​-​i​n​ ​c​o​d​e​ ​e​d​i​t​o​r​.
*/
SITE_SEO_DESCRIPTION: string
/**
* d​S​t​r​u​c​t​ ​—​ ​v​i​s​u​a​l​i​z​e​ ​L​e​e​t​C​o​d​e​ ​s​o​l​u​t​i​o​n​s
*/
SITE_SEO_TITLE: string
/**
* R​e​p​l​a​y
*/
Expand Down Expand Up @@ -1530,6 +1547,14 @@ export type TranslationFunctions = {
* Playground
*/
PLAYGROUND: () => LocalizedString
/**
* Practice {title} in dStruct — visualize solutions and run code in the browser.
*/
PLAYGROUND_PROJECT_SEO_DESCRIPTION: (arg: { title: string }) => LocalizedString
/**
* Playground | dStruct
*/
PLAYGROUND_SEO_TITLE: () => LocalizedString
/**
* Please enter your LeetCode account name:
*/
Expand Down Expand Up @@ -1954,6 +1979,14 @@ export type TranslationFunctions = {
* Category (Z-A)
*/
SORT_CATEGORY_DESC: () => LocalizedString
/**
* dStruct is a web app that helps you understand LeetCode problems. It allows you to visualize your solutions that you write in a built-in code editor.
*/
SITE_SEO_DESCRIPTION: () => LocalizedString
/**
* dStruct — visualize LeetCode solutions
*/
SITE_SEO_TITLE: () => LocalizedString
/**
* Replay
*/
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/i18n-util.async.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */

import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/i18n-util.sync.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */

import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/i18n-util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */

import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
import type { LocaleDetector } from 'typesafe-i18n/detectors'
Expand Down
24 changes: 15 additions & 9 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { InferGetStaticPropsType, NextPage } from "next";

import { MarketingHomeView } from "#/features/homePage/ui/MarketingHomeView";
import { getI18nPropsWithCanonical } from "#/i18n/getI18nProps";
import { useI18nContext } from "#/shared/hooks";
import { SiteSeoHead } from "#/shared/ui/seo/SiteSeoHead";

export const getStaticProps = getI18nPropsWithCanonical("/");
Expand All @@ -16,14 +17,19 @@ type DashboardProps = InferGetStaticPropsType<typeof getStaticProps>;
* becomes `/{locale}/internal-marketing/...` → 404). Public cutover waits on
* migrating locale routing off `next.config` `i18n`.
*/
const DashboardPage: NextPage<DashboardProps> = ({ canonicalUrl }) => (
<>
<SiteSeoHead
title="dStruct — visualize LeetCode solutions"
canonicalUrl={canonicalUrl}
/>
<MarketingHomeView />
</>
);
const DashboardPage: NextPage<DashboardProps> = ({ canonicalUrl }) => {
const { LL } = useI18nContext();

return (
<>
<SiteSeoHead
title={LL.SITE_SEO_TITLE()}
description={LL.SITE_SEO_DESCRIPTION()}
canonicalUrl={canonicalUrl}
/>
<MarketingHomeView />
</>
);
};

export default DashboardPage;
Loading
Loading