mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 19:49:01 +01:00 
			
		
		
		
	Internationalization improvements for the website (#7515)
This commit is contained in:
		
						commit
						252f8ccb1f
					
				| @ -5,6 +5,7 @@ | |||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite", | 		"dev": "vite", | ||||||
| 		"build": "vite build", | 		"build": "vite build", | ||||||
|  | 		"test": "vitest", | ||||||
| 		"preview": "pnpm build && vite preview" | 		"preview": "pnpm build && vite preview" | ||||||
| 	}, | 	}, | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ | |||||||
|     "web_clipper_content": "Grab web pages (or screenshots) and place them directly into Trilium using the web clipper browser extension." |     "web_clipper_content": "Grab web pages (or screenshots) and place them directly into Trilium using the web clipper browser extension." | ||||||
|   }, |   }, | ||||||
|   "note_types": { |   "note_types": { | ||||||
|  |     "title": "Multiple ways to represent your information", | ||||||
|     "text_title": "Text notes", |     "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.", |     "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", |     "code_title": "Code notes", | ||||||
| @ -65,6 +66,7 @@ | |||||||
|     "api_description": "Interact with Trilium programatically using its builtin REST API." |     "api_description": "Interact with Trilium programatically using its builtin REST API." | ||||||
|   }, |   }, | ||||||
|   "collections": { |   "collections": { | ||||||
|  |     "title": "Collections", | ||||||
|     "calendar_title": "Calendar", |     "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.", |     "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", |     "table_title": "Table", | ||||||
| @ -106,6 +108,11 @@ | |||||||
|     "linux_small": "for Linux", |     "linux_small": "for Linux", | ||||||
|     "more_platforms": "More platforms & server setup" |     "more_platforms": "More platforms & server setup" | ||||||
|   }, |   }, | ||||||
|  |   "header": { | ||||||
|  |     "get-started": "Get started", | ||||||
|  |     "documentation": "Documentation", | ||||||
|  |     "support-us": "Support us" | ||||||
|  |   }, | ||||||
|   "footer": { |   "footer": { | ||||||
|     "copyright_and_the": " and the ", |     "copyright_and_the": " and the ", | ||||||
|     "copyright_community": "community" |     "copyright_community": "community" | ||||||
|  | |||||||
| @ -106,6 +106,11 @@ | |||||||
|         "linux_small": "pentru Linux", |         "linux_small": "pentru Linux", | ||||||
|         "more_platforms": "Mai multe platforme și instalarea server-ului" |         "more_platforms": "Mai multe platforme și instalarea server-ului" | ||||||
|     }, |     }, | ||||||
|  |     "header": { | ||||||
|  |       "get-started": "Primii pași", | ||||||
|  |       "documentation": "Documentație", | ||||||
|  |       "support-us": "Sprijină-ne" | ||||||
|  |     }, | ||||||
|     "footer": { |     "footer": { | ||||||
|         "copyright_and_the": " și ", |         "copyright_and_the": " și ", | ||||||
|         "copyright_community": "comunitatea" |         "copyright_community": "comunitatea" | ||||||
|  | |||||||
| @ -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" />} | ||||||
|  | |||||||
| @ -3,18 +3,21 @@ import "./DownloadButton.css"; | |||||||
| import Button from "./Button.js"; | 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 { useContext, useEffect, useState } from "preact/hooks"; | ||||||
| import { t } from "../i18n.js"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import { LocaleContext } from "../index.js"; | ||||||
| 
 | 
 | ||||||
| interface DownloadButtonProps { | interface DownloadButtonProps { | ||||||
|     big?: boolean; |     big?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function DownloadButton({ big }: DownloadButtonProps) { | export default function DownloadButton({ big }: DownloadButtonProps) { | ||||||
|  |     const locale = useContext(LocaleContext); | ||||||
|  |     const { t } = useTranslation(); | ||||||
|     const [ recommendedDownload, setRecommendedDownload ] = useState<RecommendedDownload | null>(); |     const [ recommendedDownload, setRecommendedDownload ] = useState<RecommendedDownload | null>(); | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         getRecommendedDownload()?.then(setRecommendedDownload); |         getRecommendedDownload(t)?.then(setRecommendedDownload); | ||||||
|     }, []); |     }, [ t ]); | ||||||
| 
 | 
 | ||||||
|     return (recommendedDownload && |     return (recommendedDownload && | ||||||
|         <> |         <> | ||||||
| @ -35,7 +38,7 @@ export default function DownloadButton({ big }: DownloadButtonProps) { | |||||||
|             ) : ( |             ) : ( | ||||||
|                 <Button |                 <Button | ||||||
|                     className={`download-button desktop-only ${big ? "big" : ""}`} |                     className={`download-button desktop-only ${big ? "big" : ""}`} | ||||||
|                     href="/get-started/" |                     href={`/${locale}/get-started/`} | ||||||
|                     iconSvg={downloadIcon} |                     iconSvg={downloadIcon} | ||||||
|                     text={<> |                     text={<> | ||||||
|                             {t("download_now.text")} |                             {t("download_now.text")} | ||||||
|  | |||||||
| @ -5,17 +5,26 @@ footer { | |||||||
|     color: var(--muted-color); |     color: var(--muted-color); | ||||||
|     font-size: 0.8em; |     font-size: 0.8em; | ||||||
| 
 | 
 | ||||||
|     .content-wrapper { |     .row { | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         flex-direction: column-reverse; |         flex-direction: column-reverse; | ||||||
|         gap: 2em; |         gap: 2em; | ||||||
|  |         margin-bottom: 1em; | ||||||
| 
 | 
 | ||||||
|         @media (min-width: 720px) { |         @media (min-width: 720px) { | ||||||
|             flex-direction: row; |             flex-direction: row; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     nav.languages { | ||||||
|  |         flex-grow: 1; | ||||||
|  |         justify-content: center; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |         display: flex; | ||||||
|  |         gap: 0.5em 1em; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .social-buttons { | .social-buttons { | ||||||
|  | |||||||
| @ -5,24 +5,46 @@ 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 { 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() { | export default function Footer() { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const { url } = useLocation(); | ||||||
|  |     const currentLocale = useContext(LocaleContext); | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|         <footer> |         <footer> | ||||||
|             <div class="content-wrapper"> |             <div class="content-wrapper"> | ||||||
|                 <div class="footer-text"> |                 <div class="row"> | ||||||
|                     © 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 /> |                     <div class="footer-text"> | ||||||
|                     © 2017-2024 <Link href="https://github.com/zadam" openExternally>zadam</Link>. |                         © 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> |                 </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> |             </div> | ||||||
|         </footer> |         </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 | ||||||
|  | |||||||
| @ -1,13 +1,16 @@ | |||||||
| import "./Header.css"; | import "./Header.css"; | ||||||
| import { Link } from "./Button.js"; | import { Link } from "./Button.js"; | ||||||
| import { SocialButtons, SocialButton } from "./Footer.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 { useLocation } from 'preact-iso'; | ||||||
| import DownloadButton from './DownloadButton.js'; | import DownloadButton from './DownloadButton.js'; | ||||||
| import githubIcon from "../assets/boxicons/bx-github.svg?raw"; | import githubIcon from "../assets/boxicons/bx-github.svg?raw"; | ||||||
| import Icon from "./Icon.js"; | import Icon from "./Icon.js"; | ||||||
| import logoPath from "../assets/icon-color.svg"; | import logoPath from "../assets/icon-color.svg"; | ||||||
| import menuIcon from "../assets/boxicons/bx-menu.svg?raw"; | import menuIcon from "../assets/boxicons/bx-menu.svg?raw"; | ||||||
|  | import { LocaleContext } from ".."; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | import { swapLocaleInUrl } from "../i18n"; | ||||||
| 
 | 
 | ||||||
| interface HeaderLink { | interface HeaderLink { | ||||||
|     url: string; |     url: string; | ||||||
| @ -15,21 +18,26 @@ interface HeaderLink { | |||||||
|     external?: boolean; |     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}) { | export function Header(props: {repoStargazersCount: number}) { | ||||||
| 	const { url } = useLocation(); | 	const { url } = useLocation(); | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const locale = useContext(LocaleContext); | ||||||
|     const [ mobileMenuShown, setMobileMenuShown ] = useState(false); |     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 ( | 	return ( | ||||||
| 		<header> | 		<header> | ||||||
|             <div class="content-wrapper"> |             <div class="content-wrapper"> | ||||||
|                 <div class="first-row"> |                 <div class="first-row"> | ||||||
|                     <a class="banner" href="/"> |                     <a class="banner" href={`/${locale}/`}> | ||||||
|                         <img src={logoPath} width="300" height="300" alt="Trilium Notes logo" /> <span>Trilium Notes</span> |                         <img src={logoPath} width="300" height="300" alt="Trilium Notes logo" /> <span>Trilium Notes</span> | ||||||
|                     </a> |                     </a> | ||||||
| 
 | 
 | ||||||
| @ -46,16 +54,17 @@ export function Header(props: {repoStargazersCount: number}) { | |||||||
|                 </div> |                 </div> | ||||||
| 
 | 
 | ||||||
|                 <nav className={`${mobileMenuShown ? "mobile-shown" : ""}`}> |                 <nav className={`${mobileMenuShown ? "mobile-shown" : ""}`}> | ||||||
|                     {HEADER_LINKS.map(link => ( |                     {headerLinks.map(link => { | ||||||
|                         <Link |                         const linkHref = link.external ? link.url : swapLocaleInUrl(link.url, locale); | ||||||
|                             href={link.url} |                         return (<Link | ||||||
|                             className={url === link.url ? "active" : ""} |                             href={linkHref} | ||||||
|  |                             className={url === linkHref ? "active" : ""} | ||||||
|                             openExternally={link.external} |                             openExternally={link.external} | ||||||
|                             onClick={() => { |                             onClick={() => { | ||||||
|                                 setMobileMenuShown(false); |                                 setMobileMenuShown(false); | ||||||
|                             }} |                             }} | ||||||
|                         >{link.text}</Link> |                         >{link.text}</Link>) | ||||||
|                     ))} |                     })} | ||||||
| 
 | 
 | ||||||
|                     <SocialButtons className="mobile-only" withText /> |                     <SocialButtons className="mobile-only" withText /> | ||||||
|                 </nav> |                 </nav> | ||||||
| @ -74,4 +83,4 @@ export function Header(props: {repoStargazersCount: number}) { | |||||||
|             </div> |             </div> | ||||||
| 		</header> | 		</header> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
							
								
								
									
										31
									
								
								apps/website/src/i18n.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/website/src/i18n.spec.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @ -1,19 +1,50 @@ | |||||||
| import { default as i18next } from "i18next"; | interface Locale { | ||||||
| import HttpApi from 'i18next-http-backend'; |     id: string; | ||||||
| import { initReactI18next } from "react-i18next"; |     name: string; | ||||||
|  |     rtl?: boolean; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| i18next | export const LOCALES: Locale[] = [ | ||||||
|     .use(HttpApi) |     { id: "en", name: "English" }, | ||||||
|     .use(initReactI18next); |     { 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({ | export function mapLocale(locale: string) { | ||||||
|     debug: true, |     if (!locale) return 'en'; | ||||||
|     lng: "en", |     const lower = locale.toLowerCase(); | ||||||
|     fallbackLng: "en", |  | ||||||
|     backend: { |  | ||||||
|         loadPath: "/translations/{{lng}}/{{ns}}.json", |  | ||||||
|     }, |  | ||||||
|     returnEmptyString: false |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| 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; | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,29 +2,78 @@ import './style.css'; | |||||||
| import { FALLBACK_STARGAZERS_COUNT, getRepoStargazersCount } from './github-utils.js'; | import { FALLBACK_STARGAZERS_COUNT, getRepoStargazersCount } from './github-utils.js'; | ||||||
| import { Header } from './components/Header.jsx'; | import { Header } from './components/Header.jsx'; | ||||||
| import { Home } from './pages/Home/index.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 { NotFound } from './pages/_404.jsx'; | ||||||
| import Footer from './components/Footer.js'; | 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 { 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}) { | export function App(props: {repoStargazersCount: number}) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<LocationProvider> | 		<LocationProvider> | ||||||
| 			<Header repoStargazersCount={props.repoStargazersCount} /> |             <LocaleProvider> | ||||||
| 			<main> |                 <Header repoStargazersCount={props.repoStargazersCount} /> | ||||||
| 				<Router> |                 <main> | ||||||
| 					<Route path="/" component={Home} /> |                     <Router> | ||||||
| 					<Route default component={NotFound} /> |                         <Route path="/" component={Home} /> | ||||||
|                     <Route path="/get-started" component={GetStarted} /> |                         <Route path="/get-started" component={GetStarted} /> | ||||||
|                     <Route path="/support-us" component={SupportUs} /> |                         <Route path="/support-us" component={SupportUs} /> | ||||||
| 				</Router> | 
 | ||||||
| 			</main> |                         <Route path="/:locale:/" component={Home} /> | ||||||
|             <Footer /> |                         <Route path="/:locale:/get-started" component={GetStarted} /> | ||||||
|  |                         <Route path="/:locale:/support-us" component={SupportUs} /> | ||||||
|  | 
 | ||||||
|  |                         <Route default component={NotFound} /> | ||||||
|  |                     </Router> | ||||||
|  |                 </main> | ||||||
|  |                 <Footer /> | ||||||
|  |             </LocaleProvider> | ||||||
| 		</LocationProvider> | 		</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') { | if (typeof window !== 'undefined') { | ||||||
| 	hydrate(<App repoStargazersCount={FALLBACK_STARGAZERS_COUNT} />, document.getElementById('app')!); | 	hydrate(<App repoStargazersCount={FALLBACK_STARGAZERS_COUNT} />, document.getElementById('app')!); | ||||||
| } | } | ||||||
|  | |||||||
| @ -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} | ||||||
|  | |||||||
| @ -57,6 +57,8 @@ section.hero-section { | |||||||
|             color: transparent; |             color: transparent; | ||||||
|             line-height: 1.1; |             line-height: 1.1; | ||||||
|             font-weight: 400; |             font-weight: 400; | ||||||
|  |             font-size: 2em; | ||||||
|  |             margin-block: 0.65em; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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,8 +129,9 @@ 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={t("note_types.title")}> | ||||||
|             <ListWithScreenshot horizontal items={[ |             <ListWithScreenshot horizontal items={[ | ||||||
|                 { |                 { | ||||||
|                     title: t("note_types.text_title"), |                     title: t("note_types.text_title"), | ||||||
| @ -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,8 +209,9 @@ function ExtensibilityBenefitsSection() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function CollectionsSection() { | function CollectionsSection() { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|     return ( |     return ( | ||||||
|         <Section className="collections" title="Collections"> |         <Section className="collections" title={t("collections.title")}> | ||||||
|             <ListWithScreenshot items={[ |             <ListWithScreenshot items={[ | ||||||
|                 { |                 { | ||||||
|                     title: t("collections.calendar_title"), |                     title: t("collections.calendar_title"), | ||||||
| @ -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> | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -31,7 +31,13 @@ html, | |||||||
| body { | body { | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     line-height: 1.5; |     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; |     min-height: 100vh; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,4 +14,7 @@ export default defineConfig({ | |||||||
| 			}, | 			}, | ||||||
| 		}), | 		}), | ||||||
| 	], | 	], | ||||||
|  |     test: { | ||||||
|  |         environment: "happy-dom" | ||||||
|  |     } | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
|     "desktop:start": "pnpm run --filter desktop dev", |     "desktop:start": "pnpm run --filter desktop dev", | ||||||
|     "desktop:build": "pnpm run --filter desktop build", |     "desktop:build": "pnpm run --filter desktop build", | ||||||
|     "desktop:start-prod": "pnpm run --filter desktop start-prod", |     "desktop:start-prod": "pnpm run --filter desktop start-prod", | ||||||
|  |     "website:start": "pnpm run --filter website dev", | ||||||
|     "website:build": "pnpm run --filter website build", |     "website:build": "pnpm run --filter website build", | ||||||
|     "electron:build": "pnpm desktop:build", |     "electron:build": "pnpm desktop:build", | ||||||
|     "electron:start": "pnpm desktop:start", |     "electron:start": "pnpm desktop:start", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran