Preferred language for the user
This commit is contained in:
@@ -4,6 +4,7 @@ type AuthTokenResponse = {
|
||||
accessToken: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
preferredLanguage: "fi" | "en" | "sk";
|
||||
roles: string[];
|
||||
tokenType: string;
|
||||
expiresIn: number;
|
||||
@@ -64,6 +65,7 @@ export type User = {
|
||||
added: string;
|
||||
lastUpdated: string;
|
||||
displayName: string;
|
||||
preferredLanguage: "fi" | "en" | "sk";
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
@@ -71,12 +73,14 @@ export type CreateUserInput = {
|
||||
username: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
preferredLanguage: "fi" | "en" | "sk";
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export type UpdateUserInput = {
|
||||
password?: string;
|
||||
displayName: string;
|
||||
preferredLanguage: "fi" | "en" | "sk";
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -19,12 +19,14 @@ function AppShell() {
|
||||
const setSession = useSetRecoilState(sessionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
initializeLanguage(setLanguage);
|
||||
const storedPreferredLanguage = localStorage.getItem("session-preferred-language");
|
||||
initializeLanguage(setLanguage, storedPreferredLanguage === "en" || storedPreferredLanguage === "sk" ? storedPreferredLanguage : "fi");
|
||||
}, [setLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUsername = localStorage.getItem("session-username");
|
||||
const storedDisplayName = localStorage.getItem("session-display-name");
|
||||
const storedPreferredLanguage = localStorage.getItem("session-preferred-language");
|
||||
const storedRoles = localStorage.getItem("session-roles");
|
||||
const storedToken = localStorage.getItem("session-token");
|
||||
if (!storedUsername || !storedDisplayName || !storedToken) {
|
||||
@@ -35,6 +37,7 @@ function AppShell() {
|
||||
setSession({
|
||||
username: storedUsername,
|
||||
displayName: storedDisplayName,
|
||||
preferredLanguage: storedPreferredLanguage === "en" || storedPreferredLanguage === "sk" ? storedPreferredLanguage : "fi",
|
||||
roles: (storedRoles ?? "").split(",").filter(Boolean),
|
||||
token: storedToken,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,8 @@ export default function Nav() {
|
||||
setSession(null);
|
||||
localStorage.removeItem("session-username");
|
||||
localStorage.removeItem("session-display-name");
|
||||
localStorage.removeItem("session-is-admin");
|
||||
localStorage.removeItem("session-preferred-language");
|
||||
localStorage.removeItem("session-roles");
|
||||
localStorage.removeItem("session-token");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
@@ -71,6 +71,10 @@ const translations = {
|
||||
"management.username": "Username",
|
||||
"management.password": "Password",
|
||||
"management.displayName": "Display name",
|
||||
"management.preferredLanguage": "Preferred language",
|
||||
"management.language.fi": "Finnish",
|
||||
"management.language.en": "English",
|
||||
"management.language.sk": "Slovak",
|
||||
"management.added": "Added",
|
||||
"management.updated": "Last updated",
|
||||
"management.loading": "Loading users...",
|
||||
@@ -156,6 +160,10 @@ const translations = {
|
||||
"management.username": "Käyttäjätunnus",
|
||||
"management.password": "Salasana",
|
||||
"management.displayName": "Näyttönimi",
|
||||
"management.preferredLanguage": "Ensisijainen kieli",
|
||||
"management.language.fi": "Suomi",
|
||||
"management.language.en": "Englanti",
|
||||
"management.language.sk": "Slovakki",
|
||||
"management.added": "Lisätty",
|
||||
"management.updated": "Viimeksi päivitetty",
|
||||
"management.loading": "Ladataan käyttäjiä...",
|
||||
@@ -243,6 +251,10 @@ const translations = {
|
||||
"management.username": "Používateľské meno",
|
||||
"management.password": "Heslo",
|
||||
"management.displayName": "Zobrazované meno",
|
||||
"management.preferredLanguage": "Preferovaný jazyk",
|
||||
"management.language.fi": "Fínčina",
|
||||
"management.language.en": "Angličtina",
|
||||
"management.language.sk": "Slovenčina",
|
||||
"management.added": "Pridané",
|
||||
"management.updated": "Naposledy aktualizované",
|
||||
"management.loading": "Načítavajú sa používatelia...",
|
||||
@@ -268,11 +280,19 @@ const translations = {
|
||||
export type TranslationKey = keyof typeof translations.en;
|
||||
|
||||
export const normalizeLanguage = (value: unknown): Language =>
|
||||
value === "fi" || value === "sk" ? value : "en";
|
||||
value === "en" || value === "sk" ? value : "fi";
|
||||
|
||||
export const initializeLanguage = (setLanguage: (lang: Language) => void) => {
|
||||
const stored = normalizeLanguage(localStorage.getItem(STORAGE_KEY));
|
||||
setLanguage(stored);
|
||||
export const initializeLanguage = (
|
||||
setLanguage: (lang: Language) => void,
|
||||
fallbackLanguage?: Language,
|
||||
) => {
|
||||
const storedValue = localStorage.getItem(STORAGE_KEY);
|
||||
if (storedValue !== null) {
|
||||
setLanguage(normalizeLanguage(storedValue));
|
||||
return;
|
||||
}
|
||||
|
||||
setLanguage(normalizeLanguage(fallbackLanguage));
|
||||
};
|
||||
|
||||
export const useLanguage = () => {
|
||||
|
||||
@@ -2,11 +2,12 @@ import { FormEvent, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { requestAuthToken } from "~/api";
|
||||
import { useT } from "~/i18n";
|
||||
import { normalizeLanguage, useLanguage, useT } from "~/i18n";
|
||||
import { sessionAtom } from "~/state/appState";
|
||||
|
||||
export default function Login() {
|
||||
const t = useT();
|
||||
const { setLanguage } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const [session, setSession] = useRecoilState(sessionAtom);
|
||||
const [username, setUsername] = useState("");
|
||||
@@ -38,12 +39,17 @@ export default function Login() {
|
||||
setSession({
|
||||
username: auth.username,
|
||||
displayName: auth.displayName,
|
||||
preferredLanguage: auth.preferredLanguage,
|
||||
roles: auth.roles,
|
||||
token: auth.accessToken,
|
||||
});
|
||||
|
||||
const preferredLanguage = normalizeLanguage(auth.preferredLanguage);
|
||||
setLanguage(preferredLanguage);
|
||||
|
||||
localStorage.setItem("session-username", auth.username);
|
||||
localStorage.setItem("session-display-name", auth.displayName);
|
||||
localStorage.setItem("session-preferred-language", preferredLanguage);
|
||||
localStorage.setItem("session-roles", auth.roles.join(","));
|
||||
localStorage.setItem("session-token", auth.accessToken);
|
||||
setError("");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useT } from "~/i18n";
|
||||
const AVAILABLE_ROLES = ["lok", "admin"];
|
||||
|
||||
type Mode = "create" | "edit";
|
||||
type LanguageOption = "fi" | "en" | "sk";
|
||||
|
||||
export default function Management() {
|
||||
const t = useT();
|
||||
@@ -13,6 +14,7 @@ export default function Management() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [preferredLanguage, setPreferredLanguage] = useState<LanguageOption>("fi");
|
||||
const [selectedRoles, setSelectedRoles] = useState<Set<string>>(new Set());
|
||||
const [selectedUsername, setSelectedUsername] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@@ -44,6 +46,7 @@ export default function Management() {
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setDisplayName("");
|
||||
setPreferredLanguage("fi");
|
||||
setSelectedRoles(new Set());
|
||||
setSelectedUsername("");
|
||||
};
|
||||
@@ -53,6 +56,7 @@ export default function Management() {
|
||||
setUsername(user.username);
|
||||
setPassword("");
|
||||
setDisplayName(user.displayName);
|
||||
setPreferredLanguage(user.preferredLanguage);
|
||||
setSelectedRoles(new Set(user.roles));
|
||||
setSelectedUsername(user.username);
|
||||
setError("");
|
||||
@@ -81,12 +85,14 @@ export default function Management() {
|
||||
username: username.trim(),
|
||||
password: password,
|
||||
displayName: displayName.trim(),
|
||||
preferredLanguage,
|
||||
roles: [...selectedRoles],
|
||||
});
|
||||
} else {
|
||||
await updateUser(selectedUsername, {
|
||||
password: password.trim() ? password : undefined,
|
||||
displayName: displayName.trim(),
|
||||
preferredLanguage,
|
||||
roles: [...selectedRoles],
|
||||
});
|
||||
}
|
||||
@@ -153,6 +159,20 @@ export default function Management() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label htmlFor="management-language" className="block text-left">
|
||||
{t("management.preferredLanguage")}
|
||||
<select
|
||||
id="management-language"
|
||||
value={preferredLanguage}
|
||||
onChange={(event) => setPreferredLanguage(event.target.value as LanguageOption)}
|
||||
className="mt-1 block w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
|
||||
>
|
||||
<option value="fi">{t("management.language.fi")}</option>
|
||||
<option value="en">{t("management.language.en")}</option>
|
||||
<option value="sk">{t("management.language.sk")}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="block text-left">
|
||||
<span className="block">{t("management.rolesAssign")}</span>
|
||||
<div className="mt-1 flex flex-wrap gap-3">
|
||||
@@ -210,6 +230,9 @@ export default function Management() {
|
||||
<div className="text-xs text-[#8E4F24]">
|
||||
{t("management.updated")}: {new Date(user.lastUpdated).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-[#8E4F24]">
|
||||
{t("management.preferredLanguage")}: {t(`management.language.${user.preferredLanguage}` as "management.language.fi" | "management.language.en" | "management.language.sk")}
|
||||
</div>
|
||||
{user.roles.length > 0 ? (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{user.roles.map((role) => (
|
||||
|
||||
@@ -6,6 +6,7 @@ export type Language = "fi" | "en" | "sk";
|
||||
export type Session = {
|
||||
username: string;
|
||||
displayName: string;
|
||||
preferredLanguage: Language;
|
||||
roles: string[];
|
||||
token: string;
|
||||
};
|
||||
@@ -19,7 +20,7 @@ export type Toast = {
|
||||
|
||||
export const languageAtom = atom<Language>({
|
||||
key: "languageAtom",
|
||||
default: "en",
|
||||
default: "fi",
|
||||
});
|
||||
|
||||
export const sessionAtom = atom<Session | null>({
|
||||
|
||||
Reference in New Issue
Block a user