feat(i18n): localize admin users UI surfaces

This commit is contained in:
2026-06-15 16:01:19 +02:00
parent 0cbbe60299
commit 73552dbb05
13 changed files with 593 additions and 58 deletions
@@ -14,6 +14,7 @@ export default async function EditUserPage({
const { userId } = await params
const user = await getUserProfileById(userId)
const { dictionary } = await getI18n()
const copy = dictionary.admin.users
if (!user) {
notFound()
@@ -22,15 +23,20 @@ export default async function EditUserPage({
return (
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Edit User</h1>
<h1 className="text-2xl font-bold">{copy.edit.title}</h1>
</div>
<EditUserForm
formCopy={copy.form}
schemaCopy={copy.schema}
roleLabels={copy.roles}
submitButtonCopy={dictionary.common.submitButton}
user={user}
/>
<section className="flex flex-col gap-4 border-t pt-6">
<h2 className="text-xl font-semibold">Reset password</h2>
<h2 className="text-xl font-semibold">{copy.resetPassword.title}</h2>
<ResetUserPasswordForm
formCopy={copy.resetPassword}
schemaCopy={copy.schema}
submitButtonCopy={dictionary.common.submitButton}
userId={user.id}
/>
@@ -2,6 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useMemo } from "react"
import type { UseFormRegisterReturn } from "react-hook-form"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
@@ -11,26 +12,36 @@ import {
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import {
buildUpdateUserSchema,
type UpdateUserFormType,
updateUserSchema,
type UserSchemaCopy,
} from "@/schemas/user.schema"
import type { UserWithoutPassword } from "@/services/user.service"
import type { UserFormCopy, UserRoleCopy } from "./user.copy"
export default function EditUserForm({
formCopy,
schemaCopy,
roleLabels,
submitButtonCopy,
user,
}: {
formCopy: UserFormCopy
schemaCopy: UserSchemaCopy
roleLabels: UserRoleCopy
submitButtonCopy: SubmitButtonCopy
user: UserWithoutPassword
}) {
const router = useRouter()
const schema = useMemo(() => buildUpdateUserSchema(schemaCopy), [schemaCopy])
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<UpdateUserFormType>({
resolver: zodResolver(updateUserSchema),
resolver: zodResolver(schema),
defaultValues: {
id: user.id,
name: user.name,
@@ -69,50 +80,50 @@ export default function EditUserForm({
<UserTextInput
error={errors.name?.message}
id="name"
label="Name"
placeholder="Full name"
label={formCopy.nameLabel}
placeholder={formCopy.namePlaceholder}
register={register("name")}
/>
<UserTextInput
error={errors.username?.message}
id="username"
label="Username"
placeholder="username"
label={formCopy.usernameLabel}
placeholder={formCopy.usernamePlaceholder}
register={register("username")}
/>
<UserTextInput
error={errors.email?.message}
id="email"
label="Email"
placeholder="user@example.com"
label={formCopy.emailLabel}
placeholder={formCopy.emailPlaceholder}
register={register("email")}
type="email"
/>
<div className="flex flex-col gap-2">
<label htmlFor="role" className="mb-2 block text-lg">
Role
{formCopy.roleLabel}
</label>
<select
id="role"
{...register("role")}
className="w-full rounded-lg border px-4 py-2"
>
<option value="ADMIN">Admin</option>
<option value="MANAGER">Manager</option>
<option value="STAFF">Staff</option>
<option value="VIEWER">Viewer</option>
<option value="ADMIN">{roleLabels.ADMIN}</option>
<option value="MANAGER">{roleLabels.MANAGER}</option>
<option value="STAFF">{roleLabels.STAFF}</option>
<option value="VIEWER">{roleLabels.VIEWER}</option>
</select>
</div>
<label className="flex items-center gap-2">
<input type="checkbox" {...register("isActive")} />
Active user
{formCopy.activeLabel}
</label>
<SubmitButton
copy={submitButtonCopy}
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
Update User
{formCopy.updateSubmit}
</SubmitButton>
</form>
)
@@ -2,6 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useMemo } from "react"
import type { UseFormRegisterReturn } from "react-hook-form"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
@@ -11,23 +12,33 @@ import {
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import {
buildCreateUserSchema,
type CreateUserFormType,
createUserSchema,
type UserSchemaCopy,
} from "@/schemas/user.schema"
import type { UserFormCopy, UserRoleCopy } from "./user.copy"
export default function NewUserForm({
formCopy,
schemaCopy,
roleLabels,
submitButtonCopy,
}: {
formCopy: UserFormCopy
schemaCopy: UserSchemaCopy
roleLabels: UserRoleCopy
submitButtonCopy: SubmitButtonCopy
}) {
const router = useRouter()
const schema = useMemo(() => buildCreateUserSchema(schemaCopy), [schemaCopy])
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<CreateUserFormType>({
resolver: zodResolver(createUserSchema),
resolver: zodResolver(schema),
defaultValues: {
role: "STAFF",
isActive: true,
@@ -61,40 +72,44 @@ export default function NewUserForm({
<UserTextInput
error={errors.name?.message}
id="name"
label="Name"
placeholder="Full name"
label={formCopy.nameLabel}
placeholder={formCopy.namePlaceholder}
register={register("name")}
/>
<UserTextInput
error={errors.username?.message}
id="username"
label="Username"
placeholder="username"
label={formCopy.usernameLabel}
placeholder={formCopy.usernamePlaceholder}
register={register("username")}
/>
<UserTextInput
error={errors.email?.message}
id="email"
label="Email"
placeholder="user@example.com"
label={formCopy.emailLabel}
placeholder={formCopy.emailPlaceholder}
register={register("email")}
type="email"
/>
<UserTextInput
error={errors.password?.message}
id="password"
label="Password"
placeholder="Minimum 8 characters"
label={formCopy.passwordLabel}
placeholder={formCopy.passwordPlaceholder}
register={register("password")}
type="password"
/>
<RoleSelect register={register("role")} />
<RoleSelect
register={register("role")}
roleLabel={formCopy.roleLabel}
roleLabels={roleLabels}
/>
<SubmitButton
copy={submitButtonCopy}
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
Create User
{formCopy.createSubmit}
</SubmitButton>
</form>
)
@@ -132,21 +147,29 @@ function UserTextInput({
)
}
function RoleSelect({ register }: { register: UseFormRegisterReturn }) {
function RoleSelect({
register,
roleLabel,
roleLabels,
}: {
register: UseFormRegisterReturn
roleLabel: string
roleLabels: UserRoleCopy
}) {
return (
<div className="flex flex-col gap-2">
<label htmlFor="role" className="mb-2 block text-lg">
Role
{roleLabel}
</label>
<select
id="role"
{...register}
className="w-full rounded-lg border px-4 py-2"
>
<option value="ADMIN">Admin</option>
<option value="MANAGER">Manager</option>
<option value="STAFF">Staff</option>
<option value="VIEWER">Viewer</option>
<option value="ADMIN">{roleLabels.ADMIN}</option>
<option value="MANAGER">{roleLabels.MANAGER}</option>
<option value="STAFF">{roleLabels.STAFF}</option>
<option value="VIEWER">{roleLabels.VIEWER}</option>
</select>
</div>
)
@@ -1,6 +1,7 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMemo } from "react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { resetUserPasswordAction } from "@/actions/user.actions"
@@ -9,17 +10,28 @@ import {
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import {
buildResetUserPasswordSchema,
type ResetUserPasswordFormType,
resetUserPasswordSchema,
type UserSchemaCopy,
} from "@/schemas/user.schema"
import type { UserResetPasswordCopy } from "./user.copy"
export default function ResetUserPasswordForm({
formCopy,
schemaCopy,
submitButtonCopy,
userId,
}: {
formCopy: UserResetPasswordCopy
schemaCopy: UserSchemaCopy
submitButtonCopy: SubmitButtonCopy
userId: string
}) {
const schema = useMemo(
() => buildResetUserPasswordSchema(schemaCopy),
[schemaCopy],
)
const {
register,
handleSubmit,
@@ -27,7 +39,7 @@ export default function ResetUserPasswordForm({
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<ResetUserPasswordFormType>({
resolver: zodResolver(resetUserPasswordSchema),
resolver: zodResolver(schema),
defaultValues: {
id: userId,
},
@@ -60,12 +72,12 @@ export default function ResetUserPasswordForm({
<input type="hidden" {...register("id")} />
<div className="flex flex-col gap-2">
<label htmlFor="password" className="mb-2 block text-lg">
New password
{formCopy.passwordLabel}
</label>
<input
type="password"
id="password"
placeholder="Minimum 8 characters"
placeholder={formCopy.passwordPlaceholder}
{...register("password")}
className={`w-full rounded-lg border px-4 py-2 ${errors.password ? "border-error" : ""}`}
/>
@@ -78,7 +90,7 @@ export default function ResetUserPasswordForm({
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
Reset Password
{formCopy.submit}
</SubmitButton>
</form>
)
@@ -1,11 +1,18 @@
import type { Dictionary } from "@/i18n/dictionaries"
export type UserListCopy = Dictionary["admin"]["users"]["list"]
export type UserFormCopy = Dictionary["admin"]["users"]["form"]
export type UserRoleCopy = Dictionary["admin"]["users"]["roles"]
export type UserStatusCopy = Dictionary["admin"]["users"]["status"]
export type UserFallbackCopy = Dictionary["admin"]["users"]["fallback"]
export type UserResetPasswordCopy =
Dictionary["admin"]["users"]["resetPassword"]
export type UserRolesCopy = Dictionary["admin"]["users"]["roles"]
export type UserStatusCopy = Dictionary["admin"]["users"]["status"]
export type UserActionCopy = Dictionary["admin"]["users"]["actions"]
export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"]
export type UserFallbackCopy = Dictionary["admin"]["users"]["fallback"]
export function formatUserRole(
role: string,
roleCopy: UserRoleCopy,
fallbackCopy: UserFallbackCopy,
): string {
return role in roleCopy
? roleCopy[role as keyof UserRoleCopy]
: fallbackCopy.unknownRole
}
+8 -2
View File
@@ -4,13 +4,19 @@ import NewUserForm from "../_components/new.user.form"
export default async function NewUserPage() {
const { dictionary } = await getI18n()
const copy = dictionary.admin.users
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">New User</h1>
<h1 className="text-2xl font-bold">{copy.new.title}</h1>
</div>
<NewUserForm submitButtonCopy={dictionary.common.submitButton} />
<NewUserForm
formCopy={copy.form}
schemaCopy={copy.schema}
roleLabels={copy.roles}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
)
}
+29 -10
View File
@@ -4,8 +4,16 @@ import Link from "next/link"
import PageHeader from "@/components/common/pageheader"
import PaginationButtons from "@/components/common/pagination"
import { Button } from "@/components/ui/button"
import { getI18n } from "@/i18n/server"
import { getUsers } from "@/services/user.service"
import {
formatUserRole,
type UserFallbackCopy,
type UserRoleCopy,
type UserStatusCopy,
} from "./_components/user.copy"
export default async function UsersPage(props: {
searchParams?: Promise<{
page?: string
@@ -20,11 +28,14 @@ export default async function UsersPage(props: {
pageSize: 10,
search,
})
const { dictionary } = await getI18n()
const copy = dictionary.admin.users
return (
<div className="flex flex-col gap-4">
<PageHeader
title="Users"
title={copy.list.title}
addLabel={copy.list.addLabel}
link="/admin/users/new"
search={search}
data={users}
@@ -32,7 +43,7 @@ export default async function UsersPage(props: {
{users.length === 0 && currentPage === 1 && (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
No users found.
{copy.list.empty}
</div>
</div>
)}
@@ -42,22 +53,22 @@ export default async function UsersPage(props: {
<thead className="border-b">
<tr>
<th scope="col" className="p-4">
Name
{copy.list.columns.name}
</th>
<th scope="col" className="p-4">
Username
{copy.list.columns.username}
</th>
<th scope="col" className="p-4">
Email
{copy.list.columns.email}
</th>
<th scope="col" className="p-4">
Role
{copy.list.columns.role}
</th>
<th scope="col" className="p-4">
Status
{copy.list.columns.status}
</th>
<th scope="col" className="p-4">
Actions
{copy.list.columns.actions}
</th>
</tr>
</thead>
@@ -67,9 +78,17 @@ export default async function UsersPage(props: {
<td className="p-4">{user.name}</td>
<td className="p-4">{user.username}</td>
<td className="p-4">{user.email}</td>
<td className="p-4">{user.role}</td>
<td className="p-4">
{user.isActive ? "Active" : "Inactive"}
{formatUserRole(
user.role,
copy.roles as UserRoleCopy,
copy.fallback as UserFallbackCopy,
)}
</td>
<td className="p-4">
{user.isActive
? (copy.status as UserStatusCopy).active
: (copy.status as UserStatusCopy).inactive}
</td>
<td className="p-4">
<Link href={`/admin/users/${user.id}/edit`} passHref>