From 187e008914c4a19521f09b4fa6098796be045657 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 12 Jul 2026 22:58:40 +0200 Subject: [PATCH] Add search to language switcher dropdown, remove translation script --- scripts/translate.mjs | 114 --------------------------- src/components/language-switcher.tsx | 109 ++++++++++++++++--------- 2 files changed, 73 insertions(+), 150 deletions(-) delete mode 100644 scripts/translate.mjs diff --git a/scripts/translate.mjs b/scripts/translate.mjs deleted file mode 100644 index dd04cae3..00000000 --- a/scripts/translate.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; -import { translate } from "@vitalets/google-translate-api"; - -const ROOT = resolve(import.meta.dirname, ".."); -const SEP = "\n␟\n"; - -const TARGETS = [ - { code: "pt", to: "pt" }, { code: "pl", to: "pl" }, { code: "sv", to: "sv" }, - { code: "tr", to: "tr" }, { code: "ro", to: "ro" }, { code: "hu", to: "hu" }, - { code: "cs", to: "cs" }, { code: "sk", to: "sk" }, { code: "da", to: "da" }, - { code: "no", to: "no" }, { code: "el", to: "el" }, { code: "bg", to: "bg" }, - { code: "hr", to: "hr" }, { code: "sr", to: "sr" }, { code: "uk", to: "uk" }, - { code: "ru", to: "ru" }, -]; - -function flatten(o, p) { const r={}; for(const[k,v]of Object.entries(o)){const f=p?`${p}.${k}`:k; typeof v==="object"&&v!==null&&!Array.isArray(v)?Object.assign(r,flatten(v,f)):r[f]=String(v??"");} return r; } -function unflatten(flat){const r={};for(const[k,v]of Object.entries(flat)){const p=k.split(".");let c=r;for(let i=0;i= 0) { - const marker = `Ω${idx}Ω`; - markers.push({ marker, original: text.slice(start, i + 1) }); - result += marker; - idx++; - start = -1; - continue; - } - } - if (depth === 0) result += ch; - } - return { text: result, markers }; -} - -function restore(text, markers) { - let result = text; - // Sort longest first - const sorted = [...markers].sort((a, b) => b.marker.length - a.marker.length); - for (const { marker, original } of sorted) { - result = result.replaceAll(marker, original); - } - return result; -} - -async function main() { - const raw = JSON.parse(readFileSync(resolve(ROOT, "src/messages/en.json"), "utf-8")); - const flat = flatten(raw); - const keys = Object.keys(flat); - const vals = Object.values(flat); - - console.log(`Source: en.json — ${keys.length} keys`); - - // Protect all values - const protectedArr = vals.map(protect); - const allTexts = protectedArr.map((p) => p.text); - const allMarkers = protectedArr.map((p) => p.markers); - - const batch = allTexts.join(SEP); - const batchLen = batch.length; - console.log(`Batch size: ${batchLen} chars`); - - for (const { code, to } of TARGETS) { - const outPath = resolve(ROOT, "src/messages", `${code}.json`); - process.stdout.write(` ${code}... `); - - try { - const { text } = await translate(batch, { to }); - let parts = text.split(SEP); - - if (parts.length !== keys.length) { - process.stdout.write(`split fail (${parts.length}), individual fallback... `); - parts = []; - for (let i = 0; i < keys.length; i++) { - try { - const { text: t } = await translate(allTexts[i], { to }); - parts.push(t); - } catch { - parts.push(vals[i]); - } - await new Promise((r) => setTimeout(r, 200)); - } - } - - // Restore placeholders - const restored = parts.map((p, i) => restore(p, allMarkers[i])); - - const flatResult = {}; - keys.forEach((k, i) => { flatResult[k] = restored[i]; }); - writeFileSync(outPath, JSON.stringify(unflatten(flatResult), null, 2) + "\n", "utf-8"); - process.stdout.write(`✓\n`); - } catch (err) { - process.stdout.write(`FAIL: ${err.message.slice(0, 50)}\n`); - } - - await new Promise((r) => setTimeout(r, 2000)); - } - - console.log("\nDone!"); -} - -main().catch(console.error); diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx index e9b1a8af..8e316716 100644 --- a/src/components/language-switcher.tsx +++ b/src/components/language-switcher.tsx @@ -3,7 +3,8 @@ import Image from "next/image"; import { useLocale } from "next-intl"; import { useRouter } from "next/navigation"; -import { useTransition, useState, useRef, useEffect } from "react"; +import { useTransition, useState, useRef, useEffect, useMemo } from "react"; +import { Search } from "lucide-react"; const LOCALES: { code: string; label: string; lang: string }[] = [ { code: "nl", label: "Nederlands", lang: "nl" }, @@ -35,9 +36,21 @@ export function LanguageSwitcher() { const router = useRouter(); const [pending, startTransition] = useTransition(); const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); const ref = useRef(null); const current = LOCALES.find((l) => l.code === locale) ?? LOCALES[0]; + const filtered = useMemo(() => { + if (!search) return LOCALES; + const q = search.toLowerCase(); + return LOCALES.filter( + (l) => + l.label.toLowerCase().includes(q) || + l.code.toLowerCase().includes(q) || + l.lang.toLowerCase().includes(q), + ); + }, [search]); + useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); @@ -46,6 +59,10 @@ export function LanguageSwitcher() { return () => document.removeEventListener("mousedown", handleClick); }, []); + useEffect(() => { + if (!open) setSearch(""); + }, [open]); + function switchLocale(code: string) { if (code === locale) return; document.cookie = `NEXT_LOCALE=${code};path=/;max-age=31536000;samesite=strict;secure`; @@ -76,42 +93,62 @@ export function LanguageSwitcher() { {open && ( -
- {LOCALES.map((l) => ( - - ))} +
+ +
+ {filtered.length === 0 && ( +

Geen talen gevonden

+ )} + {filtered.map((l) => ( + + ))} +
)}