I18n components (#27)

* New translation keys in en-US locale

* New translation keys in de-DE locale

* New translation keys in fr-FR locale

* New translation keys in it-IT locale

* New translation keys in pl-PL locale

* New translation keys in pt-PT locale

* New translation keys in tr-TR locale

* Move into function

* Replace string matching to boolean check

* Add FIXIT in UsersTable

* Use localization for size units

* Missed and restored translation keys

* fixup! New translation keys in tr-TR locale

* Add translation keys in components
This commit is contained in:
vlalx
2025-05-25 17:41:38 +03:00
committed by GitHub
parent af3694da34
commit ea24759bb3
42 changed files with 1419 additions and 329 deletions

View File

@@ -39,11 +39,6 @@ type CreateRoleFormProps = {
afterCreate?: (res: CreateRoleResponse) => Promise<void>;
};
const formSchema = z.object({
name: z.string({ message: "Name is required" }).max(32),
description: z.string().max(255).optional()
});
export default function CreateRoleForm({
open,
setOpen,
@@ -52,6 +47,11 @@ export default function CreateRoleForm({
const { org } = useOrgContext();
const t = useTranslations();
const formSchema = z.object({
name: z.string({ message: t('nameRequired') }).max(32),
description: z.string().max(255).optional()
});
const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext());

View File

@@ -47,10 +47,6 @@ type CreateRoleFormProps = {
afterDelete?: () => void;
};
const formSchema = z.object({
newRoleId: z.string({ message: "New role is required" })
});
export default function DeleteRoleForm({
open,
roleToDelete,
@@ -65,6 +61,10 @@ export default function DeleteRoleForm({
const api = createApiClient(useEnvContext());
const formSchema = z.object({
newRoleId: z.string({ message: t('accessRoleErrorNewRequired') })
});
useEffect(() => {
async function fetchRoles() {
const res = await api

View File

@@ -24,7 +24,7 @@ export function RolesDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
title="Roles"
title={t('roles')}
searchPlaceholder={t('accessRolesSearch')}
searchColumn="name"
onAdd={createRole}

View File

@@ -222,7 +222,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
toast({
variant: "default",
title: t('userOrgRemoved'),
description: t('userOrgRemovedDescription', {email: selectedUser.email})
description: t('userOrgRemovedDescription', {email: selectedUser.email}) // FIXME
});
setUsers((prev) =>
@@ -244,7 +244,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username})}
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username})} // FIXME
</p>
<p>

View File

@@ -42,11 +42,6 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
const formSchema = z.object({
username: z.string(),
roleId: z.string().min(1, { message: "Please select a role" })
});
export default function AccessControlsPage() {
const { orgUser: user } = userOrgUserContext();
@@ -57,6 +52,13 @@ export default function AccessControlsPage() {
const [loading, setLoading] = useState(false);
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
const t = useTranslations();
const formSchema = z.object({
username: z.string(),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -65,8 +67,6 @@ export default function AccessControlsPage() {
}
});
const t = useTranslations();
useEffect(() => {
async function fetchRoles() {
const res = await api

View File

@@ -60,24 +60,6 @@ interface IdpOption {
type: string;
}
const internalFormSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
validForHours: z.string().min(1, { message: "Please select a duration" }),
roleId: z.string().min(1, { message: "Please select a role" })
});
const externalFormSchema = z.object({
username: z.string().min(1, { message: "Username is required" }),
email: z
.string()
.email({ message: "Invalid email address" })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: "Please select a role" }),
idpId: z.string().min(1, { message: "Please select an identity provider" })
});
const formatIdpType = (type: string) => {
switch (type.toLowerCase()) {
case "oidc":
@@ -104,6 +86,24 @@ export default function Page() {
const [selectedIdp, setSelectedIdp] = useState<IdpOption | null>(null);
const [dataLoaded, setDataLoaded] = useState(false);
const internalFormSchema = z.object({
email: z.string().email({ message: t('emailInvalid') }),
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
});
const externalFormSchema = z.object({
username: z.string().min(1, { message: t('usernameRequired') }),
email: z
.string()
.email({ message: t('emailInvalid') })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
idpId: z.string().min(1, { message: t('idpSelectPlease') })
});
const validFor = [
{ hours: 24, name: t('day', {count: 1}) },
{ hours: 48, name: t('day', {count: 2}) },

View File

@@ -58,42 +58,13 @@ import CopyTextBox from "@app/components/CopyTextBox";
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
import { useTranslations } from "next-intl";
const createFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters."
})
.max(255, {
message: "Name must not be longer than 255 characters."
})
});
type CreateFormValues = z.infer<typeof createFormSchema>;
const copiedFormSchema = z
.object({
copied: z.boolean()
})
.refine(
(data) => {
return data.copied;
},
{
message: "You must confirm that you have copied the API key.",
path: ["copied"]
}
);
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
export default function Page() {
const { env } = useEnvContext();
const api = createApiClient({ env });
const { orgId } = useParams();
const router = useRouter();
const t = useTranslations();
const [loadingPage, setLoadingPage] = useState(true);
const [createLoading, setCreateLoading] = useState(false);
const [apiKey, setApiKey] = useState<CreateOrgApiKeyResponse | null>(null);
@@ -101,6 +72,35 @@ export default function Page() {
Record<string, boolean>
>({});
const createFormSchema = z.object({
name: z
.string()
.min(2, {
message: t('nameMin', {len: 2})
})
.max(255, {
message: t('nameMax', {len: 255})
})
});
type CreateFormValues = z.infer<typeof createFormSchema>;
const copiedFormSchema = z
.object({
copied: z.boolean()
})
.refine(
(data) => {
return data.copied;
},
{
message: t('apiKeysConfirmCopy2'),
path: ["copied"]
}
);
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
const form = useForm<CreateFormValues>({
resolver: zodResolver(createFormSchema),
defaultValues: {

View File

@@ -162,9 +162,10 @@ export default function ResourceAuthenticationPage() {
rolesResponse.data.data.roles
.map((role) => ({
id: role.roleId.toString(),
text: role.name
text: role.name,
isAdmin: role.isAdmin
}))
.filter((role) => role.text !== "Admin")
.filter((role) => !role.isAdmin)
);
usersRolesForm.setValue(
@@ -172,9 +173,10 @@ export default function ResourceAuthenticationPage() {
resourceRolesResponse.data.data.roles
.map((i) => ({
id: i.roleId.toString(),
text: i.name
text: i.name,
isAdmin: i.isAdmin
}))
.filter((role) => role.text !== "Admin")
.filter((role) => !role.isAdmin)
);
setAllUsers(

View File

@@ -88,17 +88,6 @@ type LocalRule = ArrayElement<ListResourceRulesResponse["rules"]> & {
updated?: boolean;
};
const RuleAction = {
ACCEPT: "Always Allow",
DROP: "Always Deny"
} as const;
const RuleMatch = {
PATH: "Path",
IP: "IP",
CIDR: "IP Range"
} as const;
export default function ResourceRules(props: {
params: Promise<{ resourceId: number }>;
}) {
@@ -113,6 +102,17 @@ export default function ResourceRules(props: {
const router = useRouter();
const t = useTranslations();
const RuleAction = {
ACCEPT: t('alwaysAllow'),
DROP: t('alwaysDeny')
} as const;
const RuleMatch = {
PATH: t('path'),
IP: "IP",
CIDR: t('ipAddressRange')
} as const;
const addRuleForm = useForm({
resolver: zodResolver(addRuleSchema),
defaultValues: {

View File

@@ -204,7 +204,7 @@ export default function CreateShareLinkForm({
validForSeconds: neverExpire ? undefined : timeInSeconds,
title:
values.title ||
`${values.resourceName || "Resource" + values.resourceId} Share Link`
t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)})
}
)
.catch((e) => {

View File

@@ -69,10 +69,10 @@ export default function ShareLinksTable({
async function deleteSharelink(id: string) {
await api.delete(`/access-token/${id}`).catch((e) => {
toast({
title: "Failed to delete link",
title: t('shareErrorDelete'),
description: formatAxiosError(
e,
"An error occurred deleting link"
t('shareErrorDeleteMessage')
)
});
});
@@ -81,8 +81,8 @@ export default function ShareLinksTable({
setRows(newRows);
toast({
title: "Link deleted",
description: "The link has been deleted"
title: t('shareDeleted'),
description: t('shareDeletedDescription')
});
}

View File

@@ -229,11 +229,11 @@ export default function CreateSiteForm({
nice: data.niceId.toString(),
mbIn:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
? t('megabytes', {count: 0})
: "-",
mbOut:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
? t('megabytes', {count: 0})
: "-",
orgId: orgId as string,
type: data.type as any,
@@ -273,8 +273,6 @@ PersistentKeepalive = 5`
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
const t = useTranslations();
return loadingPage ? (
<LoaderPlaceholder height="300px" />
) : (
@@ -313,7 +311,7 @@ PersistentKeepalive = 5`
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select method" />
<SelectValue placeholder={t('methodSelect')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="local">

View File

@@ -67,10 +67,10 @@ export default function GeneralPage() {
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
title: t('siteErrorUpdate'),
description: formatAxiosError(
e,
"An error occurred while updating the site."
t('siteErrorUpdateDescription')
)
});
});
@@ -78,8 +78,8 @@ export default function GeneralPage() {
updateSite({ name: data.name });
toast({
title: "Site updated",
description: "The site has been updated."
title: t('siteUpdated'),
description: t('siteUpdatedDescription')
});
setLoading(false);

View File

@@ -381,8 +381,8 @@ WantedBy=default.target`
if (!siteDefaults || !wgConfig) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Key pair or site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateKeyPair')
});
setCreateLoading(false);
return;
@@ -399,8 +399,8 @@ WantedBy=default.target`
if (!siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateDefaults')
});
setCreateLoading(false);
return;
@@ -422,7 +422,7 @@ WantedBy=default.target`
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
title: t('siteErrorCreate'),
description: formatAxiosError(e)
});
});

View File

@@ -24,16 +24,18 @@ export default async function SitesPage(props: SitesPageProps) {
sites = res.data.data.sites;
} catch (e) {}
const t = await getTranslations();
function formatSize(mb: number, type: string): string {
if (type === "local") {
return "-"; // because we are not able to track the data use in a local site right now
}
if (mb >= 1024 * 1024) {
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)});
} else if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)} GB`;
return t('gigabytes', {count: (mb / 1024).toFixed(2)});
} else {
return `${mb.toFixed(2)} MB`;
return t('megabytes', {count: mb.toFixed(2)});
}
}
@@ -50,8 +52,6 @@ export default async function SitesPage(props: SitesPageProps) {
};
});
const t = await getTranslations();
return (
<>
{/* <SitesSplashCard /> */}

View File

@@ -124,7 +124,7 @@ export default function Page() {
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating API key",
title: t('apiKeysErrorCreate'),
description: formatAxiosError(e)
});
});
@@ -145,10 +145,10 @@ export default function Page() {
)
})
.catch((e) => {
console.error("Error setting permissions", e);
console.error(t('apiKeysErrorSetPermission'), e);
toast({
variant: "destructive",
title: "Error setting permissions",
title: t('apiKeysErrorSetPermission'),
description: formatAxiosError(e)
});
});

View File

@@ -41,7 +41,7 @@ export default function UsersTable({ users }: Props) {
const deleteUser = (id: string) => {
api.delete(`/user/${id}`)
.catch((e) => {
console.error("Error deleting user", e);
console.error(t('userErrorDelete'), e);
toast({
variant: "destructive",
title: t('userErrorDelete'),

View File

@@ -226,7 +226,7 @@ export default function VerifyEmailForm({
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{t('emailVerifySubmit')}
{t('submit')}
</Button>
</form>
</Form>