feat(i18n): foundation — react-i18next, language switcher, server-persisted choice
Set up react-i18next with locale files auto-loaded per area (Vite glob), a compact LanguageSwitcher, and language as a server-persisted preference (preferences.language) mirrored to localStorage. On first login the default UI language is guessed from the Google-reported locale (hu/en/de, else English). vite-env.d.ts types the build-time env.
This commit is contained in:
parent
844fed7d2f
commit
7aa068061d
9 changed files with 233 additions and 52 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, HttpError, type FeedFilters } from "./lib/api";
|
||||
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
||||
import {
|
||||
applyTheme,
|
||||
DEFAULT_THEME,
|
||||
|
|
@ -61,6 +63,7 @@ function loadInitialFilters(): FeedFilters {
|
|||
}
|
||||
|
||||
export default function App() {
|
||||
const { t } = useTranslation();
|
||||
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
|
||||
const [filters, setFiltersState] = useState<FeedFilters>(loadInitialFilters);
|
||||
const [view, setView] = useState<"grid" | "list">("grid");
|
||||
|
|
@ -124,6 +127,7 @@ export default function App() {
|
|||
setSidebarLayoutState(l);
|
||||
saveLayoutLocal(l);
|
||||
}
|
||||
if (isSupported(prefs.language)) setLanguage(prefs.language);
|
||||
if (prefs.notifications) configureNotifications(prefs.notifications);
|
||||
if (typeof prefs.hints === "boolean") setHintsEnabled(prefs.hints);
|
||||
if (typeof prefs.performanceMode === "boolean")
|
||||
|
|
@ -133,8 +137,8 @@ export default function App() {
|
|||
if (meQuery.data?.role === "admin" && pending > 0) {
|
||||
notify({
|
||||
level: "warning",
|
||||
title: "Access requests",
|
||||
message: `${pending} pending — review in Settings → Account.`,
|
||||
title: t("common.accessRequestsTitle"),
|
||||
message: t("common.accessRequestsMessage", { count: pending }),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -149,15 +153,21 @@ export default function App() {
|
|||
setView(v);
|
||||
api.savePrefs({ view: v }).catch(() => {});
|
||||
}
|
||||
function changeLanguage(code: LangCode) {
|
||||
setLanguage(code);
|
||||
api.savePrefs({ language: code }).catch(() => {});
|
||||
}
|
||||
|
||||
if (meQuery.isLoading)
|
||||
return <div className="min-h-screen grid place-items-center text-muted">Loading…</div>;
|
||||
return (
|
||||
<div className="min-h-screen grid place-items-center text-muted">{t("common.loading")}</div>
|
||||
);
|
||||
if (meQuery.error instanceof HttpError && meQuery.error.status === 401)
|
||||
return <Login />;
|
||||
if (meQuery.error)
|
||||
return (
|
||||
<div className="min-h-screen grid place-items-center text-muted">
|
||||
Something went wrong.
|
||||
{t("common.somethingWrong")}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -171,6 +181,7 @@ export default function App() {
|
|||
setPage={setPage}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
onOpenAbout={() => setAboutOpen(true)}
|
||||
onChangeLanguage={changeLanguage}
|
||||
onGoToFullHistory={() => {
|
||||
setChannelFilter("needs_full");
|
||||
setPage("channels");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue