♻️ Add header translations

This commit is contained in:
Meier Lukas
2023-08-05 16:02:26 +02:00
parent 04b3fa394d
commit 889853961d
5 changed files with 57 additions and 23 deletions

View File

@@ -0,0 +1,27 @@
{
"experimentalNote": {
"label": "This is an experimental feature of Homarr. Please report any issues to the official Homarr team."
},
"search": {
"label": "Search",
"engines": {
"web": "Search for {{query}} on the web",
"youtube": "Search for {{query}} on YouTube",
"torrent": "Search for {{query}} torrents",
"movie": "Search for {{query}} on {{app}}"
}
},
"actions": {
"avatar": {
"switchTheme": "Switch theme",
"preferences": "User preferences",
"defaultBoard": "Default dashboard",
"about": {
"label": "About",
"new": "New"
},
"logout": "Logout from {{username}}",
"login": "Login"
}
}
}

View File

@@ -12,6 +12,7 @@ import {
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { User } from 'next-auth'; import { User } from 'next-auth';
import { signOut, useSession } from 'next-auth/react'; import { signOut, useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Link from 'next/link'; import Link from 'next/link';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { AboutModal } from '~/components/layout/header/About/AboutModal'; import { AboutModal } from '~/components/layout/header/About/AboutModal';
@@ -21,6 +22,7 @@ import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAtt
import { REPO_URL } from '../../../../data/constants'; import { REPO_URL } from '../../../../data/constants';
export const AvatarMenu = () => { export const AvatarMenu = () => {
const { t } = useTranslation('layout/header');
const [aboutModalOpened, aboutModal] = useDisclosure(false); const [aboutModalOpened, aboutModal] = useDisclosure(false);
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const { colorScheme, toggleColorScheme } = useColorScheme(); const { colorScheme, toggleColorScheme } = useColorScheme();
@@ -37,7 +39,7 @@ export const AvatarMenu = () => {
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item icon={<Icon size="1rem" />} onClick={toggleColorScheme}> <Menu.Item icon={<Icon size="1rem" />} onClick={toggleColorScheme}>
Switch theme {t('actions.avatar.switchTheme')}
</Menu.Item> </Menu.Item>
{sessionData?.user && ( {sessionData?.user && (
<> <>
@@ -46,10 +48,10 @@ export const AvatarMenu = () => {
href="/user/preferences" href="/user/preferences"
icon={<IconUserCog size="1rem" />} icon={<IconUserCog size="1rem" />}
> >
User preferences {t('actions.avatar.preferences')}
</Menu.Item> </Menu.Item>
<Menu.Item component={Link} href="/board" icon={<IconDashboard size="1rem" />}> <Menu.Item component={Link} href="/board" icon={<IconDashboard size="1rem" />}>
Default Dashboard {t('actions.avatar.defaultBoard')}
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider />
</> </>
@@ -59,13 +61,13 @@ export const AvatarMenu = () => {
rightSection={ rightSection={
newVersionAvailable && ( newVersionAvailable && (
<Badge variant="light" color="blue"> <Badge variant="light" color="blue">
New {t('actions.avatar.about.new')}
</Badge> </Badge>
) )
} }
onClick={() => aboutModal.open()} onClick={() => aboutModal.open()}
> >
About {t('actions.avatar.about.label')}
</Menu.Item> </Menu.Item>
{sessionData?.user ? ( {sessionData?.user ? (
<Menu.Item <Menu.Item
@@ -77,11 +79,13 @@ export const AvatarMenu = () => {
}).then(() => window.location.reload()) }).then(() => window.location.reload())
} }
> >
Logout from {sessionData.user.name} {t('actions.avatar.logout', {
username: sessionData.user.name,
})}
</Menu.Item> </Menu.Item>
) : ( ) : (
<Menu.Item icon={<IconLogin size="1rem" />} component={Link} href="/auth/login"> <Menu.Item icon={<IconLogin size="1rem" />} component={Link} href="/auth/login">
Login {t('actions.avatar.login')}
</Menu.Item> </Menu.Item>
)} )}
</Menu.Dropdown> </Menu.Dropdown>

View File

@@ -9,6 +9,7 @@ import {
TablerIconsProps, TablerIconsProps,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react'; import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react';
import { useConfigContext } from '~/config/provider'; import { useConfigContext } from '~/config/provider';
@@ -21,6 +22,7 @@ type SearchProps = {
}; };
export const Search = ({ isMobile }: SearchProps) => { export const Search = ({ isMobile }: SearchProps) => {
const { t } = useTranslation('layout/header');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
useHotkeys([['mod+K', () => ref.current?.focus()]]); useHotkeys([['mod+K', () => ref.current?.focus()]]);
@@ -37,10 +39,18 @@ export const Search = ({ isMobile }: SearchProps) => {
const engines = generateEngines( const engines = generateEngines(
search, search,
userWithSettings?.settings.searchTemplate ?? 'https://www.google.com/search?q=%s' userWithSettings?.settings.searchTemplate ?? 'https://www.google.com/search?q=%s'
).filter( )
.filter(
(engine) => (engine) =>
engine.sort !== 'movie' || config?.apps.some((app) => app.integration.type === engine.value) engine.sort !== 'movie' || config?.apps.some((app) => app.integration.type === engine.value)
); )
.map((engine) => ({
...engine,
label: t(`search.engines.${engine.sort}`, {
app: engine.value,
query: search,
}),
}));
const data = [...apps, ...engines]; const data = [...apps, ...engines];
return ( return (
@@ -50,7 +60,7 @@ export const Search = ({ isMobile }: SearchProps) => {
radius="xl" radius="xl"
w={isMobile ? '100%' : 400} w={isMobile ? '100%' : 400}
variant="filled" variant="filled"
placeholder="Search..." placeholder={`${t('search.label')}...`}
hoverOnSearchChange hoverOnSearchChange
rightSection={ rightSection={
<IconSearch <IconSearch
@@ -87,7 +97,7 @@ export const Search = ({ isMobile }: SearchProps) => {
const target = userWithSettings?.settings.openSearchInNewTab ? '_blank' : '_self'; const target = userWithSettings?.settings.openSearchInNewTab ? '_blank' : '_self';
window.open(item.metaData.url, target); window.open(item.metaData.url, target);
}} }}
aria-label="Search" aria-label={t('search.label') as string}
/> />
<MovieModal <MovieModal
opened={showMovieModal} opened={showMovieModal}
@@ -104,7 +114,7 @@ export const Search = ({ isMobile }: SearchProps) => {
); );
}; };
const SearchItemComponent = forwardRef<HTMLDivElement, SearchAutoCompleteItem>( const SearchItemComponent = forwardRef<HTMLDivElement, SearchAutoCompleteItem & { label: string }>(
({ icon, label, value, sort, ...others }, ref) => { ({ icon, label, value, sort, ...others }, ref) => {
let Icon = getItemComponent(icon); let Icon = getItemComponent(icon);
@@ -150,7 +160,6 @@ const useConfigApps = (search: string) => {
type SearchAutoCompleteItem = { type SearchAutoCompleteItem = {
icon: ((props: TablerIconsProps) => ReactNode) | string; icon: ((props: TablerIconsProps) => ReactNode) | string;
label: string;
value: string; value: string;
} & ( } & (
| { | {
@@ -169,7 +178,6 @@ const generateEngines = (searchValue: string, webTemplate: string) =>
? ([ ? ([
{ {
icon: IconWorld, icon: IconWorld,
label: `Search for ${searchValue} in the web`,
value: `web`, value: `web`,
sort: 'web', sort: 'web',
metaData: { metaData: {
@@ -180,7 +188,6 @@ const generateEngines = (searchValue: string, webTemplate: string) =>
}, },
{ {
icon: IconDownload, icon: IconDownload,
label: `Search for ${searchValue} torrents`,
value: `torrent`, value: `torrent`,
sort: 'torrent', sort: 'torrent',
metaData: { metaData: {
@@ -189,7 +196,6 @@ const generateEngines = (searchValue: string, webTemplate: string) =>
}, },
{ {
icon: IconBrandYoutube, icon: IconBrandYoutube,
label: `Search for ${searchValue} on youtube`,
value: 'youtube', value: 'youtube',
sort: 'youtube', sort: 'youtube',
metaData: { metaData: {
@@ -200,7 +206,6 @@ const generateEngines = (searchValue: string, webTemplate: string) =>
(name) => (name) =>
({ ({
icon: IconMovie, icon: IconMovie,
label: `Search for ${searchValue} on ${name}`,
value: name, value: name,
sort: 'movie', sort: 'movie',
}) as const }) as const

View File

@@ -10,8 +10,7 @@ import {
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { createFormContext } from '@mantine/form'; import { createFormContext } from '@mantine/form';
import { createServerSideHelpers } from '@trpc/react-query/server'; import { GetServerSideProps } from 'next';
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Head from 'next/head'; import Head from 'next/head';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
@@ -23,7 +22,6 @@ import { createTrpcServersideHelpers } from '~/server/api/helper';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { languages } from '~/tools/language'; import { languages } from '~/tools/language';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { RouterOutputs, api } from '~/utils/api'; import { RouterOutputs, api } from '~/utils/api';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { updateSettingsValidationSchema } from '~/validations/user'; import { updateSettingsValidationSchema } from '~/validations/user';

View File

@@ -10,7 +10,7 @@ export const getServerSideTranslations = async (
req?: IncomingMessage, req?: IncomingMessage,
res?: ServerResponse res?: ServerResponse
) => { ) => {
namespaces = namespaces.concat(['common', 'zod']); namespaces = namespaces.concat(['common', 'zod', 'layout/header', 'layout/modals/about']);
if (!req || !res) { if (!req || !res) {
return serverSideTranslations(requestLocale ?? 'en', namespaces); return serverSideTranslations(requestLocale ?? 'en', namespaces);