feat(site): i18n string resolver with en fallback

This commit is contained in:
Christian Visintin
2026-06-07 21:23:25 +02:00
parent 02fec64d35
commit f544a63d19
9 changed files with 241 additions and 0 deletions

34
site/src/i18n/en.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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"
}

34
site/src/i18n/es.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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"
}

33
site/src/i18n/fr.json Normal file
View File

@@ -0,0 +1,33 @@
{
"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"
}

34
site/src/i18n/it.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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"
}

29
site/src/i18n/ui.test.ts Normal file
View File

@@ -0,0 +1,29 @@
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");
});
});

35
site/src/i18n/ui.ts Normal file
View File

@@ -0,0 +1,35 @@
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";
export const locales = ["en", "zh-CN", "it", "fr", "es"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en";
const dictionaries: Record<Locale, Record<string, string>> = {
en: en as Record<string, string>,
"zh-CN": zhCN as Record<string, string>,
it: it as Record<string, string>,
fr: fr as Record<string, string>,
es: es as Record<string, string>,
};
export function isLocale(value: string): value is Locale {
return (locales as readonly string[]).includes(value);
}
/** Resolve a translator for `locale`, falling back to en, then to the key. */
export function useTranslations(locale: Locale) {
const dict = dictionaries[locale] ?? dictionaries[defaultLocale];
return (key: string, vars?: Record<string, string>): string => {
let value = dict[key] ?? dictionaries[defaultLocale][key] ?? key;
if (vars) {
for (const [k, v] of Object.entries(vars)) {
value = value.replaceAll(`{${k}}`, v);
}
}
return value;
};
}

34
site/src/i18n/zh-CN.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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": "语言"
}

View File

@@ -1,5 +1,8 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

5
site/vitest.config.ts Normal file
View File

@@ -0,0 +1,5 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { include: ["src/**/*.test.ts", "scripts/**/*.test.ts"] },
});