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
+32 -15
View File
@@ -1,34 +1,51 @@
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
export function formatSize(bytes: number): string {
/** Locale-aware number formatting (decimal separator); cached per locale. */
const NUM_FORMATS = new Map<string, Intl.NumberFormat>();
function num(value: number, locale: string | undefined): string {
const key = locale ?? '';
let fmt = NUM_FORMATS.get(key);
if (!fmt) {
fmt = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
NUM_FORMATS.set(key, fmt);
}
return fmt.format(value);
}
export function formatSize(bytes: number, locale?: string): string {
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
value /= 1024;
unit++;
}
return `${unit === 0 ? value : value.toFixed(1)} ${SIZE_UNITS[unit]}`;
return `${unit === 0 ? num(value, locale) : num(Number(value.toFixed(1)), locale)} ${SIZE_UNITS[unit]}`;
}
const DATE_FORMAT = new Intl.DateTimeFormat(undefined, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
export function formatDate(epochMs: number): string {
const DATE_FORMATS = new Map<string, Intl.DateTimeFormat>();
export function formatDate(epochMs: number, locale?: string): string {
if (!epochMs) return '';
return DATE_FORMAT.format(new Date(epochMs));
const key = locale ?? '';
let fmt = DATE_FORMATS.get(key);
if (!fmt) {
fmt = new Intl.DateTimeFormat(locale, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
DATE_FORMATS.set(key, fmt);
}
return fmt.format(new Date(epochMs));
}
/** bytes/sec -> "14.2 MB/s" or "113.8 Mbps" depending on the configured unit. */
export function formatSpeed(bytesPerSec: number, unit: 'MBps' | 'Mbps'): string {
export function formatSpeed(bytesPerSec: number, unit: 'MBps' | 'Mbps', locale?: string): string {
if (unit === 'Mbps') {
return `${((bytesPerSec * 8) / 1_000_000).toFixed(1)} Mbps`;
return `${num(Number(((bytesPerSec * 8) / 1_000_000).toFixed(1)), locale)} Mbps`;
}
return `${formatSize(bytesPerSec)}/s`;
return `${formatSize(bytesPerSec, locale)}/s`;
}
/** Seconds -> "0:42" / "3:07" / "1:02:15". */