mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 03:29:02 +01:00 
			
		
		
		
	Internationalization improvements for the website (#7515)
This commit is contained in:
		
						commit
						252f8ccb1f
					
				| @ -5,6 +5,7 @@ | ||||
| 	"scripts": { | ||||
| 		"dev": "vite", | ||||
| 		"build": "vite build", | ||||
| 		"test": "vitest", | ||||
| 		"preview": "pnpm build && vite preview" | ||||
| 	}, | ||||
| 	"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." | ||||
|   }, | ||||
|   "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" | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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" />} | ||||
|  | ||||
| @ -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")} | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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" /> <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> | ||||
| @ -74,4 +83,4 @@ export function Header(props: {repoStargazersCount: number}) { | ||||
|             </div> | ||||
| 		</header> | ||||
| 	); | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										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"; | ||||
| 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; | ||||
| } | ||||
|  | ||||
| @ -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')!); | ||||
| } | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -57,6 +57,8 @@ section.hero-section { | ||||
|             color: transparent; | ||||
|             line-height: 1.1; | ||||
|             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 { 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> | ||||
|  | ||||
| @ -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 ( | ||||
|  | ||||
| @ -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 ( | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -14,4 +14,7 @@ export default defineConfig({ | ||||
| 			}, | ||||
| 		}), | ||||
| 	], | ||||
|     test: { | ||||
|         environment: "happy-dom" | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran