feat(i18n): add language picker to settings and sync i18n with persisted preference
- Add language picker (English/Deutsch) to settings page using pill-toggle pattern - Import useLanguage hook and i18n instance in settings - Language change persists via updateSetting and calls i18n.changeLanguage - Add useEffect in RootLayout to sync i18n language with DB setting on load - Language labels use native names (English, Deutsch) for identification Phase 34, Plan 04
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
useRouter,
|
useRouter,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
@@ -23,6 +23,7 @@ import { OnboardingFlow } from "../components/onboarding/OnboardingFlow";
|
|||||||
import { TopNav } from "../components/TopNav";
|
import { TopNav } from "../components/TopNav";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { useDeleteCandidate } from "../hooks/useCandidates";
|
import { useDeleteCandidate } from "../hooks/useCandidates";
|
||||||
|
import { useLanguage } from "../hooks/useLanguage";
|
||||||
import { useOnboardingComplete } from "../hooks/useSettings";
|
import { useOnboardingComplete } from "../hooks/useSettings";
|
||||||
import { useResolveThread, useThread } from "../hooks/useThreads";
|
import { useResolveThread, useThread } from "../hooks/useThreads";
|
||||||
import { useUIStore } from "../stores/uiStore";
|
import { useUIStore } from "../stores/uiStore";
|
||||||
@@ -82,6 +83,15 @@ function RootLayout() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { data: auth, isLoading: authLoading } = useAuth();
|
const { data: auth, isLoading: authLoading } = useAuth();
|
||||||
const isAuthenticated = !!auth?.user;
|
const isAuthenticated = !!auth?.user;
|
||||||
|
const language = useLanguage();
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
// Sync i18n language with persisted setting
|
||||||
|
useEffect(() => {
|
||||||
|
if (language && i18n.language !== language) {
|
||||||
|
i18n.changeLanguage(language);
|
||||||
|
}
|
||||||
|
}, [language, i18n]);
|
||||||
|
|
||||||
// Candidate delete state
|
// Candidate delete state
|
||||||
const confirmDeleteCandidateId = useUIStore(
|
const confirmDeleteCandidateId = useUIStore(
|
||||||
|
|||||||
@@ -9,9 +9,16 @@ import {
|
|||||||
} from "../hooks/useAuth";
|
} from "../hooks/useAuth";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useCurrency } from "../hooks/useCurrency";
|
||||||
import { useExportItems, useImportItems } from "../hooks/useItems";
|
import { useExportItems, useImportItems } from "../hooks/useItems";
|
||||||
|
import { useLanguage } from "../hooks/useLanguage";
|
||||||
import { useUpdateSetting } from "../hooks/useSettings";
|
import { useUpdateSetting } from "../hooks/useSettings";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
import { useWeightUnit } from "../hooks/useWeightUnit";
|
||||||
import type { Currency, WeightUnit } from "../lib/formatters";
|
import type { Currency, WeightUnit } from "../lib/formatters";
|
||||||
|
import i18n from "../lib/i18n";
|
||||||
|
|
||||||
|
const LANGUAGES = [
|
||||||
|
{ value: "en", label: "English" },
|
||||||
|
{ value: "de", label: "Deutsch" },
|
||||||
|
];
|
||||||
|
|
||||||
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
|
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
|
||||||
const CURRENCIES: { value: Currency; label: string }[] = [
|
const CURRENCIES: { value: Currency; label: string }[] = [
|
||||||
@@ -231,6 +238,7 @@ function getSuggestedCurrency(): Currency | null {
|
|||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const unit = useWeightUnit();
|
const unit = useWeightUnit();
|
||||||
|
const language = useLanguage();
|
||||||
const { currency, showConversions } = useCurrency();
|
const { currency, showConversions } = useCurrency();
|
||||||
const updateSetting = useUpdateSetting();
|
const updateSetting = useUpdateSetting();
|
||||||
const { data: auth } = useAuth();
|
const { data: auth } = useAuth();
|
||||||
@@ -279,6 +287,36 @@ function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6">
|
<div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900">{t("language.title")}</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
{t("language.description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
|
||||||
|
{LANGUAGES.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
updateSetting.mutate({ key: "language", value: lang.value });
|
||||||
|
i18n.changeLanguage(lang.value);
|
||||||
|
}}
|
||||||
|
className={`px-2.5 py-1 text-xs rounded-full transition-colors ${
|
||||||
|
language === lang.value
|
||||||
|
? "bg-white text-gray-700 shadow-sm font-medium"
|
||||||
|
: "text-gray-400 hover:text-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{lang.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-100" />
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-900">{t("weightUnit.title")}</h3>
|
<h3 className="text-sm font-medium text-gray-900">{t("weightUnit.title")}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user