Implement internationalization support across components
- Integrated `next-intl` for toast messages and locale handling in various components, including `Providers`, `CanvasUserMenu`, and `CreditOverview`. - Replaced hardcoded strings with translation keys to enhance localization capabilities. - Updated `RootLayout` to dynamically set the language attribute based on the user's locale. - Ensured consistent user feedback through localized toast messages in actions such as sign-out, canvas operations, and billing notifications.
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { useMutation } from "convex/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowUpRight, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { msg } from "@/lib/toast-messages";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ interface CanvasCardProps {
|
||||
}
|
||||
|
||||
export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) {
|
||||
const t = useTranslations('toasts');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editName, setEditName] = useState(canvas.name);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -60,8 +61,7 @@ export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) {
|
||||
const handleSave = useCallback(async () => {
|
||||
const trimmedName = editName.trim();
|
||||
if (!trimmedName) {
|
||||
const { title, desc } = msg.dashboard.renameEmpty;
|
||||
toast.error(title, desc);
|
||||
toast.error(t('dashboard.renameEmptyTitle'), t('dashboard.renameEmptyDesc'));
|
||||
return;
|
||||
}
|
||||
if (trimmedName === canvas.name) {
|
||||
@@ -74,15 +74,15 @@ export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await updateCanvas({ canvasId: canvas._id, name: trimmedName });
|
||||
toast.success(msg.dashboard.renameSuccess.title);
|
||||
toast.success(t('dashboard.renameSuccess'));
|
||||
setIsEditing(false);
|
||||
} catch {
|
||||
toast.error(msg.dashboard.renameFailed.title);
|
||||
toast.error(t('dashboard.renameFailed'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
saveInFlightRef.current = false;
|
||||
}
|
||||
}, [editName, canvas.name, canvas._id, updateCanvas]);
|
||||
}, [t, editName, canvas.name, canvas._id, updateCanvas]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -115,14 +115,14 @@ export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) {
|
||||
setDeleteBusy(true);
|
||||
try {
|
||||
await removeCanvas({ canvasId: canvas._id });
|
||||
toast.success(msg.dashboard.deleteSuccess.title);
|
||||
toast.success(t('dashboard.deleteSuccess'));
|
||||
setDeleteOpen(false);
|
||||
} catch {
|
||||
toast.error(msg.dashboard.deleteFailed.title);
|
||||
toast.error(t('dashboard.deleteFailed'));
|
||||
} finally {
|
||||
setDeleteBusy(false);
|
||||
}
|
||||
}, [canvas._id, removeCanvas]);
|
||||
}, [t, canvas._id, removeCanvas]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useAuthQuery } from "@/hooks/use-auth-query";
|
||||
import { useFormatter, useTranslations } from "next-intl";
|
||||
import { CreditCard } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -10,10 +11,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { formatEurFromCents } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { msg } from "@/lib/toast-messages";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier-Config — monatliches Credit-Kontingent pro Tier (in Cent)
|
||||
@@ -42,7 +41,12 @@ const TIER_BADGE_STYLES: Record<string, string> = {
|
||||
const LOW_CREDITS_THRESHOLD = 20;
|
||||
|
||||
export function CreditOverview() {
|
||||
const t = useTranslations('toasts');
|
||||
const router = useRouter();
|
||||
const format = useFormatter();
|
||||
|
||||
const formatEurFromCents = (cents: number) =>
|
||||
format.number(cents / 100, { style: "currency", currency: "EUR" });
|
||||
const balance = useAuthQuery(api.credits.getBalance);
|
||||
const subscription = useAuthQuery(api.credits.getSubscription);
|
||||
const usageStats = useAuthQuery(api.credits.getUsageStats);
|
||||
@@ -56,14 +60,13 @@ export function CreditOverview() {
|
||||
if (typeof window !== "undefined" && sessionStorage.getItem(key)) return;
|
||||
sessionStorage.setItem(key, "1");
|
||||
|
||||
const { title, desc } = msg.billing.lowCredits(available);
|
||||
toast.action(title, {
|
||||
description: desc,
|
||||
label: msg.billing.topUp,
|
||||
toast.action(t('billing.lowCreditsTitle'), {
|
||||
description: t('billing.lowCreditsDesc', { remaining: available }),
|
||||
label: t('billing.topUp'),
|
||||
onClick: () => router.push("/settings/billing"),
|
||||
type: "warning",
|
||||
});
|
||||
}, [balance, router]);
|
||||
}, [t, balance, router]);
|
||||
|
||||
// ── Loading State ──────────────────────────────────────────────────────
|
||||
if (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useAuthQuery } from "@/hooks/use-auth-query";
|
||||
import { useFormatter } from "next-intl";
|
||||
import { Activity, Coins } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { formatEurFromCents, cn } from "@/lib/utils";
|
||||
import { formatRelativeTime } from "@/lib/format-time";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -45,10 +45,28 @@ function truncatedDescription(text: string, maxLen = 40) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function RecentTransactions() {
|
||||
const format = useFormatter();
|
||||
const transactions = useAuthQuery(api.credits.getRecentTransactions, {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const formatEurFromCents = (cents: number) =>
|
||||
format.number(cents / 100, { style: "currency", currency: "EUR" });
|
||||
|
||||
const formatRelativeTime = (timestamp: number) => {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return "Gerade eben";
|
||||
if (minutes < 60) return `vor ${minutes} Min.`;
|
||||
if (hours < 24) return `vor ${hours} Std.`;
|
||||
if (days < 7) return days === 1 ? "vor 1 Tag" : `vor ${days} Tagen`;
|
||||
return format.dateTime(timestamp, { day: "numeric", month: "short" });
|
||||
};
|
||||
|
||||
// ── Loading State ──────────────────────────────────────────────────────
|
||||
if (transactions === undefined) {
|
||||
return (
|
||||
@@ -102,7 +120,7 @@ export function RecentTransactions() {
|
||||
Letzte Aktivität
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{transactions.map((t) => {
|
||||
{transactions.map((t: NonNullable<typeof transactions>[number]) => {
|
||||
const isCredit = t.amount > 0;
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user