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
+16 -14
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Layout } from "@/components/layout";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -11,6 +12,7 @@ import { customFetch } from "@workspace/api-client-react";
import { useToast } from "@/hooks/use-toast";
export default function RedundancyPage() {
const { t } = useTranslation();
const [data, setData] = useState<any[] | null>(null);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
@@ -28,10 +30,10 @@ export default function RedundancyPage() {
method: "POST",
body: JSON.stringify({ toolId, relatedToolId, betterToolId }),
});
toast({ title: "Evaluation saved" });
toast({ title: t("redundancy.toastEvalSaved") });
setData(await customFetch<any[]>("/api/admin/redundancy"));
} catch {
toast({ title: "Failed to save evaluation", variant: "destructive" });
toast({ title: t("redundancy.toastEvalFailed"), variant: "destructive" });
}
}
@@ -44,10 +46,10 @@ export default function RedundancyPage() {
<div className="space-y-6">
<div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6 text-amber-500" />
<h1 className="text-3xl font-bold">Tool Analysis & Recommendations</h1>
<h1 className="text-3xl font-bold">{t("redundancy.title")}</h1>
</div>
<p className="text-muted-foreground">
Automatic redundancy detection with cost and rating comparison. Admin can manually confirm which tool is the better choice.
{t("redundancy.subtitle")}
</p>
{loading ? (
@@ -61,12 +63,12 @@ export default function RedundancyPage() {
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-semibold capitalize">{group.category}</h2>
<p className="text-xs text-muted-foreground mt-0.5">{group.tools.length} tools, {group.pairs.length} comparisons</p>
<p className="text-xs text-muted-foreground mt-0.5">{t("redundancy.toolsComparisons", { tools: group.tools.length, pairs: group.pairs.length })}</p>
</div>
{group.totalMonthlyCost > 0 && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{group.totalMonthlyCost.toFixed(2)}/mo total
{group.totalMonthlyCost.toFixed(2)}{t("redundancy.totalMonthly")}
</span>
)}
</div>
@@ -82,12 +84,12 @@ export default function RedundancyPage() {
<span className="font-medium">{tool.name}</span>
{tool.costs?.length > 0 && tool.totalMonthly > 0 && (
<Badge variant="outline" className="text-[10px] px-1 py-0 shrink-0">
{tool.totalMonthly.toFixed(2)}/mo
{tool.totalMonthly.toFixed(2)}{t("redundancy.perMonth")}
</Badge>
)}
</div>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground flex-wrap">
<span>{tool.ratingCount} reviews</span>
<span>{tool.ratingCount} {t("redundancy.reviews")}</span>
{tool.avgCombined != null && (
<>
<span>·</span>
@@ -104,7 +106,7 @@ export default function RedundancyPage() {
))}
</div>
</div>
<Badge variant="outline" className="shrink-0">{tool.features.length} features</Badge>
<Badge variant="outline" className="shrink-0">{tool.features.length} {t("redundancy.features")}</Badge>
</div>
</CardContent>
</Card>
@@ -114,7 +116,7 @@ export default function RedundancyPage() {
{group.pairs.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">Comparisons & Recommendations</h3>
<h3 className="text-sm font-medium text-muted-foreground">{t("redundancy.comparisonsTitle")}</h3>
{group.pairs.map((pair: any, i: number) => (
<Card key={i} className={pair.recommendation.certainty === "high" ? "border-green-300" : pair.recommendation.certainty === "medium" ? "border-amber-200" : ""}>
<CardContent className="p-4">
@@ -126,11 +128,11 @@ export default function RedundancyPage() {
</span>
<span className="text-[10px] text-muted-foreground">
{pair.a.avgCombined != null ? pair.a.avgCombined.toFixed(1) : "N/A"}
{pair.a.totalMonthly > 0 ? ` · ${pair.a.totalMonthly.toFixed(2)}/mo` : ""}
{pair.a.totalMonthly > 0 ? ` · ${pair.a.totalMonthly.toFixed(2)}${t("redundancy.perMonth")}` : ""}
</span>
</div>
<div className="text-center shrink-0">
<div className="text-xs text-muted-foreground font-medium">vs</div>
<div className="text-xs text-muted-foreground font-medium">{t("redundancy.vs")}</div>
<div className="flex items-center gap-1 justify-center mt-0.5">
<Progress value={pair.overlap} className="w-12 h-1.5" />
<span className="text-[10px] text-muted-foreground">{pair.overlap}%</span>
@@ -142,7 +144,7 @@ export default function RedundancyPage() {
</span>
<span className="text-[10px] text-muted-foreground">
{pair.b.avgCombined != null ? pair.b.avgCombined.toFixed(1) : "N/A"}
{pair.b.totalMonthly > 0 ? ` · ${pair.b.totalMonthly.toFixed(2)}/mo` : ""}
{pair.b.totalMonthly > 0 ? ` · ${pair.b.totalMonthly.toFixed(2)}${t("redundancy.perMonth")}` : ""}
</span>
</div>
</div>
@@ -188,7 +190,7 @@ export default function RedundancyPage() {
))}
</div>
) : (
<p className="text-muted-foreground py-8 text-center">No tools found.</p>
<p className="text-muted-foreground py-8 text-center">{t("redundancy.noTools")}</p>
)}
</div>
</Layout>