feat(website/i18n): get translation to actually render

This commit is contained in:
Elian Doran
2025-10-25 19:13:28 +03:00
parent 49cf7ae1a3
commit e4f806ed14
10 changed files with 178 additions and 149 deletions

View File

@@ -1,7 +1,7 @@
import { ComponentChildren, HTMLAttributes } from "preact"; import { ComponentChildren, HTMLAttributes } from "preact";
import { Link } from "./Button.js"; import { Link } from "./Button.js";
import Icon from "./Icon.js"; import Icon from "./Icon.js";
import { t } from "../i18n.js"; import { useTranslation } from "react-i18next";
interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> { interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
title: ComponentChildren; title: ComponentChildren;
@@ -13,6 +13,8 @@ interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
} }
export default function Card({ title, children, imageUrl, iconSvg, className, moreInfoUrl, ...restProps }: CardProps) { export default function Card({ title, children, imageUrl, iconSvg, className, moreInfoUrl, ...restProps }: CardProps) {
const { t } = useTranslation();
return ( return (
<div className={`card ${className}`} {...restProps}> <div className={`card ${className}`} {...restProps}>
{imageUrl && <img class="image" src={imageUrl} loading="lazy" />} {imageUrl && <img class="image" src={imageUrl} loading="lazy" />}

View File

@@ -4,16 +4,17 @@ import Button from "./Button.js";
import downloadIcon from "../assets/boxicons/bx-arrow-in-down-square-half.svg?raw"; import downloadIcon from "../assets/boxicons/bx-arrow-in-down-square-half.svg?raw";
import packageJson from "../../../../package.json" with { type: "json" }; import packageJson from "../../../../package.json" with { type: "json" };
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { t } from "../i18n.js"; import { useTranslation } from "react-i18next";
interface DownloadButtonProps { interface DownloadButtonProps {
big?: boolean; big?: boolean;
} }
export default function DownloadButton({ big }: DownloadButtonProps) { export default function DownloadButton({ big }: DownloadButtonProps) {
const { t } = useTranslation();
const [ recommendedDownload, setRecommendedDownload ] = useState<RecommendedDownload | null>(); const [ recommendedDownload, setRecommendedDownload ] = useState<RecommendedDownload | null>();
useEffect(() => { useEffect(() => {
getRecommendedDownload()?.then(setRecommendedDownload); getRecommendedDownload(t)?.then(setRecommendedDownload);
}, []); }, []);
return (recommendedDownload && return (recommendedDownload &&

View File

@@ -5,9 +5,12 @@ import githubDiscussionsIcon from "../assets/boxicons/bx-discussion.svg?raw";
import matrixIcon from "../assets/boxicons/bx-message-dots.svg?raw"; import matrixIcon from "../assets/boxicons/bx-message-dots.svg?raw";
import redditIcon from "../assets/boxicons/bx-reddit.svg?raw"; import redditIcon from "../assets/boxicons/bx-reddit.svg?raw";
import { Link } from "./Button.js"; import { Link } from "./Button.js";
import { LOCALES, t } from "../i18n"; import { LOCALES } from "../i18n";
import { useTranslation } from "react-i18next";
export default function Footer() { export default function Footer() {
const { t } = useTranslation();
return ( return (
<footer> <footer>
<div class="content-wrapper"> <div class="content-wrapper">
@@ -33,6 +36,8 @@ export default function Footer() {
} }
export function SocialButtons({ className, withText }: { className?: string, withText?: boolean }) { export function SocialButtons({ className, withText }: { className?: string, withText?: boolean }) {
const { t } = useTranslation();
return ( return (
<div className={`social-buttons ${className}`}> <div className={`social-buttons ${className}`}>
<SocialButton <SocialButton

View File

@@ -1,5 +1,5 @@
import { TFunction } from 'i18next';
import rootPackageJson from '../../../package.json' with { type: "json" }; import rootPackageJson from '../../../package.json' with { type: "json" };
import { t } from './i18n';
export type App = "desktop" | "server"; export type App = "desktop" | "server";
@@ -34,151 +34,155 @@ export interface RecommendedDownload {
type DownloadMatrix = Record<App, { [ P in Platform ]?: DownloadMatrixEntry }>; type DownloadMatrix = Record<App, { [ P in Platform ]?: DownloadMatrixEntry }>;
// Keep compatibility info inline with https://github.com/electron/electron/blob/main/README.md#platform-support. // Keep compatibility info inline with https://github.com/electron/electron/blob/main/README.md#platform-support.
export const downloadMatrix: DownloadMatrix = { export function getDownloadMatrix(t: TFunction<"translation", undefined>): DownloadMatrix {
desktop: { return {
windows: { desktop: {
title: { windows: {
x64: t("download_helper_desktop_windows.title_x64"), title: {
arm64: t("download_helper_desktop_windows.title_arm64") x64: t("download_helper_desktop_windows.title_x64"),
}, arm64: t("download_helper_desktop_windows.title_arm64")
description: {
x64: t("download_helper_desktop_windows.description_x64"),
arm64: t("download_helper_desktop_windows.description_arm64"),
},
quickStartTitle: t("download_helper_desktop_windows.quick_start"),
quickStartCode: "winget install TriliumNext.Notes",
downloads: {
exe: {
recommended: true,
name: t("download_helper_desktop_windows.download_exe")
}, },
zip: { description: {
name: t("download_helper_desktop_windows.download_zip") x64: t("download_helper_desktop_windows.description_x64"),
arm64: t("download_helper_desktop_windows.description_arm64"),
}, },
scoop: { quickStartTitle: t("download_helper_desktop_windows.quick_start"),
name: t("download_helper_desktop_windows.download_scoop"), quickStartCode: "winget install TriliumNext.Notes",
url: "https://scoop.sh/#/apps?q=trilium&id=7c08bc3c105b9ee5c00dd4245efdea0f091b8a5c" downloads: {
exe: {
recommended: true,
name: t("download_helper_desktop_windows.download_exe")
},
zip: {
name: t("download_helper_desktop_windows.download_zip")
},
scoop: {
name: t("download_helper_desktop_windows.download_scoop"),
url: "https://scoop.sh/#/apps?q=trilium&id=7c08bc3c105b9ee5c00dd4245efdea0f091b8a5c"
}
}
},
linux: {
title: {
x64: t("download_helper_desktop_linux.title_x64"),
arm64: t("download_helper_desktop_linux.title_arm64")
},
description: {
x64: t("download_helper_desktop_linux.description_x64"),
arm64: t("download_helper_desktop_linux.description_arm64"),
},
quickStartTitle: t("download_helper_desktop_linux.quick_start"),
downloads: {
deb: {
recommended: true,
name: t("download_helper_desktop_linux.download_deb")
},
rpm: {
recommended: true,
name: t("download_helper_desktop_linux.download_rpm")
},
flatpak: {
name: t("download_helper_desktop_linux.download_flatpak")
},
zip: {
name: t("download_helper_desktop_linux.download_zip")
},
nixpkgs: {
name: t("download_helper_desktop_linux.download_nixpkgs"),
url: "https://search.nixos.org/packages?query=trilium-next"
},
aur: {
name: t("download_helper_desktop_linux.download_aur"),
url: "https://aur.archlinux.org/packages/triliumnext-bin"
}
}
},
macos: {
title: {
x64: t("download_helper_desktop_macos.title_x64"),
arm64: t("download_helper_desktop_macos.title_arm64")
},
description: {
x64: t("download_helper_desktop_macos.description_x64"),
arm64: t("download_helper_desktop_macos.description_arm64"),
},
quickStartTitle: t("download_helper_desktop_macos.quick_start"),
quickStartCode: "brew install --cask trilium-notes",
downloads: {
dmg: {
recommended: true,
name: t("download_helper_desktop_macos.download_dmg")
},
homebrew: {
name: t("download_helper_desktop_macos.download_homebrew_cask"),
url: "https://formulae.brew.sh/cask/trilium-notes#default"
},
zip: {
name: t("download_helper_desktop_macos.download_zip")
}
} }
} }
}, },
linux: { server: {
title: { docker: {
x64: t("download_helper_desktop_linux.title_x64"), title: t("download_helper_server_docker.title"),
arm64: t("download_helper_desktop_linux.title_arm64") description: t("download_helper_server_docker.description"),
}, helpUrl: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.html",
description: { quickStartCode: "docker pull triliumnext/trilium\ndocker run -p 8080:8080 -d -v ./data:/home/node/trilium-data triliumnext/trilium",
x64: t("download_helper_desktop_linux.description_x64"), downloads: {
arm64: t("download_helper_desktop_linux.description_arm64"), dockerhub: {
}, name: t("download_helper_server_docker.download_dockerhub"),
quickStartTitle: t("download_helper_desktop_linux.quick_start"), url: "https://hub.docker.com/r/triliumnext/trilium"
downloads: { },
deb: { ghcr: {
recommended: true, name: t("download_helper_server_docker.download_ghcr"),
name: t("download_helper_desktop_linux.download_deb") url: "https://github.com/TriliumNext/Trilium/pkgs/container/trilium"
}, }
rpm: {
recommended: true,
name: t("download_helper_desktop_linux.download_rpm")
},
flatpak: {
name: t("download_helper_desktop_linux.download_flatpak")
},
zip: {
name: t("download_helper_desktop_linux.download_zip")
},
nixpkgs: {
name: t("download_helper_desktop_linux.download_nixpkgs"),
url: "https://search.nixos.org/packages?query=trilium-next"
},
aur: {
name: t("download_helper_desktop_linux.download_aur"),
url: "https://aur.archlinux.org/packages/triliumnext-bin"
} }
}
},
macos: {
title: {
x64: t("download_helper_desktop_macos.title_x64"),
arm64: t("download_helper_desktop_macos.title_arm64")
}, },
description: { linux: {
x64: t("download_helper_desktop_macos.description_x64"), title: t("download_helper_server_linux.title"),
arm64: t("download_helper_desktop_macos.description_arm64"), description: t("download_helper_server_linux.description"),
helpUrl: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/1.%20Installing%20the%20server/Packaged%20version%20for%20Linux.html",
downloads: {
tarX64: {
recommended: true,
name: t("download_helper_server_linux.download_tar_x64"),
url: `https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-Server-v${version}-linux-x64.tar.xz`,
},
tarArm64: {
recommended: true,
name: t("download_helper_server_linux.download_tar_arm64"),
url: `https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-Server-v${version}-linux-arm64.tar.xz`
},
nixos: {
name: t("download_helper_server_linux.download_nixos"),
url: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/On%20NixOS"
}
}
}, },
quickStartTitle: t("download_helper_desktop_macos.quick_start"), pikapod: {
quickStartCode: "brew install --cask trilium-notes", title: t("download_helper_server_hosted.title"),
downloads: { description: t("download_helper_server_hosted.description"),
dmg: { downloads: {
recommended: true, pikapod: {
name: t("download_helper_desktop_macos.download_dmg") recommended: true,
}, name: t("download_helper_server_hosted.download_pikapod"),
homebrew: { url: "https://www.pikapods.com/pods?run=trilium-next"
name: t("download_helper_desktop_macos.download_homebrew_cask"), },
url: "https://formulae.brew.sh/cask/trilium-notes#default" triliumcc: {
}, name: t("download_helper_server_hosted.download_triliumcc"),
zip: { url: "https://trilium.cc/"
name: t("download_helper_desktop_macos.download_zip") }
}
}
}
},
server: {
docker: {
title: t("download_helper_server_docker.title"),
description: t("download_helper_server_docker.description"),
helpUrl: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.html",
quickStartCode: "docker pull triliumnext/trilium\ndocker run -p 8080:8080 -d -v ./data:/home/node/trilium-data triliumnext/trilium",
downloads: {
dockerhub: {
name: t("download_helper_server_docker.download_dockerhub"),
url: "https://hub.docker.com/r/triliumnext/trilium"
},
ghcr: {
name: t("download_helper_server_docker.download_ghcr"),
url: "https://github.com/TriliumNext/Trilium/pkgs/container/trilium"
}
}
},
linux: {
title: t("download_helper_server_linux.title"),
description: t("download_helper_server_linux.description"),
helpUrl: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/1.%20Installing%20the%20server/Packaged%20version%20for%20Linux.html",
downloads: {
tarX64: {
recommended: true,
name: t("download_helper_server_linux.download_tar_x64"),
url: `https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-Server-v${version}-linux-x64.tar.xz`,
},
tarArm64: {
recommended: true,
name: t("download_helper_server_linux.download_tar_arm64"),
url: `https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-Server-v${version}-linux-arm64.tar.xz`
},
nixos: {
name: t("download_helper_server_linux.download_nixos"),
url: "https://docs.triliumnotes.org/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/On%20NixOS"
}
}
},
pikapod: {
title: t("download_helper_server_hosted.title"),
description: t("download_helper_server_hosted.description"),
downloads: {
pikapod: {
recommended: true,
name: t("download_helper_server_hosted.download_pikapod"),
url: "https://www.pikapods.com/pods?run=trilium-next"
},
triliumcc: {
name: t("download_helper_server_hosted.download_triliumcc"),
url: "https://trilium.cc/"
} }
} }
} }
} }
}; };
export function buildDownloadUrl(app: App, platform: Platform, format: string, architecture: Architecture): string { export function buildDownloadUrl(t: TFunction<"translation", undefined>, app: App, platform: Platform, format: string, architecture: Architecture): string {
const downloadMatrix = getDownloadMatrix(t);
if (app === "desktop") { if (app === "desktop") {
return downloadMatrix.desktop[platform]?.downloads[format].url ?? return downloadMatrix.desktop[platform]?.downloads[format].url ??
`https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-v${version}-${platform}-${architecture}.${format}`; `https://github.com/TriliumNext/Trilium/releases/download/v${version}/TriliumNotes-v${version}-${platform}-${architecture}.${format}`;
@@ -218,8 +222,9 @@ export function getPlatform(): Platform | null {
} }
} }
export async function getRecommendedDownload(): Promise<RecommendedDownload | null> { export async function getRecommendedDownload(t: TFunction<"translation", undefined>): Promise<RecommendedDownload | null> {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
const downloadMatrix = getDownloadMatrix(t);
const architecture = await getArchitecture(); const architecture = await getArchitecture();
const platform = getPlatform(); const platform = getPlatform();
@@ -233,7 +238,7 @@ export async function getRecommendedDownload(): Promise<RecommendedDownload | nu
if (!recommendedDownload) return null; if (!recommendedDownload) return null;
const format = recommendedDownload[0]; const format = recommendedDownload[0];
const url = buildDownloadUrl("desktop", platform, format || 'zip', architecture); const url = buildDownloadUrl(t, "desktop", platform, format || 'zip', architecture);
const platformTitle = platformInfo.title; const platformTitle = platformInfo.title;
const name = typeof platformTitle === "string" ? platformTitle : platformTitle[architecture] as string; const name = typeof platformTitle === "string" ? platformTitle : platformTitle[architecture] as string;

View File

@@ -33,5 +33,3 @@ await i18next.init({
}, },
returnEmptyString: false returnEmptyString: false
}); });
export const t = i18next.t;

View File

@@ -8,6 +8,8 @@ import Footer from './components/Footer.js';
import GetStarted from './pages/GetStarted/get-started.js'; import GetStarted from './pages/GetStarted/get-started.js';
import SupportUs from './pages/SupportUs/SupportUs.js'; import SupportUs from './pages/SupportUs/SupportUs.js';
import { createContext } from 'preact'; import { createContext } from 'preact';
import { useEffect } from 'preact/hooks';
import { changeLanguage } from 'i18next';
export const LocaleContext = createContext('en'); export const LocaleContext = createContext('en');
@@ -34,6 +36,10 @@ export function LocaleProvider({ children }) {
const { path } = useLocation(); const { path } = useLocation();
const locale = path.split('/')[1] || 'en'; const locale = path.split('/')[1] || 'en';
useEffect(() => {
changeLanguage(locale);
}, [ locale ]);
return ( return (
<LocaleContext.Provider value={locale}> <LocaleContext.Provider value={locale}>
{children} {children}

View File

@@ -1,18 +1,20 @@
import { useLayoutEffect, useState } from "preact/hooks"; import { useLayoutEffect, useState } from "preact/hooks";
import Card from "../../components/Card.js"; import Card from "../../components/Card.js";
import Section from "../../components/Section.js"; import Section from "../../components/Section.js";
import { App, Architecture, buildDownloadUrl, downloadMatrix, DownloadMatrixEntry, getArchitecture, getPlatform, Platform } from "../../download-helper.js"; import { App, Architecture, buildDownloadUrl, DownloadMatrixEntry, getArchitecture, getDownloadMatrix, getPlatform, Platform } from "../../download-helper.js";
import { usePageTitle } from "../../hooks.js"; import { usePageTitle } from "../../hooks.js";
import Button, { Link } from "../../components/Button.js"; import Button, { Link } from "../../components/Button.js";
import Icon from "../../components/Icon.js"; import Icon from "../../components/Icon.js";
import helpIcon from "../../assets/boxicons/bx-help-circle.svg?raw"; import helpIcon from "../../assets/boxicons/bx-help-circle.svg?raw";
import "./get-started.css"; import "./get-started.css";
import packageJson from "../../../../../package.json" with { type: "json" }; import packageJson from "../../../../../package.json" with { type: "json" };
import { t } from "../../i18n.js"; import { useTranslation } from "react-i18next";
export default function DownloadPage() { export default function DownloadPage() {
const { t } = useTranslation();
const [ currentArch, setCurrentArch ] = useState<Architecture>("x64"); const [ currentArch, setCurrentArch ] = useState<Architecture>("x64");
const [ userPlatform, setUserPlatform ] = useState<Platform>(); const [ userPlatform, setUserPlatform ] = useState<Platform>();
const downloadMatrix = getDownloadMatrix(t);
useLayoutEffect(() => { useLayoutEffect(() => {
getArchitecture().then((arch) => setCurrentArch(arch ?? "x64")); getArchitecture().then((arch) => setCurrentArch(arch ?? "x64"));
@@ -71,6 +73,7 @@ export function DownloadCard({ app, arch, entry: [ platform, entry ], isRecommen
return (typeof text === "string" ? text : text[arch]); return (typeof text === "string" ? text : text[arch]);
} }
const { t } = useTranslation();
const allDownloads = Object.entries(entry.downloads); const allDownloads = Object.entries(entry.downloads);
const recommendedDownloads = allDownloads.filter(download => download[1].recommended); const recommendedDownloads = allDownloads.filter(download => download[1].recommended);
const restDownloads = allDownloads.filter(download => !download[1].recommended); const restDownloads = allDownloads.filter(download => !download[1].recommended);
@@ -107,7 +110,7 @@ export function DownloadCard({ app, arch, entry: [ platform, entry ], isRecommen
{recommendedDownloads.map(recommendedDownload => ( {recommendedDownloads.map(recommendedDownload => (
<Button <Button
className="recommended" className="recommended"
href={buildDownloadUrl(app, platform as Platform, recommendedDownload[0], arch)} href={buildDownloadUrl(t, app, platform as Platform, recommendedDownload[0], arch)}
text={recommendedDownload[1].name} text={recommendedDownload[1].name}
openExternally={!!recommendedDownload[1].url} openExternally={!!recommendedDownload[1].url}
/> />
@@ -117,7 +120,7 @@ export function DownloadCard({ app, arch, entry: [ platform, entry ], isRecommen
<div class="other-options"> <div class="other-options">
{restDownloads.map(download => ( {restDownloads.map(download => (
<Link <Link
href={buildDownloadUrl(app, platform as Platform, download[0], arch)} href={buildDownloadUrl(t, app, platform as Platform, download[0], arch)}
openExternally={!!download[1].url} openExternally={!!download[1].url}
> >
{download[1].name} {download[1].name}

View File

@@ -31,8 +31,7 @@ import boardIcon from "../../assets/boxicons/bx-columns-3.svg?raw";
import geomapIcon from "../../assets/boxicons/bx-map.svg?raw"; import geomapIcon from "../../assets/boxicons/bx-map.svg?raw";
import { getPlatform } from '../../download-helper.js'; import { getPlatform } from '../../download-helper.js';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';
import { t } from '../../i18n.js'; import { Trans, useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
export function Home() { export function Home() {
usePageTitle(""); usePageTitle("");
@@ -52,6 +51,7 @@ export function Home() {
} }
function HeroSection() { function HeroSection() {
const { t } = useTranslation();
const platform = getPlatform(); const platform = getPlatform();
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const [ screenshotUrl, setScreenshotUrl ] = useState<string>(); const [ screenshotUrl, setScreenshotUrl ] = useState<string>();
@@ -96,6 +96,7 @@ function HeroSection() {
} }
function OrganizationBenefitsSection() { function OrganizationBenefitsSection() {
const { t } = useTranslation();
return ( return (
<> <>
<Section className="benefits" title={t("organization_benefits.title")}> <Section className="benefits" title={t("organization_benefits.title")}>
@@ -110,6 +111,7 @@ function OrganizationBenefitsSection() {
} }
function ProductivityBenefitsSection() { function ProductivityBenefitsSection() {
const { t } = useTranslation();
return ( return (
<> <>
<Section className="benefits accented" title={t("productivity_benefits.title")}> <Section className="benefits accented" title={t("productivity_benefits.title")}>
@@ -127,6 +129,7 @@ function ProductivityBenefitsSection() {
} }
function NoteTypesSection() { function NoteTypesSection() {
const { t } = useTranslation();
return ( return (
<Section className="note-types" title="Multiple ways to represent your information"> <Section className="note-types" title="Multiple ways to represent your information">
<ListWithScreenshot horizontal items={[ <ListWithScreenshot horizontal items={[
@@ -190,6 +193,7 @@ function NoteTypesSection() {
} }
function ExtensibilityBenefitsSection() { function ExtensibilityBenefitsSection() {
const { t } = useTranslation();
return ( return (
<> <>
<Section className="benefits accented" title={t("extensibility_benefits.title")}> <Section className="benefits accented" title={t("extensibility_benefits.title")}>
@@ -205,6 +209,7 @@ function ExtensibilityBenefitsSection() {
} }
function CollectionsSection() { function CollectionsSection() {
const { t } = useTranslation();
return ( return (
<Section className="collections" title="Collections"> <Section className="collections" title="Collections">
<ListWithScreenshot items={[ <ListWithScreenshot items={[
@@ -247,6 +252,7 @@ function ListWithScreenshot({ items, horizontal, cardExtra }: {
cardExtra?: ComponentChildren; cardExtra?: ComponentChildren;
}) { }) {
const [ selectedItem, setSelectedItem ] = useState(items[0]); const [ selectedItem, setSelectedItem ] = useState(items[0]);
const { t } = useTranslation();
return ( return (
<div className={`list-with-screenshot ${horizontal ? "horizontal" : ""}`}> <div className={`list-with-screenshot ${horizontal ? "horizontal" : ""}`}>
@@ -278,6 +284,7 @@ function ListWithScreenshot({ items, horizontal, cardExtra }: {
} }
function FaqSection() { function FaqSection() {
const { t } = useTranslation();
return ( return (
<Section className="faq" title={t("faq.title")}> <Section className="faq" title={t("faq.title")}>
<div class="grid-2-cols"> <div class="grid-2-cols">
@@ -301,6 +308,7 @@ function FaqItem({ question, children }: { question: string; children: Component
} }
function FinalCta() { function FinalCta() {
const { t } = useTranslation();
return ( return (
<Section className="final-cta accented" title={t("final_cta.title")}> <Section className="final-cta accented" title={t("final_cta.title")}>
<p>{t("final_cta.description")}</p> <p>{t("final_cta.description")}</p>

View File

@@ -6,10 +6,10 @@ import buyMeACoffeeIcon from "../../assets/boxicons/bx-buy-me-a-coffee.svg?raw";
import Button, { Link } from "../../components/Button.js"; import Button, { Link } from "../../components/Button.js";
import Card from "../../components/Card.js"; import Card from "../../components/Card.js";
import { usePageTitle } from "../../hooks.js"; import { usePageTitle } from "../../hooks.js";
import { t } from "../../i18n.js"; import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
export default function Donate() { export default function Donate() {
const { t } = useTranslation();
usePageTitle(t("support_us.title")); usePageTitle(t("support_us.title"));
return ( return (

View File

@@ -1,9 +1,10 @@
import { useTranslation } from "react-i18next";
import Section from "../components/Section.js"; import Section from "../components/Section.js";
import { usePageTitle } from "../hooks.js"; import { usePageTitle } from "../hooks.js";
import { t } from "../i18n.js";
import "./_404.css"; import "./_404.css";
export function NotFound() { export function NotFound() {
const { t } = useTranslation();
usePageTitle(t("404.title")); usePageTitle(t("404.title"));
return ( return (