Internationalization improvements for the website (#7515)

This commit is contained in:
Elian Doran 2025-10-25 23:46:43 +03:00 committed by GitHub
commit 252f8ccb1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 398 additions and 200 deletions

View File

@ -5,6 +5,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"preview": "pnpm build && vite preview"
},
"dependencies": {

View File

@ -39,6 +39,7 @@
"web_clipper_content": "Grab web pages (or screenshots) and place them directly into Trilium using the web clipper browser extension."
},
"note_types": {
"title": "Multiple ways to represent your information",
"text_title": "Text notes",
"text_description": "The notes are edited using a visual (WYSIWYG) editor, with support for tables, images, math expressions, code blocks with syntax highlighting. Quickly format the text using Markdown-like syntax or using slash commands.",
"code_title": "Code notes",
@ -65,6 +66,7 @@
"api_description": "Interact with Trilium programatically using its builtin REST API."
},
"collections": {
"title": "Collections",
"calendar_title": "Calendar",
"calendar_description": "Organize your personal or professional events using a calendar, with support for all-day and multi-day events. See your events at a glance with the week, month and year views. Easy interaction to add or drag events.",
"table_title": "Table",
@ -106,6 +108,11 @@
"linux_small": "for Linux",
"more_platforms": "More platforms & server setup"
},
"header": {
"get-started": "Get started",
"documentation": "Documentation",
"support-us": "Support us"
},
"footer": {
"copyright_and_the": " and the ",
"copyright_community": "community"

View File

@ -106,6 +106,11 @@
"linux_small": "pentru Linux",
"more_platforms": "Mai multe platforme și instalarea server-ului"
},
"header": {
"get-started": "Primii pași",
"documentation": "Documentație",
"support-us": "Sprijină-ne"
},
"footer": {
"copyright_and_the": " și ",
"copyright_community": "comunitatea"

View File

@ -1,7 +1,7 @@
import { ComponentChildren, HTMLAttributes } from "preact";
import { Link } from "./Button.js";
import Icon from "./Icon.js";
import { t } from "../i18n.js";
import { useTranslation } from "react-i18next";
interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
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) {
const { t } = useTranslation();
return (
<div className={`card ${className}`} {...restProps}>
{imageUrl && <img class="image" src={imageUrl} loading="lazy" />}

View File

@ -3,18 +3,21 @@ import "./DownloadButton.css";
import Button from "./Button.js";
import downloadIcon from "../assets/boxicons/bx-arrow-in-down-square-half.svg?raw";
import packageJson from "../../../../package.json" with { type: "json" };
import { useEffect, useState } from "preact/hooks";
import { t } from "../i18n.js";
import { useContext, useEffect, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import { LocaleContext } from "../index.js";
interface DownloadButtonProps {
big?: boolean;
}
export default function DownloadButton({ big }: DownloadButtonProps) {
const locale = useContext(LocaleContext);
const { t } = useTranslation();
const [ recommendedDownload, setRecommendedDownload ] = useState<RecommendedDownload | null>();
useEffect(() => {
getRecommendedDownload()?.then(setRecommendedDownload);
}, []);
getRecommendedDownload(t)?.then(setRecommendedDownload);
}, [ t ]);
return (recommendedDownload &&
<>
@ -35,7 +38,7 @@ export default function DownloadButton({ big }: DownloadButtonProps) {
) : (
<Button
className={`download-button desktop-only ${big ? "big" : ""}`}
href="/get-started/"
href={`/${locale}/get-started/`}
iconSvg={downloadIcon}
text={<>
{t("download_now.text")}

View File

@ -5,17 +5,26 @@ footer {
color: var(--muted-color);
font-size: 0.8em;
.content-wrapper {
.row {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column-reverse;
gap: 2em;
margin-bottom: 1em;
@media (min-width: 720px) {
flex-direction: row;
}
}
nav.languages {
flex-grow: 1;
justify-content: center;
flex-wrap: wrap;
display: flex;
gap: 0.5em 1em;
}
}
.social-buttons {

View File

@ -5,24 +5,46 @@ import githubDiscussionsIcon from "../assets/boxicons/bx-discussion.svg?raw";
import matrixIcon from "../assets/boxicons/bx-message-dots.svg?raw";
import redditIcon from "../assets/boxicons/bx-reddit.svg?raw";
import { Link } from "./Button.js";
import { t } from "../i18n";
import { LOCALES, swapLocaleInUrl } from "../i18n";
import { useTranslation } from "react-i18next";
import { useLocation } from "preact-iso";
import { useContext } from "preact/hooks";
import { LocaleContext } from "..";
export default function Footer() {
const { t } = useTranslation();
const { url } = useLocation();
const currentLocale = useContext(LocaleContext);
return (
<footer>
<div class="content-wrapper">
<div class="footer-text">
© 2024-2025 <Link href="https://github.com/eliandoran" openExternally>Elian Doran</Link>{t("footer.copyright_and_the")}<Link href="https://github.com/TriliumNext/Trilium/graphs/contributors" openExternally>{t("footer.copyright_community")}</Link>.<br />
© 2017-2024 <Link href="https://github.com/zadam" openExternally>zadam</Link>.
<div class="row">
<div class="footer-text">
© 2024-2025 <Link href="https://github.com/eliandoran" openExternally>Elian Doran</Link>{t("footer.copyright_and_the")}<Link href="https://github.com/TriliumNext/Trilium/graphs/contributors" openExternally>{t("footer.copyright_community")}</Link>.<br />
© 2017-2024 <Link href="https://github.com/zadam" openExternally>zadam</Link>.
</div>
<SocialButtons />
</div>
<SocialButtons />
<div class="row">
<nav class="languages">
{LOCALES.map(locale => (
locale.id !== currentLocale
? <Link href={swapLocaleInUrl(url, locale.id)}>{locale.name}</Link>
: <span className="active">{locale.name}</span>
))}
</nav>
</div>
</div>
</footer>
)
}
export function SocialButtons({ className, withText }: { className?: string, withText?: boolean }) {
const { t } = useTranslation();
return (
<div className={`social-buttons ${className}`}>
<SocialButton

View File

@ -1,13 +1,16 @@
import "./Header.css";
import { Link } from "./Button.js";
import { SocialButtons, SocialButton } from "./Footer.js";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { useLocation } from 'preact-iso';
import DownloadButton from './DownloadButton.js';
import githubIcon from "../assets/boxicons/bx-github.svg?raw";
import Icon from "./Icon.js";
import logoPath from "../assets/icon-color.svg";
import menuIcon from "../assets/boxicons/bx-menu.svg?raw";
import { LocaleContext } from "..";
import { useTranslation } from "react-i18next";
import { swapLocaleInUrl } from "../i18n";
interface HeaderLink {
url: string;
@ -15,21 +18,26 @@ interface HeaderLink {
external?: boolean;
}
const HEADER_LINKS: HeaderLink[] = [
{ url: "/get-started/", text: "Get started" },
{ url: "https://docs.triliumnotes.org/", text: "Documentation", external: true },
{ url: "/support-us/", text: "Support us" }
]
export function Header(props: {repoStargazersCount: number}) {
const { url } = useLocation();
const { t } = useTranslation();
const locale = useContext(LocaleContext);
const [ mobileMenuShown, setMobileMenuShown ] = useState(false);
const [ headerLinks, setHeaderLinks ] = useState<HeaderLink[]>([]);
useEffect(() => {
setHeaderLinks([
{ url: "/get-started", text: t("header.get-started") },
{ url: "https://docs.triliumnotes.org/", text: t("header.documentation"), external: true },
{ url: "/support-us", text: t("header.support-us") }
]);
}, [ locale, t ]);
return (
<header>
<div class="content-wrapper">
<div class="first-row">
<a class="banner" href="/">
<a class="banner" href={`/${locale}/`}>
<img src={logoPath} width="300" height="300" alt="Trilium Notes logo" />&nbsp;<span>Trilium Notes</span>
</a>
@ -46,16 +54,17 @@ export function Header(props: {repoStargazersCount: number}) {
</div>
<nav className={`${mobileMenuShown ? "mobile-shown" : ""}`}>
{HEADER_LINKS.map(link => (
<Link
href={link.url}
className={url === link.url ? "active" : ""}
{headerLinks.map(link => {
const linkHref = link.external ? link.url : swapLocaleInUrl(link.url, locale);
return (<Link
href={linkHref}
className={url === linkHref ? "active" : ""}
openExternally={link.external}
onClick={() => {
setMobileMenuShown(false);
}}
>{link.text}</Link>
))}
>{link.text}</Link>)
})}
<SocialButtons className="mobile-only" withText />
</nav>

View File

@ -1,5 +1,5 @@
import { TFunction } from 'i18next';
import rootPackageJson from '../../../package.json' with { type: "json" };
import { t } from './i18n';
export type App = "desktop" | "server";
@ -34,151 +34,155 @@ export interface RecommendedDownload {
type DownloadMatrix = Record<App, { [ P in Platform ]?: DownloadMatrixEntry }>;
// Keep compatibility info inline with https://github.com/electron/electron/blob/main/README.md#platform-support.
export const downloadMatrix: DownloadMatrix = {
desktop: {
windows: {
title: {
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")
export function getDownloadMatrix(t: TFunction<"translation", undefined>): DownloadMatrix {
return {
desktop: {
windows: {
title: {
x64: t("download_helper_desktop_windows.title_x64"),
arm64: t("download_helper_desktop_windows.title_arm64")
},
zip: {
name: t("download_helper_desktop_windows.download_zip")
description: {
x64: t("download_helper_desktop_windows.description_x64"),
arm64: t("download_helper_desktop_windows.description_arm64"),
},
scoop: {
name: t("download_helper_desktop_windows.download_scoop"),
url: "https://scoop.sh/#/apps?q=trilium&id=7c08bc3c105b9ee5c00dd4245efdea0f091b8a5c"
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: {
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: {
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"
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"
}
}
}
},
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"),
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"
}
}
},
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")
}
}
}
},
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/"
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") {
return downloadMatrix.desktop[platform]?.downloads[format].url ??
`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;
const downloadMatrix = getDownloadMatrix(t);
const architecture = await getArchitecture();
const platform = getPlatform();
@ -233,7 +238,7 @@ export async function getRecommendedDownload(): Promise<RecommendedDownload | nu
if (!recommendedDownload) return null;
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 name = typeof platformTitle === "string" ? platformTitle : platformTitle[architecture] as string;

View File

@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { extractLocaleFromUrl, mapLocale, swapLocaleInUrl } from "./i18n";
describe("mapLocale", () => {
it("maps Chinese", () => {
expect(mapLocale("zh-TW")).toStrictEqual("zh-Hant");
expect(mapLocale("zh-CN")).toStrictEqual("zh-Hans");
});
it("maps languages without countries", () => {
expect(mapLocale("ro-RO")).toStrictEqual("ro");
expect(mapLocale("ro")).toStrictEqual("ro");
});
});
describe("swapLocale", () => {
it("swap locale in URL", () => {
expect(swapLocaleInUrl("/get-started", "ro")).toStrictEqual("/ro/get-started");
expect(swapLocaleInUrl("/ro/get-started", "ro")).toStrictEqual("/ro/get-started");
expect(swapLocaleInUrl("/en/get-started", "ro")).toStrictEqual("/ro/get-started");
expect(swapLocaleInUrl("/ro/", "en")).toStrictEqual("/en/");
});
});
describe("extractLocaleFromUrl", () => {
it("properly extracts locale", () => {
expect(extractLocaleFromUrl("/en/get-started")).toStrictEqual("en");
expect(extractLocaleFromUrl("/get-started")).toStrictEqual(undefined);
expect(extractLocaleFromUrl("/")).toStrictEqual(undefined);
});
});

View File

@ -1,19 +1,50 @@
import { default as i18next } from "i18next";
import HttpApi from 'i18next-http-backend';
import { initReactI18next } from "react-i18next";
interface Locale {
id: string;
name: string;
rtl?: boolean;
}
i18next
.use(HttpApi)
.use(initReactI18next);
export const LOCALES: Locale[] = [
{ id: "en", name: "English" },
{ id: "ro", name: "Română" },
{ id: "zh-Hans", name: "简体中文" },
{ id: "zh-Hant", name: "繁體中文" },
{ id: "fr", name: "Français" },
{ id: "it", name: "Italiano" },
{ id: "ja", name: "日本語" },
{ id: "pl", name: "Polski" },
{ id: "es", name: "Español" },
{ id: "ar", name: "اَلْعَرَبِيَّةُ", rtl: true },
].toSorted((a, b) => a.name.localeCompare(b.name));
await i18next.init({
debug: true,
lng: "en",
fallbackLng: "en",
backend: {
loadPath: "/translations/{{lng}}/{{ns}}.json",
},
returnEmptyString: false
});
export function mapLocale(locale: string) {
if (!locale) return 'en';
const lower = locale.toLowerCase();
export const t = i18next.t;
if (lower.startsWith('zh')) {
if (lower.includes('tw') || lower.includes('hk') || lower.includes('mo') || lower.includes('hant')) {
return 'zh-Hant';
}
return 'zh-Hans';
}
// Default for everything else
return locale.split('-')[0]; // e.g. "en-US" -> "en"
}
export function swapLocaleInUrl(url: string, newLocale: string) {
const components = url.split("/");
if (components.length === 2) {
return `/${newLocale}${url}`;
} else {
components[1] = newLocale;
return components.join("/");
}
}
export function extractLocaleFromUrl(url: string) {
const localeId = url.split('/')[1];
const correspondingLocale = LOCALES.find(l => l.id === localeId);
if (!correspondingLocale) return undefined;
return localeId;
}

View File

@ -2,29 +2,78 @@ import './style.css';
import { FALLBACK_STARGAZERS_COUNT, getRepoStargazersCount } from './github-utils.js';
import { Header } from './components/Header.jsx';
import { Home } from './pages/Home/index.jsx';
import { LocationProvider, Router, Route, hydrate, prerender as ssr } from 'preact-iso';
import { LocationProvider, Router, Route, hydrate, prerender as ssr, useLocation } from 'preact-iso';
import { NotFound } from './pages/_404.jsx';
import Footer from './components/Footer.js';
import GetStarted from './pages/GetStarted/get-started.js';
import SupportUs from './pages/SupportUs/SupportUs.js';
import { createContext } from 'preact';
import { useLayoutEffect, useState } from 'preact/hooks';
import { default as i18next, changeLanguage } from 'i18next';
import { extractLocaleFromUrl, LOCALES, mapLocale } from './i18n';
import HttpApi from 'i18next-http-backend';
import { initReactI18next } from "react-i18next";
export const LocaleContext = createContext('en');
export function App(props: {repoStargazersCount: number}) {
return (
<LocationProvider>
<Header repoStargazersCount={props.repoStargazersCount} />
<main>
<Router>
<Route path="/" component={Home} />
<Route default component={NotFound} />
<Route path="/get-started" component={GetStarted} />
<Route path="/support-us" component={SupportUs} />
</Router>
</main>
<Footer />
<LocaleProvider>
<Header repoStargazersCount={props.repoStargazersCount} />
<main>
<Router>
<Route path="/" component={Home} />
<Route path="/get-started" component={GetStarted} />
<Route path="/support-us" component={SupportUs} />
<Route path="/:locale:/" component={Home} />
<Route path="/:locale:/get-started" component={GetStarted} />
<Route path="/:locale:/support-us" component={SupportUs} />
<Route default component={NotFound} />
</Router>
</main>
<Footer />
</LocaleProvider>
</LocationProvider>
);
}
export function LocaleProvider({ children }) {
const { path } = useLocation();
const localeId = mapLocale(extractLocaleFromUrl(path) || navigator.language);
const [ loaded, setLoaded ] = useState(false);
useLayoutEffect(() => {
i18next
.use(HttpApi)
.use(initReactI18next);
i18next.init({
lng: localeId,
fallbackLng: "en",
backend: {
loadPath: "/translations/{{lng}}/{{ns}}.json",
},
returnEmptyString: false
}).then(() => setLoaded(true))
}, []);
useLayoutEffect(() => {
if (!loaded) return;
changeLanguage(localeId);
const correspondingLocale = LOCALES.find(l => l.id === localeId);
document.documentElement.lang = localeId;
document.documentElement.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}, [ loaded, localeId ]);
return (
<LocaleContext.Provider value={localeId}>
{loaded && children}
</LocaleContext.Provider>
);
}
if (typeof window !== 'undefined') {
hydrate(<App repoStargazersCount={FALLBACK_STARGAZERS_COUNT} />, document.getElementById('app')!);
}

View File

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

View File

@ -57,6 +57,8 @@ section.hero-section {
color: transparent;
line-height: 1.1;
font-weight: 400;
font-size: 2em;
margin-block: 0.65em;
}
}

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

View File

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

View File

@ -31,7 +31,13 @@ html,
body {
margin: 0;
line-height: 1.5;
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
font-family: Inter,
system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", "Noto Sans CJK SC",
"Hiragino Sans", "Hiragino Kaku Gothic ProN",
"Microsoft YaHei", "Meiryo", "Malgun Gothic",
"PingFang SC", "Source Han Sans SC",
"Source Han Sans JP", "Source Han Sans KR";
min-height: 100vh;
}

View File

@ -14,4 +14,7 @@ export default defineConfig({
},
}),
],
test: {
environment: "happy-dom"
}
});

View File

@ -17,6 +17,7 @@
"desktop:start": "pnpm run --filter desktop dev",
"desktop:build": "pnpm run --filter desktop build",
"desktop:start-prod": "pnpm run --filter desktop start-prod",
"website:start": "pnpm run --filter website dev",
"website:build": "pnpm run --filter website build",
"electron:build": "pnpm desktop:build",
"electron:start": "pnpm desktop:start",