+
{!isLoading && (
isAuthenticated ? (
@@ -152,7 +159,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
) : (
)
)}
diff --git a/artifacts/toolrate/src/i18n/index.ts b/artifacts/toolrate/src/i18n/index.ts
new file mode 100644
index 0000000..207426b
--- /dev/null
+++ b/artifacts/toolrate/src/i18n/index.ts
@@ -0,0 +1,37 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import en from "./locales/en.json";
+import de from "./locales/de.json";
+
+export const SUPPORTED_LANGUAGES = [
+ { code: "en", label: "English" },
+ { code: "de", label: "Deutsch" },
+] as const;
+
+export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]["code"];
+
+const STORAGE_KEY = "toolr-language";
+
+export function getStoredLanguage(): LanguageCode {
+ if (typeof window === "undefined") return "en";
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ if (stored === "en" || stored === "de") return stored;
+ const nav = window.navigator.language?.toLowerCase() ?? "";
+ return nav.startsWith("de") ? "de" : "en";
+}
+
+export function setStoredLanguage(lang: LanguageCode) {
+ window.localStorage.setItem(STORAGE_KEY, lang);
+}
+
+void i18n.use(initReactI18next).init({
+ resources: {
+ en: { translation: en },
+ de: { translation: de },
+ },
+ lng: getStoredLanguage(),
+ fallbackLng: "en",
+ interpolation: { escapeValue: false },
+});
+
+export default i18n;
diff --git a/artifacts/toolrate/src/i18n/locales/de.json b/artifacts/toolrate/src/i18n/locales/de.json
new file mode 100644
index 0000000..77a9de8
--- /dev/null
+++ b/artifacts/toolrate/src/i18n/locales/de.json
@@ -0,0 +1,168 @@
+{
+ "app": {
+ "name": "toolr",
+ "tagline": "Tool-Bewertungsplattform"
+ },
+ "nav": {
+ "home": "Start",
+ "browseTools": "Tools durchsuchen",
+ "addTool": "Tool hinzufügen",
+ "analytics": "Analysen",
+ "watchlist": "Merkliste",
+ "trash": "Papierkorb",
+ "admin": "Admin",
+ "redundancy": "Redundanz"
+ },
+ "auth": {
+ "signIn": "Anmelden",
+ "signInKeycloak": "Mit Keycloak anmelden",
+ "signOut": "Abmelden",
+ "username": "Benutzername",
+ "password": "Passwort",
+ "login": "Anmelden",
+ "invalidCredentials": "Ungültiger Benutzername oder ungültiges Passwort",
+ "notAuthenticated": "Nicht angemeldet",
+ "loginSubtitle": "Melde dich bei deinem Konto an",
+ "loginDescription": "Gib deine Zugangsdaten ein, um fortzufahren"
+ },
+ "common": {
+ "cancel": "Abbrechen",
+ "save": "Speichern",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "clear": "Leeren",
+ "clearAll": "Alle löschen",
+ "search": "Suchen",
+ "add": "Hinzufügen",
+ "back": "Zurück",
+ "backToTools": "Zurück zu den Tools",
+ "browseTools": "Tools durchsuchen",
+ "viewAll": "Alle anzeigen",
+ "loading": "Wird geladen…",
+ "all": "Alle",
+ "none": "Keine",
+ "language": "Sprache"
+ },
+ "home": {
+ "welcome": "Willkommen bei toolr",
+ "tagline": "Die Community-Plattform, auf der Ingenieure die Tools, die sie täglich nutzen, ehrlich bewerten.",
+ "totalTools": "Tools gesamt",
+ "totalRatings": "Bewertungen gesamt",
+ "avgRating": "Ø Bewertung",
+ "topRated": "Bestbewertete Tools",
+ "topRatedSub": "Höchste kombinierte Nützlichkeit und Benutzerfreundlichkeit.",
+ "recentlyAdded": "Zuletzt hinzugefügt",
+ "recentlyAddedSub": "Die neuesten Ergänzungen im Katalog.",
+ "noRatingsYet": "Noch keine Bewertungen",
+ "noRatingsSub": "Bewerte als Erster ein Tool und hilf der Community.",
+ "noTools": "Keine Tools gefunden",
+ "noToolsSub": "Es sind noch keine Tools in der Datenbank vorhanden."
+ },
+ "browse": {
+ "filters": "Filter",
+ "subtitle": "Entdecke und bewerte die besten Tools für deinen Stack.",
+ "allCategories": "Alle Kategorien",
+ "showingTool_one": "{{count}} Tool wird angezeigt",
+ "showingTool_other": "{{count}} Tools werden angezeigt",
+ "tool": "Tool",
+ "category": "Kategorie",
+ "rating": "Bewertung",
+ "reviews": "Bewertungen",
+ "noToolsFound": "Keine Tools gefunden",
+ "noToolsMatch": "Wir konnten keine Tools finden, die zu deinen Filtern passen. Versuche, die Suchkriterien anzupassen, oder füge ein neues Tool hinzu.",
+ "addTool": "Tool hinzufügen",
+ "compare": "Vergleichen",
+ "compareCount": "Vergleichen ({{count}})",
+ "addToCompare": "Zum Vergleich hinzufügen",
+ "removeFromCompare": "Aus dem Vergleich entfernen",
+ "comparePremiumTitle": "Vergleichen ist ein Premium-Feature",
+ "comparePremiumSub": "Das Vergleichen von Tools nebeneinander ist für Premium- und Enterprise-Nutzer verfügbar. Erweitere deinen Plan, um es freizuschalten.",
+ "notNow": "Später",
+ "upgradePremium": "Auf Premium upgraden",
+ "sortNewest": "Neueste",
+ "sortTopRated": "Bestbewertet",
+ "sortMostReviewed": "Meiste Bewertungen",
+ "sortNameAsc": "Name (A–Z)",
+ "sortNameDesc": "Name (Z–A)",
+ "sortRecentlyUpdated": "Zuletzt aktualisiert"
+ },
+ "filter": {
+ "tags": "Tags",
+ "features": "Funktionen",
+ "minRating": "Mindestbewertung",
+ "any": "Beliebig",
+ "clearFilters": "Filter zurücksetzen"
+ },
+ "detail": {
+ "ratingBreakdown": "Bewertungsaufschlüsselung",
+ "usefulness": "Nützlichkeit",
+ "usability": "Benutzerfreundlichkeit",
+ "scoreDistribution": "Punkteverteilung",
+ "scoreTrend": "Punkteverlauf",
+ "notEnoughRatings": "Noch nicht genügend Bewertungen für einen Verlauf.",
+ "reviews": "Bewertungen",
+ "basedOnReview_one": "Basierend auf {{count}} Bewertung",
+ "basedOnReview_other": "Basierend auf {{count}} Bewertungen",
+ "saveToWatchlist": "Zur Merkliste hinzufügen",
+ "savedToWatchlist": "Zur Merkliste hinzugefügt",
+ "removeFromWatchlist": "Von der Merkliste entfernen",
+ "visitWebsite": "Website besuchen",
+ "edit": "Bearbeiten",
+ "delete": "Löschen",
+ "noReviews": "Noch keine Bewertungen.",
+ "addReview": "Bewertung hinzufügen",
+ "yourRating": "Deine Bewertung",
+ "submit": "Bewertung absenden",
+ "relatedTools": "Ähnliche Tools",
+ "costs": "Kosten",
+ "addCost": "Kosten hinzufügen",
+ "recentRatings": "Letzte Bewertungen"
+ },
+ "compare": {
+ "title": "Tools vergleichen",
+ "subtitle": "{{count}} Tool(s) werden nebeneinander verglichen.",
+ "sideBySide": "Vergleich nebeneinander",
+ "bestValue": "Der beste Wert jeder Zeile ist hervorgehoben.",
+ "attribute": "Eigenschaft",
+ "rating": "Bewertung",
+ "reviews": "Bewertungen",
+ "description": "Beschreibung",
+ "lastUpdated": "Zuletzt aktualisiert",
+ "requiresPremium": "Vergleichen erfordert einen höheren Tarif",
+ "requiresPremiumSub": "Das Vergleichen von Tools nebeneinander ist für Premium- und Enterprise-Nutzer verfügbar.",
+ "nothingToCompare": "Nichts zu vergleichen",
+ "nothingToCompareSub": "Wähle mindestens ein Tool aus und kehre hierher zurück.",
+ "backToBrowse": "Zurück zur Übersicht"
+ },
+ "watchlist": {
+ "title": "Merkliste",
+ "subtitle": "Tools, die du für später gespeichert hast, mit Live-Punkten.",
+ "requiresPremium": "Die Merkliste erfordert einen höheren Tarif",
+ "requiresPremiumSub": "Das Speichern von Tools in deiner Merkliste ist für Premium- und Enterprise-Nutzer verfügbar.",
+ "empty": "Deine Merkliste ist leer",
+ "emptySub": "Durchsuche die Tools und klicke auf das Lesezeichen, um sie hier zu speichern.",
+ "remove": "Von der Merkliste entfernen"
+ },
+ "trash": {
+ "title": "Papierkorb",
+ "subtitle": "Gelöschte Tools werden hier aufbewahrt, bis sie wiederhergestellt oder endgültig entfernt werden.",
+ "requiresPremium": "Der Papierkorb erfordert einen höheren Tarif",
+ "requiresPremiumSub": "Der Papierkorb ist für Premium- und Enterprise-Nutzer verfügbar.",
+ "empty": "Der Papierkorb ist leer.",
+ "noMatch": "Keine Tools entsprechen deiner Suche.",
+ "trashedTools": "Gelöschte Tools",
+ "toolsInTrash_one": "{{count}} Tool im Papierkorb",
+ "toolsInTrash_other": "{{count}} Tools im Papierkorb",
+ "selected_one": "{{count}} ausgewählt",
+ "selected_other": "{{count}} ausgewählt",
+ "restore": "Wiederherstellen",
+ "deletePermanently": "Endgültig löschen",
+ "emptyTrash": "Papierkorb leeren",
+ "deletedAt": "Gelöscht am",
+ "deletedBy": "Gelöscht von"
+ },
+ "notFound": {
+ "text": "Diese Seite existiert nicht.",
+ "backHome": "Zurück zur Startseite"
+ }
+}
\ No newline at end of file
diff --git a/artifacts/toolrate/src/i18n/locales/en.json b/artifacts/toolrate/src/i18n/locales/en.json
new file mode 100644
index 0000000..db5f533
--- /dev/null
+++ b/artifacts/toolrate/src/i18n/locales/en.json
@@ -0,0 +1,168 @@
+{
+ "app": {
+ "name": "toolr",
+ "tagline": "Tool-Bewertungsplattform"
+ },
+ "nav": {
+ "home": "Home",
+ "browseTools": "Browse Tools",
+ "addTool": "Add Tool",
+ "analytics": "Analytics",
+ "watchlist": "Watchlist",
+ "trash": "Trash",
+ "admin": "Admin",
+ "redundancy": "Redundancy"
+ },
+ "auth": {
+ "signIn": "Sign in",
+ "signInKeycloak": "Sign in with Keycloak",
+ "signOut": "Sign out",
+ "username": "Username",
+ "password": "Password",
+ "login": "Login",
+ "invalidCredentials": "Invalid username or password",
+ "notAuthenticated": "Not authenticated",
+ "loginSubtitle": "Sign in to your account",
+ "loginDescription": "Enter your credentials to continue"
+ },
+ "common": {
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "edit": "Edit",
+ "clear": "Clear",
+ "clearAll": "Clear all",
+ "search": "Search",
+ "add": "Add",
+ "back": "Back",
+ "backToTools": "Back to tools",
+ "browseTools": "Browse tools",
+ "viewAll": "View all",
+ "loading": "Loading…",
+ "all": "All",
+ "none": "None",
+ "language": "Language"
+ },
+ "home": {
+ "welcome": "Welcome to toolr",
+ "tagline": "The community hub where engineers honestly rate the tools they use daily.",
+ "totalTools": "Total Tools",
+ "totalRatings": "Total Ratings",
+ "avgRating": "Avg Rating",
+ "topRated": "Top Rated Tools",
+ "topRatedSub": "Highest combined usefulness and usability.",
+ "recentlyAdded": "Recently Added",
+ "recentlyAddedSub": "The latest additions to the catalog.",
+ "noRatingsYet": "No ratings yet",
+ "noRatingsSub": "Be the first to rate a tool and help the community.",
+ "noTools": "No tools found",
+ "noToolsSub": "There are no tools in the database yet."
+ },
+ "browse": {
+ "filters": "Filters",
+ "subtitle": "Discover and evaluate the best tools for your stack.",
+ "allCategories": "All Categories",
+ "showingTool_one": "Showing {{count}} tool",
+ "showingTool_other": "Showing {{count}} tools",
+ "tool": "Tool",
+ "category": "Category",
+ "rating": "Rating",
+ "reviews": "Reviews",
+ "noToolsFound": "No tools found",
+ "noToolsMatch": "We couldn't find any tools matching your current filters. Try adjusting your search criteria or add a new tool.",
+ "addTool": "Add Tool",
+ "compare": "Compare",
+ "compareCount": "Compare ({{count}})",
+ "addToCompare": "Add to compare",
+ "removeFromCompare": "Remove from compare",
+ "comparePremiumTitle": "Compare is a Premium feature",
+ "comparePremiumSub": "Comparing tools side-by-side is available to Premium and Enterprise users. Upgrade your plan to unlock it.",
+ "notNow": "Not now",
+ "upgradePremium": "Upgrade to Premium",
+ "sortNewest": "Newest",
+ "sortTopRated": "Top Rated",
+ "sortMostReviewed": "Most Reviewed",
+ "sortNameAsc": "Name (A–Z)",
+ "sortNameDesc": "Name (Z–A)",
+ "sortRecentlyUpdated": "Recently Updated"
+ },
+ "filter": {
+ "tags": "Tags",
+ "features": "Features",
+ "minRating": "Min. rating",
+ "any": "Any",
+ "clearFilters": "Clear filters"
+ },
+ "detail": {
+ "ratingBreakdown": "Rating Breakdown",
+ "usefulness": "Usefulness",
+ "usability": "Usability",
+ "scoreDistribution": "Score Distribution",
+ "scoreTrend": "Score Trend",
+ "notEnoughRatings": "Not enough ratings yet to show a trend.",
+ "reviews": "Reviews",
+ "basedOnReview_one": "Based on {{count}} review",
+ "basedOnReview_other": "Based on {{count}} reviews",
+ "saveToWatchlist": "Save to watchlist",
+ "savedToWatchlist": "Saved to watchlist",
+ "removeFromWatchlist": "Remove from watchlist",
+ "visitWebsite": "Visit Website",
+ "edit": "Edit",
+ "delete": "Delete",
+ "noReviews": "No reviews yet.",
+ "addReview": "Add Review",
+ "yourRating": "Your Rating",
+ "submit": "Submit Rating",
+ "relatedTools": "Related Tools",
+ "costs": "Costs",
+ "addCost": "Add Cost",
+ "recentRatings": "Recent Ratings"
+ },
+ "compare": {
+ "title": "Compare Tools",
+ "subtitle": "Comparing {{count}} tool(s) side-by-side.",
+ "sideBySide": "Side-by-side comparison",
+ "bestValue": "Best value in each row is highlighted.",
+ "attribute": "Attribute",
+ "rating": "Rating",
+ "reviews": "Reviews",
+ "description": "Description",
+ "lastUpdated": "Last updated",
+ "requiresPremium": "Compare requires a higher tier",
+ "requiresPremiumSub": "Compare tools side-by-side is available to Premium and Enterprise users.",
+ "nothingToCompare": "Nothing to compare",
+ "nothingToCompareSub": "Select at least one tool to compare and come back here.",
+ "backToBrowse": "Back to browse"
+ },
+ "watchlist": {
+ "title": "Watchlist",
+ "subtitle": "Tools you saved for later, with live scores.",
+ "requiresPremium": "Watchlist requires a higher tier",
+ "requiresPremiumSub": "Saving tools to your watchlist is available to Premium and Enterprise users.",
+ "empty": "Your watchlist is empty",
+ "emptySub": "Browse tools and click the bookmark to save them here.",
+ "remove": "Remove from watchlist"
+ },
+ "trash": {
+ "title": "Trash",
+ "subtitle": "Deleted tools are kept here until they are restored or permanently removed.",
+ "requiresPremium": "Trash requires a higher tier",
+ "requiresPremiumSub": "The trash is available to Premium and Enterprise users.",
+ "empty": "The trash is empty.",
+ "noMatch": "No tools match your search.",
+ "trashedTools": "Trashed Tools",
+ "toolsInTrash_one": "{{count}} tool in trash",
+ "toolsInTrash_other": "{{count}} tools in trash",
+ "selected_one": "{{count}} selected",
+ "selected_other": "{{count}} selected",
+ "restore": "Restore",
+ "deletePermanently": "Delete permanently",
+ "emptyTrash": "Empty trash",
+ "deletedAt": "Deleted at",
+ "deletedBy": "Deleted by"
+ },
+ "notFound": {
+ "text": "This page doesn't exist.",
+ "backHome": "Back to Home"
+ }
+}
\ No newline at end of file
diff --git a/artifacts/toolrate/src/pages/compare.tsx b/artifacts/toolrate/src/pages/compare.tsx
index eab88ad..e359b70 100644
--- a/artifacts/toolrate/src/pages/compare.tsx
+++ b/artifacts/toolrate/src/pages/compare.tsx
@@ -15,12 +15,14 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ShieldAlert, Trophy, Scale } from "lucide-react";
import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
function fmt(v: number | null | undefined): string {
return v != null ? v.toFixed(1) : "–";
}
export default function Compare() {
+ const { t } = useTranslation();
const { hasFeature, isLoading: authLoading } = useAuth();
const search = useSearch();
@@ -50,10 +52,10 @@ export default function Compare() {
-
Compare requires a higher tier
-
Compare tools side-by-side is available to Premium and Enterprise users.
+
{t("compare.requiresPremium")}
+
{t("compare.requiresPremiumSub")}
@@ -77,10 +79,10 @@ export default function Compare() {
-
Nothing to compare
-
Select at least one tool to compare and come back here.
+
{t("compare.nothingToCompare")}
+
{t("compare.nothingToCompareSub")}
@@ -99,20 +101,20 @@ export default function Compare() {
-
Compare Tools
-
Comparing {list.length} tool(s) side-by-side.
+
{t("compare.title")}
+
{t("compare.subtitle", { count: list.length })}
- Side-by-side comparison
- Best value in each row is highlighted.
+ {t("compare.sideBySide")}
+ {t("compare.bestValue")}
- Attribute
+ {t("compare.attribute")}
{list.map((t) => (
@@ -127,7 +129,7 @@ export default function Compare() {
- Rating
+ {t("compare.rating")}
{list.map((t) => (
@@ -155,7 +157,7 @@ export default function Compare() {
))}
- Reviews
+ {t("compare.reviews")}
{list.map((t) => (
{t.ratingCount}
@@ -163,7 +165,7 @@ export default function Compare() {
))}
- Description
+ {t("compare.description")}
{list.map((t) => (
{t.description}
@@ -197,7 +199,7 @@ export default function Compare() {
))}
- Last updated
+ {t("compare.lastUpdated")}
{list.map((t) => (
{new Date(t.updatedAt).toLocaleDateString()}
@@ -210,7 +212,7 @@ export default function Compare() {
diff --git a/artifacts/toolrate/src/pages/home.tsx b/artifacts/toolrate/src/pages/home.tsx
index 731cb87..a915849 100644
--- a/artifacts/toolrate/src/pages/home.tsx
+++ b/artifacts/toolrate/src/pages/home.tsx
@@ -11,8 +11,10 @@ import { ToolCard } from "@/components/tool-card";
import { Star, Wrench, MessageSquare, ArrowRight } from "lucide-react";
import { Link } from "wouter";
import { Button } from "@/components/ui/button";
+import { useTranslation } from "react-i18next";
export default function Home() {
+ const { t } = useTranslation();
const { data: summary, isLoading: loadingSummary } = useGetAnalyticsSummary();
const { data: topTools, isLoading: loadingTopTools } = useGetTopTools({ limit: 4, metric: GetTopToolsMetric.combined });
const { data: recentTools, isLoading: loadingRecent } = useListTools({ sort: "newest" });
@@ -21,8 +23,8 @@ export default function Home() {
-
Welcome to toolr
-
The community hub where engineers honestly rate the tools they use daily.
+
{t("home.welcome")}
+
{t("home.tagline")}
{/* Stats Banner */}
@@ -33,7 +35,7 @@ export default function Home() {
-
Total Tools
+
{t("home.totalTools")}
{loadingSummary ? (
) : (
@@ -49,7 +51,7 @@ export default function Home() {
-
Total Ratings
+
{t("home.totalRatings")}
{loadingSummary ? (
) : (
@@ -65,7 +67,7 @@ export default function Home() {
-
Avg Rating
+
{t("home.avgRating")}
{loadingSummary ? (
) : (
@@ -80,12 +82,12 @@ export default function Home() {
-
Top Rated Tools
-
Highest combined usefulness and usability.
+
{t("home.topRated")}
+
{t("home.topRatedSub")}
@@ -106,10 +108,10 @@ export default function Home() {
- No ratings yet
- Be the first to rate a tool and help the community.
+ {t("home.noRatingsYet")}
+ {t("home.noRatingsSub")}
@@ -120,12 +122,12 @@ export default function Home() {
-
Recently Added
-
The latest additions to the catalog.
+
{t("home.recentlyAdded")}
+
{t("home.recentlyAddedSub")}
@@ -146,10 +148,10 @@ export default function Home() {
- No tools found
- There are no tools in the database yet.
+ {t("home.noTools")}
+ {t("home.noToolsSub")}
diff --git a/artifacts/toolrate/src/pages/login.tsx b/artifacts/toolrate/src/pages/login.tsx
index 453274a..20ad8bc 100644
--- a/artifacts/toolrate/src/pages/login.tsx
+++ b/artifacts/toolrate/src/pages/login.tsx
@@ -8,8 +8,10 @@ import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ThemeToggle } from "@/components/theme-toggle";
import { Wrench, AlertCircle } from "lucide-react";
+import { useTranslation } from "react-i18next";
export default function Login() {
+ const { t } = useTranslation();
const [, setLocation] = useLocation();
const search = useSearch();
const params = new URLSearchParams(search);
@@ -33,7 +35,7 @@ export default function Login() {
setLocation(returnTo);
},
onError: (err) => {
- setError(err.data?.error || err.message || "Invalid username or password");
+ setError(err.data?.error || err.message || t("auth.invalidCredentials"));
},
},
);
@@ -50,18 +52,18 @@ export default function Login() {
toolr
- Sign in to your account
+ {t("auth.loginSubtitle")}
- Sign in
- Enter your credentials to continue
+ {t("auth.signIn")}
+ {t("auth.loginDescription")}