feat(i18n): full app + docs localization (de/en)
Build & Push Docker Image / build (push) Successful in 2m33s

- translate remaining pages/components (admin, analytics, redundancy,
  trash, compare, browse, watchlist, admin tools tab, category combobox,
  theme toggle, tool preview card, tool-new/edit) to t() calls
- locales: 453 keys each, parity verified
- docs: bilingual handbook + release notes via *.en.md variants,
  language-aware docs.tsx (markdown paths, nav titles, search index),
  LanguageSwitcher in docs header
- generate-docs: emit per-locale handbook/release/search output,
  localized index.json fields (fileEn/titleEn), en-aware snapshots
This commit is contained in:
opencode
2026-08-04 08:35:05 +02:00
parent a65e2eb92c
commit 3de59ccaaf
77 changed files with 3786 additions and 440 deletions
+29 -30
View File
@@ -11,7 +11,6 @@ import {
getListAllTagsQueryKey,
getGetTopToolsQueryKey,
getGetAnalyticsSummaryQueryKey,
type Tool,
} from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
@@ -91,12 +90,12 @@ export default function Trash() {
{ data: { ids } },
{
onSuccess: (res) => {
toast({ title: "Tools restored", description: `${res.restored ?? ids.length} tool(s) restored.` });
toast({ title: t("trash.toastRestored"), description: t("trash.toastRestoredSub", { count: res.restored ?? ids.length }) });
setSelected(new Set());
invalidate();
},
onError: (err) => {
toast({ title: "Failed to restore", description: err.data?.error ?? err.message, variant: "destructive" });
toast({ title: t("trash.toastRestoreFailed"), description: err.data?.error ?? err.message, variant: "destructive" });
},
},
);
@@ -107,13 +106,13 @@ export default function Trash() {
{ data: { ids } },
{
onSuccess: () => {
toast({ title: "Tools deleted", description: `${ids.length} tool(s) permanently removed.` });
toast({ title: t("trash.toastDeleted"), description: t("trash.toastDeletedSub", { count: ids.length }) });
setSelected(new Set());
setConfirmDelete(false);
invalidate();
},
onError: (err) => {
toast({ title: "Failed to delete", description: err.data?.error ?? err.message, variant: "destructive" });
toast({ title: t("trash.toastDeleteFailed"), description: err.data?.error ?? err.message, variant: "destructive" });
},
},
);
@@ -124,13 +123,13 @@ export default function Trash() {
undefined,
{
onSuccess: (res) => {
toast({ title: "Trash emptied", description: `${res.deleted ?? 0} tool(s) permanently removed.` });
toast({ title: t("trash.toastEmptied"), description: t("trash.toastEmptiedSub", { count: res.deleted ?? 0 }) });
setSelected(new Set());
setConfirmEmpty(false);
invalidate();
},
onError: (err) => {
toast({ title: "Failed to empty trash", description: err.data?.error ?? err.message, variant: "destructive" });
toast({ title: t("trash.toastEmptyFailed"), description: err.data?.error ?? err.message, variant: "destructive" });
},
},
);
@@ -227,48 +226,48 @@ export default function Trash() {
<Checkbox
checked={selected.size === trashed.length && trashed.length > 0}
onCheckedChange={toggleAll}
aria-label="Select all"
aria-label={t("trash.selectAll")}
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Category</TableHead>
<TableHead>{t("trash.name")}</TableHead>
<TableHead>{t("trash.category")}</TableHead>
<TableHead>{t("trash.deletedAt")}</TableHead>
<TableHead>{t("trash.deletedBy")}</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead className="text-right">{t("trash.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{trashed.map((t: Tool) => (
<TableRow key={t.id} className={selected.has(t.id) ? "bg-muted/40" : undefined}>
{trashed.map((tool) => (
<TableRow key={tool.id} className={selected.has(tool.id) ? "bg-muted/40" : undefined}>
<TableCell>
<Checkbox
checked={selected.has(t.id)}
onCheckedChange={() => toggle(t.id)}
aria-label={`Select ${t.name}`}
checked={selected.has(tool.id)}
onCheckedChange={() => toggle(tool.id)}
aria-label={t("trash.selectName", { name: tool.name })}
/>
</TableCell>
<TableCell className="font-medium">{t.name}</TableCell>
<TableCell className="font-medium">{tool.name}</TableCell>
<TableCell>
<Badge variant="outline">{t.category}</Badge>
<Badge variant="outline">{tool.category}</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{t.deletedAt ? format(new Date(t.deletedAt), "dd.MM.yyyy HH:mm") : "—"}
{tool.deletedAt ? format(new Date(tool.deletedAt), "dd.MM.yyyy HH:mm") : "—"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{t.deletedBy ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{tool.deletedBy ?? "—"}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="sm" onClick={() => handleRestore([t.id])} disabled={restore.isPending}>
<RotateCcw className="w-3.5 h-3.5" /> Restore
<Button variant="ghost" size="sm" onClick={() => handleRestore([tool.id])} disabled={restore.isPending}>
<RotateCcw className="w-3.5 h-3.5" /> {t("trash.restoreAction")}
</Button>
{isAdmin && (
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() => handleDeletePermanent([t.id])}
onClick={() => handleDeletePermanent([tool.id])}
disabled={deletePermanent.isPending}
>
<TrashIcon className="w-3.5 h-3.5" /> Delete
<TrashIcon className="w-3.5 h-3.5" /> {t("trash.deleteAction")}
</Button>
)}
</div>
@@ -285,13 +284,13 @@ export default function Trash() {
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {selectedIds.length} tool(s) permanently?</AlertDialogTitle>
<AlertDialogTitle>{t("trash.deleteConfirmTitle", { count: selectedIds.length })}</AlertDialogTitle>
<AlertDialogDescription>
This permanently removes the selected tools along with all their ratings, costs and relations. This cannot be undone.
{t("trash.deleteConfirmSub")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => handleDeletePermanent(selectedIds)}
@@ -305,13 +304,13 @@ export default function Trash() {
<AlertDialog open={confirmEmpty} onOpenChange={setConfirmEmpty}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Empty the trash?</AlertDialogTitle>
<AlertDialogTitle>{t("trash.emptyConfirmTitle")}</AlertDialogTitle>
<AlertDialogDescription>
This permanently removes all {trashed.length} tool(s) in the trash along with their ratings, costs and relations. This cannot be undone.
{t("trash.emptyConfirmSub", { count: trashed.length })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={handleEmpty}