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
+13 -9
View File
@@ -1,5 +1,6 @@
import { Component, computed, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service';
import type { TransferSnapshot } from '@shared/types';
import { formatSpeed } from '../format';
import { PanelsService } from '../panels.service';
@@ -17,26 +18,26 @@ import { TransfersService } from '../transfers.service';
<button
(click)="transfers.expanded.set(!transfers.expanded())"
class="flex min-w-0 items-center gap-1.5 hover:text-foreground"
title="Show transfer details"
[title]="i18n.t('transfer.showDetails')"
>
@if (transfers.currentJob(); as job) {
<ng-icon name="mynaSpinner" size="0.8rem" class="shrink-0 animate-spin text-primary" />
<span class="truncate">
Copying {{ job.name }} · {{ percent(job) }}%{{ speedSuffix(job) }}
{{ i18n.t('transfer.copying', { name: job.name, percent: percent(job), speed: speedSuffix(job) }) }}
@if (transfers.queuedCount(); as queued) {
· {{ queued }} queued
· {{ i18n.t('transfer.queued', { count: queued }) }}
}
</span>
} @else if (transfers.failedJobs().length; as failed) {
<ng-icon name="mynaXCircle" size="0.8rem" class="shrink-0 text-destructive" />
<span class="text-destructive">
{{ failed }} transfer{{ failed === 1 ? '' : 's' }} failed
{{ i18n.t('transfer.failed', { count: failed }) }}
</span>
} @else if (transfers.hasJobs()) {
<ng-icon name="mynaCheckCircle" size="0.8rem" class="shrink-0 text-success" />
<span>Transfers finished</span>
<span>{{ i18n.t('transfer.finished') }}</span>
} @else {
<span>Ready</span>
<span>{{ i18n.t('transfer.ready') }}</span>
}
</button>
@@ -47,16 +48,19 @@ import { TransfersService } from '../transfers.service';
size="0.8rem"
[class.text-primary]="activePanel().sessionId() !== 'local'"
/>
<span class="max-w-56 truncate">{{ activePanel().title() }}</span>
<span>· {{ activePanel().sorted().length }} items</span>
<span class="max-w-56 truncate">
{{ activePanel().sessionId() === 'local' ? i18n.t('common.local') : activePanel().title() }}
</span>
<span>· {{ i18n.t('transfer.items', { count: activePanel().sorted().length }) }}</span>
@if (activePanel().selected().size; as count) {
<span>· {{ count }} selected</span>
<span>· {{ i18n.t('transfer.selected', { count }) }}</span>
}
</div>
</div>
`,
})
export class StatusBar {
readonly i18n = inject(I18nService);
readonly transfers = inject(TransfersService);
readonly panels = inject(PanelsService);
readonly settings = inject(SettingsService);