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
+14 -10
View File
@@ -1,8 +1,9 @@
import { Component, computed, inject, input, signal } from '@angular/core';
import { I18nService } from '../i18n.service';
import { DialogsService, type ChmodRequest } from '../dialogs.service';
import { FocusService } from '../focus.service';
const ROLES = ['Owner', 'Group', 'Others'] as const;
const ROLES = ['roleOwner', 'roleGroup', 'roleOthers'] as const;
const PERMS = ['r', 'w', 'x'] as const;
@Component({
@@ -14,22 +15,22 @@ const PERMS = ['r', 'w', 'x'] as const;
(mousedown)="$event.stopPropagation()"
(keydown.escape)="cancel()"
>
<h2 class="text-sm font-semibold">Permissions</h2>
<h2 class="text-sm font-semibold">{{ i18n.t('dialogs.chmodTitle') }}</h2>
<p class="mt-1 truncate text-xs text-muted-foreground">{{ targetLabel() }}</p>
<table class="mt-3 w-full text-xs">
<thead>
<tr class="text-muted-foreground">
<th class="pb-1 text-left font-normal"></th>
<th class="pb-1 font-normal">Read</th>
<th class="pb-1 font-normal">Write</th>
<th class="pb-1 font-normal">Execute</th>
<th class="pb-1 font-normal">{{ i18n.t('dialogs.read') }}</th>
<th class="pb-1 font-normal">{{ i18n.t('dialogs.write') }}</th>
<th class="pb-1 font-normal">{{ i18n.t('dialogs.execute') }}</th>
</tr>
</thead>
<tbody>
@for (role of roles; track role; let r = $index) {
<tr>
<td class="py-1 pr-2 text-muted-foreground">{{ role }}</td>
<td class="py-1 pr-2 text-muted-foreground">{{ i18n.t('dialogs.' + role) }}</td>
@for (perm of perms; track perm; let p = $index) {
<td class="py-1 text-center">
<input
@@ -46,7 +47,7 @@ const PERMS = ['r', 'w', 'x'] as const;
</table>
<div class="mt-3 flex items-center gap-2">
<label class="text-xs text-muted-foreground">Octal</label>
<label class="text-xs text-muted-foreground">{{ i18n.t('dialogs.octal') }}</label>
<input
[value]="octal()"
(input)="setOctal($any($event.target).value)"
@@ -61,13 +62,13 @@ const PERMS = ['r', 'w', 'x'] as const;
(click)="cancel()"
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
>
Cancel
{{ i18n.t('common.cancel') }}
</button>
<button
(click)="submit()"
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
Apply
{{ i18n.t('common.apply') }}
</button>
</div>
</div>
@@ -75,6 +76,7 @@ const PERMS = ['r', 'w', 'x'] as const;
`,
})
export class ChmodDialog {
readonly i18n = inject(I18nService);
readonly request = input.required<ChmodRequest>();
readonly roles = ROLES;
@@ -90,7 +92,9 @@ export class ChmodDialog {
targetLabel(): string {
const targets = this.request().targets;
return targets.length === 1 ? targets[0] : `${targets.length} items`;
return targets.length === 1
? targets[0]
: this.i18n.t('dialogs.chmodItems', { count: targets.length });
}
hasBit(role: number, perm: number): boolean {