mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 23:39:07 +01:00
switch lang tag on language switch, lint
This commit is contained in:
@@ -34,7 +34,7 @@ export const Badge = forwardRef<HTMLSpanElement, Props>(function Badge(
|
|||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{Icon && <Icon size={iconSize} />}
|
{Icon && <Icon size={iconSize} aria-hidden={"true"} />}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className={"ml-auto h-4 w-4"} />
|
<ChevronRight className={"ml-auto h-4 w-4"} aria-hidden={"true"} />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
));
|
));
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ function NumberStepper({
|
|||||||
"flex w-9 flex-1 cursor-default items-center justify-center text-nb-gray-300 transition-colors hover:bg-nb-gray-800"
|
"flex w-9 flex-1 cursor-default items-center justify-center text-nb-gray-300 transition-colors hover:bg-nb-gray-800"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChevronUp size={12} />
|
<ChevronUp size={12} aria-hidden={"true"} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type={"button"}
|
type={"button"}
|
||||||
@@ -184,16 +184,22 @@ function NumberStepper({
|
|||||||
"border-t border-neutral-200 dark:border-nb-gray-700",
|
"border-t border-neutral-200 dark:border-nb-gray-700",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronDown size={12} />
|
<ChevronDown size={12} aria-hidden={"true"} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldMessage({ error, warning }: Readonly<{ error?: string; warning?: string }>) {
|
function FieldMessage({
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
}: Readonly<{ id?: string; error?: string; warning?: string }>) {
|
||||||
if (!error && !warning) return null;
|
if (!error && !warning) return null;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
id={id}
|
||||||
|
role={error ? "alert" : "status"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 inline-flex items-center gap-1 text-xs",
|
"mt-2 inline-flex items-center gap-1 text-xs",
|
||||||
error ? "text-red-500" : "text-orange-400",
|
error ? "text-red-500" : "text-orange-400",
|
||||||
@@ -232,7 +238,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
const isNumber = type === "number";
|
const isNumber = type === "number";
|
||||||
|
|
||||||
const reactId = useId();
|
const reactId = useId();
|
||||||
const inputId = id ?? (label ? `input-${reactId}` : undefined);
|
const fallbackId = `input-${reactId}`;
|
||||||
|
const inputId = id ?? (label ? fallbackId : undefined);
|
||||||
|
const messageId = error || warning ? `${inputId ?? fallbackId}-message` : undefined;
|
||||||
|
|
||||||
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -268,8 +276,13 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
onClick={() => setShowPassword((s) => !s)}
|
onClick={() => setShowPassword((s) => !s)}
|
||||||
className={"pointer-events-auto transition-all hover:text-white"}
|
className={"pointer-events-auto transition-all hover:text-white"}
|
||||||
aria-label={t("common.togglePasswordVisibility")}
|
aria-label={t("common.togglePasswordVisibility")}
|
||||||
|
aria-pressed={showPassword}
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
{showPassword ? (
|
||||||
|
<EyeOff size={18} aria-hidden={"true"} />
|
||||||
|
) : (
|
||||||
|
<Eye size={18} aria-hidden={"true"} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -293,7 +306,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
className={"pointer-events-auto transition-all hover:text-white"}
|
className={"pointer-events-auto transition-all hover:text-white"}
|
||||||
aria-label={t("common.copy")}
|
aria-label={t("common.copy")}
|
||||||
>
|
>
|
||||||
{copied ? <Check size={16} /> : <Copy size={16} />}
|
{copied ? (
|
||||||
|
<Check size={16} aria-hidden={"true"} />
|
||||||
|
) : (
|
||||||
|
<Copy size={16} aria-hidden={"true"} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -332,6 +349,12 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
id={inputId}
|
id={inputId}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
ref={setRefs}
|
ref={setRefs}
|
||||||
|
aria-invalid={error ? true : undefined}
|
||||||
|
aria-describedby={
|
||||||
|
messageId
|
||||||
|
? [props["aria-describedby"], messageId].filter(Boolean).join(" ")
|
||||||
|
: props["aria-describedby"]
|
||||||
|
}
|
||||||
{...props}
|
{...props}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
/>
|
/>
|
||||||
@@ -343,7 +366,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
<NumberStepper error={error} disabled={props.disabled} onStep={stepBy} />
|
<NumberStepper error={error} disabled={props.disabled} onStep={stepBy} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FieldMessage error={error} warning={warning} />
|
<FieldMessage id={messageId} error={error} warning={warning} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default function FancyToggleSwitch({
|
|||||||
id={switchId}
|
id={switchId}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
dataCy={dataCy}
|
dataCy={dataCy}
|
||||||
aria-describedby={helpText ? descriptionId : undefined}
|
aria-describedby={helpText ? descriptionId : undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ const isKeyboardEvent = (e: KeyboardEvent) => {
|
|||||||
return e.key === "Tab" || e.key === "Escape" || e.key.startsWith("Arrow");
|
return e.key === "Tab" || e.key === "Escape" || e.key.startsWith("Arrow");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (globalThis.window !== undefined) {
|
||||||
window.addEventListener(
|
globalThis.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (isKeyboardEvent(e)) setModality("keyboard");
|
if (isKeyboardEvent(e)) setModality("keyboard");
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
window.addEventListener("pointerdown", () => setModality("pointer"), true);
|
globalThis.addEventListener("pointerdown", () => setModality("pointer"), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFocusVisible = (): boolean => {
|
export const useFocusVisible = (): boolean => {
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ export async function initI18n(): Promise<void> {
|
|||||||
returnNull: false,
|
returnNull: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
syncDocumentLang();
|
||||||
|
i18next.on("languageChanged", syncDocumentLang);
|
||||||
|
|
||||||
Events.On("netbird:preferences:changed", (e) => {
|
Events.On("netbird:preferences:changed", (e) => {
|
||||||
const next = e.data?.language;
|
const next = e.data?.language;
|
||||||
if (next && next !== i18next.language) {
|
if (next && next !== i18next.language) {
|
||||||
@@ -87,6 +90,12 @@ export async function initI18n(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncDocumentLang() {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.lang = i18next.language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadLanguages() {
|
export async function loadLanguages() {
|
||||||
return I18n.Languages();
|
return I18n.Languages();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const MainHeader = () => {
|
|||||||
className={"select-none"}
|
className={"select-none"}
|
||||||
aria-label={t("header.menu.open")}
|
aria-label={t("header.menu.open")}
|
||||||
aria-haspopup={"menu"}
|
aria-haspopup={"menu"}
|
||||||
|
aria-expanded={menuOpen}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@@ -150,7 +151,7 @@ export const MainHeader = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"wails-draggable relative z-10 shrink-0 cursor-default",
|
"wails-draggable relative z-10 shrink-0 cursor-default",
|
||||||
"top-3 flex h-12 items-center",
|
"top-3 flex h-12 items-center",
|
||||||
@@ -169,7 +170,7 @@ export const MainHeader = () => {
|
|||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<div className={"absolute right-[1.3rem] top-1/2 -translate-y-1/2"}>{settingsSlot}</div>
|
<div className={"absolute right-[1.3rem] top-1/2 -translate-y-1/2"}>{settingsSlot}</div>
|
||||||
</div>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const MainBody = () => {
|
|||||||
const isAdvanced = viewMode === "advanced";
|
const isAdvanced = viewMode === "advanced";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"wails-draggable flex min-h-0 flex-1"}>
|
<main className={"wails-draggable flex min-h-0 flex-1"}>
|
||||||
{/* Windows narrower width compensates for the OS frame Wails counts differently than macOS.
|
{/* Windows narrower width compensates for the OS frame Wails counts differently than macOS.
|
||||||
See https://github.com/wailsapp/wails/issues/3260 */}
|
See https://github.com/wailsapp/wails/issues/3260 */}
|
||||||
<div
|
<div
|
||||||
@@ -65,7 +65,7 @@ const MainBody = () => {
|
|||||||
<AdvancedAppRightPanel />
|
<AdvancedAppRightPanel />
|
||||||
</NavSectionProvider>
|
</NavSectionProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }:
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
initialModeRef.current = mode;
|
initialModeRef.current = mode;
|
||||||
const id = window.setTimeout(() => {
|
const id = globalThis.setTimeout(() => {
|
||||||
nameRef.current?.focus();
|
nameRef.current?.focus();
|
||||||
nameRef.current?.select();
|
nameRef.current?.select();
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => window.clearTimeout(id);
|
return () => globalThis.clearTimeout(id);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }:
|
|||||||
}
|
}
|
||||||
const target = normalizeManagementUrl(trimmed);
|
const target = normalizeManagementUrl(trimmed);
|
||||||
|
|
||||||
const unchanged = initial && target === initial.managementUrl;
|
const unchanged = target === initial?.managementUrl;
|
||||||
return { url: target, needsReachCheck: !unchanged };
|
return { url: target, needsReachCheck: !unchanged };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -504,11 +504,9 @@ const RowActions = ({
|
|||||||
}: RowActionsProps) => {
|
}: RowActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const deleteDisabled = isDefault || isActive;
|
const deleteDisabled = isDefault || isActive;
|
||||||
const deleteDisabledReason = isDefault
|
let deleteDisabledReason: string | null = null;
|
||||||
? t("profile.delete.disabledDefault")
|
if (isDefault) deleteDisabledReason = t("profile.delete.disabledDefault");
|
||||||
: isActive
|
else if (isActive) deleteDisabledReason = t("profile.delete.disabledActive");
|
||||||
? t("profile.delete.disabledActive")
|
|
||||||
: null;
|
|
||||||
return (
|
return (
|
||||||
<div className={"inline-flex items-center gap-1"}>
|
<div className={"inline-flex items-center gap-1"}>
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
|
|||||||
@@ -87,43 +87,45 @@ export const SettingsPage = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className={"h-px shrink-0 bg-nb-gray-920/0"} />
|
<div className={"h-px shrink-0 bg-nb-gray-920/0"} />
|
||||||
)}
|
)}
|
||||||
<VerticalTabs value={active} onValueChange={setActive}>
|
<main className={"flex min-h-0 flex-1"}>
|
||||||
<SettingsNavigation />
|
<VerticalTabs value={active} onValueChange={setActive}>
|
||||||
<AppRightPanel>
|
<SettingsNavigation />
|
||||||
<AutostartSettingsProvider>
|
<AppRightPanel>
|
||||||
<SettingsProvider>
|
<AutostartSettingsProvider>
|
||||||
<ScrollArea.Root
|
<SettingsProvider>
|
||||||
key={active}
|
<ScrollArea.Root
|
||||||
type={"auto"}
|
key={active}
|
||||||
className={"min-h-0 flex-1 overflow-hidden"}
|
type={"auto"}
|
||||||
>
|
className={"min-h-0 flex-1 overflow-hidden"}
|
||||||
<ScrollArea.Viewport className={"h-full w-full"}>
|
|
||||||
<div className={"px-7 py-6"}>
|
|
||||||
{visibleTabs.map((tab) => (
|
|
||||||
<VerticalTabs.Content key={tab} value={tab}>
|
|
||||||
{TAB_CONTENT[tab]}
|
|
||||||
</VerticalTabs.Content>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea.Viewport>
|
|
||||||
<ScrollArea.Scrollbar
|
|
||||||
orientation={"vertical"}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
"w-1.5 bg-transparent py-1",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ScrollArea.Thumb
|
<ScrollArea.Viewport className={"h-full w-full"}>
|
||||||
className={
|
<div className={"px-7 py-6"}>
|
||||||
"relative flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700"
|
{visibleTabs.map((tab) => (
|
||||||
}
|
<VerticalTabs.Content key={tab} value={tab}>
|
||||||
/>
|
{TAB_CONTENT[tab]}
|
||||||
</ScrollArea.Scrollbar>
|
</VerticalTabs.Content>
|
||||||
</ScrollArea.Root>
|
))}
|
||||||
</SettingsProvider>
|
</div>
|
||||||
</AutostartSettingsProvider>
|
</ScrollArea.Viewport>
|
||||||
</AppRightPanel>
|
<ScrollArea.Scrollbar
|
||||||
</VerticalTabs>
|
orientation={"vertical"}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
"w-1.5 bg-transparent py-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb
|
||||||
|
className={
|
||||||
|
"relative flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</SettingsProvider>
|
||||||
|
</AutostartSettingsProvider>
|
||||||
|
</AppRightPanel>
|
||||||
|
</VerticalTabs>
|
||||||
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user