🌐 Add missing translations

This commit is contained in:
Manuel
2023-08-05 23:06:40 +02:00
parent cdb88ff941
commit 14c366bddd
9 changed files with 161 additions and 60 deletions

View File

@@ -0,0 +1,50 @@
{
"steps": {
"account": {
"title": "First step",
"text": "Create account",
"username": {
"label": "Username"
},
"email": {
"label": "E-Mail"
}
},
"security": {
"title": "Second step",
"text": "Password",
"password": {
"label": "Password",
"requirement": "Includes at least 6 characters"
}
},
"finish": {
"title": "Final step",
"text": "Save to database",
"card": {
"title": "Review your inputs",
"text": "After you submit your data to the database, the user will be able to log in. Are you sure that you want to store this user in the database and activate the login?"
},
"table": {
"header": {
"property": "Property",
"value": "Value",
"username": "Username",
"email": "E-Mail",
"password": "Password"
},
"notSet": "Not set",
"valid": "Valid"
},
"alertConfirmed": "User has been created in the database. They can now log in."
}
},
"buttons": {
"next": "Next",
"previous": "Previous",
"confirm": "Confirm",
"generateRandomPw": "Generate random",
"createAnother": "Create another",
"goBack": "Go back to users"
}
}

View File

@@ -0,0 +1,21 @@
{
"title": "Manage user invites",
"text": "Using invites, you can invite users to your Homarr instance. An invitation will only be valid for a certain time-span and can be used once. The expiration must be between 5 minutes and 12 months upon creation.",
"button": {
"createInvite": "Create invitation",
"deleteInvite": "Delete invite"
},
"table": {
"header": {
"id": "ID",
"creator": "Creator",
"expires": "Expires",
"action": "Actions"
},
"data": {
"expiresAt": "expired {{at}}",
"expiresIn": "in {{in}}"
}
},
"noInvites": "There are no invitations yet."
}

View File

@@ -0,0 +1,16 @@
{
"title": "Manage users",
"text": "Using users, you have granular control who can access, edit or delete resources on your Homarr instance.",
"buttons": {
"create": "Create"
},
"table": {
"header": {
"user": "User"
}
},
"modals": {
"delete": "Delete user {{name}}"
},
"searchDoesntMatch": "Your search does not match any entries. Please adjust your filter."
}

View File

