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
+11 -4
View File
@@ -1,9 +1,10 @@
import { Injectable, computed, effect, inject, signal, type WritableSignal } from '@angular/core';
import type { ColumnKey, ConnectionConfig, FileEntry, SavedConnection } from '@shared/types';
import { fileKind } from './file-icons';
import { fileKindKey } from './file-icons';
import { FocusService } from './focus.service';
import { formatMode, isRootPath, joinPath } from './format';
import { RpcService } from './rpc.service';
import { I18nService } from './i18n.service';
import { ToastService } from './toast.service';
export type Side = 'left' | 'right';
@@ -12,7 +13,7 @@ export type SortKey = 'name' | ColumnKey;
/** Display string the text-based columns sort by. */
function sortText(entry: FileEntry, key: 'kind' | 'rights' | 'owner' | 'group'): string {
if (key === 'kind') return fileKind(entry);
if (key === 'kind') return fileKindKey(entry);
if (key === 'rights') return formatMode(entry.mode);
return (key === 'owner' ? entry.owner : entry.group) ?? '';
}
@@ -225,6 +226,7 @@ export interface Workspace {
@Injectable({ providedIn: 'root' })
export class PanelsService {
private readonly i18n = inject(I18nService);
readonly workspaces = signal<Workspace[]>([]);
readonly activeWorkspaceId = signal(0);
readonly activeWorkspace = computed(() => {
@@ -248,7 +250,12 @@ export class PanelsService {
for (const workspace of this.workspaces()) {
for (const panel of [workspace.left, workspace.right]) {
if (panel.sessionId() === event.sessionId) {
this.toast.error(`${panel.title()}: disconnected (${event.reason})`);
this.toast.error(
this.i18n.t('toasts.disconnectedReason', {
title: panel.title(),
reason: event.reason,
}),
);
this.resetToLocal(panel);
}
}
@@ -453,7 +460,7 @@ export class PanelsService {
const remoteTitles = [workspace.left, workspace.right]
.filter((p) => p.sessionId() !== 'local')
.map((p) => p.title());
return remoteTitles.length ? remoteTitles.join(' ⇄ ') : 'Local';
return remoteTitles.length ? remoteTitles.join(' ⇄ ') : this.i18n.t('common.local');
}
workspaceIsRemote(workspace: Workspace): boolean {