Internationalization: 7 languages with live switching

Custom signal-based runtime (no i18n dependency): shared src/shared/i18n
dictionaries, ui I18nService.t() reactive by construction so language
switches live, and a backend i18n.ts for native menus + thrown errors.
Every user-facing string keyed across templates, toasts, dialogs, file
kinds, columns, macOS menu/context menus, and errors. Language picker in
Options (auto-detects OS locale, falls back to English); switching
relabels the webview and native chrome instantly. English/Spanish/French/
German fully drafted; Chinese/Japanese/Basque drafted, flagged for native
review. Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:52:54 +02:00
co-authored by Claude Fable 5
parent d8cfc54049
commit a0f56822cd
43 changed files with 2755 additions and 322 deletions
+29 -35
View File
@@ -1,25 +1,21 @@
import { ContextMenu, Utils, type ApplicationMenuItemConfig } from "electrobun/bun";
import type { ColumnKey } from "../shared/types";
import { t } from "./i18n";
/** Optional list columns, in display order (Name is fixed). */
const COLUMNS: { key: ColumnKey; label: string }[] = [
{ key: "size", label: "Size" },
{ key: "mtime", label: "Modified" },
{ key: "kind", label: "Kind" },
{ key: "rights", label: "Rights" },
{ key: "owner", label: "Owner" },
{ key: "group", label: "Group" },
];
const COLUMN_KEYS: ColumnKey[] = ["size", "mtime", "kind", "rights", "owner", "group"];
// PORTABILITY: shell-integration wording per OS
// PORTABILITY: shell-integration wording per OS (resolved at show() for locale)
const IS_WIN = process.platform === "win32";
const REVEAL_LABEL =
process.platform === "darwin"
? "Reveal in Finder"
: IS_WIN
? "Show in Explorer"
: "Show in File Manager";
const TRASH_LABEL = IS_WIN ? "Move to Recycle Bin" : "Move to Trash";
const revealLabel = (): string =>
t(
process.platform === "darwin"
? "ctx.revealFinder"
: IS_WIN
? "ctx.revealExplorer"
: "ctx.revealFileManager",
);
const trashLabel = (): string => t(IS_WIN ? "ctx.moveToRecycleBin" : "ctx.moveToTrash");
export interface RowMenuContext {
path: string;
@@ -75,8 +71,8 @@ export class PanelContextMenu {
/** Column picker for the list headers: checkable toggles per column. */
showColumns(visible: ColumnKey[]): void {
ContextMenu.showContextMenu(
COLUMNS.map(({ key, label }) => ({
label,
COLUMN_KEYS.map((key) => ({
label: t(`columns.${key}`),
action: `col:${key}`,
checked: visible.includes(key),
})),
@@ -88,47 +84,45 @@ export class PanelContextMenu {
if (!ctx.hasEntry || ctx.name === "..") {
// empty space (or the parent row): folder-level actions
items.push({ label: "New Folder", action: "ctx-new-folder" });
items.push({ label: "New File", action: "ctx-new-file" });
items.push({ label: t("ctx.newFolder"), action: "ctx-new-folder" });
items.push({ label: t("ctx.newFile"), action: "ctx-new-file" });
items.push({ type: "separator" });
items.push({ label: "Refresh", action: "ctx-refresh" });
items.push({ label: t("ctx.refresh"), action: "ctx-refresh" });
if (ctx.isLocal) {
items.push({ type: "separator" });
items.push({ label: REVEAL_LABEL, action: "ctx-reveal" });
items.push({ label: revealLabel(), action: "ctx-reveal" });
}
return items;
}
if (ctx.type === "dir") {
items.push({ label: "Open", action: "ctx-open" });
items.push({ label: t("ctx.open"), action: "ctx-open" });
} else {
if (ctx.isLocal) {
items.push({ label: "Open", action: "ctx-open-default", tooltip: "Open with the default application" });
items.push({ label: t("ctx.open"), action: "ctx-open-default", tooltip: t("ctx.openDefault") });
}
items.push({ label: "Edit", action: "ctx-edit", accelerator: "e" });
items.push({ label: t("ctx.edit"), action: "ctx-edit", accelerator: "e" });
}
items.push({ label: "Copy to Other Panel", action: "ctx-copy" });
items.push({ label: "Rename", action: "ctx-rename" });
items.push({ label: t("ctx.copyToOther"), action: "ctx-copy" });
items.push({ label: t("ctx.rename"), action: "ctx-rename" });
// chmod on Windows local files only flips the read-only bit — hide the lie
if (!(ctx.isLocal && IS_WIN)) {
items.push({ label: "Permissions", action: "ctx-chmod" });
items.push({ label: t("ctx.permissions"), action: "ctx-chmod" });
}
items.push({ type: "separator" });
items.push({ label: "Copy Path", action: "ctx-copy-path" });
items.push({ label: t("ctx.copyPath"), action: "ctx-copy-path" });
if (ctx.isLocal) {
items.push({ label: REVEAL_LABEL, action: "ctx-reveal" });
items.push({ label: revealLabel(), action: "ctx-reveal" });
}
items.push({ type: "separator" });
if (ctx.isLocal) {
items.push({
label: TRASH_LABEL,
label: trashLabel(),
action: "ctx-trash",
...(process.platform === "darwin"
? { tooltip: "Finder's Restore is not available for items trashed from here" }
: {}),
...(process.platform === "darwin" ? { tooltip: t("ctx.trashTooltip") } : {}),
});
}
items.push({ label: "Delete", action: "ctx-delete" });
items.push({ label: t("ctx.delete"), action: "ctx-delete" });
return items;
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { t } from "../i18n";
import { LocalProvider } from "./local";
import type { FileSystemProvider } from "./provider";
@@ -15,7 +16,7 @@ export class SessionRegistry {
get(sessionId: string): FileSystemProvider {
const provider = this.sessions.get(sessionId);
if (!provider) throw new Error(`Session "${sessionId}" is not connected`);
if (!provider) throw new Error(t("errors.sessionNotConnected", { id: sessionId }));
return provider;
}
@@ -27,7 +28,7 @@ export class SessionRegistry {
async remove(sessionId: string): Promise<void> {
if (sessionId === LOCAL_SESSION_ID)
throw new Error("The local session cannot be disconnected");
throw new Error(t("errors.localCannotDisconnect"));
const provider = this.sessions.get(sessionId);
if (!provider) return;
this.sessions.delete(sessionId);
+3 -2
View File
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { Client, type ConnectConfig, type SFTPWrapper } from "ssh2";
import type { ConnectionConfig, EntryType, FileEntry } from "../../shared/types";
import { t } from "../i18n";
import { expandTilde } from "../store/ssh-config";
import type { FileSystemProvider } from "./provider";
@@ -63,7 +64,7 @@ export class SftpProvider implements FileSystemProvider {
connectConfig.password = config.auth.password;
break;
case "agent":
if (!agentSock) throw new Error("ssh-agent not available (SSH_AUTH_SOCK is not set)");
if (!agentSock) throw new Error(t("errors.sshAgentUnavailable"));
connectConfig.agent = agentSock;
break;
case "key": {
@@ -73,7 +74,7 @@ export class SftpProvider implements FileSystemProvider {
connectConfig.privateKey = readFileSync(expandTilde(config.auth.privateKeyPath));
} catch {
if (!agentSock)
throw new Error(`identity file not found: ${config.auth.privateKeyPath}`);
throw new Error(t("errors.identityFileNotFound", { path: config.auth.privateKeyPath }));
}
if (config.auth.passphrase) connectConfig.passphrase = config.auth.passphrase;
else if (agentSock) connectConfig.agent = agentSock;
+38
View File
@@ -0,0 +1,38 @@
import { DICTS, type Messages, resolveLocale } from "../shared/i18n";
import type { AppSettings, SupportedLocale } from "../shared/types";
// Backend-side translator for the native menus and thrown error messages
// (the Angular I18nService can't reach OS chrome). The locale is cached and
// refreshed from settings (see index.ts wiring); no signals here.
let locale: SupportedLocale = "en";
/** OS locale for the "system" setting, e.g. "es-ES" -> resolves to "es". */
function osLocale(): string {
try {
return new Intl.DateTimeFormat().resolvedOptions().locale;
} catch {
return "en";
}
}
export function setLocale(setting: AppSettings["locale"]): void {
locale = resolveLocale(setting, osLocale());
}
function lookup(dict: Messages, key: string): string | undefined {
let node: string | Messages | undefined = dict;
for (const part of key.split(".")) {
if (typeof node !== "object") return undefined;
node = node[part];
}
return typeof node === "string" ? node : undefined;
}
/** Translate a key with {name} interpolation; falls back to English then the key. */
export function t(key: string, params?: Record<string, string | number>): string {
const value = lookup(DICTS[locale], key) ?? lookup(DICTS.en, key) ?? key;
if (!params) return value;
return value.replace(/\{(\w+)\}/g, (_, name: string) =>
name in params ? String(params[name]) : `{${name}}`,
);
}
+7
View File
@@ -8,6 +8,7 @@ import { EditorWindows } from "./editor-windows";
import { SessionRegistry } from "./fs/registry";
import { LocalDirWatcher } from "./fs/watcher";
import { ManagerWindow } from "./manager-window";
import { setLocale as setBackendLocale } from "./i18n";
import { applyApplicationMenu } from "./menu";
import { createAppRPC } from "./rpc-handlers";
import { ConnectionStore } from "./store/connections";
@@ -64,6 +65,7 @@ const url = await getMainViewUrl();
// window's preload so the first paint is already themed (no flash). Kept
// fresh via the saveSettings flow below.
let currentSettings = await settings.load();
setBackendLocale(currentSettings.locale); // native menus/errors follow the UI language
const isMac = process.platform === "darwin";
@@ -115,7 +117,12 @@ const manager = new ManagerWindow({
events.openEditor = (ctx) => editors.open(ctx);
events.settingsChanged = (s) => {
const localeChanged = s.locale !== currentSettings.locale;
currentSettings = s; // future windows' preloads bake the fresh theme
if (localeChanged) {
setBackendLocale(s.locale);
void applyApplicationMenu(store); // relabel the native menu in the new language
}
editors.broadcastSettings(s);
manager.broadcastSettings(s);
};
+15 -14
View File
@@ -1,6 +1,7 @@
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun";
import { BRANDING } from "../shared/branding";
import type { SavedConnection } from "../shared/types";
import { t } from "./i18n";
import type { ConnectionStore } from "./store/connections";
/**
@@ -27,37 +28,37 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
...looseItems,
...stackMenus,
...(looseItems.length || stackMenus.length ? [{ type: "separator" } as const] : []),
{ label: "Connection Manager…", action: "manage-connections" },
{ label: t("menu.connectionManager"), action: "manage-connections" },
];
ApplicationMenu.setApplicationMenu([
{
submenu: [
{ label: `About ${BRANDING.appName}`, action: "about" },
{ label: t("menu.about", { app: BRANDING.appName }), action: "about" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "showAll" },
{ type: "separator" },
{ label: "Quit", role: "quit" },
{ label: t("menu.quit"), role: "quit" },
],
},
{
label: "File",
label: t("menu.file"),
submenu: [
{ label: "New Tab", action: "new-tab", accelerator: "t" },
{ label: "Close Tab", action: "close-tab", accelerator: "w" },
{ label: t("menu.newTab"), action: "new-tab", accelerator: "t" },
{ label: t("menu.closeTab"), action: "close-tab", accelerator: "w" },
{ type: "separator" },
{ label: "New Folder", action: "new-folder" },
{ label: "New File", action: "new-file" },
{ label: t("menu.newFolder"), action: "new-folder" },
{ label: t("menu.newFile"), action: "new-file" },
{ type: "separator" },
{ label: "Edit File", action: "edit-file", accelerator: "e" },
{ label: t("menu.editFile"), action: "edit-file", accelerator: "e" },
{ type: "separator" },
{ label: "Connect", submenu: connectSubmenu },
{ label: t("menu.connect"), submenu: connectSubmenu },
],
},
{
label: "Edit",
label: t("menu.edit"),
submenu: [
{ role: "undo" },
{ role: "redo" },
@@ -71,15 +72,15 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
],
},
{
label: "View",
label: t("menu.view"),
submenu: [
{ label: "Refresh", action: "refresh-active", accelerator: "r" },
{ label: t("menu.refresh"), action: "refresh-active", accelerator: "r" },
{ type: "separator" },
{ role: "toggleFullScreen" },
],
},
{
label: "Window",
label: t("menu.window"),
submenu: [
{ role: "minimize" },
{ role: "zoom" },
+5 -4
View File
@@ -4,6 +4,7 @@ import { BRANDING } from "../../shared/branding";
import type { EntryType, TransferSnapshot, TransferState } from "../../shared/types";
import type { FileSystemProvider } from "../fs/provider";
import type { SessionRegistry } from "../fs/registry";
import { t } from "../i18n";
import { walkTree } from "./walk";
const PROGRESS_INTERVAL_MS = 100;
@@ -59,7 +60,7 @@ async function ensureDir(fs: FileSystemProvider, path: string): Promise<void> {
try {
const st = await fs.stat(path);
if (st.type === "dir") return;
throw new Error(`"${path}" exists and is not a directory`);
throw new Error(t("errors.notADirectory", { path }));
} catch {
await fs.mkdir(path);
}
@@ -235,7 +236,7 @@ export class TransferEngine {
if (sameFs) {
for (const file of files) {
if (file.srcPath === file.dstPath) {
throw new Error(`"${file.name}": source and destination are the same file`);
throw new Error(t("errors.sameFileTransfer", { name: file.name }));
}
}
}
@@ -343,10 +344,10 @@ export class TransferEngine {
if (samePath) {
throw new Error(
isDir ? "Cannot copy a folder onto itself" : "Source and destination are the same file",
isDir ? t("errors.folderOntoItself") : t("errors.sameFileNoName"),
);
}
throw new Error("Cannot copy a folder into itself");
throw new Error(t("errors.folderIntoItself"));
}
/**
+35
View File
@@ -0,0 +1,35 @@
# Translations
`en.ts` is the **source of truth**. Every other locale mirrors its key shape;
any missing key falls back to English at lookup time (see `index.ts` +
`I18nService`/backend `i18n.ts`), so partial translations are safe.
## Adding / editing strings
1. Add the key to `en.ts` (nested namespace, `{param}` interpolation). For
plurals, use a `{ one, other }` map instead of a string — the runtime picks
the CLDR category via `Intl.PluralRules` (languages without plural, e.g. zh
and ja, only need `other`).
2. Reference it as `i18n.t('ns.key', { param })` in the UI (or backend `t(...)`
for native menus / thrown errors).
3. Add the translation to the other locale files as convenient — until then
English shows through.
The format is kept **Transloco-compatible** (nested namespaces, `{param}`) so
swapping in a real i18n library later is a drop-in of these dictionaries.
## Review status
| Locale | File | Status |
|---|---|---|
| English | `en.ts` | Source |
| Spanish | `es.ts` | Full draft |
| French | `fr.ts` | Full draft |
| German | `de.ts` | Full draft |
| Chinese (Simplified) | `zh.ts` | **AI draft — needs native review** |
| Japanese | `ja.ts` | **AI draft — needs native review** |
| Basque | `eu.ts` | **AI draft — needs native review (highest priority; machine/AI Basque is unreliable)** |
Native-speaker review is especially important for Basque and the CJK locales:
grammatical agreement, counters/measure words, and UI-context word choice are
easy to get subtly wrong.
+299
View File
@@ -0,0 +1,299 @@
// German translations. Missing keys fall back to English (see en.ts / index.ts).
export const de = {
common: {
cancel: "Abbrechen",
save: "Speichern",
close: "Schließen",
delete: "Löschen",
rename: "Umbenennen",
create: "Erstellen",
apply: "Anwenden",
choose: "Wählen…",
connect: "Verbinden",
local: "Lokal",
},
app: {
notConnectedTitle: "Nicht mit der App verbunden",
notConnectedBody: "Diese Seite läuft in einem normalen Browser. Starte die Desktop-App, um sie zu verwenden.",
managerOpen: "Der Verbindungsmanager ist geöffnet.",
showConnections: "Verbindungen anzeigen",
},
tabBar: {
connections: "Verbindungen",
manageConnections: "Verbindungen verwalten",
newTab: "Neuer Tab ({mod}T)",
closeTab: "Tab schließen ({mod}W)",
options: "Optionen",
},
options: {
title: "Optionen",
language: "Sprache",
system: "System",
theme: { label: "Design", system: "System", light: "Hell", dark: "Dunkel" },
tone: { label: "Ton", dark: "Dunkel", darker: "Dunkler" },
accent: { label: "Akzent" },
accentNames: {
graphite: "Graphit",
blue: "Blau",
indigo: "Indigo",
purple: "Lila",
rose: "Rosa",
orange: "Orange",
green: "Grün",
teal: "Petrol",
},
fileIcons: { label: "Datei-Symbole", plain: "Einfach", multi: "Farbig" },
interface: { label: "Oberfläche", compact: "Kompakt", default: "Normal", large: "Groß" },
speedUnit: { label: "Geschwindigkeitseinheit" },
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
atOnce: "Gleichzeitig",
editor: { label: "Editor", builtin: "Integriert", custom: "Benutzerdefiniert" },
systemDefault: "Systemstandard",
resetDefault: "Auf Standard zurücksetzen",
finishedTransfers: { label: "Abgeschlossene Übertragungen", autoClear: "Auto-löschen", keep: "Behalten" },
},
kinds: {
folder: "Ordner",
symlink: "Symbolischer Link",
file: "Datei",
image: "Bild",
audio: "Audio",
video: "Video",
archive: "Archiv",
code: "Code",
document: "Dokument",
},
columns: {
name: "Name",
size: "Größe",
mtime: "Geändert",
kind: "Typ",
rights: "Rechte",
owner: "Eigentümer",
group: "Gruppe",
},
panel: {
choosePanel: "Wählen, was dieses Panel anzeigt",
copy: "Kopieren",
copyToRight: "In rechtes Panel kopieren (F5)",
copyToLeft: "In linkes Panel kopieren (F5)",
edit: "Bearbeiten",
editFile: "Datei bearbeiten (F4)",
rename: "Umbenennen (F2)",
newFolder: "Neuer Ordner (F7)",
newFile: "Neue Datei",
permissions: "Berechtigungen",
delete: "Löschen",
deleteHint: "Löschen (F8 / Entf)",
parentDir: "Übergeordneter Ordner (Rücktaste)",
refresh: "Aktualisieren",
clearFilter: "Filter löschen (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "Verbinde mit {name}…",
emptyDir: "Leerer Ordner",
noMatches: "Keine Treffer",
matches: { one: "{count} Treffer · Esc", other: "{count} Treffer · Esc" },
},
connMgr: {
title: "Verbindungen",
importWinscp: "Aus WinSCP importieren…",
importWinscpHint: "Sitzungen aus einem WinSCP.ini-Export importieren",
newConnection: "Verbindung",
newConnectionHint: "Neue Verbindung",
newStack: "Gruppe",
newStackHint: "Neue Gruppe",
expand: "Aufklappen",
collapse: "Zuklappen",
renameStack: "Gruppe umbenennen",
deleteStack: "Gruppe löschen",
openStack: "Gruppe in Tabs öffnen",
empty: "leer",
connectionsHeader: "Verbindungen",
noneTitle: "Noch keine gespeicherten Verbindungen.",
noneBody: "Erstelle eine oder importiere deine WinSCP-Sitzungen.",
name: "Name",
namePlaceholder: "mein Server",
host: "Host",
port: "Port",
stack: "Gruppe",
stackNone: "(keine)",
username: "Benutzername",
remotePath: "Entfernter Pfad (optional)",
remotePathPlaceholder: "Home-Verzeichnis",
auth: "Authentifizierung",
authPassword: "Passwort",
authKey: "Privater Schlüssel",
password: "Passwort",
privateKeyFile: "Datei mit privatem Schlüssel",
passphrase: "Passphrase (optional)",
localDir: "Lokales Verzeichnis (optional)",
localDirPlaceholder: "~/projekte/mein-server",
localDirHint: "Das andere Panel öffnet sich hier, wenn es auf Lokal steht und nicht navigiert wurde.",
more: "Mehr…",
less: "Weniger…",
selectPrompt: "Wähle eine Verbindung oder erstelle eine neue.",
newStackTitle: "Neue Gruppe",
newStackPrompt: "Gruppenname",
renameStackTitle: "Gruppe umbenennen",
deleteStackTitle: "Gruppe löschen",
deleteStackBody: 'Gruppe „{name}" löschen? Ihre Verbindungen bleiben, ohne Gruppe.',
deleteConnectionTitle: "Verbindung löschen",
deleteConnectionBody: '„{name}" löschen?',
},
editor: {
noFileTitle: "Keine Datei zum Bearbeiten",
noFileBody: "Dieses Fenster wurde ohne Editor-Kontext geöffnet. Öffne Dateien aus der App.",
wrap: "Umbruch",
wrapHint: "Lange Zeilen umbrechen — blendet Zeilennummern aus ({alt}Z)",
findHint: "Suchen ({mod}F)",
findPlaceholder: "In Datei suchen",
prevMatch: "Vorheriger Treffer (⇧↵)",
nextMatch: "Nächster Treffer (↵)",
closeHint: "Schließen (Esc)",
disconnected: "Verbindung zu {name} getrennt — Speichern schlägt fehl, aber dein Text bleibt in diesem Fenster erhalten.",
cantOpen: "{name} kann nicht geöffnet werden",
closeWindow: "Fenster schließen",
shortcuts: "{mod}S speichern · {mod}F suchen · {mod}W schließen",
lines: { one: "{count} Zeile", other: "{count} Zeilen" },
close: "Schließen",
save: "Speichern",
saveChangesTitle: 'Änderungen an „{name}" speichern?',
saveChangesBody: "Deine Änderungen gehen verloren, wenn du sie nicht speicherst.",
dontSave: "Nicht speichern",
countLabel: "{current} von {total}",
noMatches: "Keine Treffer",
},
transfer: {
summary: "{active} aktiv · {finished} abgeschlossen",
clearFinished: "Abgeschlossene löschen",
cancel: "Abbrechen",
dismiss: "Verwerfen",
showDetails: "Übertragungsdetails anzeigen",
copying: "Kopiere {name} · {percent}%{speed}",
queued: "{count} in Warteschlange",
failed: { one: "{count} Übertragung fehlgeschlagen", other: "{count} Übertragungen fehlgeschlagen" },
finished: "Übertragungen abgeschlossen",
ready: "Bereit",
items: "{count} Elemente",
selected: "{count} ausgewählt",
queuedState: "In Warteschlange",
scanning: "Analysiere…",
failedState: "Fehlgeschlagen",
cancelled: "Abgebrochen",
completedFiles: { one: "{count} Datei · {size}", other: "{count} Dateien · {size}" },
left: "{eta} übrig",
},
dialogs: {
chmodTitle: "Berechtigungen",
chmodItems: "{count} Elemente",
read: "Lesen",
write: "Schreiben",
execute: "Ausführen",
roleOwner: "Eigentümer",
roleGroup: "Gruppe",
roleOthers: "Andere",
octal: "Oktal",
mergeFolder: "Ordner zusammenführen",
replace: "Ersetzen",
merge: "Zusammenführen",
mergeAll: "Alle zusammenführen",
replaceAll: "Alle ersetzen",
skip: "Überspringen",
skipAll: "Alle überspringen",
ofTotal: "{index} von {total}",
overwriteDirBody: "Am Ziel existiert bereits ein Ordner mit diesem Namen. Zusammenführen kann Dateien darin ersetzen.",
overwriteFileBody: "Am Ziel existiert bereits eine Datei mit diesem Namen.",
source: "Quelle",
destination: "Ziel",
newer: "neuer",
},
toasts: {
movedToTrash: '„{name}" in den Papierkorb verschoben',
editingExternal: "Bearbeite {name} — Speichern lädt automatisch hoch",
nothingToCopy: "Nichts zu kopieren (symbolische Links werden übersprungen)",
nothingToCopyAllSkipped: "Nichts zu kopieren — alle Konflikte wurden übersprungen",
sameFolder: "Beide Panels zeigen denselben Ordner",
sameFile: '„{name}" übersprungen — es ist dieselbe Datei',
typeMismatch: '„{name}" übersprungen — ein anderer Elementtyp ist im Weg',
transferFailed: 'Übertragung „{name}" fehlgeschlagen: {error}',
unknownError: "unbekannter Fehler",
saved: "{name} gespeichert",
disconnectedReason: "{title}: getrennt ({reason})",
imported: { one: "{count} Verbindung importiert", other: "{count} Verbindungen importiert" },
importedIntoStacks: { one: " in {count} neue Gruppe", other: " in {count} neue Gruppen" },
importedSkipped: " ({skipped} übersprungen)",
leftoverTitle: "Übrig gebliebene bearbeitete Dateien",
leftoverMore: ", +{count} weitere",
leftoverBody: {
one: "Eine frühere Sitzung hinterließ {count} temporäre bearbeitete Datei ({names}). Löschen?",
other: "Eine frühere Sitzung hinterließ {count} temporäre bearbeitete Dateien ({names}). Löschen?",
},
leftoverClear: "Löschen",
},
fileActions: {
deleteTitle: "Löschen",
deleteBody: "{what} löschen? Ordner werden rekursiv gelöscht. Dies kann nicht rückgängig gemacht werden.",
deleteConfirm: "Löschen",
renameTitle: '„{name}" umbenennen',
renameConfirm: "Umbenennen",
newFolderTitle: "Neuer Ordner",
newFolderPlaceholder: "Ordnername",
newFileTitle: "Neue Datei",
newFilePlaceholder: "Dateiname",
createConfirm: "Erstellen",
itemsCount: "{count} Elemente",
},
menu: {
about: "Über {app}",
quit: "Beenden",
file: "Datei",
newTab: "Neuer Tab",
closeTab: "Tab schließen",
newFolder: "Neuer Ordner",
newFile: "Neue Datei",
editFile: "Datei bearbeiten",
connect: "Verbinden",
connectionManager: "Verbindungsmanager…",
edit: "Bearbeiten",
view: "Ansicht",
refresh: "Aktualisieren",
window: "Fenster",
},
ctx: {
size: "Größe",
modified: "Geändert",
kind: "Typ",
rights: "Rechte",
owner: "Eigentümer",
group: "Gruppe",
revealFinder: "Im Finder anzeigen",
revealExplorer: "Im Explorer anzeigen",
revealFileManager: "Im Dateimanager anzeigen",
moveToTrash: "In den Papierkorb",
moveToRecycleBin: "In den Papierkorb",
newFolder: "Neuer Ordner",
newFile: "Neue Datei",
refresh: "Aktualisieren",
open: "Öffnen",
openDefault: "Mit Standard-App öffnen",
edit: "Bearbeiten",
copyToOther: "In anderes Panel kopieren",
rename: "Umbenennen",
permissions: "Berechtigungen",
copyPath: "Pfad kopieren",
delete: "Löschen",
columns: "Spalten",
trashTooltip: "Finders Wiederherstellen ist für von hier gelöschte Elemente nicht verfügbar",
},
errors: {
sshAgentUnavailable: "ssh-agent nicht verfügbar (SSH_AUTH_SOCK ist nicht gesetzt)",
identityFileNotFound: "Identitätsdatei nicht gefunden: {path}",
sessionNotConnected: 'Sitzung „{id}" ist nicht verbunden',
localCannotDisconnect: "Die lokale Sitzung kann nicht getrennt werden",
notADirectory: '„{path}" existiert und ist kein Ordner',
sameFileTransfer: '„{name}": Quelle und Ziel sind dieselbe Datei',
sameFileNoName: "Quelle und Ziel sind dieselbe Datei",
folderOntoItself: "Ein Ordner kann nicht auf sich selbst kopiert werden",
folderIntoItself: "Ein Ordner kann nicht in sich selbst kopiert werden",
},
} as const;
+303
View File
@@ -0,0 +1,303 @@
// English is the source of truth. Other locales mirror this shape; missing keys
// fall back to English at lookup time. Placeholders use {name} interpolation.
// Keep Transloco-compatible (nested namespaces, {param}) so a later migration
// to a real i18n library is a drop-in of these dictionaries.
export const en = {
common: {
cancel: "Cancel",
save: "Save",
close: "Close",
delete: "Delete",
rename: "Rename",
create: "Create",
apply: "Apply",
choose: "Choose…",
connect: "Connect",
local: "Local",
},
app: {
notConnectedTitle: "Not connected to the app",
notConnectedBody: "This page is running in a plain browser. Launch the desktop app to use it.",
managerOpen: "The connection manager is open.",
showConnections: "Show Connections",
},
tabBar: {
connections: "Connections",
manageConnections: "Manage connections",
newTab: "New tab ({mod}T)",
closeTab: "Close tab ({mod}W)",
options: "Options",
},
options: {
title: "Options",
language: "Language",
system: "System",
theme: { label: "Theme", system: "System", light: "Light", dark: "Dark" },
tone: { label: "Tone", dark: "Dark", darker: "Darker" },
accent: { label: "Accent" },
accentNames: {
graphite: "Graphite",
blue: "Blue",
indigo: "Indigo",
purple: "Purple",
rose: "Rose",
orange: "Orange",
green: "Green",
teal: "Teal",
},
fileIcons: { label: "File icons", plain: "Plain", multi: "Multi" },
interface: { label: "Interface", compact: "Compact", default: "Default", large: "Large" },
speedUnit: { label: "Speed unit" },
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
atOnce: "At once",
editor: { label: "Editor", builtin: "Built-in", custom: "Custom" },
systemDefault: "System default",
resetDefault: "Reset to system default",
finishedTransfers: { label: "Finished transfers", autoClear: "Auto-clear", keep: "Keep" },
},
kinds: {
folder: "Folder",
symlink: "Symlink",
file: "File",
image: "Image",
audio: "Audio",
video: "Video",
archive: "Archive",
code: "Code",
document: "Document",
},
columns: {
name: "Name",
size: "Size",
mtime: "Modified",
kind: "Kind",
rights: "Rights",
owner: "Owner",
group: "Group",
},
panel: {
choosePanel: "Choose what this panel shows",
copy: "Copy",
copyToRight: "Copy to right panel (F5)",
copyToLeft: "Copy to left panel (F5)",
edit: "Edit",
editFile: "Edit file (F4)",
rename: "Rename (F2)",
newFolder: "New folder (F7)",
newFile: "New file",
permissions: "Permissions",
delete: "Delete",
deleteHint: "Delete (F8 / Del)",
parentDir: "Parent directory (Backspace)",
refresh: "Refresh",
clearFilter: "Clear filter (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "Connecting to {name}…",
emptyDir: "Empty directory",
noMatches: "No matches",
matches: { one: "{count} match · Esc", other: "{count} matches · Esc" },
},
connMgr: {
title: "Connections",
importWinscp: "Import from WinSCP…",
importWinscpHint: "Import sessions from a WinSCP.ini export",
newConnection: "Connection",
newConnectionHint: "New connection",
newStack: "Stack",
newStackHint: "New stack",
expand: "Expand",
collapse: "Collapse",
renameStack: "Rename stack",
deleteStack: "Delete stack",
openStack: "Open stack in tabs",
empty: "empty",
connectionsHeader: "Connections",
noneTitle: "No saved connections yet.",
noneBody: "Create one, or import your WinSCP sessions.",
name: "Name",
namePlaceholder: "my server",
host: "Host",
port: "Port",
stack: "Stack",
stackNone: "(none)",
username: "Username",
remotePath: "Remote path (optional)",
remotePathPlaceholder: "home directory",
auth: "Auth",
authPassword: "Password",
authKey: "Private key",
password: "Password",
privateKeyFile: "Private key file",
passphrase: "Passphrase (optional)",
localDir: "Local directory (optional)",
localDirPlaceholder: "~/projects/my-server",
localDirHint: "The other panel opens here when it's on Local and hasn't been navigated.",
more: "More…",
less: "Less…",
selectPrompt: "Select a connection or create a new one.",
newStackTitle: "New stack",
newStackPrompt: "stack name",
renameStackTitle: "Rename stack",
deleteStackTitle: "Delete stack",
deleteStackBody: 'Delete stack "{name}"? Its connections stay, ungrouped.',
deleteConnectionTitle: "Delete connection",
deleteConnectionBody: 'Delete "{name}"?',
},
editor: {
noFileTitle: "No file to edit",
noFileBody: "This window was opened without an editor context. Launch files from the app.",
wrap: "Wrap",
wrapHint: "Wrap long lines — hides line numbers ({alt}Z)",
findHint: "Find ({mod}F)",
findPlaceholder: "Find in file",
prevMatch: "Previous match (⇧↵)",
nextMatch: "Next match (↵)",
closeHint: "Close (Esc)",
disconnected: "Disconnected from {name} — saving will fail, but your text is preserved in this window.",
cantOpen: "Can't open {name}",
closeWindow: "Close Window",
shortcuts: "{mod}S save · {mod}F find · {mod}W close",
lines: { one: "{count} line", other: "{count} lines" },
close: "Close",
save: "Save",
saveChangesTitle: 'Save changes to "{name}"?',
saveChangesBody: "Your changes will be lost if you don't save them.",
dontSave: "Don't Save",
countLabel: "{current} of {total}",
noMatches: "No matches",
},
transfer: {
summary: "{active} active · {finished} finished",
clearFinished: "Clear finished",
cancel: "Cancel",
dismiss: "Dismiss",
showDetails: "Show transfer details",
copying: "Copying {name} · {percent}%{speed}",
queued: "{count} queued",
failed: { one: "{count} transfer failed", other: "{count} transfers failed" },
finished: "Transfers finished",
ready: "Ready",
items: "{count} items",
selected: "{count} selected",
queuedState: "Queued",
scanning: "Scanning…",
failedState: "Failed",
cancelled: "Cancelled",
completedFiles: { one: "{count} file · {size}", other: "{count} files · {size}" },
left: "{eta} left",
},
dialogs: {
chmodTitle: "Permissions",
chmodItems: "{count} items",
read: "Read",
write: "Write",
execute: "Execute",
roleOwner: "Owner",
roleGroup: "Group",
roleOthers: "Others",
octal: "Octal",
mergeFolder: "Merge folder",
replace: "Replace",
merge: "Merge",
mergeAll: "Merge all",
replaceAll: "Replace all",
skip: "Skip",
skipAll: "Skip all",
ofTotal: "{index} of {total}",
overwriteDirBody: "The destination already has a folder with this name. Merging may replace files inside it.",
overwriteFileBody: "The destination already has a file with this name.",
source: "Source",
destination: "Destination",
newer: "newer",
},
toasts: {
movedToTrash: 'Moved "{name}" to Trash',
editingExternal: "Editing {name} — saves upload automatically",
nothingToCopy: "Nothing to copy (symlinks are skipped)",
nothingToCopyAllSkipped: "Nothing to copy — all conflicting items were skipped",
sameFolder: "Both panels show the same folder",
sameFile: 'Skipped "{name}" — it is the same file',
typeMismatch: 'Skipped "{name}" — a different kind of item is in the way',
transferFailed: 'Transfer "{name}" failed: {error}',
unknownError: "unknown error",
saved: "Saved {name}",
disconnectedReason: "{title}: disconnected ({reason})",
imported: { one: "Imported {count} connection", other: "Imported {count} connections" },
importedIntoStacks: { one: " into {count} new stack", other: " into {count} new stacks" },
importedSkipped: " ({skipped} skipped)",
leftoverTitle: "Leftover edited files",
leftoverBody: {
one: "A previous session left {count} temporary edited file behind ({names}). Clear them?",
other: "A previous session left {count} temporary edited files behind ({names}). Clear them?",
},
leftoverMore: ", +{count} more",
leftoverClear: "Clear",
},
fileActions: {
deleteTitle: "Delete",
deleteBody: "Delete {what}? Folders are deleted recursively. This cannot be undone.",
deleteConfirm: "Delete",
renameTitle: 'Rename "{name}"',
renameConfirm: "Rename",
newFolderTitle: "New folder",
newFolderPlaceholder: "folder name",
newFileTitle: "New file",
newFilePlaceholder: "file name",
createConfirm: "Create",
itemsCount: "{count} items",
},
menu: {
about: "About {app}",
quit: "Quit",
file: "File",
newTab: "New Tab",
closeTab: "Close Tab",
newFolder: "New Folder",
newFile: "New File",
editFile: "Edit File",
connect: "Connect",
connectionManager: "Connection Manager…",
edit: "Edit",
view: "View",
refresh: "Refresh",
window: "Window",
},
ctx: {
size: "Size",
modified: "Modified",
kind: "Kind",
rights: "Rights",
owner: "Owner",
group: "Group",
revealFinder: "Reveal in Finder",
revealExplorer: "Show in Explorer",
revealFileManager: "Show in File Manager",
moveToTrash: "Move to Trash",
moveToRecycleBin: "Move to Recycle Bin",
newFolder: "New Folder",
newFile: "New File",
refresh: "Refresh",
open: "Open",
openDefault: "Open with the default application",
edit: "Edit",
copyToOther: "Copy to Other Panel",
rename: "Rename",
permissions: "Permissions",
copyPath: "Copy Path",
delete: "Delete",
columns: "Columns",
trashTooltip: "Finder's Restore is not available for items trashed from here",
},
errors: {
sshAgentUnavailable: "ssh-agent not available (SSH_AUTH_SOCK is not set)",
identityFileNotFound: "identity file not found: {path}",
sessionNotConnected: 'Session "{id}" is not connected',
localCannotDisconnect: "The local session cannot be disconnected",
notADirectory: '"{path}" exists and is not a directory',
sameFileTransfer: '"{name}": source and destination are the same file',
sameFileNoName: "Source and destination are the same file",
folderOntoItself: "Cannot copy a folder onto itself",
folderIntoItself: "Cannot copy a folder into itself",
},
} as const;
+299
View File
@@ -0,0 +1,299 @@
// Spanish translations. Missing keys fall back to English (see en.ts / index.ts).
export const es = {
common: {
cancel: "Cancelar",
save: "Guardar",
close: "Cerrar",
delete: "Eliminar",
rename: "Renombrar",
create: "Crear",
apply: "Aplicar",
choose: "Elegir…",
connect: "Conectar",
local: "Local",
},
app: {
notConnectedTitle: "No conectado a la aplicación",
notConnectedBody: "Esta página se ejecuta en un navegador normal. Abre la aplicación de escritorio para usarla.",
managerOpen: "El gestor de conexiones está abierto.",
showConnections: "Mostrar conexiones",
},
tabBar: {
connections: "Conexiones",
manageConnections: "Gestionar conexiones",
newTab: "Nueva pestaña ({mod}T)",
closeTab: "Cerrar pestaña ({mod}W)",
options: "Opciones",
},
options: {
title: "Opciones",
language: "Idioma",
system: "Sistema",
theme: { label: "Tema", system: "Sistema", light: "Claro", dark: "Oscuro" },
tone: { label: "Tono", dark: "Oscuro", darker: "Más oscuro" },
accent: { label: "Acento" },
accentNames: {
graphite: "Grafito",
blue: "Azul",
indigo: "Índigo",
purple: "Morado",
rose: "Rosa",
orange: "Naranja",
green: "Verde",
teal: "Turquesa",
},
fileIcons: { label: "Iconos", plain: "Simple", multi: "Color" },
interface: { label: "Interfaz", compact: "Compacta", default: "Normal", large: "Grande" },
speedUnit: { label: "Unidad de velocidad" },
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
atOnce: "A la vez",
editor: { label: "Editor", builtin: "Integrado", custom: "Personalizado" },
systemDefault: "Predeterminado del sistema",
resetDefault: "Restablecer al predeterminado",
finishedTransfers: { label: "Transferencias completadas", autoClear: "Auto-limpiar", keep: "Mantener" },
},
kinds: {
folder: "Carpeta",
symlink: "Enlace simbólico",
file: "Archivo",
image: "Imagen",
audio: "Audio",
video: "Vídeo",
archive: "Archivo comprimido",
code: "Código",
document: "Documento",
},
columns: {
name: "Nombre",
size: "Tamaño",
mtime: "Modificado",
kind: "Tipo",
rights: "Permisos",
owner: "Propietario",
group: "Grupo",
},
panel: {
choosePanel: "Elige qué muestra este panel",
copy: "Copiar",
copyToRight: "Copiar al panel derecho (F5)",
copyToLeft: "Copiar al panel izquierdo (F5)",
edit: "Editar",
editFile: "Editar archivo (F4)",
rename: "Renombrar (F2)",
newFolder: "Nueva carpeta (F7)",
newFile: "Nuevo archivo",
permissions: "Permisos",
delete: "Eliminar",
deleteHint: "Eliminar (F8 / Supr)",
parentDir: "Carpeta superior (Retroceso)",
refresh: "Actualizar",
clearFilter: "Borrar filtro (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "Conectando con {name}…",
emptyDir: "Carpeta vacía",
noMatches: "Sin coincidencias",
matches: { one: "{count} coincidencia · Esc", other: "{count} coincidencias · Esc" },
},
connMgr: {
title: "Conexiones",
importWinscp: "Importar de WinSCP…",
importWinscpHint: "Importar sesiones de una exportación WinSCP.ini",
newConnection: "Conexión",
newConnectionHint: "Nueva conexión",
newStack: "Grupo",
newStackHint: "Nuevo grupo",
expand: "Expandir",
collapse: "Contraer",
renameStack: "Renombrar grupo",
deleteStack: "Eliminar grupo",
openStack: "Abrir grupo en pestañas",
empty: "vacío",
connectionsHeader: "Conexiones",
noneTitle: "Aún no hay conexiones guardadas.",
noneBody: "Crea una o importa tus sesiones de WinSCP.",
name: "Nombre",
namePlaceholder: "mi servidor",
host: "Servidor",
port: "Puerto",
stack: "Grupo",
stackNone: "(ninguno)",
username: "Usuario",
remotePath: "Ruta remota (opcional)",
remotePathPlaceholder: "carpeta de inicio",
auth: "Autenticación",
authPassword: "Contraseña",
authKey: "Clave privada",
password: "Contraseña",
privateKeyFile: "Archivo de clave privada",
passphrase: "Frase de contraseña (opcional)",
localDir: "Carpeta local (opcional)",
localDirPlaceholder: "~/proyectos/mi-servidor",
localDirHint: "El otro panel se abre aquí cuando está en Local y no se ha navegado.",
more: "Más…",
less: "Menos…",
selectPrompt: "Selecciona una conexión o crea una nueva.",
newStackTitle: "Nuevo grupo",
newStackPrompt: "nombre del grupo",
renameStackTitle: "Renombrar grupo",
deleteStackTitle: "Eliminar grupo",
deleteStackBody: '¿Eliminar el grupo "{name}"? Sus conexiones se mantienen, sin agrupar.',
deleteConnectionTitle: "Eliminar conexión",
deleteConnectionBody: '¿Eliminar "{name}"?',
},
editor: {
noFileTitle: "Ningún archivo que editar",
noFileBody: "Esta ventana se abrió sin contexto de editor. Abre archivos desde la aplicación.",
wrap: "Ajustar",
wrapHint: "Ajustar líneas largas — oculta los números de línea ({alt}Z)",
findHint: "Buscar ({mod}F)",
findPlaceholder: "Buscar en el archivo",
prevMatch: "Coincidencia anterior (⇧↵)",
nextMatch: "Coincidencia siguiente (↵)",
closeHint: "Cerrar (Esc)",
disconnected: "Desconectado de {name} — no se podrá guardar, pero tu texto se conserva en esta ventana.",
cantOpen: "No se puede abrir {name}",
closeWindow: "Cerrar ventana",
shortcuts: "{mod}S guardar · {mod}F buscar · {mod}W cerrar",
lines: { one: "{count} línea", other: "{count} líneas" },
close: "Cerrar",
save: "Guardar",
saveChangesTitle: '¿Guardar los cambios en "{name}"?',
saveChangesBody: "Perderás tus cambios si no los guardas.",
dontSave: "No guardar",
countLabel: "{current} de {total}",
noMatches: "Sin coincidencias",
},
transfer: {
summary: "{active} activas · {finished} completadas",
clearFinished: "Limpiar completadas",
cancel: "Cancelar",
dismiss: "Descartar",
showDetails: "Ver detalles de transferencias",
copying: "Copiando {name} · {percent}%{speed}",
queued: "{count} en cola",
failed: { one: "{count} transferencia fallida", other: "{count} transferencias fallidas" },
finished: "Transferencias completadas",
ready: "Listo",
items: "{count} elementos",
selected: "{count} seleccionados",
queuedState: "En cola",
scanning: "Analizando…",
failedState: "Fallida",
cancelled: "Cancelada",
completedFiles: { one: "{count} archivo · {size}", other: "{count} archivos · {size}" },
left: "{eta} restante",
},
dialogs: {
chmodTitle: "Permisos",
chmodItems: "{count} elementos",
read: "Lectura",
write: "Escritura",
execute: "Ejecución",
roleOwner: "Propietario",
roleGroup: "Grupo",
roleOthers: "Otros",
octal: "Octal",
mergeFolder: "Combinar carpeta",
replace: "Reemplazar",
merge: "Combinar",
mergeAll: "Combinar todo",
replaceAll: "Reemplazar todo",
skip: "Omitir",
skipAll: "Omitir todo",
ofTotal: "{index} de {total}",
overwriteDirBody: "El destino ya tiene una carpeta con este nombre. Combinar puede reemplazar archivos dentro.",
overwriteFileBody: "El destino ya tiene un archivo con este nombre.",
source: "Origen",
destination: "Destino",
newer: "más reciente",
},
toasts: {
movedToTrash: 'Se movió "{name}" a la papelera',
editingExternal: "Editando {name} — los cambios se suben automáticamente",
nothingToCopy: "Nada que copiar (los enlaces simbólicos se omiten)",
nothingToCopyAllSkipped: "Nada que copiar — se omitieron todos los elementos en conflicto",
sameFolder: "Ambos paneles muestran la misma carpeta",
sameFile: 'Se omitió "{name}" — es el mismo archivo',
typeMismatch: 'Se omitió "{name}" — hay otro tipo de elemento en el camino',
transferFailed: 'La transferencia "{name}" falló: {error}',
unknownError: "error desconocido",
saved: "Se guardó {name}",
disconnectedReason: "{title}: desconectado ({reason})",
imported: { one: "Se importó {count} conexión", other: "Se importaron {count} conexiones" },
importedIntoStacks: { one: " en {count} grupo nuevo", other: " en {count} grupos nuevos" },
importedSkipped: " ({skipped} omitidas)",
leftoverTitle: "Archivos editados sobrantes",
leftoverMore: ", +{count} más",
leftoverBody: {
one: "Una sesión anterior dejó {count} archivo editado temporal ({names}). ¿Eliminarlo?",
other: "Una sesión anterior dejó {count} archivos editados temporales ({names}). ¿Eliminarlos?",
},
leftoverClear: "Eliminar",
},
fileActions: {
deleteTitle: "Eliminar",
deleteBody: "¿Eliminar {what}? Las carpetas se eliminan de forma recursiva. Esto no se puede deshacer.",
deleteConfirm: "Eliminar",
renameTitle: 'Renombrar "{name}"',
renameConfirm: "Renombrar",
newFolderTitle: "Nueva carpeta",
newFolderPlaceholder: "nombre de la carpeta",
newFileTitle: "Nuevo archivo",
newFilePlaceholder: "nombre del archivo",
createConfirm: "Crear",
itemsCount: "{count} elementos",
},
menu: {
about: "Acerca de {app}",
quit: "Salir",
file: "Archivo",
newTab: "Nueva pestaña",
closeTab: "Cerrar pestaña",
newFolder: "Nueva carpeta",
newFile: "Nuevo archivo",
editFile: "Editar archivo",
connect: "Conectar",
connectionManager: "Gestor de conexiones…",
edit: "Edición",
view: "Ver",
refresh: "Actualizar",
window: "Ventana",
},
ctx: {
size: "Tamaño",
modified: "Modificado",
kind: "Tipo",
rights: "Permisos",
owner: "Propietario",
group: "Grupo",
revealFinder: "Mostrar en Finder",
revealExplorer: "Mostrar en el Explorador",
revealFileManager: "Mostrar en el gestor de archivos",
moveToTrash: "Mover a la papelera",
moveToRecycleBin: "Mover a la papelera de reciclaje",
newFolder: "Nueva carpeta",
newFile: "Nuevo archivo",
refresh: "Actualizar",
open: "Abrir",
openDefault: "Abrir con la aplicación predeterminada",
edit: "Editar",
copyToOther: "Copiar al otro panel",
rename: "Renombrar",
permissions: "Permisos",
copyPath: "Copiar ruta",
delete: "Eliminar",
columns: "Columnas",
trashTooltip: "La opción Restaurar de Finder no está disponible para elementos enviados a la papelera desde aquí",
},
errors: {
sshAgentUnavailable: "ssh-agent no disponible (SSH_AUTH_SOCK no está definido)",
identityFileNotFound: "archivo de identidad no encontrado: {path}",
sessionNotConnected: 'La sesión "{id}" no está conectada',
localCannotDisconnect: "La sesión local no se puede desconectar",
notADirectory: '"{path}" existe y no es una carpeta',
sameFileTransfer: '"{name}": el origen y el destino son el mismo archivo',
sameFileNoName: "El origen y el destino son el mismo archivo",
folderOntoItself: "No se puede copiar una carpeta sobre sí misma",
folderIntoItself: "No se puede copiar una carpeta dentro de sí misma",
},
} as const;
+301
View File
@@ -0,0 +1,301 @@
// Basque (Euskara) translations. Fall back to English for missing keys.
// TODO(i18n): AI draft — REQUIRES native-speaker review (Basque is error-prone
// for machine/AI translation; treat every string as provisional).
export const eu = {
common: {
cancel: "Utzi",
save: "Gorde",
close: "Itxi",
delete: "Ezabatu",
rename: "Berrizendatu",
create: "Sortu",
apply: "Aplikatu",
choose: "Aukeratu…",
connect: "Konektatu",
local: "Lokala",
},
app: {
notConnectedTitle: "Ez dago aplikaziora konektatuta",
notConnectedBody: "Orri hau nabigatzaile arrunt batean exekutatzen ari da. Ireki mahaigaineko aplikazioa erabiltzeko.",
managerOpen: "Konexio-kudeatzailea irekita dago.",
showConnections: "Erakutsi konexioak",
},
tabBar: {
connections: "Konexioak",
manageConnections: "Kudeatu konexioak",
newTab: "Fitxa berria ({mod}T)",
closeTab: "Itxi fitxa ({mod}W)",
options: "Aukerak",
},
options: {
title: "Aukerak",
language: "Hizkuntza",
system: "Sistema",
theme: { label: "Gaia", system: "Sistema", light: "Argia", dark: "Iluna" },
tone: { label: "Tonua", dark: "Iluna", darker: "Ilunagoa" },
accent: { label: "Azentua" },
accentNames: {
graphite: "Grafitoa",
blue: "Urdina",
indigo: "Indigoa",
purple: "Morea",
rose: "Arrosa",
orange: "Laranja",
green: "Berdea",
teal: "Turkesa",
},
fileIcons: { label: "Fitxategi-ikonoak", plain: "Soila", multi: "Koloretsua" },
interface: { label: "Interfazea", compact: "Trinkoa", default: "Lehenetsia", large: "Handia" },
speedUnit: { label: "Abiadura-unitatea" },
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
atOnce: "Aldi berean",
editor: { label: "Editorea", builtin: "Barnekoa", custom: "Pertsonalizatua" },
systemDefault: "Sistemaren lehenetsia",
resetDefault: "Berrezarri lehenetsira",
finishedTransfers: { label: "Amaitutako transferentziak", autoClear: "Garbitu", keep: "Mantendu" },
},
kinds: {
folder: "Karpeta",
symlink: "Esteka sinbolikoa",
file: "Fitxategia",
image: "Irudia",
audio: "Audioa",
video: "Bideoa",
archive: "Artxiboa",
code: "Kodea",
document: "Dokumentua",
},
columns: {
name: "Izena",
size: "Tamaina",
mtime: "Aldatze-data",
kind: "Mota",
rights: "Baimenak",
owner: "Jabea",
group: "Taldea",
},
panel: {
choosePanel: "Aukeratu panel honek zer erakusten duen",
copy: "Kopiatu",
copyToRight: "Kopiatu eskuineko panelera (F5)",
copyToLeft: "Kopiatu ezkerreko panelera (F5)",
edit: "Editatu",
editFile: "Editatu fitxategia (F4)",
rename: "Berrizendatu (F2)",
newFolder: "Karpeta berria (F7)",
newFile: "Fitxategi berria",
permissions: "Baimenak",
delete: "Ezabatu",
deleteHint: "Ezabatu (F8 / Del)",
parentDir: "Karpeta gurasoa (Atzera-tekla)",
refresh: "Freskatu",
clearFilter: "Garbitu iragazkia (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "{name}(e)ra konektatzen…",
emptyDir: "Karpeta hutsa",
noMatches: "Bat-etortzerik ez",
matches: { one: "bat-etortze {count} · Esc", other: "{count} bat-etortze · Esc" },
},
connMgr: {
title: "Konexioak",
importWinscp: "Inportatu WinSCPtik…",
importWinscpHint: "Inportatu saioak WinSCP.ini esportazio batetik",
newConnection: "Konexioa",
newConnectionHint: "Konexio berria",
newStack: "Taldea",
newStackHint: "Talde berria",
expand: "Zabaldu",
collapse: "Tolestu",
renameStack: "Berrizendatu taldea",
deleteStack: "Ezabatu taldea",
openStack: "Ireki taldea fitxetan",
empty: "hutsik",
connectionsHeader: "Konexioak",
noneTitle: "Oraindik ez dago gordetako konexiorik.",
noneBody: "Sortu bat edo inportatu zure WinSCP saioak.",
name: "Izena",
namePlaceholder: "nire zerbitzaria",
host: "Ostalaria",
port: "Ataka",
stack: "Taldea",
stackNone: "(bat ere ez)",
username: "Erabiltzailea",
remotePath: "Urruneko bidea (aukerakoa)",
remotePathPlaceholder: "hasierako karpeta",
auth: "Autentifikazioa",
authPassword: "Pasahitza",
authKey: "Gako pribatua",
password: "Pasahitza",
privateKeyFile: "Gako pribatuaren fitxategia",
passphrase: "Pasaesaldia (aukerakoa)",
localDir: "Karpeta lokala (aukerakoa)",
localDirPlaceholder: "~/proiektuak/nire-zerbitzaria",
localDirHint: "Beste panela hemen irekiko da Lokalean dagoenean eta nabigatu ez denean.",
more: "Gehiago…",
less: "Gutxiago…",
selectPrompt: "Hautatu konexio bat edo sortu berri bat.",
newStackTitle: "Talde berria",
newStackPrompt: "taldearen izena",
renameStackTitle: "Berrizendatu taldea",
deleteStackTitle: "Ezabatu taldea",
deleteStackBody: 'Ezabatu "{name}" taldea? Bere konexioak mantendu egingo dira, taldekatu gabe.',
deleteConnectionTitle: "Ezabatu konexioa",
deleteConnectionBody: 'Ezabatu "{name}"?',
},
editor: {
noFileTitle: "Ez dago editatzeko fitxategirik",
noFileBody: "Leiho hau editore-testuingururik gabe ireki da. Ireki fitxategiak aplikaziotik.",
wrap: "Doitu",
wrapHint: "Doitu lerro luzeak — lerro-zenbakiak ezkutatzen ditu ({alt}Z)",
findHint: "Bilatu ({mod}F)",
findPlaceholder: "Bilatu fitxategian",
prevMatch: "Aurreko bat-etortzea (⇧↵)",
nextMatch: "Hurrengo bat-etortzea (↵)",
closeHint: "Itxi (Esc)",
disconnected: "{name}(e)tik deskonektatuta — gordetzeak huts egingo du, baina zure testua leiho honetan gordeta dago.",
cantOpen: "Ezin da {name} ireki",
closeWindow: "Itxi leihoa",
shortcuts: "{mod}S gorde · {mod}F bilatu · {mod}W itxi",
lines: { one: "lerro {count}", other: "{count} lerro" },
close: "Itxi",
save: "Gorde",
saveChangesTitle: 'Gorde "{name}"(r)en aldaketak?',
saveChangesBody: "Zure aldaketak galduko dira gordetzen ez badituzu.",
dontSave: "Ez gorde",
countLabel: "{total}(e)tik {current}",
noMatches: "Bat-etortzerik ez",
},
transfer: {
summary: "{active} aktibo · {finished} amaituta",
clearFinished: "Garbitu amaituak",
cancel: "Utzi",
dismiss: "Baztertu",
showDetails: "Erakutsi transferentzien xehetasunak",
copying: "{name} kopiatzen · %{percent}{speed}",
queued: "{count} ilaran",
failed: { one: "transferentzia {count}(e)k huts egin du", other: "{count} transferentziak huts egin dute" },
finished: "Transferentziak amaituta",
ready: "Prest",
items: "{count} elementu",
selected: "{count} hautatuta",
queuedState: "Ilaran",
scanning: "Aztertzen…",
failedState: "Huts egin du",
cancelled: "Bertan behera utzita",
completedFiles: { one: "fitxategi {count} · {size}", other: "{count} fitxategi · {size}" },
left: "{eta} geratzen da",
},
dialogs: {
chmodTitle: "Baimenak",
chmodItems: "{count} elementu",
read: "Irakurri",
write: "Idatzi",
execute: "Exekutatu",
roleOwner: "Jabea",
roleGroup: "Taldea",
roleOthers: "Besteak",
octal: "Zortzitarra",
mergeFolder: "Batu karpeta",
replace: "Ordeztu",
merge: "Batu",
mergeAll: "Batu guztiak",
replaceAll: "Ordeztu guztiak",
skip: "Saltatu",
skipAll: "Saltatu guztiak",
ofTotal: "{total}(e)tik {index}",
overwriteDirBody: "Helmugan izen bereko karpeta bat dago jada. Batzeak barneko fitxategiak ordeztu ditzake.",
overwriteFileBody: "Helmugan izen bereko fitxategi bat dago jada.",
source: "Iturburua",
destination: "Helmuga",
newer: "berriagoa",
},
toasts: {
movedToTrash: '"{name}" zakarrontzira eraman da',
editingExternal: "{name} editatzen — gordetzean automatikoki igotzen da",
nothingToCopy: "Ez dago kopiatzeko ezer (esteka sinbolikoak saltatu egiten dira)",
nothingToCopyAllSkipped: "Ez dago kopiatzeko ezer — gatazkan zeuden elementu guztiak saltatu dira",
sameFolder: "Bi panelek karpeta bera erakusten dute",
sameFile: '"{name}" saltatu da — fitxategi bera da',
typeMismatch: '"{name}" saltatu da — beste mota bateko elementu bat dago bidean',
transferFailed: '"{name}" transferentziak huts egin du: {error}',
unknownError: "errore ezezaguna",
saved: "{name} gorde da",
disconnectedReason: "{title}: deskonektatuta ({reason})",
imported: { one: "konexio {count} inportatu da", other: "{count} konexio inportatu dira" },
importedIntoStacks: { one: " talde berri {count}ean", other: " {count} talde berritan" },
importedSkipped: " ({skipped} saltatuta)",
leftoverTitle: "Geratutako editatutako fitxategiak",
leftoverMore: ", +{count} gehiago",
leftoverBody: {
one: "Aurreko saio batek editatutako aldi baterako fitxategi {count} utzi zuen ({names}). Garbitu?",
other: "Aurreko saio batek editatutako aldi baterako {count} fitxategi utzi zituen ({names}). Garbitu?",
},
leftoverClear: "Garbitu",
},
fileActions: {
deleteTitle: "Ezabatu",
deleteBody: "Ezabatu {what}? Karpetak errekurtsiboki ezabatzen dira. Ezin da desegin.",
deleteConfirm: "Ezabatu",
renameTitle: 'Berrizendatu "{name}"',
renameConfirm: "Berrizendatu",
newFolderTitle: "Karpeta berria",
newFolderPlaceholder: "karpetaren izena",
newFileTitle: "Fitxategi berria",
newFilePlaceholder: "fitxategiaren izena",
createConfirm: "Sortu",
itemsCount: "{count} elementu",
},
menu: {
about: "{app} (r)i buruz",
quit: "Irten",
file: "Fitxategia",
newTab: "Fitxa berria",
closeTab: "Itxi fitxa",
newFolder: "Karpeta berria",
newFile: "Fitxategi berria",
editFile: "Editatu fitxategia",
connect: "Konektatu",
connectionManager: "Konexio-kudeatzailea…",
edit: "Editatu",
view: "Ikusi",
refresh: "Freskatu",
window: "Leihoa",
},
ctx: {
size: "Tamaina",
modified: "Aldatze-data",
kind: "Mota",
rights: "Baimenak",
owner: "Jabea",
group: "Taldea",
revealFinder: "Erakutsi Finder-en",
revealExplorer: "Erakutsi Explorer-en",
revealFileManager: "Erakutsi fitxategi-kudeatzailean",
moveToTrash: "Eraman zakarrontzira",
moveToRecycleBin: "Eraman birziklatze-ontzira",
newFolder: "Karpeta berria",
newFile: "Fitxategi berria",
refresh: "Freskatu",
open: "Ireki",
openDefault: "Ireki lehenetsitako aplikazioarekin",
edit: "Editatu",
copyToOther: "Kopiatu beste panelera",
rename: "Berrizendatu",
permissions: "Baimenak",
copyPath: "Kopiatu bidea",
delete: "Ezabatu",
columns: "Zutabeak",
trashTooltip: "Finder-en «Jarri berriro» ez dago erabilgarri hemendik zakarrontzira botatako elementuentzat",
},
errors: {
sshAgentUnavailable: "ssh-agent ez dago erabilgarri (SSH_AUTH_SOCK ez dago ezarrita)",
identityFileNotFound: "identitate-fitxategia ez da aurkitu: {path}",
sessionNotConnected: '"{id}" saioa ez dago konektatuta',
localCannotDisconnect: "Saio lokala ezin da deskonektatu",
notADirectory: '"{path}" badago eta ez da karpeta bat',
sameFileTransfer: '"{name}": iturburua eta helmuga fitxategi bera dira',
sameFileNoName: "Iturburua eta helmuga fitxategi bera dira",
folderOntoItself: "Ezin da karpeta bat bere gainean kopiatu",
folderIntoItself: "Ezin da karpeta bat bere barnean kopiatu",
},
} as const;
+299
View File
@@ -0,0 +1,299 @@
// French translations. Missing keys fall back to English (see en.ts / index.ts).
export const fr = {
common: {
cancel: "Annuler",
save: "Enregistrer",
close: "Fermer",
delete: "Supprimer",
rename: "Renommer",
create: "Créer",
apply: "Appliquer",
choose: "Choisir…",
connect: "Connecter",
local: "Local",
},
app: {
notConnectedTitle: "Non connecté à l'application",
notConnectedBody: "Cette page s'exécute dans un navigateur classique. Lancez l'application de bureau pour l'utiliser.",
managerOpen: "Le gestionnaire de connexions est ouvert.",
showConnections: "Afficher les connexions",
},
tabBar: {
connections: "Connexions",
manageConnections: "Gérer les connexions",
newTab: "Nouvel onglet ({mod}T)",
closeTab: "Fermer l'onglet ({mod}W)",
options: "Options",
},
options: {
title: "Options",
language: "Langue",
system: "Système",
theme: { label: "Thème", system: "Système", light: "Clair", dark: "Sombre" },
tone: { label: "Ton", dark: "Sombre", darker: "Plus sombre" },
accent: { label: "Accent" },
accentNames: {
graphite: "Graphite",
blue: "Bleu",
indigo: "Indigo",
purple: "Violet",
rose: "Rose",
orange: "Orange",
green: "Vert",
teal: "Sarcelle",
},
fileIcons: { label: "Icônes", plain: "Simple", multi: "Couleur" },
interface: { label: "Interface", compact: "Compacte", default: "Normale", large: "Grande" },
speedUnit: { label: "Unité de vitesse" },
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
atOnce: "Simultanés",
editor: { label: "Éditeur", builtin: "Intégré", custom: "Personnalisé" },
systemDefault: "Valeur par défaut du système",
resetDefault: "Réinitialiser par défaut",
finishedTransfers: { label: "Transferts terminés", autoClear: "Effacer auto.", keep: "Garder" },
},
kinds: {
folder: "Dossier",
symlink: "Lien symbolique",
file: "Fichier",
image: "Image",
audio: "Audio",
video: "Vidéo",
archive: "Archive",
code: "Code",
document: "Document",
},
columns: {
name: "Nom",
size: "Taille",
mtime: "Modifié",
kind: "Type",
rights: "Droits",
owner: "Propriétaire",
group: "Groupe",
},
panel: {
choosePanel: "Choisir ce qu'affiche ce panneau",
copy: "Copier",
copyToRight: "Copier vers le panneau droit (F5)",
copyToLeft: "Copier vers le panneau gauche (F5)",
edit: "Éditer",
editFile: "Éditer le fichier (F4)",
rename: "Renommer (F2)",
newFolder: "Nouveau dossier (F7)",
newFile: "Nouveau fichier",
permissions: "Autorisations",
delete: "Supprimer",
deleteHint: "Supprimer (F8 / Suppr)",
parentDir: "Dossier parent (Retour arrière)",
refresh: "Actualiser",
clearFilter: "Effacer le filtre (Échap)",
sshConfig: "~/.ssh/config",
connectingTo: "Connexion à {name}…",
emptyDir: "Dossier vide",
noMatches: "Aucun résultat",
matches: { one: "{count} résultat · Échap", other: "{count} résultats · Échap" },
},
connMgr: {
title: "Connexions",
importWinscp: "Importer depuis WinSCP…",
importWinscpHint: "Importer des sessions depuis un export WinSCP.ini",
newConnection: "Connexion",
newConnectionHint: "Nouvelle connexion",
newStack: "Groupe",
newStackHint: "Nouveau groupe",
expand: "Développer",
collapse: "Réduire",
renameStack: "Renommer le groupe",
deleteStack: "Supprimer le groupe",
openStack: "Ouvrir le groupe dans des onglets",
empty: "vide",
connectionsHeader: "Connexions",
noneTitle: "Aucune connexion enregistrée.",
noneBody: "Créez-en une ou importez vos sessions WinSCP.",
name: "Nom",
namePlaceholder: "mon serveur",
host: "Hôte",
port: "Port",
stack: "Groupe",
stackNone: "(aucun)",
username: "Utilisateur",
remotePath: "Chemin distant (facultatif)",
remotePathPlaceholder: "dossier personnel",
auth: "Authentification",
authPassword: "Mot de passe",
authKey: "Clé privée",
password: "Mot de passe",
privateKeyFile: "Fichier de clé privée",
passphrase: "Phrase secrète (facultative)",
localDir: "Dossier local (facultatif)",
localDirPlaceholder: "~/projets/mon-serveur",
localDirHint: "L'autre panneau s'ouvre ici lorsqu'il est sur Local et n'a pas été parcouru.",
more: "Plus…",
less: "Moins…",
selectPrompt: "Sélectionnez une connexion ou créez-en une nouvelle.",
newStackTitle: "Nouveau groupe",
newStackPrompt: "nom du groupe",
renameStackTitle: "Renommer le groupe",
deleteStackTitle: "Supprimer le groupe",
deleteStackBody: 'Supprimer le groupe « {name} » ? Ses connexions sont conservées, sans groupe.',
deleteConnectionTitle: "Supprimer la connexion",
deleteConnectionBody: 'Supprimer « {name} » ?',
},
editor: {
noFileTitle: "Aucun fichier à éditer",
noFileBody: "Cette fenêtre a été ouverte sans contexte d'éditeur. Ouvrez des fichiers depuis l'application.",
wrap: "Retour",
wrapHint: "Retour à la ligne — masque les numéros de ligne ({alt}Z)",
findHint: "Rechercher ({mod}F)",
findPlaceholder: "Rechercher dans le fichier",
prevMatch: "Résultat précédent (⇧↵)",
nextMatch: "Résultat suivant (↵)",
closeHint: "Fermer (Échap)",
disconnected: "Déconnecté de {name} — l'enregistrement échouera, mais votre texte est conservé dans cette fenêtre.",
cantOpen: "Impossible d'ouvrir {name}",
closeWindow: "Fermer la fenêtre",
shortcuts: "{mod}S enregistrer · {mod}F rechercher · {mod}W fermer",
lines: { one: "{count} ligne", other: "{count} lignes" },
close: "Fermer",
save: "Enregistrer",
saveChangesTitle: 'Enregistrer les modifications de « {name} » ?',
saveChangesBody: "Vos modifications seront perdues si vous ne les enregistrez pas.",
dontSave: "Ne pas enregistrer",
countLabel: "{current} sur {total}",
noMatches: "Aucun résultat",
},
transfer: {
summary: "{active} en cours · {finished} terminés",
clearFinished: "Effacer les terminés",
cancel: "Annuler",
dismiss: "Ignorer",
showDetails: "Afficher les détails des transferts",
copying: "Copie de {name} · {percent}%{speed}",
queued: "{count} en file",
failed: { one: "{count} transfert échoué", other: "{count} transferts échoués" },
finished: "Transferts terminés",
ready: "Prêt",
items: "{count} éléments",
selected: "{count} sélectionnés",
queuedState: "En file",
scanning: "Analyse…",
failedState: "Échec",
cancelled: "Annulé",
completedFiles: { one: "{count} fichier · {size}", other: "{count} fichiers · {size}" },
left: "{eta} restant",
},
dialogs: {
chmodTitle: "Autorisations",
chmodItems: "{count} éléments",
read: "Lecture",
write: "Écriture",
execute: "Exécution",
roleOwner: "Propriétaire",
roleGroup: "Groupe",
roleOthers: "Autres",
octal: "Octal",
mergeFolder: "Fusionner le dossier",
replace: "Remplacer",
merge: "Fusionner",
mergeAll: "Tout fusionner",
replaceAll: "Tout remplacer",
skip: "Ignorer",
skipAll: "Tout ignorer",
ofTotal: "{index} sur {total}",
overwriteDirBody: "La destination contient déjà un dossier de ce nom. La fusion peut remplacer des fichiers.",
overwriteFileBody: "La destination contient déjà un fichier de ce nom.",
source: "Source",
destination: "Destination",
newer: "plus récent",
},
toasts: {
movedToTrash: '« {name} » mis à la corbeille',
editingExternal: "Édition de {name} — les enregistrements sont envoyés automatiquement",
nothingToCopy: "Rien à copier (les liens symboliques sont ignorés)",
nothingToCopyAllSkipped: "Rien à copier — tous les éléments en conflit ont été ignorés",
sameFolder: "Les deux panneaux affichent le même dossier",
sameFile: "« {name} » ignoré — c'est le même fichier",
typeMismatch: "« {name} » ignoré — un autre type d'élément est déjà là",
transferFailed: 'Le transfert « {name} » a échoué : {error}',
unknownError: "erreur inconnue",
saved: "{name} enregistré",
disconnectedReason: "{title} : déconnecté ({reason})",
imported: { one: "{count} connexion importée", other: "{count} connexions importées" },
importedIntoStacks: { one: " dans {count} nouveau groupe", other: " dans {count} nouveaux groupes" },
importedSkipped: " ({skipped} ignorées)",
leftoverTitle: "Fichiers édités restants",
leftoverMore: ", +{count} de plus",
leftoverBody: {
one: "Une session précédente a laissé {count} fichier édité temporaire ({names}). Les supprimer ?",
other: "Une session précédente a laissé {count} fichiers édités temporaires ({names}). Les supprimer ?",
},
leftoverClear: "Supprimer",
},
fileActions: {
deleteTitle: "Supprimer",
deleteBody: "Supprimer {what} ? Les dossiers sont supprimés récursivement. Action irréversible.",
deleteConfirm: "Supprimer",
renameTitle: 'Renommer « {name} »',
renameConfirm: "Renommer",
newFolderTitle: "Nouveau dossier",
newFolderPlaceholder: "nom du dossier",
newFileTitle: "Nouveau fichier",
newFilePlaceholder: "nom du fichier",
createConfirm: "Créer",
itemsCount: "{count} éléments",
},
menu: {
about: "À propos de {app}",
quit: "Quitter",
file: "Fichier",
newTab: "Nouvel onglet",
closeTab: "Fermer l'onglet",
newFolder: "Nouveau dossier",
newFile: "Nouveau fichier",
editFile: "Éditer le fichier",
connect: "Connecter",
connectionManager: "Gestionnaire de connexions…",
edit: "Édition",
view: "Affichage",
refresh: "Actualiser",
window: "Fenêtre",
},
ctx: {
size: "Taille",
modified: "Modifié",
kind: "Type",
rights: "Droits",
owner: "Propriétaire",
group: "Groupe",
revealFinder: "Afficher dans le Finder",
revealExplorer: "Afficher dans l'Explorateur",
revealFileManager: "Afficher dans le gestionnaire de fichiers",
moveToTrash: "Mettre à la corbeille",
moveToRecycleBin: "Mettre à la corbeille",
newFolder: "Nouveau dossier",
newFile: "Nouveau fichier",
refresh: "Actualiser",
open: "Ouvrir",
openDefault: "Ouvrir avec l'application par défaut",
edit: "Éditer",
copyToOther: "Copier vers l'autre panneau",
rename: "Renommer",
permissions: "Autorisations",
copyPath: "Copier le chemin",
delete: "Supprimer",
columns: "Colonnes",
trashTooltip: "La restauration du Finder n'est pas disponible pour les éléments mis à la corbeille d'ici",
},
errors: {
sshAgentUnavailable: "ssh-agent indisponible (SSH_AUTH_SOCK non défini)",
identityFileNotFound: "fichier d'identité introuvable : {path}",
sessionNotConnected: "La session « {id} » n'est pas connectée",
localCannotDisconnect: "La session locale ne peut pas être déconnectée",
notADirectory: "« {path} » existe et n'est pas un dossier",
sameFileTransfer: '« {name} » : la source et la destination sont le même fichier',
sameFileNoName: "La source et la destination sont le même fichier",
folderOntoItself: "Impossible de copier un dossier sur lui-même",
folderIntoItself: "Impossible de copier un dossier dans lui-même",
},
} as const;
+43
View File
@@ -0,0 +1,43 @@
import type { SupportedLocale } from "../types";
import { de } from "./de";
import { en } from "./en";
import { es } from "./es";
import { eu } from "./eu";
import { fr } from "./fr";
import { ja } from "./ja";
import { zh } from "./zh";
/** A translation tree: leaves are strings; a leaf may be a plural map. */
export type Messages = { readonly [key: string]: string | Messages };
export const DICTS: Record<SupportedLocale, Messages> = { en, es, eu, fr, de, zh, ja };
/** Order shown in the language picker. */
export const SUPPORTED: readonly SupportedLocale[] = ["en", "es", "eu", "fr", "de", "zh", "ja"];
/** Each language named in itself (endonyms) for the picker. */
export const LOCALE_LABELS: Record<SupportedLocale, string> = {
en: "English",
es: "Español",
eu: "Euskara",
fr: "Français",
de: "Deutsch",
zh: "中文",
ja: "日本語",
};
/**
* Resolve the effective locale: an explicit choice wins; "system" maps the OS
* BCP-47 tag (e.g. "es-ES", "zh-Hans-CN") to a supported language, else English.
*/
export function resolveLocale(
setting: "system" | SupportedLocale,
osLocale: string | undefined,
): SupportedLocale {
if (setting !== "system") return setting;
const tag = (osLocale ?? "en").toLowerCase();
const primary = tag.split("-")[0];
return (SUPPORTED as readonly string[]).includes(primary)
? (primary as SupportedLocale)
: "en";
}
+299
View File
@@ -0,0 +1,299 @@
// Japanese translations. Fall back to English for missing keys.
// TODO(i18n): AI draft — needs native-speaker review.
export const ja = {
common: {
cancel: "キャンセル",
save: "保存",
close: "閉じる",
delete: "削除",
rename: "名前を変更",
create: "作成",
apply: "適用",
choose: "選択…",
connect: "接続",
local: "ローカル",
},
app: {
notConnectedTitle: "アプリに接続されていません",
notConnectedBody: "このページは通常のブラウザで動作しています。デスクトップアプリを起動して使用してください。",
managerOpen: "接続マネージャーが開いています。",
showConnections: "接続を表示",
},
tabBar: {
connections: "接続",
manageConnections: "接続を管理",
newTab: "新しいタブ ({mod}T)",
closeTab: "タブを閉じる ({mod}W)",
options: "オプション",
},
options: {
title: "オプション",
language: "言語",
system: "システム",
theme: { label: "テーマ", system: "システム", light: "ライト", dark: "ダーク" },
tone: { label: "トーン", dark: "ダーク", darker: "より暗く" },
accent: { label: "アクセント" },
accentNames: {
graphite: "グラファイト",
blue: "ブルー",
indigo: "インディゴ",
purple: "パープル",
rose: "ローズ",
orange: "オレンジ",
green: "グリーン",
teal: "ティール",
},
fileIcons: { label: "ファイルアイコン", plain: "シンプル", multi: "カラー" },
interface: { label: "インターフェース", compact: "コンパクト", default: "標準", large: "大" },
speedUnit: { label: "速度の単位" },
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
atOnce: "同時数",
editor: { label: "エディタ", builtin: "内蔵", custom: "カスタム" },
systemDefault: "システムの既定",
resetDefault: "既定に戻す",
finishedTransfers: { label: "完了した転送", autoClear: "自動クリア", keep: "保持" },
},
kinds: {
folder: "フォルダ",
symlink: "シンボリックリンク",
file: "ファイル",
image: "画像",
audio: "オーディオ",
video: "動画",
archive: "アーカイブ",
code: "コード",
document: "ドキュメント",
},
columns: {
name: "名前",
size: "サイズ",
mtime: "更新日時",
kind: "種類",
rights: "権限",
owner: "所有者",
group: "グループ",
},
panel: {
choosePanel: "このパネルに表示する内容を選択",
copy: "コピー",
copyToRight: "右のパネルへコピー (F5)",
copyToLeft: "左のパネルへコピー (F5)",
edit: "編集",
editFile: "ファイルを編集 (F4)",
rename: "名前を変更 (F2)",
newFolder: "新規フォルダ (F7)",
newFile: "新規ファイル",
permissions: "アクセス権",
delete: "削除",
deleteHint: "削除 (F8 / Del)",
parentDir: "親フォルダ (Backspace)",
refresh: "更新",
clearFilter: "フィルターをクリア (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "{name} に接続中…",
emptyDir: "空のフォルダ",
noMatches: "一致なし",
matches: { other: "{count} 件一致 · Esc" },
},
connMgr: {
title: "接続",
importWinscp: "WinSCP からインポート…",
importWinscpHint: "WinSCP.ini エクスポートからセッションをインポート",
newConnection: "接続",
newConnectionHint: "新規接続",
newStack: "グループ",
newStackHint: "新規グループ",
expand: "展開",
collapse: "折りたたむ",
renameStack: "グループ名を変更",
deleteStack: "グループを削除",
openStack: "グループをタブで開く",
empty: "空",
connectionsHeader: "接続",
noneTitle: "保存された接続はまだありません。",
noneBody: "作成するか、WinSCP セッションをインポートしてください。",
name: "名前",
namePlaceholder: "マイサーバー",
host: "ホスト",
port: "ポート",
stack: "グループ",
stackNone: "(なし)",
username: "ユーザー名",
remotePath: "リモートパス(任意)",
remotePathPlaceholder: "ホームディレクトリ",
auth: "認証",
authPassword: "パスワード",
authKey: "秘密鍵",
password: "パスワード",
privateKeyFile: "秘密鍵ファイル",
passphrase: "パスフレーズ(任意)",
localDir: "ローカルディレクトリ(任意)",
localDirPlaceholder: "~/projects/my-server",
localDirHint: "もう一方のパネルがローカルで未操作のとき、ここが開きます。",
more: "詳細…",
less: "閉じる…",
selectPrompt: "接続を選択するか、新しく作成してください。",
newStackTitle: "新規グループ",
newStackPrompt: "グループ名",
renameStackTitle: "グループ名を変更",
deleteStackTitle: "グループを削除",
deleteStackBody: 'グループ "{name}" を削除しますか?含まれる接続は残り、グループ解除されます。',
deleteConnectionTitle: "接続を削除",
deleteConnectionBody: '"{name}" を削除しますか?',
},
editor: {
noFileTitle: "編集するファイルがありません",
noFileBody: "このウィンドウはエディタコンテキストなしで開かれました。アプリからファイルを開いてください。",
wrap: "折り返し",
wrapHint: "長い行を折り返す — 行番号を非表示 ({alt}Z)",
findHint: "検索 ({mod}F)",
findPlaceholder: "ファイル内を検索",
prevMatch: "前の一致 (⇧↵)",
nextMatch: "次の一致 (↵)",
closeHint: "閉じる (Esc)",
disconnected: "{name} から切断されました — 保存は失敗しますが、テキストはこのウィンドウに保持されます。",
cantOpen: "{name} を開けません",
closeWindow: "ウィンドウを閉じる",
shortcuts: "{mod}S 保存 · {mod}F 検索 · {mod}W 閉じる",
lines: { other: "{count} 行" },
close: "閉じる",
save: "保存",
saveChangesTitle: '"{name}" への変更を保存しますか?',
saveChangesBody: "保存しないと変更は失われます。",
dontSave: "保存しない",
countLabel: "{total} 件中 {current} 件目",
noMatches: "一致なし",
},
transfer: {
summary: "{active} 件実行中 · {finished} 件完了",
clearFinished: "完了分をクリア",
cancel: "キャンセル",
dismiss: "閉じる",
showDetails: "転送の詳細を表示",
copying: "{name} をコピー中 · {percent}%{speed}",
queued: "{count} 件待機中",
failed: { other: "{count} 件の転送が失敗" },
finished: "転送が完了しました",
ready: "準備完了",
items: "{count} 項目",
selected: "{count} 件選択中",
queuedState: "待機中",
scanning: "スキャン中…",
failedState: "失敗",
cancelled: "キャンセル済み",
completedFiles: { other: "{count} 個のファイル · {size}" },
left: "残り {eta}",
},
dialogs: {
chmodTitle: "アクセス権",
chmodItems: "{count} 項目",
read: "読み取り",
write: "書き込み",
execute: "実行",
roleOwner: "所有者",
roleGroup: "グループ",
roleOthers: "その他",
octal: "8進数",
mergeFolder: "フォルダを結合",
replace: "置き換える",
merge: "結合",
mergeAll: "すべて結合",
replaceAll: "すべて置き換える",
skip: "スキップ",
skipAll: "すべてスキップ",
ofTotal: "{total} 件中 {index} 件目",
overwriteDirBody: "コピー先に同名のフォルダが既にあります。結合すると中のファイルが置き換わる可能性があります。",
overwriteFileBody: "コピー先に同名のファイルが既にあります。",
source: "コピー元",
destination: "コピー先",
newer: "新しい",
},
toasts: {
movedToTrash: '"{name}" をゴミ箱に移動しました',
editingExternal: "{name} を編集中 — 保存すると自動でアップロードされます",
nothingToCopy: "コピーする項目がありません(シンボリックリンクはスキップされます)",
nothingToCopyAllSkipped: "コピーする項目がありません — 競合する項目はすべてスキップされました",
sameFolder: "両方のパネルが同じフォルダを表示しています",
sameFile: '"{name}" をスキップしました — 同じファイルです',
typeMismatch: '"{name}" をスキップしました — 別の種類の項目があります',
transferFailed: '転送 "{name}" が失敗しました: {error}',
unknownError: "不明なエラー",
saved: "{name} を保存しました",
disconnectedReason: "{title}: 切断されました ({reason})",
imported: { other: "{count} 件の接続をインポートしました" },
importedIntoStacks: { other: "({count} 件の新規グループに)" },
importedSkipped: "({skipped} 件スキップ)",
leftoverTitle: "残った編集済みファイル",
leftoverMore: "、他 {count} 件",
leftoverBody: {
other: "前回のセッションで {count} 件の一時編集ファイルが残っています({names})。クリアしますか?",
},
leftoverClear: "クリア",
},
fileActions: {
deleteTitle: "削除",
deleteBody: "{what} を削除しますか?フォルダは再帰的に削除されます。この操作は取り消せません。",
deleteConfirm: "削除",
renameTitle: '"{name}" の名前を変更',
renameConfirm: "名前を変更",
newFolderTitle: "新規フォルダ",
newFolderPlaceholder: "フォルダ名",
newFileTitle: "新規ファイル",
newFilePlaceholder: "ファイル名",
createConfirm: "作成",
itemsCount: "{count} 項目",
},
menu: {
about: "{app} について",
quit: "終了",
file: "ファイル",
newTab: "新しいタブ",
closeTab: "タブを閉じる",
newFolder: "新規フォルダ",
newFile: "新規ファイル",
editFile: "ファイルを編集",
connect: "接続",
connectionManager: "接続マネージャー…",
edit: "編集",
view: "表示",
refresh: "更新",
window: "ウィンドウ",
},
ctx: {
size: "サイズ",
modified: "更新日時",
kind: "種類",
rights: "権限",
owner: "所有者",
group: "グループ",
revealFinder: "Finder で表示",
revealExplorer: "エクスプローラーで表示",
revealFileManager: "ファイルマネージャーで表示",
moveToTrash: "ゴミ箱に移動",
moveToRecycleBin: "ごみ箱に移動",
newFolder: "新規フォルダ",
newFile: "新規ファイル",
refresh: "更新",
open: "開く",
openDefault: "既定のアプリで開く",
edit: "編集",
copyToOther: "もう一方のパネルへコピー",
rename: "名前を変更",
permissions: "アクセス権",
copyPath: "パスをコピー",
delete: "削除",
columns: "列",
trashTooltip: "ここからゴミ箱に入れた項目は Finder の「戻す」を利用できません",
},
errors: {
sshAgentUnavailable: "ssh-agent が利用できません(SSH_AUTH_SOCK が未設定)",
identityFileNotFound: "認証鍵ファイルが見つかりません: {path}",
sessionNotConnected: 'セッション "{id}" は接続されていません',
localCannotDisconnect: "ローカルセッションは切断できません",
notADirectory: '"{path}" は存在し、フォルダではありません',
sameFileTransfer: '"{name}": コピー元とコピー先が同じファイルです',
sameFileNoName: "コピー元とコピー先が同じファイルです",
folderOntoItself: "フォルダをそれ自身の上にコピーすることはできません",
folderIntoItself: "フォルダをそれ自身の中にコピーすることはできません",
},
} as const;
+299
View File
@@ -0,0 +1,299 @@
// Simplified Chinese translations. Fall back to English for missing keys.
// TODO(i18n): AI draft — needs native-speaker review.
export const zh = {
common: {
cancel: "取消",
save: "保存",
close: "关闭",
delete: "删除",
rename: "重命名",
create: "创建",
apply: "应用",
choose: "选择…",
connect: "连接",
local: "本地",
},
app: {
notConnectedTitle: "未连接到应用",
notConnectedBody: "此页面运行在普通浏览器中。请启动桌面应用来使用。",
managerOpen: "连接管理器已打开。",
showConnections: "显示连接",
},
tabBar: {
connections: "连接",
manageConnections: "管理连接",
newTab: "新建标签页 ({mod}T)",
closeTab: "关闭标签页 ({mod}W)",
options: "选项",
},
options: {
title: "选项",
language: "语言",
system: "系统",
theme: { label: "主题", system: "系统", light: "浅色", dark: "深色" },
tone: { label: "色调", dark: "深色", darker: "更深" },
accent: { label: "强调色" },
accentNames: {
graphite: "石墨",
blue: "蓝色",
indigo: "靛蓝",
purple: "紫色",
rose: "玫红",
orange: "橙色",
green: "绿色",
teal: "青色",
},
fileIcons: { label: "文件图标", plain: "简约", multi: "彩色" },
interface: { label: "界面", compact: "紧凑", default: "默认", large: "大" },
speedUnit: { label: "速度单位" },
transfers: { label: "传输", single: "单个", multi: "多个" },
atOnce: "同时",
editor: { label: "编辑器", builtin: "内置", custom: "自定义" },
systemDefault: "系统默认",
resetDefault: "恢复为系统默认",
finishedTransfers: { label: "已完成传输", autoClear: "自动清除", keep: "保留" },
},
kinds: {
folder: "文件夹",
symlink: "符号链接",
file: "文件",
image: "图像",
audio: "音频",
video: "视频",
archive: "压缩包",
code: "代码",
document: "文档",
},
columns: {
name: "名称",
size: "大小",
mtime: "修改时间",
kind: "类型",
rights: "权限",
owner: "所有者",
group: "组",
},
panel: {
choosePanel: "选择此面板显示的内容",
copy: "复制",
copyToRight: "复制到右侧面板 (F5)",
copyToLeft: "复制到左侧面板 (F5)",
edit: "编辑",
editFile: "编辑文件 (F4)",
rename: "重命名 (F2)",
newFolder: "新建文件夹 (F7)",
newFile: "新建文件",
permissions: "权限",
delete: "删除",
deleteHint: "删除 (F8 / Del)",
parentDir: "上级目录 (退格)",
refresh: "刷新",
clearFilter: "清除筛选 (Esc)",
sshConfig: "~/.ssh/config",
connectingTo: "正在连接 {name}…",
emptyDir: "空目录",
noMatches: "无匹配项",
matches: { other: "{count} 个匹配 · Esc" },
},
connMgr: {
title: "连接",
importWinscp: "从 WinSCP 导入…",
importWinscpHint: "从 WinSCP.ini 导出文件导入会话",
newConnection: "连接",
newConnectionHint: "新建连接",
newStack: "分组",
newStackHint: "新建分组",
expand: "展开",
collapse: "折叠",
renameStack: "重命名分组",
deleteStack: "删除分组",
openStack: "在标签页中打开分组",
empty: "空",
connectionsHeader: "连接",
noneTitle: "尚无已保存的连接。",
noneBody: "创建一个,或导入你的 WinSCP 会话。",
name: "名称",
namePlaceholder: "我的服务器",
host: "主机",
port: "端口",
stack: "分组",
stackNone: "(无)",
username: "用户名",
remotePath: "远程路径(可选)",
remotePathPlaceholder: "主目录",
auth: "认证",
authPassword: "密码",
authKey: "私钥",
password: "密码",
privateKeyFile: "私钥文件",
passphrase: "密钥口令(可选)",
localDir: "本地目录(可选)",
localDirPlaceholder: "~/projects/my-server",
localDirHint: "当另一个面板处于本地且未浏览过时,将在此打开。",
more: "更多…",
less: "收起…",
selectPrompt: "选择一个连接或新建一个。",
newStackTitle: "新建分组",
newStackPrompt: "分组名称",
renameStackTitle: "重命名分组",
deleteStackTitle: "删除分组",
deleteStackBody: '删除分组 "{name}"?其中的连接将保留但不再分组。',
deleteConnectionTitle: "删除连接",
deleteConnectionBody: '删除 "{name}"?',
},
editor: {
noFileTitle: "没有要编辑的文件",
noFileBody: "此窗口在没有编辑器上下文的情况下打开。请从应用中打开文件。",
wrap: "换行",
wrapHint: "长行自动换行 — 隐藏行号 ({alt}Z)",
findHint: "查找 ({mod}F)",
findPlaceholder: "在文件中查找",
prevMatch: "上一个匹配 (⇧↵)",
nextMatch: "下一个匹配 (↵)",
closeHint: "关闭 (Esc)",
disconnected: "已从 {name} 断开 — 保存将失败,但你的文本会保留在此窗口中。",
cantOpen: "无法打开 {name}",
closeWindow: "关闭窗口",
shortcuts: "{mod}S 保存 · {mod}F 查找 · {mod}W 关闭",
lines: { other: "{count} 行" },
close: "关闭",
save: "保存",
saveChangesTitle: '保存对 "{name}" 的更改?',
saveChangesBody: "如果不保存,你的更改将丢失。",
dontSave: "不保存",
countLabel: "第 {current} / {total}",
noMatches: "无匹配项",
},
transfer: {
summary: "{active} 进行中 · {finished} 已完成",
clearFinished: "清除已完成",
cancel: "取消",
dismiss: "忽略",
showDetails: "显示传输详情",
copying: "正在复制 {name} · {percent}%{speed}",
queued: "{count} 个排队中",
failed: { other: "{count} 个传输失败" },
finished: "传输已完成",
ready: "就绪",
items: "{count} 项",
selected: "已选 {count} 项",
queuedState: "排队中",
scanning: "扫描中…",
failedState: "失败",
cancelled: "已取消",
completedFiles: { other: "{count} 个文件 · {size}" },
left: "剩余 {eta}",
},
dialogs: {
chmodTitle: "权限",
chmodItems: "{count} 项",
read: "读",
write: "写",
execute: "执行",
roleOwner: "所有者",
roleGroup: "组",
roleOthers: "其他",
octal: "八进制",
mergeFolder: "合并文件夹",
replace: "替换",
merge: "合并",
mergeAll: "全部合并",
replaceAll: "全部替换",
skip: "跳过",
skipAll: "全部跳过",
ofTotal: "第 {index} / {total}",
overwriteDirBody: "目标位置已存在同名文件夹。合并可能会替换其中的文件。",
overwriteFileBody: "目标位置已存在同名文件。",
source: "源",
destination: "目标",
newer: "较新",
},
toasts: {
movedToTrash: '已将 "{name}" 移到废纸篓',
editingExternal: "正在编辑 {name} — 保存后会自动上传",
nothingToCopy: "没有可复制的内容(符号链接将被跳过)",
nothingToCopyAllSkipped: "没有可复制的内容 — 所有冲突项都被跳过",
sameFolder: "两个面板显示的是同一个文件夹",
sameFile: '已跳过 "{name}" — 它是同一个文件',
typeMismatch: '已跳过 "{name}" — 目标处是不同类型的项目',
transferFailed: '传输 "{name}" 失败:{error}',
unknownError: "未知错误",
saved: "已保存 {name}",
disconnectedReason: "{title}:已断开({reason})",
imported: { other: "已导入 {count} 个连接" },
importedIntoStacks: { other: ",归入 {count} 个新分组" },
importedSkipped: "(跳过 {skipped} 个)",
leftoverTitle: "残留的已编辑文件",
leftoverMore: ",还有 {count} 个",
leftoverBody: {
other: "上一次会话遗留了 {count} 个临时编辑文件({names})。要清除吗?",
},
leftoverClear: "清除",
},
fileActions: {
deleteTitle: "删除",
deleteBody: "删除 {what}?文件夹将被递归删除。此操作无法撤销。",
deleteConfirm: "删除",
renameTitle: '重命名 "{name}"',
renameConfirm: "重命名",
newFolderTitle: "新建文件夹",
newFolderPlaceholder: "文件夹名称",
newFileTitle: "新建文件",
newFilePlaceholder: "文件名称",
createConfirm: "创建",
itemsCount: "{count} 项",
},
menu: {
about: "关于 {app}",
quit: "退出",
file: "文件",
newTab: "新建标签页",
closeTab: "关闭标签页",
newFolder: "新建文件夹",
newFile: "新建文件",
editFile: "编辑文件",
connect: "连接",
connectionManager: "连接管理器…",
edit: "编辑",
view: "查看",
refresh: "刷新",
window: "窗口",
},
ctx: {
size: "大小",
modified: "修改时间",
kind: "类型",
rights: "权限",
owner: "所有者",
group: "组",
revealFinder: "在访达中显示",
revealExplorer: "在资源管理器中显示",
revealFileManager: "在文件管理器中显示",
moveToTrash: "移到废纸篓",
moveToRecycleBin: "移到回收站",
newFolder: "新建文件夹",
newFile: "新建文件",
refresh: "刷新",
open: "打开",
openDefault: "用默认应用打开",
edit: "编辑",
copyToOther: "复制到另一个面板",
rename: "重命名",
permissions: "权限",
copyPath: "复制路径",
delete: "删除",
columns: "列",
trashTooltip: "从此处移入废纸篓的项目无法使用访达的放回原处功能",
},
errors: {
sshAgentUnavailable: "ssh-agent 不可用(未设置 SSH_AUTH_SOCK)",
identityFileNotFound: "找不到身份文件:{path}",
sessionNotConnected: '会话 "{id}" 未连接',
localCannotDisconnect: "无法断开本地会话",
notADirectory: '"{path}" 已存在且不是文件夹',
sameFileTransfer: '"{name}":源和目标是同一个文件',
sameFileNoName: "源和目标是同一个文件",
folderOntoItself: "无法将文件夹复制到其自身之上",
folderIntoItself: "无法将文件夹复制到其自身内部",
},
} as const;
+6
View File
@@ -62,6 +62,9 @@ export interface ConnectionsFile {
connections: SavedConnection[];
}
/** UI languages the app ships translations for. */
export type SupportedLocale = "en" | "es" | "eu" | "fr" | "de" | "zh" | "ja";
export type AccentTone =
| "graphite"
| "blue"
@@ -73,6 +76,8 @@ export type AccentTone =
| "teal";
export interface AppSettings {
/** UI language; "system" resolves to the OS locale (fallback English). */
locale: "system" | SupportedLocale;
theme: "system" | "light" | "dark";
/** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */
darkTone: "dark" | "darker";
@@ -102,6 +107,7 @@ export interface AppSettings {
}
export const DEFAULT_SETTINGS: AppSettings = {
locale: "system",
theme: "system",
darkTone: "dark",
accent: "blue",