@@ -1,6 +1,7 @@
import { Button, Card, Flex, TextInput } from '@mantine/core'; import { Button, Card, Flex, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form'; import { useForm, zodResolver } from '@mantine/form';
import { IconArrowRight, IconAt, IconUser } from '@tabler/icons-react'; import { IconArrowRight, IconAt, IconUser } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod'; import { z } from 'zod';
interface CreateAccountStepProps { interface CreateAccountStepProps {
@@ -20,11 +21,13 @@ export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: C
validate: zodResolver(createAccountStepValidationSchema), validate: zodResolver(createAccountStepValidationSchema),
}); });
const { t } = useTranslation('user/create');
return ( return (
<Card mih={400}> <Card mih={400}>
<TextInput <TextInput
icon={<IconUser size="0.8rem" />} icon={<IconUser size="0.8rem" />}
label="Username" label={t('steps.account.username.label')}
variant="filled" variant="filled"
mb="md" mb="md"
withAsterisk withAsterisk
@@ -32,7 +35,7 @@ export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: C
/> />
<TextInput <TextInput
icon={<IconAt size="0.8rem" />} icon={<IconAt size="0.8rem" />}
label="E-Mail" label={t('steps.account.email.label')}
variant="filled" variant="filled"
mb="md" mb="md"
{...form.getInputProps('eMail')} {...form.getInputProps('eMail')}
@@ -51,7 +54,7 @@ export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: C
variant="light" variant="light"
px="xl" px="xl"
> >
Next {t('buttons.next')}
</Button> </Button>
</Flex> </Flex>
</Card> </Card>

View File

@@ -19,6 +19,7 @@ import {
IconX, IconX,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { passwordSchema } from '~/validations/user'; import { passwordSchema } from '~/validations/user';
@@ -73,6 +74,8 @@ export const CreateAccountSecurityStep = ({
/> />
)); ));
const { t } = useTranslation('user/create');
const strength = getStrength(form.values.password); const strength = getStrength(form.values.password);
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red'; const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
@@ -95,7 +98,7 @@ export const CreateAccountSecurityStep = ({
style={{ style={{
flexGrow: 1, flexGrow: 1,
}} }}
label="Password" label={t('steps.security.password.label')}
variant="filled" variant="filled"
mb="md" mb="md"
withAsterisk withAsterisk
@@ -111,7 +114,7 @@ export const CreateAccountSecurityStep = ({
variant="default" variant="default"
mt="xl" mt="xl"
> >
Generate random {t('buttons.generateRandomPw')}
</Button> </Button>
</Flex> </Flex>
</div> </div>
@@ -119,7 +122,7 @@ export const CreateAccountSecurityStep = ({
<Popover.Dropdown> <Popover.Dropdown>
<Progress color={color} value={strength} size={5} mb="xs" /> <Progress color={color} value={strength} size={5} mb="xs" />
<PasswordRequirement <PasswordRequirement
label="Includes at least 6 characters" label={t('steps.security.password.requirement')}
meets={form.values.password.length > 5} meets={form.values.password.length > 5}
/> />
{checks} {checks}
@@ -128,7 +131,7 @@ export const CreateAccountSecurityStep = ({
<Group position="apart" noWrap> <Group position="apart" noWrap>
<Button leftIcon={<IconArrowLeft size="1rem" />} onClick={prevStep} variant="light" px="xl"> <Button leftIcon={<IconArrowLeft size="1rem" />} onClick={prevStep} variant="light" px="xl">
Previous {t('buttons.previous')}
</Button> </Button>
<Button <Button
rightIcon={<IconArrowRight size="1rem" />} rightIcon={<IconArrowRight size="1rem" />}
@@ -141,7 +144,7 @@ export const CreateAccountSecurityStep = ({
px="xl" px="xl"
disabled={!form.isValid()} disabled={!form.isValid()}
> >
Next {t('buttons.next')}
</Button> </Button>
</Group> </Group>
</Card> </Card>

View File

@@ -1,4 +1,4 @@
import { Alert, Button, Card, Flex, Group, Stepper, Table, Text, Title } from '@mantine/core'; import { Alert, Button, Card, Group, Stepper, Table, Text, Title } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form'; import { useForm, zodResolver } from '@mantine/form';
import { import {
IconArrowLeft, IconArrowLeft,
@@ -14,6 +14,7 @@ import { GetServerSideProps } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { import {
CreateAccountStep, CreateAccountStep,
@@ -27,6 +28,7 @@ import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
const CreateNewUserPage = () => { const CreateNewUserPage = () => {
const [active, setActive] = useState(0); const [active, setActive] = useState(0);
@@ -61,6 +63,8 @@ const CreateNewUserPage = () => {
}, },
}); });
const { t } = useTranslation('user/create');
return ( return (
<ManageLayout> <ManageLayout>
<Head> <Head>
@@ -72,8 +76,8 @@ const CreateNewUserPage = () => {
allowStepClick={false} allowStepClick={false}
allowStepSelect={false} allowStepSelect={false}
icon={<IconUser />} icon={<IconUser />}
label="First step" label={t('steps.account.title')}
description="Create account" description={t('steps.account.text')}
> >
<CreateAccountStep <CreateAccountStep
defaultUsername={form.values.account.username} defaultUsername={form.values.account.username}
@@ -88,8 +92,8 @@ const CreateNewUserPage = () => {
allowStepClick={false} allowStepClick={false}
allowStepSelect={false} allowStepSelect={false}
icon={<IconKey />} icon={<IconKey />}
label="Second step" label={t('steps.security.title')}
description="Password" description={t('steps.security.text')}
> >
<CreateAccountSecurityStep <CreateAccountSecurityStep
defaultPassword={form.values.security.password} defaultPassword={form.values.security.password}
@@ -104,21 +108,18 @@ const CreateNewUserPage = () => {
allowStepClick={false} allowStepClick={false}
allowStepSelect={false} allowStepSelect={false}
icon={<IconMailCheck />} icon={<IconMailCheck />}
label="Final step" label={t('steps.finish.title')}
description="Save to database" description={t('steps.finish.title')}
> >
<Card mih={400}> <Card mih={400}>
<Title order={5}>Review your inputs</Title> <Title order={5}>{t('steps.finish.card.title')}</Title>
<Text mb="xl"> <Text mb="xl">{t('steps.finish.card.text')}</Text>
After you submit your data to the database, the user will be able to log in. Are you
sure that you want to store this user in the database and activate the login?
</Text>
<Table mb="lg" withBorder highlightOnHover> <Table mb="lg" withBorder highlightOnHover>
<thead> <thead>
<tr> <tr>
<th>Property</th> <th>{t('steps.finish.table.header.property')}</th>
<th>Value</th> <th>{t('steps.finish.table.header.value')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -126,7 +127,7 @@ const CreateNewUserPage = () => {
<td> <td>
<Group spacing="xs"> <Group spacing="xs">
<IconUser size="1rem" /> <IconUser size="1rem" />
<Text>Username</Text> <Text>{t('steps.finish.table.header.username')}</Text>
</Group> </Group>
</td> </td>
<td>{form.values.account.username}</td> <td>{form.values.account.username}</td>
@@ -135,7 +136,7 @@ const CreateNewUserPage = () => {
<td> <td>
<Group spacing="xs"> <Group spacing="xs">
<IconMail size="1rem" /> <IconMail size="1rem" />
<Text>E-Mail</Text> <Text>{t('steps.finish.table.header.email')}</Text>
</Group> </Group>
</td> </td>
<td> <td>
@@ -144,7 +145,7 @@ const CreateNewUserPage = () => {
) : ( ) : (
<Group spacing="xs"> <Group spacing="xs">
<IconInfoCircle size="1rem" color="orange" /> <IconInfoCircle size="1rem" color="orange" />
<Text color="orange">Not set</Text> <Text color="orange">{t('steps.finish.table.notSet')}</Text>
</Group> </Group>
)} )}
</td> </td>
@@ -153,13 +154,13 @@ const CreateNewUserPage = () => {
<td> <td>
<Group spacing="xs"> <Group spacing="xs">
<IconKey size="1rem" /> <IconKey size="1rem" />
<Text>Password</Text> <Text>{t('steps.finish.table.password')}</Text>
</Group> </Group>
</td> </td>
<td> <td>
<Group spacing="xs"> <Group spacing="xs">
<IconCheck size="1rem" color="green" /> <IconCheck size="1rem" color="green" />
<Text color="green">Valid</Text> <Text color="green">{t('steps.finish.table.valid')}</Text>
</Group> </Group>
</td> </td>
</tr> </tr>
@@ -173,7 +174,7 @@ const CreateNewUserPage = () => {
variant="light" variant="light"
px="xl" px="xl"
> >
Previous {t('buttons.previous')}
</Button> </Button>
<Button <Button
onClick={async () => { onClick={async () => {
@@ -188,14 +189,14 @@ const CreateNewUserPage = () => {
variant="light" variant="light"
px="xl" px="xl"
> >
Confirm {t('buttons.confirm')}
</Button> </Button>
</Group> </Group>
</Card> </Card>
</Stepper.Step> </Stepper.Step>
<Stepper.Completed> <Stepper.Completed>
<Alert title="User was created" color="green" mb="md"> <Alert title="User was created" color="green" mb="md">
User has been created in the database. They can now log in. {t('steps.finish.alertConfirmed')}
</Alert> </Alert>
<Group> <Group>
@@ -207,7 +208,7 @@ const CreateNewUserPage = () => {
leftIcon={<IconUserPlus size="1rem" />} leftIcon={<IconUserPlus size="1rem" />}
variant="default" variant="default"
> >
Create another {t('buttons.createAnother')}
</Button> </Button>
<Button <Button
component={Link} component={Link}
@@ -215,7 +216,7 @@ const CreateNewUserPage = () => {
variant="default" variant="default"
href="/manage/users" href="/manage/users"
> >
Go back to users {t('buttons.goBack')}
</Button> </Button>
</Group> </Group>
</Stepper.Completed> </Stepper.Completed>
@@ -234,7 +235,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
} }
const translations = await getServerSideTranslations( const translations = await getServerSideTranslations(
['common'], manageNamespaces,
ctx.locale, ctx.locale,
undefined, undefined,
undefined undefined

View File

@@ -18,6 +18,7 @@ import { GetServerSideProps } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
@@ -32,17 +33,16 @@ const ManageUsersPage = () => {
search: debouncedSearch, search: debouncedSearch,
}); });
const { t } = useTranslation('user/manage');
return ( return (
<ManageLayout> <ManageLayout>
<Head> <Head>
<title>Users Homarr</title> <title>Users Homarr</title>
</Head> </Head>
<Title mb="md">Manage users</Title> <Title mb="md">{t('title')}</Title>
<Text mb="xl"> <Text mb="xl">{t('text')}</Text>
Using users, you have granular control who can access, edit or delete resources on your
Homarr instance.
</Text>
<Flex columnGap={10} justify="end" mb="md"> <Flex columnGap={10} justify="end" mb="md">
<Autocomplete <Autocomplete
@@ -61,7 +61,7 @@ const ManageUsersPage = () => {
href="/manage/users/create" href="/manage/users/create"
variant="default" variant="default"
> >
Create {t('buttons.create')}
</Button> </Button>
</Flex> </Flex>
@@ -70,7 +70,7 @@ const ManageUsersPage = () => {
<Table mb="md" withBorder highlightOnHover> <Table mb="md" withBorder highlightOnHover>
<thead> <thead>
<tr> <tr>
<th>User</th> <th>{t('table.header.user')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -87,7 +87,9 @@ const ManageUsersPage = () => {
onClick={() => { onClick={() => {
openContextModal({ openContextModal({
modal: 'deleteUserModal', modal: 'deleteUserModal',
title: <Text weight="bold">Delete user {user.name}</Text>, title: (
<Text weight="bold">{t('modals.delete', { name: user.name })}</Text>
),
innerProps: { innerProps: {
userId: user.id, userId: user.id,
username: user.name ?? '', username: user.name ?? '',
@@ -110,7 +112,7 @@ const ManageUsersPage = () => {
<td colSpan={1}> <td colSpan={1}>
<Box p={15}> <Box p={15}>
<Text> <Text>
Your search does not match any entries. Please adjust your filter. {t('searchDoesntMatch')}
</Text> </Text>
</Box> </Box>
</td> </td>

View File

@@ -14,11 +14,12 @@ import { IconPlus, IconTrash } from '@tabler/icons-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { GetServerSideProps } from 'next'; import { GetServerSideProps } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
const ManageUserInvitesPage = () => { const ManageUserInvitesPage = () => {
@@ -26,7 +27,6 @@ const ManageUserInvitesPage = () => {
const { data } = api.invites.all.useQuery({ const { data } = api.invites.all.useQuery({
page: activePage, page: activePage,
}); });
const router = useRouter();
const { classes } = useStyles(); const { classes } = useStyles();
@@ -38,17 +38,15 @@ const ManageUserInvitesPage = () => {
setActivePage((prev) => prev - 1); setActivePage((prev) => prev - 1);
}; };
const { t } = useTranslation('user/invites');
return ( return (
<ManageLayout> <ManageLayout>
<Head> <Head>
<title>User invites Homarr</title> <title>User invites Homarr</title>
</Head> </Head>
<Title mb="md">Manage user invites</Title> <Title mb="md">{t('title')}</Title>
<Text mb="xl"> <Text mb="xl">{t('text')}</Text>
Using invites, you can invite users to your Homarr instance. An invitation will only be
valid for a certain time-span and can be used once. The expiration must be between 5 minutes
and 12 months upon creation.
</Text>
<Flex justify="end" mb="md"> <Flex justify="end" mb="md">
<Button <Button
@@ -62,7 +60,7 @@ const ManageUserInvitesPage = () => {
leftIcon={<IconPlus size="1rem" />} leftIcon={<IconPlus size="1rem" />}
variant="default" variant="default"
> >
Create invitation {t('button.createInvite')}
</Button> </Button>
</Flex> </Flex>
@@ -71,10 +69,10 @@ const ManageUserInvitesPage = () => {
<Table mb="md" withBorder highlightOnHover> <Table mb="md" withBorder highlightOnHover>
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>{t('table.header.id')}</th>
<th>Creator</th> <th>{t('table.header.creator')}</th>
<th>Expires</th> <th>{t('table.header.expires')}</th>
<th>Actions</th> <th>{t('table.header.action')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -88,9 +86,13 @@ const ManageUserInvitesPage = () => {
</td> </td>
<td className={classes.tableCell}> <td className={classes.tableCell}>
{dayjs(dayjs()).isAfter(invite.expires) ? ( {dayjs(dayjs()).isAfter(invite.expires) ? (
<Text>expired {dayjs(invite.expires).fromNow()}</Text> <Text>
{t('table.data.expiresAt', { at: dayjs(invite.expires).fromNow() })}
</Text>
) : ( ) : (
<Text>in {dayjs(invite.expires).fromNow(true)}</Text> <Text>
{t('table.data.expiresIn', { in: dayjs(invite.expires).fromNow(true) })}
</Text>
)} )}
</td> </td>
<td className={classes.tableCell}> <td className={classes.tableCell}>
@@ -98,7 +100,7 @@ const ManageUserInvitesPage = () => {
onClick={() => { onClick={() => {
modals.openContextModal({ modals.openContextModal({
modal: 'deleteInviteModal', modal: 'deleteInviteModal',
title: <Text weight="bold">Delete invite</Text>, title: <Text weight="bold">{t('button.deleteInvite')}</Text>,
innerProps: { innerProps: {
tokenId: invite.id, tokenId: invite.id,
}, },
@@ -116,7 +118,7 @@ const ManageUserInvitesPage = () => {
<tr> <tr>
<td colSpan={3}> <td colSpan={3}>
<Center p="md"> <Center p="md">
<Text color="dimmed">There are no invitations yet.</Text> <Text color="dimmed">{t('noInvites')}</Text>
</Center> </Center>
</td> </td>
</tr> </tr>
@@ -164,7 +166,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
} }
const translations = await getServerSideTranslations( const translations = await getServerSideTranslations(
['common'], manageNamespaces,
ctx.locale, ctx.locale,
undefined, undefined,
undefined undefined

View File

@@ -43,6 +43,9 @@ export const boardNamespaces = [
export const manageNamespaces = [ export const manageNamespaces = [
'user/preferences', 'user/preferences',
'user/manage',
'user/invite',
'user/create',
'boards/manage', 'boards/manage',
'zod', 'zod',
]; ];