feat: language selector (#484)

* feat: language selector

* refactor: move user general page

* feat: language selector

* refactor: move user general page

* feat: add language combobox in user general
This commit is contained in:
Manuel
2024-05-18 13:54:43 +02:00
committed by GitHub
parent 8e8f9081b0
commit 31c2694185
17 changed files with 188 additions and 29 deletions

View File

@@ -47,6 +47,7 @@
"chroma-js": "^2.4.2",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"flag-icons": "^7.2.1",
"glob": "^10.3.15",
"jotai": "^2.8.0",
"next": "^14.2.3",

View File

@@ -0,0 +1,12 @@
import { Stack, Title } from "@mantine/core";
import { LanguageCombobox } from "~/components/language/language-combobox";
export const ProfileLanguageChange = () => {
return (
<Stack mb="lg">
<Title order={2}>Language & Region</Title>
<LanguageCombobox />
</Stack>
);
};

View File

@@ -10,10 +10,11 @@ import {
DangerZoneRoot,
} from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { DeleteUserButton } from "./_delete-user-button";
import { UserProfileAvatarForm } from "./_profile-avatar-form";
import { UserProfileForm } from "./_profile-form";
import { canAccessUserEditPage } from "./access";
import { canAccessUserEditPage } from "../access";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
import { ProfileLanguageChange } from "./_components/_profile-language-change";
interface Props {
params: {
@@ -67,6 +68,8 @@ export default async function EditUserPage({ params }: Props) {
</Box>
</Group>
<ProfileLanguageChange />
<DangerZoneRoot>
<DangerZoneItem
label={t("user.action.delete.label")}

View File

@@ -69,7 +69,7 @@ export default async function Layout({
<Stack>
<Stack gap={0}>
<NavigationLink
href={`/manage/users/${params.userId}`}
href={`/manage/users/${params.userId}/general`}
label={tUser("setting.general.title")}
icon={<IconSettings size="1rem" stroke={1.5} />}
/>

View File

@@ -7,7 +7,7 @@ import { getScopedI18n } from "@homarr/translation/server";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { canAccessUserEditPage } from "../access";
import { ChangePasswordForm } from "./_change-password-form";
import { ChangePasswordForm } from "./_components/_change-password-form";
interface Props {
params: {

View File

@@ -33,7 +33,7 @@ export const UserListComponent = ({
header: t("user.field.username.label"),
grow: 100,
Cell: ({ renderedCellValue, row }) => (
<Link href={`/manage/users/${row.original.id}`}>
<Link href={`/manage/users/${row.original.id}/general`}>
<Group>
<Avatar size="sm"></Avatar>
{renderedCellValue}

View File

@@ -0,0 +1,3 @@
.flagIcon {
border-radius: 4px;
}

View File

@@ -0,0 +1,94 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeAttributes, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import classes from "./language-combobox.module.css";
export const LanguageCombobox = () => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
const currentLocale = useCurrentLocale();
const changeLocale = useChangeLocale();
const handleOnOptionSubmit = React.useCallback(
(value: string) => {
if (!value) {
return;
}
changeLocale(value as SupportedLanguage);
combobox.closeDropdown();
},
[changeLocale, combobox],
);
const handleOnClick = React.useCallback(() => {
combobox.toggleDropdown();
}, [combobox]);
return (
<Combobox store={combobox} onOptionSubmit={handleOnOptionSubmit}>
<Combobox.Target>
<InputBase
component="button"
type="button"
pointer
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={handleOnClick}
variant="filled"
>
<OptionItem currentLocale={currentLocale} localeKey={currentLocale} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
{supportedLanguages.map((languageKey) => (
<Combobox.Option value={languageKey} key={languageKey}>
<OptionItem
currentLocale={currentLocale}
localeKey={languageKey}
showCheck
/>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
};
const OptionItem = ({
currentLocale,
localeKey,
showCheck,
}: {
currentLocale: SupportedLanguage;
localeKey: SupportedLanguage;
showCheck?: boolean;
}) => {
return (
<Group wrap="nowrap" justify="space-between">
<Group wrap="nowrap">
<span
className={`fi fi-${localeAttributes[localeKey].flagIcon} ${classes.flagIcon}`}
></span>
<Group wrap="nowrap" gap="xs">
<Text>{localeAttributes[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit>
({localeAttributes[localeKey].translatedName})
</Text>
</Group>
</Group>
{showCheck && localeKey === currentLocale && (
<IconCheck color="currentColor" size={16} />
)}
</Group>
);
};

View File

@@ -27,6 +27,10 @@ import { signOut, useSession } from "@homarr/auth/client";
import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import "flag-icons/css/flag-icons.min.css";
import { LanguageCombobox } from "./language/language-combobox";
interface UserAvatarMenuProps {
children: ReactNode;
}
@@ -57,7 +61,7 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
}, [openModal, router]);
return (
<Menu width={200} withArrow withinPortal>
<Menu width={300} withArrow withinPortal>
<Menu.Dropdown>
<Menu.Item
onClick={toggleColorScheme}
@@ -72,23 +76,32 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
>
{t("navigateDefaultBoard")}
</Menu.Item>
{Boolean(session.data) && (
<Menu.Item
component={Link}
href={`/manage/users/${session.data?.user.id}`}
leftSection={<IconSettings size="1rem" />}
>
{t("preferences")}
</Menu.Item>
)}
<Menu.Item
component={Link}
href="/manage"
leftSection={<IconTool size="1rem" />}
>
{t("management")}
<Menu.Divider />
<Menu.Item p={0} closeMenuOnClick={false}>
<LanguageCombobox />
</Menu.Item>
<Menu.Divider />
{Boolean(session.data) && (
<>
<Menu.Item
component={Link}
href={`/manage/users/${session.data?.user.id}`}
leftSection={<IconSettings size="1rem" />}
>
{t("preferences")}
</Menu.Item>
<Menu.Item
component={Link}
href="/manage"
leftSection={<IconTool size="1rem" />}
>
{t("management")}
</Menu.Item>
</>
)}
<Menu.Divider />
{session.status === "authenticated" ? (
<Menu.Item
onClick={handleSignout}

View File

@@ -5,9 +5,12 @@ import { createI18nClient } from "next-international/client";
import { languageMapping } from "./lang";
import enTranslation from "./lang/en";
export const { useI18n, useScopedI18n, I18nProviderClient } = createI18nClient(
languageMapping(),
{
fallbackLocale: enTranslation,
},
);
export const {
useI18n,
useScopedI18n,
useCurrentLocale,
useChangeLocale,
I18nProviderClient,
} = createI18nClient(languageMapping(), {
fallbackLocale: enTranslation,
});

View File

@@ -1,6 +1,7 @@
import type { stringOrTranslation, TranslationFunction } from "./type";
export * from "./type";
export * from "./locale-attributes";
export const supportedLanguages = ["en", "de"] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];

View File

@@ -0,0 +1,21 @@
import type { SupportedLanguage } from ".";
export const localeAttributes: Record<
SupportedLanguage,
{
name: string;
translatedName: string;
flagIcon: string;
}
> = {
de: {
name: "Deutsch",
translatedName: "German",
flagIcon: "de",
},
en: {
name: "English",
translatedName: "English",
flagIcon: "us",
},
};

8
pnpm-lock.yaml generated
View File

@@ -159,6 +159,9 @@ importers:
dotenv:
specifier: ^16.4.5
version: 16.4.5
flag-icons:
specifier: ^7.2.1
version: 7.2.1
glob:
specifier: ^10.3.15
version: 10.3.15
@@ -3423,6 +3426,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
flag-icons@7.2.1:
resolution: {integrity: sha512-EaU4XZmFt1BOilz9nMmJKjma5pOaNjzL7somOhadrrilollh4xj6aaXI2M1sd00VUfVWN0E25Q6xaW3SNt0k/Q==}
flat-cache@3.2.0:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -8283,6 +8289,8 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
flag-icons@7.2.1: {}
flat-cache@3.2.0:
dependencies:
flatted: 3.2.9