update language switcher, remove flags

This commit is contained in:
Eduard Gert
2026-06-02 12:36:29 +02:00
parent 45095818bc
commit c3a0c1beeb
7 changed files with 28 additions and 73 deletions

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-de" viewBox="0 0 512 512">
<path fill="#ffce00" d="M0 341.3h512V512H0z"/>
<path fill="#000001" d="M0 0h512v170.7H0z"/>
<path fill="red" d="M0 170.7h512v170.6H0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 232 B

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 512 512">
<path fill="#012169" d="M0 0h512v512H0z"/>
<path fill="#FFF" d="M512 0v64L322 256l190 187v69h-67L254 324 68 512H0v-68l186-187L0 74V0h62l192 188L440 0z"/>
<path fill="#C8102E" d="m184 324 11 34L42 512H0v-3zm124-12 54 8 150 147v45zM512 0 320 196l-4-44L466 0zM0 1l193 189-59-8L0 49z"/>
<path fill="#FFF" d="M176 0v512h160V0zM0 176v160h512V176z"/>
<path fill="#C8102E" d="M0 208v96h512v-96zM208 0v512h96V0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 505 B

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-hu" viewBox="0 0 512 512">
<g fill-rule="evenodd">
<path fill="#fff" d="M512 512H0V0h512z"/>
<path fill="#388d00" d="M512 512H0V341.3h512z"/>
<path fill="#d43516" d="M512 170.8H0V.1h512z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 273 B

View File

@@ -4,7 +4,7 @@ import * as Popover from "@radix-ui/react-popover";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Command } from "cmdk";
import { errorDialog } from "@/lib/dialogs.ts";
import { CheckIcon, ChevronDown, Search } from "lucide-react";
import { CheckIcon, ChevronDown, LanguagesIcon, Search } from "lucide-react";
import { Preferences } from "@bindings/services";
import { LanguageCode, type Language } from "@bindings/i18n/models.js";
import { HelpText } from "@/components/typography/HelpText";
@@ -13,43 +13,17 @@ import { loadLanguages } from "@/lib/i18n";
import { cn } from "@/lib/cn";
import { formatErrorMessage } from "@/lib/errors";
// Flags live alongside the rest of the SVG flag library under
// assets/flags/1x1 and are filename-matched to the language code
// (de → de.svg, en → en.svg, hu → hu.svg). Vite eager-globs them at
// build time; the JS bundle only holds URL refs, not the SVG bytes.
const FLAG_URLS = import.meta.glob<string>("@/assets/flags/1x1/*.svg", {
eager: true,
import: "default",
query: "?url",
});
// Intentionally no flag icons here: flags represent countries, not
// languages (German is spoken across DE/AT/CH; English across US/UK/AU/
// etc.). Each label shows the endonym followed by the englishName in
// parentheses when the two differ (e.g. "Deutsch (German)"), in both
// the trigger and the dropdown rows.
// See: https://www.flagsarenotlanguages.com/blog/
const flagByCode: Record<string, string> = {};
for (const path in FLAG_URLS) {
const match = path.match(/1x1\/([^/]+)\.svg$/);
if (match) flagByCode[match[1]] = FLAG_URLS[path];
}
const flagFor = (code: string): string | undefined => flagByCode[code.toLowerCase().split("-")[0]];
function Flag({ code, label }: { code: string; label: string }) {
const src = flagFor(code);
if (!src) {
return (
<span
className={"h-3.5 w-3.5 rounded-full bg-nb-gray-800 shrink-0 inline-block"}
aria-hidden
/>
);
}
return (
<img
src={src}
alt={label}
className={"h-3.5 w-3.5 rounded-full object-cover shrink-0 select-none"}
draggable={false}
/>
);
}
const labelFor = (lang: Language): string =>
lang.englishName && lang.englishName !== lang.displayName
? `${lang.displayName} (${lang.englishName})`
: lang.displayName;
export function LanguagePicker() {
const { t, i18n } = useTranslation();
@@ -121,9 +95,9 @@ export function LanguagePicker() {
"disabled:opacity-50",
)}
>
{current && <Flag code={current.code} label={current.displayName} />}
<LanguagesIcon size={16} className={"text-nb-gray-200 shrink-0"} />
<span className={"truncate flex-1 text-left"}>
{current?.displayName ?? "—"}
{current ? labelFor(current) : "—"}
</span>
<ChevronDown size={12} className={"text-nb-gray-400 shrink-0"} />
</button>
@@ -193,12 +167,8 @@ export function LanguagePicker() {
"data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50",
)}
>
<Flag
code={lang.code}
label={lang.displayName}
/>
<span className={"flex-1 truncate"}>
{lang.displayName}
<span className={"flex-1 min-w-0 truncate"}>
{labelFor(lang)}
</span>
<span
className={

View File

@@ -29,17 +29,21 @@ for (const path in bundleModules) {
}
// detectBrowserLanguage walks navigator.language + navigator.languages
// and returns the first base code ("de" from "de-DE") that has a shipped
// bundle. Returns null when none match, so the caller can fall back to
// English. We only ever match against the lowercased base — region tags
// don't have separate bundles today.
// and returns the first shipped bundle that matches. We try an exact
// case-insensitive match first (so "en-GB" picks the en-GB bundle when
// shipped), then fall back to the base code ("de" from "de-DE"). Returns
// null when nothing matches, so the caller can fall back to English.
function detectBrowserLanguage(available: string[]): string | null {
const tags = [navigator.language, ...(navigator.languages ?? [])].filter(
(tag): tag is string => typeof tag === "string" && tag.length > 0,
);
const byLower = new Map(available.map((code) => [code.toLowerCase(), code]));
for (const tag of tags) {
const base = tag.toLowerCase().split("-")[0];
if (available.includes(base)) return base;
const lower = tag.toLowerCase();
const exact = byLower.get(lower);
if (exact) return exact;
const base = byLower.get(lower.split("-")[0]);
if (base) return base;
}
return null;
}

View File

@@ -1,6 +1,6 @@
{
"languages": [
{"code": "en", "displayName": "English", "englishName": "English"},
{"code": "en", "displayName": "English (US)", "englishName": "English (US)"},
{"code": "de", "displayName": "Deutsch", "englishName": "German"},
{"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"}
]

View File

@@ -245,7 +245,7 @@
"settings.troubleshooting.duration.suffix": "Minute(s)",
"settings.troubleshooting.create": "Create Bundle",
"settings.troubleshooting.progress.description": "Collecting logs, system details, and connection state. This usually takes a moment — keep this window open until it completes.",
"settings.troubleshooting.cancelling": "Cancelling…",
"settings.troubleshooting.cancelling": "Canceling…",
"settings.troubleshooting.done.uploadedTitle": "Debug bundle successfully uploaded!",
"settings.troubleshooting.done.savedTitle": "Bundle saved",
"settings.troubleshooting.done.uploadedDescription": "Share the upload key below with NetBird support. A local copy was also saved on your device.",
@@ -261,7 +261,7 @@
"settings.troubleshooting.stage.restoring": "Restoring previous log level…",
"settings.troubleshooting.stage.bundling": "Generating debug bundle…",
"settings.troubleshooting.stage.uploading": "Uploading to NetBird…",
"settings.troubleshooting.stage.cancelling": "Cancelling…",
"settings.troubleshooting.stage.cancelling": "Canceling…",
"settings.about.client": "NetBird Client v{version}",
"settings.about.clientName": "NetBird Client",