mirror of
https://github.com/veeso/termscp.git
synced 2026-07-24 00:27:19 +02:00
refactor(site): english-only, remove i18n machinery
This commit is contained in:
@@ -1,9 +1,5 @@
|
||||
---
|
||||
import { localizePath, useTranslations, DOCS_URL, type Locale } from "../i18n/ui";
|
||||
interface Props { locale: Locale; }
|
||||
const { locale } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
const p = (sub: string) => localizePath(locale, sub);
|
||||
import { DOCS_URL } from "../consts";
|
||||
|
||||
const local = [
|
||||
{ name: " ..", size: "DIR", kind: "dir" },
|
||||
@@ -19,16 +15,17 @@ const remote = [
|
||||
{ name: " docker-compose.yml", size: "1.7 KB", kind: "file" },
|
||||
{ name: " logs/", size: "DIR", kind: "dir" },
|
||||
];
|
||||
const status = "↹ switch · ↑↓ move · ↵ open · SPACE transfer";
|
||||
---
|
||||
<section class="mx-auto max-w-5xl px-4 pt-12">
|
||||
<p class="text-green text-sm tracking-widest uppercase">{t("hero.kicker")}</p>
|
||||
<p class="text-green text-sm tracking-widest uppercase">// terminal file transfer</p>
|
||||
<div class="mt-4 overflow-hidden rounded-lg border border-line bg-mantle shadow-2xl">
|
||||
<div class="border-b border-line bg-crust px-3 py-2 text-xs text-overlay">
|
||||
{t("hero.tabs", { host: "192.168.1.10" })}<span class="blink ml-1"></span>
|
||||
termscp — 192.168.1.10 (sftp) connected<span class="blink ml-1"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 bg-base">
|
||||
<div>
|
||||
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-teal">{t("hero.local")}</div>
|
||||
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-teal">/home/veeso (local)</div>
|
||||
{local.map((r) => (
|
||||
<div class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${r.kind === "dir" ? "text-blue" : "text-text"}`}>
|
||||
<span class="truncate min-w-0">{r.name}</span><span class="text-overlay shrink-0">{r.size}</span>
|
||||
@@ -36,7 +33,7 @@ const remote = [
|
||||
))}
|
||||
</div>
|
||||
<div class="border-l border-line">
|
||||
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-mauve">{t("hero.remote")}</div>
|
||||
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-mauve">/var/www (remote)</div>
|
||||
{remote.map((r) => (
|
||||
<div class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${
|
||||
r.kind === "sel" ? "bg-blue text-base font-bold" : r.kind === "dir" ? "text-blue" : "text-text"
|
||||
@@ -47,17 +44,17 @@ const remote = [
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* set:html is safe: status is a static author-controlled string, never user input. */}
|
||||
<div class="flex justify-between border-t border-line bg-crust px-3 py-1.5 text-xs text-overlay">
|
||||
{/* set:html is safe: hero.status is author-controlled i18n content, never user input. Do not interpolate untrusted data here. */}
|
||||
<span set:html={t("hero.status").replace(/(↹|↑↓|↵|SPACE)/g, '<span class="text-yellow">$1</span>')}></span>
|
||||
<span set:html={status.replace(/(↹|↑↓|↵|SPACE)/g, '<span class="text-yellow">$1</span>')}></span>
|
||||
<span>4 files · 1.2 GB</span>
|
||||
</div>
|
||||
<div class="bg-base px-6 py-6 text-center">
|
||||
<h1 class="text-2xl font-bold text-text">{t("hero.title")}</h1>
|
||||
<p class="mx-auto mt-2 max-w-xl text-subtext">{t("hero.subtitle")}</p>
|
||||
<h1 class="text-2xl font-bold text-text">The dual-pane explorer for your terminal</h1>
|
||||
<p class="mx-auto mt-2 max-w-xl text-subtext">Local left, remote right. Transfer, sync, edit — never leave the keyboard.</p>
|
||||
<div class="mt-5 flex flex-wrap justify-center gap-3">
|
||||
<a href={p("/install")} class="rounded-lg bg-green px-5 py-2.5 font-bold text-crust">{t("cta.install")}</a>
|
||||
<a href={DOCS_URL} class="rounded-lg border border-line px-5 py-2.5 text-text">{t("cta.manual")}</a>
|
||||
<a href="/install" class="rounded-lg bg-green px-5 py-2.5 font-bold text-crust">Install termscp →</a>
|
||||
<a href={DOCS_URL} class="rounded-lg border border-line px-5 py-2.5 text-text">User manual</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
---
|
||||
import { useTranslations, DOCS_URL, type Locale } from "../i18n/ui";
|
||||
interface Props { locale: Locale; }
|
||||
const { locale } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
import { DOCS_URL, GITHUB_URL } from "../consts";
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
<footer class="mt-auto border-t border-line bg-mantle">
|
||||
<div class="mx-auto flex max-w-5xl flex-col items-center gap-2 px-4 py-8 text-sm text-overlay">
|
||||
<div class="flex gap-5">
|
||||
<a href="https://github.com/veeso/termscp" class="hover:text-text">GitHub</a>
|
||||
<a href={GITHUB_URL} class="hover:text-text">GitHub</a>
|
||||
<a href="https://crates.io/crates/termscp" class="hover:text-text">crates.io</a>
|
||||
<a href={DOCS_URL} class="hover:text-text">{t("nav.manual")}</a>
|
||||
<a href="https://ko-fi.com/veeso" class="hover:text-text">{t("footer.support")}</a>
|
||||
<a href={DOCS_URL} class="hover:text-text">User manual</a>
|
||||
<a href="https://ko-fi.com/veeso" class="hover:text-text">Support me</a>
|
||||
</div>
|
||||
<p>© {year} Christian Visintin · {t("footer.rights")}</p>
|
||||
<p>© {year} Christian Visintin · Released under the MIT license.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
import { locales, localizePath, type Locale } from "../i18n/ui";
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
path: string; // unprefixed path, e.g. "/install" or "/"
|
||||
}
|
||||
const { locale, path } = Astro.props;
|
||||
const labels: Record<Locale, string> = {
|
||||
en: "EN",
|
||||
"zh-CN": "中文",
|
||||
it: "IT",
|
||||
fr: "FR",
|
||||
es: "ES",
|
||||
};
|
||||
const href = (l: Locale) => localizePath(l, path);
|
||||
---
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
{locales.map((l) => (
|
||||
<a
|
||||
href={href(l)}
|
||||
class={l === locale ? "text-green" : "text-overlay hover:text-text"}
|
||||
>{labels[l]}</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -1,29 +1,19 @@
|
||||
---
|
||||
import { localizePath, useTranslations, DOCS_URL, type Locale } from "../i18n/ui";
|
||||
import ThemeToggle from "./ThemeToggle.astro";
|
||||
import LangPicker from "./LangPicker.astro";
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
path: string;
|
||||
}
|
||||
const { locale, path } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
const p = (sub: string) => localizePath(locale, sub);
|
||||
import { DOCS_URL, GITHUB_URL } from "../consts";
|
||||
---
|
||||
<header class="sticky top-0 z-50 border-b border-line bg-mantle/90 backdrop-blur">
|
||||
<nav class="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
|
||||
<a href={p("/")} class="flex items-center gap-2 text-text">
|
||||
<a href="/" class="flex items-center gap-2 text-text">
|
||||
<img src="/assets/images/termscp.webp" alt="termscp" class="h-7 w-7" />
|
||||
<span class="font-bold">termscp</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden items-center gap-5 text-sm sm:flex">
|
||||
<a href={p("/install")} class="text-overlay hover:text-text">{t("nav.install")}</a>
|
||||
<a href={DOCS_URL} class="text-overlay hover:text-text">{t("nav.manual")}</a>
|
||||
<a href="https://github.com/veeso/termscp" class="text-overlay hover:text-text">{t("nav.github")}</a>
|
||||
<LangPicker locale={locale} path={path} />
|
||||
<a href="/install" class="text-overlay hover:text-text">Install</a>
|
||||
<a href={DOCS_URL} class="text-overlay hover:text-text">User manual</a>
|
||||
<a href={GITHUB_URL} class="text-overlay hover:text-text">GitHub</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
@@ -31,10 +21,9 @@ const p = (sub: string) => localizePath(locale, sub);
|
||||
<details class="relative sm:hidden">
|
||||
<summary class="cursor-pointer list-none rounded p-2 text-text hover:bg-mantle" aria-label="Menu">☰</summary>
|
||||
<div class="absolute right-0 mt-2 flex w-48 flex-col gap-3 rounded-lg border border-line bg-mantle p-4 text-sm shadow-xl">
|
||||
<a href={p("/install")} class="text-overlay hover:text-text">{t("nav.install")}</a>
|
||||
<a href={DOCS_URL} class="text-overlay hover:text-text">{t("nav.manual")}</a>
|
||||
<a href="https://github.com/veeso/termscp" class="text-overlay hover:text-text">{t("nav.github")}</a>
|
||||
<LangPicker locale={locale} path={path} />
|
||||
<a href="/install" class="text-overlay hover:text-text">Install</a>
|
||||
<a href={DOCS_URL} class="text-overlay hover:text-text">User manual</a>
|
||||
<a href={GITHUB_URL} class="text-overlay hover:text-text">GitHub</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
---
|
||||
import { useTranslations, type Locale } from "../i18n/ui";
|
||||
interface Props { locale: Locale; }
|
||||
const { locale } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
const protocols = ["SCP", "SFTP", "FTP/S", "S3", "Kube", "SMB", "WebDAV"];
|
||||
---
|
||||
<section class="mx-auto max-w-5xl px-4 py-12 text-center">
|
||||
<h2 class="text-overlay text-sm uppercase tracking-widest">{t("protocols.heading")}</h2>
|
||||
<h2 class="text-overlay text-sm uppercase tracking-widest">Speaks every protocol</h2>
|
||||
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
||||
{protocols.map((proto) => (
|
||||
<span class="rounded-full border border-line px-4 py-1 text-sm text-text">{proto}</span>
|
||||
|
||||
2
site/src/consts.ts
Normal file
2
site/src/consts.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DOCS_URL = "https://docs.termscp.rs";
|
||||
export const GITHUB_URL = "https://github.com/veeso/termscp";
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"nav.home": "Home",
|
||||
"nav.install": "Install",
|
||||
"nav.manual": "User manual",
|
||||
"nav.github": "GitHub",
|
||||
"hero.tabs": "termscp — {host} (sftp) connected",
|
||||
"hero.local": "/home/veeso (local)",
|
||||
"hero.remote": "/var/www (remote)",
|
||||
"hero.status": "↹ switch · ↑↓ move · ↵ open · SPACE transfer",
|
||||
"hero.title": "The dual-pane explorer for your terminal",
|
||||
"hero.subtitle": "Local left, remote right. Transfer, sync, edit — never leave the keyboard.",
|
||||
"hero.kicker": "// terminal file transfer",
|
||||
"cta.install": "Install termscp →",
|
||||
"cta.manual": "User manual",
|
||||
"features.heading": "Why termscp",
|
||||
"features.handy.title": "Handy UI",
|
||||
"features.handy.body": "Explore and operate on the remote and local file system with a handy UI.",
|
||||
"features.cross.title": "Cross platform",
|
||||
"features.cross.body": "Runs on Windows, macOS, Linux and BSD.",
|
||||
"features.custom.title": "Customizable",
|
||||
"features.custom.body": "Customize the explorer, the text editor and default options.",
|
||||
"features.bookmarks.title": "Bookmarks",
|
||||
"features.bookmarks.body": "Connect to favourite hosts via bookmarks and recent connections.",
|
||||
"features.security.title": "Security first",
|
||||
"features.security.body": "Store passwords in your operating system key vault.",
|
||||
"features.performance.title": "Eye on performance",
|
||||
"features.performance.body": "Built with an eye on performance to keep CPU usage low.",
|
||||
"protocols.heading": "Speaks every protocol",
|
||||
"install.heading": "Install termscp",
|
||||
"install.subtitle": "Pick your platform. One command and you're in.",
|
||||
"footer.support": "Support me",
|
||||
"footer.rights": "Released under the MIT license.",
|
||||
"lang.label": "Language"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"nav.home": "Inicio",
|
||||
"nav.install": "Instalar",
|
||||
"nav.manual": "Manual de usuario",
|
||||
"nav.github": "GitHub",
|
||||
"hero.tabs": "termscp — {host} (sftp) conectado",
|
||||
"hero.local": "/home/veeso (local)",
|
||||
"hero.remote": "/var/www (remoto)",
|
||||
"hero.status": "↹ cambiar · ↑↓ mover · ↵ abrir · ESPACIO transferir",
|
||||
"hero.title": "El explorador de doble panel para tu terminal",
|
||||
"hero.subtitle": "Local a la izquierda, remoto a la derecha. Transfiere, sincroniza, edita — sin soltar el teclado.",
|
||||
"hero.kicker": "// transferencia de archivos en terminal",
|
||||
"cta.install": "Instalar termscp →",
|
||||
"cta.manual": "Manual de usuario",
|
||||
"features.heading": "Por qué termscp",
|
||||
"features.handy.title": "Interfaz práctica",
|
||||
"features.handy.body": "Explora y opera en el sistema de archivos remoto y local con una interfaz práctica.",
|
||||
"features.cross.title": "Multiplataforma",
|
||||
"features.cross.body": "Funciona en Windows, macOS, Linux y BSD.",
|
||||
"features.custom.title": "Personalizable",
|
||||
"features.custom.body": "Personaliza el explorador, el editor de texto y las opciones predeterminadas.",
|
||||
"features.bookmarks.title": "Marcadores",
|
||||
"features.bookmarks.body": "Conéctate a tus hosts favoritos mediante marcadores y conexiones recientes.",
|
||||
"features.security.title": "Seguridad primero",
|
||||
"features.security.body": "Guarda tus contraseñas en la bóveda de claves de tu sistema operativo.",
|
||||
"features.performance.title": "Ojo en el rendimiento",
|
||||
"features.performance.body": "Desarrollado teniendo en cuenta el rendimiento para mantener bajo el uso de CPU.",
|
||||
"protocols.heading": "Habla todos los protocolos",
|
||||
"install.heading": "Instalar termscp",
|
||||
"install.subtitle": "Elige tu plataforma. Un comando y listo.",
|
||||
"footer.support": "Apóyame",
|
||||
"footer.rights": "Publicado bajo la licencia MIT.",
|
||||
"lang.label": "Idioma"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"nav.home": "Accueil",
|
||||
"nav.install": "Installer",
|
||||
"nav.github": "GitHub",
|
||||
"hero.tabs": "termscp — {host} (sftp) connecté",
|
||||
"hero.local": "/home/veeso (local)",
|
||||
"hero.remote": "/var/www (distant)",
|
||||
"hero.status": "↹ basculer · ↑↓ déplacer · ↵ ouvrir · ESPACE transférer",
|
||||
"hero.title": "L'explorateur à deux panneaux pour votre terminal",
|
||||
"hero.subtitle": "Local à gauche, distant à droite. Transférez, synchronisez, éditez — sans quitter le clavier.",
|
||||
"hero.kicker": "// transfert de fichiers en terminal",
|
||||
"cta.install": "Installer termscp →",
|
||||
"cta.manual": "Manuel d'utilisateur",
|
||||
"features.heading": "Pourquoi termscp",
|
||||
"features.handy.title": "Interface pratique",
|
||||
"features.handy.body": "Explorez et utilisez le système de fichiers distant et local avec une interface pratique.",
|
||||
"features.cross.title": "Multi-plateforme",
|
||||
"features.cross.body": "Fonctionne sur Windows, macOS, Linux et BSD.",
|
||||
"features.custom.title": "Personnalisable",
|
||||
"features.custom.body": "Personnalisez l'explorateur, l'éditeur de texte et les options par défaut.",
|
||||
"features.bookmarks.title": "Signets",
|
||||
"features.bookmarks.body": "Connectez-vous à vos hôtes préférés via des signets et des connexions récentes.",
|
||||
"features.security.title": "Sécurité avant tout",
|
||||
"features.security.body": "Enregistrez vos mots de passe dans le coffre-fort de votre système.",
|
||||
"features.performance.title": "Regard sur les performances",
|
||||
"features.performance.body": "Développé en gardant un œil sur les performances pour éviter une utilisation élevée du processeur.",
|
||||
"protocols.heading": "Parle tous les protocoles",
|
||||
"install.heading": "Installer termscp",
|
||||
"install.subtitle": "Choisissez votre plateforme. Une commande et c'est parti.",
|
||||
"footer.support": "Me soutenir",
|
||||
"footer.rights": "Publié sous licence MIT.",
|
||||
"lang.label": "Langue"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"nav.home": "Home",
|
||||
"nav.install": "Installa",
|
||||
"nav.manual": "Manuale utente",
|
||||
"nav.github": "GitHub",
|
||||
"hero.tabs": "termscp — {host} (sftp) connesso",
|
||||
"hero.local": "/home/veeso (locale)",
|
||||
"hero.remote": "/var/www (remoto)",
|
||||
"hero.status": "↹ cambia · ↑↓ muovi · ↵ apri · SPAZIO trasferisci",
|
||||
"hero.title": "L'explorer a doppio pannello per il tuo terminale",
|
||||
"hero.subtitle": "Locale a sinistra, remoto a destra. Trasferisci, sincronizza, modifica — senza mai lasciare la tastiera.",
|
||||
"hero.kicker": "// file transfer da terminale",
|
||||
"cta.install": "Installa termscp →",
|
||||
"cta.manual": "Manuale utente",
|
||||
"features.heading": "Perché termscp",
|
||||
"features.handy.title": "UI ergonomica",
|
||||
"features.handy.body": "Naviga e lavora sul file system remoto e locale attraverso una UI di facile utilizzo.",
|
||||
"features.cross.title": "Multi piattaforma",
|
||||
"features.cross.body": "Gira su Windows, macOS, Linux e BSD.",
|
||||
"features.custom.title": "Personalizzabile",
|
||||
"features.custom.body": "Personalizza l'explorer, l'editor di testo e le opzioni predefinite.",
|
||||
"features.bookmarks.title": "Preferiti",
|
||||
"features.bookmarks.body": "Connettiti agli host preferiti tramite preferiti e connessioni recenti.",
|
||||
"features.security.title": "Sicurezza in prima classe",
|
||||
"features.security.body": "Salva le password nel vault del tuo sistema operativo.",
|
||||
"features.performance.title": "Focus sulle performance",
|
||||
"features.performance.body": "Sviluppato con un occhio alle performance per mantenere basso l'uso della CPU.",
|
||||
"protocols.heading": "Parla ogni protocollo",
|
||||
"install.heading": "Installa termscp",
|
||||
"install.subtitle": "Scegli la tua piattaforma. Un comando e sei dentro.",
|
||||
"footer.support": "Supportami",
|
||||
"footer.rights": "Rilasciato sotto licenza MIT.",
|
||||
"lang.label": "Lingua"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useTranslations, locales, defaultLocale } from "./ui";
|
||||
|
||||
describe("i18n", () => {
|
||||
it("exposes the five supported locales with en default", () => {
|
||||
expect(locales).toEqual(["en", "zh-CN", "it", "fr", "es"]);
|
||||
expect(defaultLocale).toBe("en");
|
||||
});
|
||||
|
||||
it("resolves a key in the requested locale", () => {
|
||||
const t = useTranslations("it");
|
||||
expect(t("nav.install")).toBe("Installa");
|
||||
});
|
||||
|
||||
it("falls back to en when a key is missing in the locale", () => {
|
||||
const t = useTranslations("fr");
|
||||
expect(t("nav.manual")).toBe("User manual");
|
||||
});
|
||||
|
||||
it("returns the key itself when missing everywhere", () => {
|
||||
const t = useTranslations("en");
|
||||
expect(t("does.not.exist")).toBe("does.not.exist");
|
||||
});
|
||||
|
||||
it("interpolates {vars}", () => {
|
||||
const t = useTranslations("en");
|
||||
expect(t("hero.tabs", { host: "1.2.3.4" })).toContain("1.2.3.4");
|
||||
});
|
||||
|
||||
it("interpolates the full string exactly", () => {
|
||||
const t = useTranslations("en");
|
||||
expect(t("hero.tabs", { host: "10.0.0.1" })).toBe(
|
||||
"termscp — 10.0.0.1 (sftp) connected",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import en from "./en.json";
|
||||
import zhCN from "./zh-CN.json";
|
||||
import it from "./it.json";
|
||||
import fr from "./fr.json";
|
||||
import es from "./es.json";
|
||||
|
||||
/** External documentation site (mdbook), deployed separately. */
|
||||
export const DOCS_URL = "https://docs.termscp.rs";
|
||||
|
||||
export const locales = ["en", "zh-CN", "it", "fr", "es"] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
export const defaultLocale: Locale = "en";
|
||||
|
||||
/** Authoritative set of translation keys, derived from the en dictionary. */
|
||||
export type TranslationKey = keyof typeof en;
|
||||
|
||||
// `en` is left uncast so it remains the source of truth for `TranslationKey`.
|
||||
// Other locales may omit keys, so they are typed as partial dictionaries.
|
||||
const dictionaries = {
|
||||
en,
|
||||
"zh-CN": zhCN as Partial<Record<TranslationKey, string>>,
|
||||
it: it as Partial<Record<TranslationKey, string>>,
|
||||
fr: fr as Partial<Record<TranslationKey, string>>,
|
||||
es: es as Partial<Record<TranslationKey, string>>,
|
||||
} satisfies Record<Locale, Partial<Record<TranslationKey, string>>>;
|
||||
|
||||
export function isLocale(value: string): value is Locale {
|
||||
return (locales as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Prefix a path with the locale, except for the default locale. */
|
||||
export function localizePath(locale: Locale, path: string): string {
|
||||
return locale === defaultLocale ? path : `/${locale}${path}`;
|
||||
}
|
||||
|
||||
/** Resolve a translator for `locale`, falling back to en, then to the key. */
|
||||
export function useTranslations(locale: Locale) {
|
||||
const dict = dictionaries[locale] ?? dictionaries[defaultLocale];
|
||||
// `TranslationKey | (string & {})` keeps autocomplete for known keys while
|
||||
// still accepting arbitrary strings, preserving the "return key if missing"
|
||||
// contract at runtime.
|
||||
return (
|
||||
key: TranslationKey | (string & {}),
|
||||
vars?: Record<string, string>,
|
||||
): string => {
|
||||
let value =
|
||||
dict[key as TranslationKey] ??
|
||||
dictionaries[defaultLocale][key as TranslationKey] ??
|
||||
key;
|
||||
if (vars) {
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
value = value.replaceAll(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"nav.home": "主页",
|
||||
"nav.install": "安装",
|
||||
"nav.manual": "用户手册",
|
||||
"nav.github": "GitHub",
|
||||
"hero.tabs": "termscp — {host} (sftp) 已连接",
|
||||
"hero.local": "/home/veeso (本地)",
|
||||
"hero.remote": "/var/www (远程)",
|
||||
"hero.status": "↹ 切换 · ↑↓ 移动 · ↵ 打开 · 空格 传输",
|
||||
"hero.title": "为你的终端打造的双栏文件浏览器",
|
||||
"hero.subtitle": "左边本地,右边远程。传输、同步、编辑 — 始终不离键盘。",
|
||||
"hero.kicker": "// 终端文件传输",
|
||||
"cta.install": "安装 termscp →",
|
||||
"cta.manual": "用户手册",
|
||||
"features.heading": "为什么选择 termscp",
|
||||
"features.handy.title": "方便的用户界面",
|
||||
"features.handy.body": "使用方便的 UI 在远程和本地文件系统上探索和操作。",
|
||||
"features.cross.title": "跨平台",
|
||||
"features.cross.body": "在 Windows、macOS、Linux 和 BSD 上运行。",
|
||||
"features.custom.title": "可定制",
|
||||
"features.custom.body": "自定义文件浏览器、文本编辑器和默认选项。",
|
||||
"features.bookmarks.title": "书签",
|
||||
"features.bookmarks.body": "通过书签和最近的连接连接到你最喜欢的主机。",
|
||||
"features.security.title": "安全第一",
|
||||
"features.security.body": "将你的密码保存到操作系统的密钥保管库中。",
|
||||
"features.performance.title": "关注性能",
|
||||
"features.performance.body": "在开发时关注性能,以保持较低的 CPU 使用率。",
|
||||
"protocols.heading": "支持各种协议",
|
||||
"install.heading": "安装 termscp",
|
||||
"install.subtitle": "选择你的平台。一条命令即可上手。",
|
||||
"footer.support": "支持我",
|
||||
"footer.rights": "基于 MIT 许可证发布。",
|
||||
"lang.label": "语言"
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
---
|
||||
import "../styles/theme.css";
|
||||
import { defaultLocale, locales, type Locale } from "../i18n/ui";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
locale?: Locale;
|
||||
/** Path WITHOUT locale prefix, e.g. "/install". "/" for home. */
|
||||
/** Path for canonical/og, e.g. "/install" or "/". */
|
||||
path: string;
|
||||
}
|
||||
|
||||
const { title, description, locale = defaultLocale, path } = Astro.props;
|
||||
const { title, description, path } = Astro.props;
|
||||
const site = Astro.site?.href ?? "https://termscp.rs/";
|
||||
const localizedPath = locale === defaultLocale ? path : `/${locale}${path}`;
|
||||
const canonical = new URL(localizedPath, site).href;
|
||||
const canonical = new URL(path, site).href;
|
||||
const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={locale} data-theme="frappe">
|
||||
<html lang="en" data-theme="frappe">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -26,16 +23,6 @@ const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- hreflang alternates -->
|
||||
{locales.map((l) => (
|
||||
<link
|
||||
rel="alternate"
|
||||
hreflang={l}
|
||||
href={new URL(l === defaultLocale ? path : `/${l}${path}`, site).href}
|
||||
/>
|
||||
))}
|
||||
<link rel="alternate" hreflang="x-default" href={new URL(path, site).href} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="termscp" />
|
||||
@@ -43,11 +30,11 @@ const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:alt" content="termscp — terminal file transfer and explorer" />
|
||||
<meta property="og:image:width" content="1280" />
|
||||
<meta property="og:image:height" content="640" />
|
||||
<meta property="og:image:alt" content="termscp — terminal file transfer and explorer" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
@@ -61,7 +48,7 @@ const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
|
||||
<!-- Font preload -->
|
||||
<link rel="preload" href="/fonts/jetbrains-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<!-- No-flash theme bootstrap. Contract: localStorage 'theme' is "frappe" | "latte"; the ThemeToggle component writes the same values. -->
|
||||
<!-- No-flash theme bootstrap. Contract: localStorage 'theme' is "frappe" | "latte"; ThemeToggle writes the same. -->
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const t = localStorage.getItem("theme") ||
|
||||
|
||||
@@ -5,30 +5,33 @@ import Footer from "../components/Footer.astro";
|
||||
import ExplorerHero from "../components/ExplorerHero.astro";
|
||||
import ProtocolStrip from "../components/ProtocolStrip.astro";
|
||||
import FeatureCard from "../components/FeatureCard.astro";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
|
||||
const locale = "en" as const;
|
||||
const t = useTranslations(locale);
|
||||
const features = ["handy", "cross", "custom", "bookmarks", "security", "performance"];
|
||||
const features = [
|
||||
{ title: "Handy UI", body: "Explore and operate on the remote and local file system with a handy UI." },
|
||||
{ title: "Cross platform", body: "Runs on Windows, macOS, Linux and BSD." },
|
||||
{ title: "Customizable", body: "Customize the explorer, the text editor and default options." },
|
||||
{ title: "Bookmarks", body: "Connect to favourite hosts via bookmarks and recent connections." },
|
||||
{ title: "Security first", body: "Store passwords in your operating system key vault." },
|
||||
{ title: "Eye on performance", body: "Built with an eye on performance to keep CPU usage low." },
|
||||
];
|
||||
---
|
||||
<Base
|
||||
title="termscp — terminal file transfer & explorer for SCP/SFTP/FTP/S3/Kube/SMB/WebDAV"
|
||||
description="A feature-rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB."
|
||||
locale={locale}
|
||||
path="/"
|
||||
>
|
||||
<Nav locale={locale} path="/" />
|
||||
<Nav />
|
||||
<main class="flex-1">
|
||||
<ExplorerHero locale={locale} />
|
||||
<ExplorerHero />
|
||||
<section class="mx-auto max-w-5xl px-4 py-12">
|
||||
<h2 class="mb-6 text-center text-xl text-text">{t("features.heading")}</h2>
|
||||
<h2 class="mb-6 text-center text-xl text-text">Why termscp</h2>
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((f) => (
|
||||
<FeatureCard title={t(`features.${f}.title`)} body={t(`features.${f}.body`)} />
|
||||
<FeatureCard title={f.title} body={f.body} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<ProtocolStrip locale={locale} />
|
||||
<ProtocolStrip />
|
||||
</main>
|
||||
<Footer locale={locale} />
|
||||
<Footer />
|
||||
</Base>
|
||||
|
||||
@@ -3,14 +3,10 @@ import Base from "../layouts/Base.astro";
|
||||
import Nav from "../components/Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import InstallTabs from "../components/InstallTabs.astro";
|
||||
import { useTranslations, DOCS_URL } from "../i18n/ui";
|
||||
|
||||
const locale = "en" as const;
|
||||
const t = useTranslations(locale);
|
||||
import { DOCS_URL } from "../consts";
|
||||
|
||||
const script = "curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh";
|
||||
|
||||
// Verified package managers (see repo README). Cargo is universal.
|
||||
const methods = [
|
||||
{ id: "cargo", label: "Cargo", cmd: "cargo install termscp --locked" },
|
||||
{ id: "arch", label: "Arch Linux", cmd: "pacman -S termscp" },
|
||||
@@ -21,15 +17,13 @@ const methods = [
|
||||
<Base
|
||||
title="Install termscp"
|
||||
description="Install termscp on Linux, macOS, BSD and Windows — one shell command, or via Cargo, pacman, pkgin and Chocolatey."
|
||||
locale={locale}
|
||||
path="/install"
|
||||
>
|
||||
<Nav locale={locale} path="/install" />
|
||||
<Nav />
|
||||
<main class="mx-auto w-full max-w-3xl flex-1 px-4 py-12">
|
||||
<h1 class="text-2xl font-bold text-text">{t("install.heading")}</h1>
|
||||
<p class="mt-2 text-subtext">{t("install.subtitle")}</p>
|
||||
<h1 class="text-2xl font-bold text-text">Install termscp</h1>
|
||||
<p class="mt-2 text-subtext">Pick your platform. One command and you're in.</p>
|
||||
|
||||
<!-- Primary: install script -->
|
||||
<div class="mt-6 rounded-lg border border-line bg-mantle p-1">
|
||||
<div class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay">
|
||||
<span class="text-green">$</span> Linux · macOS · BSD
|
||||
@@ -38,17 +32,14 @@ const methods = [
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-overlay">Requires <span class="text-text">curl</span>. macOS uses Homebrew if available, otherwise builds from source.</p>
|
||||
|
||||
<!-- Secondary: package managers -->
|
||||
<h2 class="mt-10 mb-3 text-lg text-text">Package managers</h2>
|
||||
<InstallTabs methods={methods} />
|
||||
|
||||
<p class="mt-8 text-sm text-overlay">
|
||||
Update anytime with <span class="text-green">termscp --update</span>.
|
||||
</p>
|
||||
<p class="mt-8 text-sm text-overlay">Update anytime with <span class="text-green">termscp --update</span>.</p>
|
||||
<p class="mt-2 text-sm text-overlay">
|
||||
Linux build deps: <span class="text-text">libdbus-1</span>, <span class="text-text">pkg-config</span>, <span class="text-text">libsmbclient</span>.
|
||||
More details in the <a href={DOCS_URL} class="text-blue hover:underline">docs</a>.
|
||||
</p>
|
||||
</main>
|
||||
<Footer locale={locale} />
|
||||
<Footer />
|
||||
</Base>
|
||||
|
||||
Reference in New Issue
Block a user