add i18n to frontend

This commit is contained in:
Eduard Gert
2026-05-15 16:22:14 +02:00
parent cccb0e9230
commit 5bdccfe8f4
44 changed files with 1953 additions and 932 deletions

View File

@@ -1,4 +1,5 @@
import { FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as Dialog from "@/components/Dialog";
import { Input } from "@/components/Input";
import { Button } from "@/components/Button";
@@ -10,6 +11,7 @@ type Props = {
};
export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
const { t } = useTranslation();
const [name, setName] = useState("");
useEffect(() => {
@@ -34,17 +36,16 @@ export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
>
<form onSubmit={handleSubmit}>
<div className="px-8 pt-2">
<Dialog.Title>New Profile</Dialog.Title>
<Dialog.Title>{t("profile.dialog.title")}</Dialog.Title>
<Dialog.Description>
Profiles let you keep separate NetBird connections
side by side. Give your profile a memorable name.
{t("profile.dialog.description")}
</Dialog.Description>
</div>
<div className="px-8 pt-3">
<Input
autoFocus
placeholder="e.g. Work"
placeholder={t("profile.dialog.placeholder")}
value={name}
onChange={(e) => setName(e.target.value)}
/>
@@ -56,14 +57,14 @@ export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
variant="secondary"
onClick={() => onOpenChange(false)}
>
Cancel
{t("common.cancel")}
</Button>
<Button
type="submit"
variant="primary"
disabled={!canSubmit}
>
Create
{t("common.create")}
</Button>
</Dialog.Footer>
</form>

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import * as Popover from "@radix-ui/react-popover";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -14,6 +15,7 @@ import { useProfile } from "@/modules/profile/ProfileContext.tsx";
const DEFAULT_PROFILE = "default";
export const ProfileSelector = () => {
const { t } = useTranslation();
const {
profiles,
activeProfile,
@@ -53,34 +55,38 @@ export const ProfileSelector = () => {
const handleSelect = (name: string) => {
setOpen(false);
if (name === activeProfile) return;
void guarded("Switch Profile Failed", () => switchProfile(name));
void guarded(t("profile.error.switchTitle"), () => switchProfile(name));
};
const handleDeregister = async (name: string) => {
const cancelLabel = t("common.cancel");
const confirmLabel = t("profile.deregister.confirm");
const result = await Dialogs.Warning({
Title: "Deregister Profile",
Message: `Are you sure you want to deregister "${name}"? You will need to log in again to use it.`,
Title: t("profile.deregister.title"),
Message: t("profile.deregister.message", { name }),
Buttons: [
{ Label: "Cancel", IsCancel: true },
{ Label: "Deregister", IsDefault: true },
{ Label: cancelLabel, IsCancel: true },
{ Label: confirmLabel, IsDefault: true },
],
});
if (result !== "Deregister") return;
void guarded("Deregister Profile Failed", () => logoutProfile(name));
if (result !== confirmLabel) return;
void guarded(t("profile.error.deregisterTitle"), () => logoutProfile(name));
};
const handleDelete = async (name: string) => {
if (name === DEFAULT_PROFILE) return;
const cancelLabel = t("common.cancel");
const confirmLabel = t("common.delete");
const result = await Dialogs.Warning({
Title: "Delete Profile",
Message: `Are you sure you want to delete "${name}"? This action cannot be undone.`,
Title: t("profile.delete.title"),
Message: t("profile.delete.message", { name }),
Buttons: [
{ Label: "Cancel", IsCancel: true },
{ Label: "Delete", IsDefault: true },
{ Label: cancelLabel, IsCancel: true },
{ Label: confirmLabel, IsDefault: true },
],
});
if (result !== "Delete") return;
void guarded("Delete Profile Failed", () => removeProfile(name));
if (result !== confirmLabel) return;
void guarded(t("profile.error.deleteTitle"), () => removeProfile(name));
};
const handleNewProfile = () => {
@@ -89,10 +95,12 @@ export const ProfileSelector = () => {
};
const handleCreateProfile = (name: string) => {
void guarded("Create Profile Failed", () => addProfile(name));
void guarded(t("profile.error.createTitle"), () => addProfile(name));
};
const displayName = selected?.name ?? (loaded ? "No profile" : "Loading...");
const displayName =
selected?.name ??
(loaded ? t("profile.selector.noProfile") : t("profile.selector.loading"));
const initial = (selected?.name ?? "?").charAt(0).toUpperCase();
const initialColor = generateColorFromString(selected?.name);
@@ -155,7 +163,7 @@ export const ProfileSelector = () => {
<Search size={12} className="text-nb-gray-300 shrink-0" />
<Command.Input
autoFocus
placeholder="Search profile by name..."
placeholder={t("profile.selector.searchPlaceholder")}
className={cn(
"w-full bg-transparent text-xs text-nb-gray-200 placeholder:text-nb-gray-400",
"outline-none border-none",
@@ -170,11 +178,10 @@ export const ProfileSelector = () => {
<Command.Empty>
<div className="flex flex-col items-center text-center px-4 pt-2 pb-3">
<h3 className="text-xs font-semibold text-nb-gray-200">
No Profiles Found
{t("profile.selector.emptyTitle")}
</h3>
<p className="text-[0.7rem] leading-snug text-nb-gray-400 mt-1 text-balance">
Try a different search term or create a new
profile.
{t("profile.selector.emptyDescription")}
</p>
</div>
</Command.Empty>
@@ -220,7 +227,7 @@ export const ProfileSelector = () => {
>
<PlusCircle size={12} className="text-netbird" />
</div>
<span className="text-xs font-semibold">New Profile</span>
<span className="text-xs font-semibold">{t("profile.selector.newProfile")}</span>
</button>
</Command>
</Popover.Content>
@@ -252,6 +259,7 @@ const ProfileRow = ({
onDelete,
deletable,
}: ProfileRowProps) => {
const { t } = useTranslation();
const [menuOpen, setMenuOpen] = useState(false);
const initial = profile.name.charAt(0).toUpperCase();
const initialColor = generateColorFromString(profile.name);
@@ -300,7 +308,7 @@ const ProfileRow = ({
"hover:bg-nb-gray-800 hover:text-nb-gray-200 outline-none",
"data-[state=open]:bg-nb-gray-800 data-[state=open]:text-nb-gray-200",
)}
aria-label="More options"
aria-label={t("profile.selector.moreOptions")}
>
<MoreVertical size={14} />
</button>
@@ -328,7 +336,7 @@ const ProfileRow = ({
)}
>
<UserMinus size={14} className="text-nb-gray-300" />
<span>Deregister</span>
<span>{t("profile.selector.deregister")}</span>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={!deletable}
@@ -347,7 +355,7 @@ const ProfileRow = ({
)}
>
<Trash2 size={14} />
<span>Delete Profile</span>
<span>{t("profile.selector.delete")}</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>

View File

@@ -33,7 +33,7 @@ export const SwitchItemGroup = ({ value, onChange, children, className }: Props)
value={value}
onValueChange={onChange}
className={cn(
"flex shrink-0 rounded-lg border border-nb-gray-850 bg-nb-gray-910 p-1",
"flex shrink-0 rounded-lg border border-nb-gray-850 bg-nb-gray-910 p-1 overflow-hidden",
className,
)}
>