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;
}
}