mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-06 21:45:47 +01:00
♻️ Add header translations
This commit is contained in:
27
public/locales/en/layout/header.json
Normal file
27
public/locales/en/layout/header.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user