switch lang tag on language switch, lint

This commit is contained in:
Eduard Gert
2026-06-18 16:16:45 +02:00
parent 461ec1c8f5
commit 731e87be72
11 changed files with 94 additions and 60 deletions
+1 -1
View File
@@ -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 => {
+9
View File
@@ -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>
</> </>
); );
}; };