feat(i18n): extract strings from navigation, dialogs, onboarding, settings, and login

- Add useTranslation() to TopNav, BottomTabBar, FabMenu, UserMenu
- Internationalize ConfirmDialog, AuthPromptModal, ExternalLinkDialog
- Extract all onboarding flow strings (Welcome, HobbyPicker, ItemBrowser, Review, Done)
- Internationalize settings page (weight unit, currency, API keys, import/export)
- Internationalize login page and root error boundary
- All dialogs in __root.tsx use t() for UI chrome

Phase 34, Plan 02 (core navigation and global UI)
This commit is contained in:
2026-04-13 18:19:29 +02:00
parent 8c0fb31df2
commit 672b17fd13
15 changed files with 123 additions and 98 deletions

View File

@@ -8,6 +8,7 @@ import {
useRouter,
} from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Toaster } from "sonner";
import "../app.css";
import { AddToCollectionModal } from "../components/AddToCollectionModal";
@@ -33,6 +34,7 @@ export const Route = createRootRoute({
function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
const router = useRouter();
const { t } = useTranslation();
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
@@ -53,12 +55,12 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
</svg>
</div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">
Something went wrong
{t("errors.somethingWentWrong")}
</h1>
<p className="text-sm text-gray-500 mb-6">
{error instanceof Error
? error.message
: "An unexpected error occurred"}
: t("errors.unexpectedError")}
</p>
<button
type="button"
@@ -68,7 +70,7 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
}}
className="px-5 py-2.5 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg transition-colors"
>
Try again
{t("actions.tryAgain")}
</button>
</div>
</div>
@@ -205,6 +207,7 @@ function CandidateDeleteDialog({
threadId: number;
onClose: () => void;
}) {
const { t } = useTranslation();
const deleteCandidate = useDeleteCandidate(threadId);
const { data: thread } = useThread(threadId);
const candidate = thread?.candidates.find((c) => c.id === candidateId);
@@ -227,12 +230,10 @@ function CandidateDeleteDialog({
/>
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Delete Candidate
{t("confirm.deleteCandidate")}
</h3>
<p className="text-sm text-gray-600 mb-6">
Are you sure you want to delete{" "}
<span className="font-medium">{candidateName}</span>? This action
cannot be undone.
{t("confirm.deleteCandidateMessage", { name: candidateName })}
</p>
<div className="flex justify-end gap-3">
<button
@@ -240,7 +241,7 @@ function CandidateDeleteDialog({
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Cancel
{t("actions.cancel")}
</button>
<button
type="button"
@@ -248,7 +249,7 @@ function CandidateDeleteDialog({
disabled={deleteCandidate.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors"
>
{deleteCandidate.isPending ? "Deleting..." : "Delete"}
{deleteCandidate.isPending ? t("actions.deleting") : t("actions.delete")}
</button>
</div>
</div>
@@ -267,6 +268,7 @@ function ResolveDialog({
onClose: () => void;
onResolved: () => void;
}) {
const { t } = useTranslation();
const resolveThread = useResolveThread();
const { data: thread } = useThread(threadId);
const candidate = thread?.candidates.find((c) => c.id === candidateId);
@@ -290,11 +292,10 @@ function ResolveDialog({
/>
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Pick Winner
{t("confirm.pickWinner")}
</h3>
<p className="text-sm text-gray-600 mb-6">
Pick <span className="font-medium">{candidateName}</span> as the
winner? This will add it to your collection and archive the thread.
{t("confirm.pickWinnerMessage", { name: candidateName })}
</p>
<div className="flex justify-end gap-3">
<button
@@ -302,7 +303,7 @@ function ResolveDialog({
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Cancel
{t("actions.cancel")}
</button>
<button
type="button"
@@ -310,7 +311,7 @@ function ResolveDialog({
disabled={resolveThread.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 disabled:opacity-50 rounded-lg transition-colors"
>
{resolveThread.isPending ? "Resolving..." : "Pick Winner"}
{resolveThread.isPending ? t("actions.saving") : t("confirm.pickWinner")}
</button>
</div>
</div>