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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user